ENG-2027: See individual Canvas frame in a block (dg-canvas frame embed)#1222
Open
mattakamatsu wants to merge 10 commits into
Open
ENG-2027: See individual Canvas frame in a block (dg-canvas frame embed)#1222mattakamatsu wants to merge 10 commits into
mattakamatsu wants to merge 10 commits into
Conversation
…erge fixes Feature (per canvas-frame-embed spec v0): a dg-frame block embed that mounts the live canvas anchored to a named tldraw frame, referenced by shape id with name fallback. Frame-anchored embeds re-zoom to the frame on every mount (with a re-center button); frameless embeds remember a per-instance viewport via an instanceKey suffix on the canvas session storage key. Includes a two-step insert dialog (canvas page -> frame) wired to a slash command, and embeds default to tldraw focus mode (cmd+. restores the controls). Fixes found while testing the embed on real canvases: - tldrawStyles.ts hid ALL Roam blocks on any page hosting a tldraw container unless it was inside .dg-canvas-embed, so mounting a dg-frame embed blanked the whole page (silently - pure CSS). The exclusion list now covers every embed wrapper class. - useRoamStore's pull-watch handler applied the raw persisted store via applyDiff, which validates against the live schema without migrating, so any record predating MigrateNodeTypeToDiscourseNode (legacy node-uid shape types) threw an uncaught ValidationError inside mergeRemoteChanges and broke the canvas - for dg-canvas embeds too, triggered by any watched change on the canvas page (including its own first-mount State-block write). The merge logic now lives in canvasRemoteMerge.ts: incoming snapshots run through store.schema.migrateStoreSnapshot (same machinery as loadSnapshot), still-invalid records are dropped individually instead of aborting, failed merges are reported instead of thrown, bare legacy-format props no longer TypeError, and a stale merge timer can no longer outlive the stateId guard and clobber just-saved local edits. - refreshConfigTree now tolerates phantom null children in config-page pulls, which previously aborted the entire extension load. Verified live in plugin-testing-akamatsulab2 (repro pages seeded with a raw legacy-typed shape reproduced the exact ValidationError pre-fix; post-fix the same triggers merge cleanly, migrating records in flight). Unit tests cover parsing, session-key instancing, and the remote merge against real tldraw stores. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…attribute
Per product decision: one block syntax, {{dg-canvas: [[Canvas]]}}, drives both
behaviors. A plain reference renders the original whole-canvas embed unchanged
(the classic code path, .dg-canvas-embed wrapper). An optional frame argument
- "Frame Name" and/or shape:ID - routes to the frame-anchored embed instead,
but only when it maps to a real frame on the canvas.
Degradation contract (a broken frame reference must never cost the user their
canvas): the parser ignores any tail that is not a clean "name"/shape:id
argument, and the renderer resolves the frame against the persisted store
before mounting - an unmatched name or id silently falls back to the
whole-canvas embed rather than showing a not-found notice.
- dgFrameEmbed.ts -> dgCanvasEmbed.ts: parse/serialize {{dg-canvas}} with an
optional frame argument; a malformed tail parses as whole-canvas.
- CanvasFrameEmbed.tsx exports findCanvasFrameRef (editor-less frame existence
check via getRoamCanvasSnapshot) and the CanvasFrameEmbed component;
renderCanvasEmbed routes plain vs frame and picks the wrapper class.
- Removed the dg-frame button observer and renderCanvasFrameEmbed; the frame
picker slash command now writes {{dg-canvas: ... "Frame" shape:ID}}.
Tests rewritten for the merged syntax including the degradation cases; verified
live in plugin-testing-akamatsulab2 that plain / valid-frame / unknown-frame /
malformed-tail each route to the expected embed with no errors.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A `}` inside a serialized frame name aborted the whole-embed regex match, so parseDgCanvasEmbed returned null and renderCanvasEmbed rendered nothing — violating the contract that a bad frame reference always degrades to the whole canvas. Make the tail lazy (`[\s\S]*?` to the `}}` terminator) so a stray `}` is handled by FRAME_ARGS_REGEX instead, and strip curly braces in the serializer as defense-in-depth for picker-written blocks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The requestAnimationFrame scheduled at editor mount was never cancelled, so if the embed unmounted within a frame the callback ran zoomToFrame against a disposed tldraw editor and set state on an unmounted component. Track the handle and cancel it in an unmount effect. Also have the re-center (⌖) handler re-resolve the frame so a delete/rename after mount clears or raises the "not found" notice instead of leaving it stale. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
findCanvasFrameRef and the frame picker both called getRoamCanvasSnapshot, which builds and migrates a full throwaway TLStore just to list frame shapes — duplicating the parse the editor mount does moments later, and throwing (then swallowing) on legacy canvases whose node shapes need the discourse migration. Add getPersistedCanvasStore / getCanvasFrameShapes that read frame records straight from block props (frames are default shapes, present in both formats and untouched by migration), and route both callers through them. One source of frame enumeration, no throwaway editor, and legacy canvases list frames. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
instanceKey was threaded through CanvasEmbedOptions, the session storage key,
and the persistence registration to give frameless embeds an independent
viewport — but merging the frame embed into {{dg-canvas}} left the plain path
on the original TldrawCanvas with no embedOptions, so no caller ever set it.
Remove the dead parameter, its storage-key branch, and its tests, and correct
the CanvasEmbedOptions doc comment that still referenced the removed
{{dg-frame}} syntax. (Two same-canvas embeds share one viewport — the original
{{dg-canvas}} behavior, intentionally unchanged.)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ⌖ re-center button and the frame-not-found notice backed their readability with an inline rgba(255,255,255,0.9) — a new hardcoded shading color (AGENTS.md: don't introduce new shading colors). Use a default (non-minimal) Blueprint Button and a Blueprint Card, whose themed surfaces provide the background and elevation in both light and dark themes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AGENTS.md asks for explicit return types. Annotate the new exports that were missing them: renderCanvasFrameEmbedDialog and filterUserRecords. (calculateDiff is relocated code with any-typed internals that resist a precise return type without a rewrite — left as-is.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
|
This pull request has been ignored for the connected project Preview Branches by Supabase. |
Resolves the CanvasEmbed.tsx conflict with the "Edit Block" chrome that landed on main (#—, commit 69f7d22). Both sides rewrote renderCanvasEmbed's tail; the combined version keeps main's handleEditBlock/CanvasEmbedChrome for the whole-canvas path and this branch's frame routing for the frame path: frame ? <CanvasFrameEmbed> : <CanvasEmbedChrome title location> Title/frame parsing stays on parseDgCanvasEmbed (supersedes the removed BLOCK_TEXT_REGEX/extractCanvasTitle); both `frame` and `location` are computed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…f any Clears the 12 lint warnings CI flagged on canvasRemoteMerge.ts (the file reads as newly added, so the diff-helper code relocated from useRoamStore.ts is re-linted). diffObjects/calculateDiff now use `unknown` with narrowed casts and typed filter predicates; pruneState drops the unused destructured key. Behavior unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
PR size/scope checkThis PR is over our review-size guideline.
Please split this into smaller PRs unless there is a clear reason the changes need to land together. If keeping it as one PR, please add a brief justification covering:
|
Contributor
Author
|
it also picked up a couple of pre-existing bugs and linting issues; those can be ignored/rejected if desired |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes ENG-2027. See the issue for the product framing; this is the implementation + self-review notes.
What
One block syntax,
{{dg-canvas:}}, now drives both the whole-canvas embed and a frame-anchored one:{{dg-canvas: [[Canvas]]}}→ the original whole-canvas embed, unchanged.{{dg-canvas: [[Canvas]] "Frame" shape:ID}}→ live embed zoomed to that frame; tracks it as it moves/resizes, ⌖ re-center button, tldraw chrome hidden by default (⌘. restores it).The
{{dg-frame}}attribute from the first draft is gone — merged into{{dg-canvas}}.Shared-code bugs fixed along the way
Both pre-existing, surfaced by exercising the embed on real canvases:
ValidationErrorthat blanked canvas pages. The pull-watch handler applied the raw persisted store viaapplyDiff, which validates but never migrates — so any record predatingMigrateNodeTypeToDiscourseNode(a legacy node-uid shape) threw insidemergeRemoteChangesand tore the page down, for plain{{dg-canvas}}too. Extracted tocanvasRemoteMerge.ts: migrate the incoming snapshot first (same machinery asloadSnapshot), drop still-invalid records individually, report failures instead of throwing. Unit-tested against real tldraw stores.:has()rule only excluded.dg-canvas-embed; any new embed wrapper blanked the host page.Self-review (10-angle pass, findings addressed in the fix commits)
fix: a}in a frame name no longer parses to null and blanks the block (lazy tail + serializer sanitize).fix: cancel the zoom-on-mountrequestAnimationFrameon unmount; re-center refreshes the not-found state.perf: resolve frames from the raw persisted store instead of building a throwaway migrated editor (also dedupes with the picker and stops a legacy-store throw).chore: remove the unusedinstanceKeysession-key plumbing left dead by the merge.style: use Blueprint surfaces for the embed chrome instead of a hardcodedrgba(...)(AGENTS.md).chore: explicit return types on the new exports.For reviewers:
refreshConfigTree.tsgained a null-guard (a phantom null child was aborting extension load). It's unrelated to this feature and only hardens one of the ~16 callers of that helper — happy to split it into its own PR if preferred. Canvas concurrency remains last-write-wins (a remote edit in the same 350 ms window as your own save can be dropped until the next sync), unchanged in spirit from today.Verification
check-types,lint(0 errors), andtest(62) green.plugin-testing-akamatsulab2: all four routing cases (plain / valid-frame / unknown-frame / malformed-tail) route as expected with no errors, the frame embed zooms to its frame, and the ⌖ button + degradation paths work.🤖 Generated with Claude Code