feat: token editor and live canvas (milestone 3) #3

Merged
marti merged 7 commits from feature/m3-editor-and-canvas into main 2026-10-27 00:20:02 +01:00
Owner

Milestone 3 from docs/roadmap/v1-remaining-work.md. Design in ADR 0009.

Summary

sequenceDiagram
    participant E as editor (apps/web)
    participant F as preview frame (/preview/shadcn.html or daisyui.html)
    participant A as api
    F->>E: ready
    E->>F: render (token CSS for every mode + theme mapping, mode, tokens, contrast)
    Note over E: edit -> operations applied locally -> previewTheme()
    E->>F: render (before paint, no reload)
    E->>A: PATCH draft (after 400 ms, coalesced operations, revision)
packages/
+- export/    themeMappings: shadcn/ui and daisyUI variables <- token paths (starter names first)
+- preview/   protocol (zod), previewTheme(), registry, frame runtime, swatches,
|             13 shadcn + 13 daisyUI examples, one Tailwind stylesheet per library
+- core/      + coalesceOperations, inGamut
apps/web/
+- preview/*.html                     extra Vite entries (the frame pages)
+- features/editor/                   tree, 13 type forms, alias picker, relations, contrast, useDraftEditor
+- features/canvas/                   PreviewFrame host, library / mode / viewport / compare controls
+- routes/.../design-systems.$designSystemId_.editor.tsx

Decisions (ADR 0009):

  • Iframe sandbox: the frame uses sandbox="allow-scripts allow-same-origin". A fully opaque origin would need CORS on module scripts and a dev server that accepts the null origin.
  • Framing header: Caddy's X-Frame-Options changes from DENY to SAMEORIGIN.
  • shadcn examples: shadcn markup written by hand in packages/preview, with no shadcn package added. The AGENTS.md "no shadcn/ui" rule covers the app's own UI.
  • Stylesheet cascade: library defaults live in cascade layers, and the injected theme is an unlayered :root rule appended last, so it always wins.

trail check flags web files against the "token documents are json, not jsonb" learning. These are keyword matches only; this PR changes no database schema.

Evidence

Done-when scenario: apps/web/src/features/editor/EditorPage.test.tsx

open editor (starter draft) -> select color.brand.600 -> set #fafafa
  frame receives render: css has new --color-brand-600, contrast has light primary pair failing
  form shows the contrast warning
  one coalesced PATCH with the right revision; 409 -> conflict alert + reload; 422 -> message
  • Alias chain: TokenRelations.test.tsx covers chain and dependents across a three-level chain (component -> semantic -> primitive).
  • Host half: PreviewFrame.test.tsx checks that a ready from its own frame gets a render, that other sources and malformed data are ignored, and that a theme change posts again without remounting.
  • Frame half: runtime.test.tsx checks the single style element, that a second render replaces it rather than adding one, data-mode, ignored foreign messages, the ready post, and the light-mode contrast warning.
  • Galleries: galleries.test.tsx checks 13 examples per library and that each one renders.
  • Built CSS: the utilities read variables at runtime (.bg-primary{background-color:var(--primary)}, .btn-primary{--btn-color:var(--color-primary)}). The preview pages were checked in headless Chromium against the built dist with posted render messages: colours, radius and font changed, and light and dark both worked on both libraries.
  • Not run: a real browser against the full editor-to-frame round trip. jsdom cannot load the frame, so each half of the protocol is tested on its own.
Suite Before After
core 42 45
export - 3
preview - 42
web 36 61
api 45 45

pnpm lint and pnpm typecheck are clean, and pnpm --filter @designbase/web build emits preview/shadcn.html and preview/daisyui.html.

Merge Danger

Door: two-way

There are no API or database changes. The Caddy header change only loosens framing to the same origin.

Blast Radius: web

  • Bundle size: each preview page bundles both libraries' examples (static imports in the runtime). A dynamic import per library would split them if size matters.
  • Narrow canvas: at the app's maximum width the canvas column is about 500 px, so the Desktop viewport scrolls sideways inside it.

🤖 Generated with Claude Code

Milestone 3 from `docs/roadmap/v1-remaining-work.md`. Design in ADR 0009. ## Summary ```mermaid sequenceDiagram participant E as editor (apps/web) participant F as preview frame (/preview/shadcn.html or daisyui.html) participant A as api F->>E: ready E->>F: render (token CSS for every mode + theme mapping, mode, tokens, contrast) Note over E: edit -> operations applied locally -> previewTheme() E->>F: render (before paint, no reload) E->>A: PATCH draft (after 400 ms, coalesced operations, revision) ``` ```text packages/ +- export/ themeMappings: shadcn/ui and daisyUI variables <- token paths (starter names first) +- preview/ protocol (zod), previewTheme(), registry, frame runtime, swatches, | 13 shadcn + 13 daisyUI examples, one Tailwind stylesheet per library +- core/ + coalesceOperations, inGamut apps/web/ +- preview/*.html extra Vite entries (the frame pages) +- features/editor/ tree, 13 type forms, alias picker, relations, contrast, useDraftEditor +- features/canvas/ PreviewFrame host, library / mode / viewport / compare controls +- routes/.../design-systems.$designSystemId_.editor.tsx ``` Decisions (ADR 0009): - **Iframe sandbox:** the frame uses `sandbox="allow-scripts allow-same-origin"`. A fully opaque origin would need CORS on module scripts and a dev server that accepts the `null` origin. - **Framing header:** Caddy's `X-Frame-Options` changes from `DENY` to `SAMEORIGIN`. - **shadcn examples:** shadcn markup written by hand in `packages/preview`, with no shadcn package added. The AGENTS.md "no shadcn/ui" rule covers the app's own UI. - **Stylesheet cascade:** library defaults live in cascade layers, and the injected theme is an unlayered `:root` rule appended last, so it always wins. `trail check` flags web files against the "token documents are `json`, not `jsonb`" learning. These are keyword matches only; this PR changes no database schema. ## Evidence Done-when scenario: `apps/web/src/features/editor/EditorPage.test.tsx` ```text open editor (starter draft) -> select color.brand.600 -> set #fafafa frame receives render: css has new --color-brand-600, contrast has light primary pair failing form shows the contrast warning one coalesced PATCH with the right revision; 409 -> conflict alert + reload; 422 -> message ``` - **Alias chain:** `TokenRelations.test.tsx` covers chain and dependents across a three-level chain (component -> semantic -> primitive). - **Host half:** `PreviewFrame.test.tsx` checks that a `ready` from its own frame gets a render, that other sources and malformed data are ignored, and that a theme change posts again without remounting. - **Frame half:** `runtime.test.tsx` checks the single style element, that a second render replaces it rather than adding one, `data-mode`, ignored foreign messages, the `ready` post, and the light-mode contrast warning. - **Galleries:** `galleries.test.tsx` checks 13 examples per library and that each one renders. - **Built CSS:** the utilities read variables at runtime (`.bg-primary{background-color:var(--primary)}`, `.btn-primary{--btn-color:var(--color-primary)}`). The preview pages were checked in headless Chromium against the built dist with posted render messages: colours, radius and font changed, and light and dark both worked on both libraries. - **Not run:** a real browser against the full editor-to-frame round trip. jsdom cannot load the frame, so each half of the protocol is tested on its own. | Suite | Before | After | | --- | --- | --- | | core | 42 | 45 | | export | - | 3 | | preview | - | 42 | | web | 36 | 61 | | api | 45 | 45 | `pnpm lint` and `pnpm typecheck` are clean, and `pnpm --filter @designbase/web build` emits `preview/shadcn.html` and `preview/daisyui.html`. ## Merge Danger **Door:** two-way There are no API or database changes. The Caddy header change only loosens framing to the same origin. **Blast Radius:** web - **Bundle size:** each preview page bundles both libraries' examples (static imports in the runtime). A dynamic import per library would split them if size matters. - **Narrow canvas:** at the app's maximum width the canvas column is about 500 px, so the Desktop viewport scrolls sideways inside it. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
packages/export holds the one mapping from semantic tokens to shadcn/ui and
daisyUI theme variables. packages/preview starts with what the app and the
preview frame share: the render/ready message protocol with zod schemas,
previewTheme (token CSS for every mode plus the mapping, swatch tokens and
contrast checks) and the example component registry. ADR 0009.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The preview pages (/preview/shadcn.html, /preview/daisyui.html) build beside
the app, each with its own Tailwind stylesheet compiled against the library
variables. The frame runtime applies render messages from its parent (one
theme style element, data-mode on the root) and announces itself with ready.
The canvas shows token swatches with contrast warnings for the current mode
and 13 registered examples per library. The web server now allows same-origin
framing (ADR 0009).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
coalesceOperations keeps the last set/delete per layer and path without
crossing mode or group operations, so the editor can send one PATCH for a
burst of edits. inGamut tells the colour editor whether a colour fits sRGB,
Display P3 or Rec. 2020. getLayer is exported for the editor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PreviewFrame embeds a library's preview page in a sandboxed iframe, answers
its ready message with the current theme and posts every new theme before
paint, without remounting. The canvas panel switches library, mode and
viewport width and compares the draft with the latest version or two
versions side by side.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The editor page shows the draft as a searchable tree per layer and mode,
with forms for all 13 DTCG types (OKLCH colour with gamut and hex fallback,
composites part by part), alias picking from tokens of the same type, the
alias chain and dependents of the selected token, and inline contrast
warnings per mode. Edits apply locally at once, update the canvas, and are
saved as one coalesced PATCH after a pause; a revision conflict reloads and
says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs: the token editor and canvas
All checks were successful
ci / check (pull_request) Successful in 2m1s
517143e3bd
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
marti merged commit 8ec0476543 into main 2026-10-27 00:20:02 +01:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
marti/designbase!3
No description provided.