.canvas

A portable directory of standalone documents plus a single .canvas.json manifest — order, 2D layout, and skip. A container format, not a scene format. Copy it, zip it, check it into git: no database, no absolute paths, no host coupling.

$npm i dotcanvas
deck.canvas.canvas.json001.svg002.svg003.svg

What a .canvas is

A .canvas is a directory containing a root .canvas.json. Its presence — not the directory name — declares the bundle. Every other file is opaque to the format: never required, never validated.

A reader interprets whatever is on disk; it does not reject. An absent, partial, or malformed manifest degrades to implicit mode. The resolved view is recomputed on every read — there is no cache and no private IR. .canvas.json plus the files on disk are the only state.

The manifest is authoritative for order and placement; the directory listing is authoritative for existence.

.canvas.json

JSON. The minimal valid manifest is {} — every field is optional and the reader fills defaults. All paths are relative to the bundle root; .. escape and absolute paths are out of V1, and containment is enforced by the host, not the reader.

The marker is .canvas.json — hidden and JSON-typed, so editor tooling and $schemastill apply, and it stays distinct from JSONCanvas's *.canvas.

.canvas.json
{
  // OPTIONAL. Editor-tooling hint only; readers ignore it.
  "$schema": "https://grida.co/schema/dotcanvas/v1.json",

  // OPTIONAL. Spec version this manifest targets. Missing -> current.
  "version": "1",

  // EDITOR — which editor opens the bundle (à la Figma's editorType).
  //   "slides" (linear deck) | "board" (freeform canvas) | "unknown"
  "editor": "board",

  // CONTENT — which root files are documents. Glob; "*" is the only wildcard.
  // Missing -> ["*.svg"]. Explicit [] derives nothing.
  "files": ["*.svg"],

  // OPTIONAL. The ordered set of documents. Absent -> derived from disk.
  "documents": [
    {
      "src": "001.svg",        // the only field that must resolve on disk
      "id": "n_a1b2",          // OPTIONAL stable identity; absent -> src
      "layout": { "x": 0, "y": 0, "w": 320, "h": 200, "z": 0 }
    },
    { "src": "002.svg", "skip": true }  // off the slides order; still on the board
  ],

  // OPTIONAL. Vendor bag. Readers round-trip unknown keys; never interpret them.
  "ext": {}
}
versionstring?Spec version targeted. Missing → current.
$schemastring?Editor-tooling hint. Ignored by readers.
editorenum?Which editor opens it. slides · board · unknown. Unrecognized → unknown.
filesstring[]?Content globs. Missing → ["*.svg"]. [] derives nothing.
thumbnailstring?Explicit thumbnail path. Overrides the filename convention.
documentsarray?Ordered set of documents. Absent → derived from disk.
documents[].srcstringRelative path. The only field that must resolve on disk.
documents[].idstring?Stable identity. Absent → src is the identity.
documents[].layoutobject?2D placement { x, y, w, h, z }. Absent → no canvas position.
documents[].skipboolean?Omit from the linear slides view. Advisory; still exists.
extobject?Vendor bag. Round-tripped, never interpreted.

There is no name / titlefield. A human label is the document's own content's job — for an SVG, its <title> element.

editor and files

Two orthogonal axes. editor selects the editorthat opens the bundle — how documents are read/presented (à la Figma's editorType). files selects the content — which root files are documents. Any editor holds any content kind.

slidesLinear deck. documents[] order is the primary presentation; layout is an additive canvas view.
boardFreeform canvas. Each document's layout is primary; order is secondary.
unknownNo assumption. Default for a missing or unrecognized editor and for implicit mode.
  • files are glob patterns matched against root basenames. * (any run) is the only wildcard in V1.
  • — Missing → ["*.svg"]. Explicit [] derives nothing; membership then comes only from documents.
  • — It drives disk-derivation and signals the document kind a host opens an editor for.

Reader semantics

A reader reconciles the manifest against the directory listing. The manifest is authoritative for order and placement; the listing is authoritative for existence. Each situation has one defined behavior.

.canvas.json missingOpen in implicit mode, editor: unknown; MAY derive documents from disk.
.canvas.json is malformed JSONDegrade to implicit mode and surface a warning. No hard-fail.
Unknown top-level field or editorIgnore on read; SHOULD preserve on write.
documents absentDerive from disk: root files matching files, ordered lexically by filename.
A documents[].src points at a missing fileSkip it with a warning. Disk is authoritative for existence.
Disk has matching files not in documentsMAY append after the listed ones. Disk wins existence.
Two entries share an id / srcWarning; the reader keeps the first.

Ordering is documents order, then disk-only matches appended lexically. There is no auto-renumber and no re-sort mode.

dotcanvas

dotcanvas is the reference reader/writer. ESM-only, zero runtime dependencies. read / write operate over an injected { list(); read() } port; the pure transforms take (manifest, …) and return a new manifest without mutating the input or throwing.

reader + transforms
import { dotcanvas } from "dotcanvas";

// `fs` is any { list(); read() } port — no node:fs, no DOM.
const canvas = await dotcanvas.read(fs);

canvas.editor;    //=> "slides" | "board" | "unknown"   the editor
canvas.files;     //=> ["*.svg"]                         the content globs
canvas.documents; //=> reconciled, ordered ResolvedDocument[]
canvas.warnings;  //=> non-fatal observations (read never throws)

// Pure transforms: (manifest, …) -> manifest. Never mutate, never throw.
let m = canvas.manifest ?? {};
m = dotcanvas.reorder(m, ["002.svg", "001.svg"]);
m = dotcanvas.setLayout(m, "001.svg", { x: 0, y: 0 });

// Reconcile against disk (drop missing, append disk-only), then persist.
await dotcanvas.write(fs, dotcanvas.heal(m, await fs.list()));

It is not a renderer, a validator that rejects, a filesystem, or a converter for any document's internal format. It resolves paths, order, and warnings — nothing else.