Render dg-canvas frame embeds as static snapshots by default (v1)#1223
Render dg-canvas frame embeds as static snapshots by default (v1)#1223mattakamatsu wants to merge 2 commits into
Conversation
Frame-anchored {{dg-canvas}} embeds now render a read-only SVG image of
the frame exported from the persisted canvas data, instead of mounting a
full interactive tldraw editor per embed. A trailing `live` token in the
block text opts back into the live editor; sync-mode canvases always
fall back to the live embed (their block-props snapshot is frozen at
switch time) with an explanatory badge. Frameless embeds are unchanged.
Why not tldraw's <TldrawImage>: in 2.4.6 it builds its internal store
without custom binding utils or migrations, so discourse-graph canvases
(per-relation binding records, legacy node-uid shape types) throw during
store creation. CanvasFrameSnapshot replicates its export flow
(temporary headless Editor -> getSvgString -> blob URL -> <img>) on top
of a store built with the exact live-mount recipe (buildFrameExportStore),
so legacy records migrate on load exactly as they do in the live embed.
The export uses tldraw's single-frame semantics: descendants included
and clipped to the frame, neighbors excluded, no frame border/label,
bounds = frame page bounds - no store filtering, no hand-rolled
transforms, rotated/nested frames included.
Snapshot chrome: "Open canvas" navigates to the canvas page and zooms
to the frame once on arrival (one-shot sessionStorage hint consumed by
the full-page canvas mount); "Edit here" swaps the live embed in place
until the block re-renders. Snapshots recompute on every mount, which
is the feature's whole refresh model - no watchers.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
This pull request has been ignored for the connected project Preview Branches by Supabase. |
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Open-canvas zoom-to-frame relied on a single requestAnimationFrame after
onMount. On a warm canvas load that works, but on a cold load (first
navigation to the canvas page) the editor's viewport is not measured yet
one frame in, and zoomToBounds is a no-op against a zero-size viewport —
so the camera was left at the default origin instead of the frame.
Verified live in plugin-testing-akamatsulab2: warm loads framed the
target correctly; a genuine cold load (session camera cleared) ended at
the default {0,0} camera even though the hint was consumed and the frame
resolved. Manual zoomToBounds after the viewport measured always worked.
Fix: retry across frames until getViewportScreenBounds reports a measured
(non-zero) viewport and zoomToFrame succeeds, then zoom exactly once.
Bounded to ~90 frames so a deleted frame or never-sized container gives
up instead of spinning; the existing cleanup already cancels the pending
frame on unmount.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Live-verified in
One bug found and fixed during testing (commit 8c66e4d): Open-canvas zoom-on-arrival used a single |
Stacked on #1222 (v0 frame embeds) — do not merge before it; retarget to
mainafter v0 lands.What
Frame-anchored
{{dg-canvas: [[Page]] "Frame" shape:ID}}embeds now render a read-only SVG image of the frame exported from persisted canvas data, instead of mounting a full interactive tldraw editor per embed. Spec:dg-prototypes/canvas-frame-embed/README.md(v1 section, decisions resolved 2026-07-11).livetoken ({{dg-canvas: [[Page]] "Frame" shape:ID live}}) opts back into the v0 live editor. Frameless embeds unchanged (always live).Why not
TldrawImageIn tldraw 2.4.6,
TldrawImagebuilds its internal store withuseTLStore({ snapshot, shapeUtils })only — nobindingUtils, nomigrations— so DG canvases (per-relation-id binding records, legacy node-uid shape types) throw during store creation.CanvasFrameSnapshotreplicates its export flow (temporary headlessEditor→getSvgString→ blob URL →<img>) on top of a store built with the exact live-mount recipe (buildFrameExportStore), so legacy records migrate on load the same way they do in the live embed. tldraw's single-frame export handles the rest: descendants included and clipped to the frame, neighbors excluded, no frame border/label, bounds = frame page bounds — no store filtering, no hand-rolled transforms.Tests
dgCanvasEmbed.test.ts:livetoken grammar (all arg shapes, degradation on misplaced/miscased/junk tails, serialize round-trip) +getFrameEmbedModematrixcanvasFrameExportStore.test.ts: legacy node-uid-typed child of a frame migrates on load; nested-frame parentId chain survives; missing migrations throw (documents why the dep-builder matters)canvasFrameZoomHint.test.ts: one-shot semantics, per-page scoping, TTL expiry, corrupt-value cleanup, null-storage safetybuildAllAddReferencedNodeByAction.test.ts: extraction fromTldraw.tsxpinnednpm test(82 passing),check-types, eslint, andnpm run buildall clean.Not yet done
plugin-testing-akamatsulab2(the verification matrix is in the spec README — snapshot vs live rendering, legacy canvas, placeholders, Open-canvas zoom, Edit-here swap). The on-diskapps/roam/distis built from this branch and ready to load.DiscourseNodeUtil.toSvgdoesn't embed font defs) — noted as an optional follow-up in the spec.🤖 Generated with Claude Code