.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.
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.
.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.
{
// 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": {}
}slides · board · unknown. Unrecognized → unknown.["*.svg"]. [] derives nothing.src is the identity.{ x, y, w, h, z }. Absent → no canvas position.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.
documents[] order is the primary presentation; layout is an additive canvas view.layout is primary; order is secondary.- —
filesare glob patterns matched against root basenames.*(any run) is the only wildcard in V1. - — Missing →
["*.svg"]. Explicit[]derives nothing; membership then comes only fromdocuments. - — 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.documents absentDerive from disk: root files matching files, ordered lexically by filename.documents[].src points at a missing fileSkip it with a warning. Disk is authoritative for existence.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.
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.