ENG-2150 Nested pages in tldraw canvas - #1308
Conversation
Ports the nested sub-page portals prototype (dg-prototypes/nested-pages SPEC.md) into the Roam canvas. A dg-subpage shape is a portal into another tldraw page: a colored title bar (click = enter the target page) over a live scaled map of the target page's shapes — discourse-type colors, images in place, grammar-prefix classification derived from the live node formats. Pages stay flat siblings; hierarchy lives only in page.meta.dgNested.parentPageId plus the portal's props.targetPageId. The classifier, lineage walk, layout projection, and label thresholds are pure functions in utils/nestedPages.ts with unit tests. The shape registers in both store paths with a day-one migration sequence. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The breadcrumb/back bar renders in the HelperButtons UI slot (composed with the default helper buttons) and only appears on nested pages; all navigation funnels through one enterPage. "Create sub-canvas portal" is a new action surfaced in the canvas context menu: it guards maxPages loudly before creating anything, creates the child page eagerly with lineage meta, and titles the portal from the actual (possibly deduped) page name. 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. |
Feedback from first live test: (1) renaming the target page did not update the portal header — the header (and nested-portal preview boxes) now read the live page name, with props.title kept only as the missing-page fallback; (2) EVD-style titles buried the key image — node titles now drop their format prefix (the type code chip already carries it, and Roam titles literally contain "[[EVD]] - "), labels cap at 90 chars, and image boxes show the full image with a max-2-line label strip overlaid at the bottom, in both the live and SVG renderers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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:
|
|
@mdroidian I'm not confident whether this is PR-quality yet; so you may not need to give a detailed review yet; more like "hey matt's agent, for the next round do X, then it'll be ready for my review" then I'll submit the PR |
| style={{ | ||
| pointerEvents: "all", | ||
| display: "flex", | ||
| alignItems: "center", | ||
| gap: 8, | ||
| margin: "6px 0 0 8px", | ||
| padding: "5px 10px", | ||
| background: "rgba(255,255,255,0.94)", | ||
| border: "1px solid #e3e5e9", | ||
| borderRadius: 9, | ||
| boxShadow: "0 1px 6px rgba(20,20,40,0.10)", | ||
| font: "13px var(--tl-font-sans, Inter, system-ui, sans-serif)", | ||
| backdropFilter: "blur(6px)", | ||
| maxWidth: "70vw", | ||
| overflow: "hidden", | ||
| width: "fit-content", | ||
| }} |
There was a problem hiding this comment.
🟡 New canvas breadcrumb bar introduces its own colors and hand-rolled styling instead of the host app's design system
The nested sub-page breadcrumb bar is styled with a brand-new inline color/shadow palette (background: "rgba(255,255,255,0.94)" … boxShadow/backdropFilter at apps/roam/src/components/canvas/DgSubpageBreadcrumb.tsx:24-40) rather than Tailwind classes or platform-native components, so the new UI looks like a separate visual language from the rest of the extension.
Impact: Users see canvas chrome that does not match Roam's look and feel, and future style changes have to be maintained twice.
Repository styling rules that this violates
apps/roam/AGENTS.md states: "Platform-native UI - use BlueprintJS 3 components and Tailwind CSS" and "Do not introduce arbitrary visual styling, new shading colors, background palettes, gradients, accent colors, border colors, or text colors unless the user explicitly asks for them." The root AGENTS.md adds "Use Tailwind CSS for styling where possible" and "When refactoring inline styles, use tailwind classes".
The breadcrumb container (DgSubpageBreadcrumb.tsx:24-40), the back button (DgSubpageBreadcrumb.tsx:48-58) and the crumb buttons (DgSubpageBreadcrumb.tsx:83-95) all hard-code hex colors (#e3e5e9, #f7f8fa, #3a3d42, #5b6bd6, #b9bdc4), a custom shadow and a blur filter. The same pattern is repeated in the portal shape chrome (apps/roam/src/components/canvas/DgSubpageUtil.tsx:409-427). Existing canvas UI in this repo composes tldraw UI primitives (TldrawUiButton, TldrawUiIcon in apps/roam/src/components/canvas/uiOverrides.tsx:97-116) or Tailwind classes instead.
Prompt for agents
The new nested-page UI chrome introduces a bespoke inline visual palette, which apps/roam/AGENTS.md forbids ("Do not introduce arbitrary visual styling, new shading colors, background palettes, gradients, accent colors, border colors, or text colors unless the user explicitly asks for them") and the root AGENTS.md asks for Tailwind/platform-native components. Rework DgSubpageBreadcrumb.tsx (container at lines 24-40, back button 48-58, crumb buttons 83-95) so the bar is built from tldraw UI primitives (e.g. TldrawUiButton, as uiOverrides.tsx already does) and/or Tailwind utility classes that reuse existing repo styling patterns, instead of hard-coded hex colors, custom box-shadow and backdrop-filter. Do the same for the portal header/body chrome in DgSubpageUtil.tsx (lines 409-468) where practical, keeping only the layout styles tldraw requires for absolutely positioned shape content.
Was this helpful? React with 👍 or 👎 to provide feedback.
| import { DefaultHelperButtons, useEditor, useValue } from "tldraw"; | ||
| import { enterPage, getLineage } from "./nestedPageNavigation"; | ||
|
|
||
| const DgSubpageBreadcrumb = () => { |
There was a problem hiding this comment.
🟡 Several new functions omit explicit return types required by the repository style guide
The new breadcrumb components and navigation helpers are declared without explicit return types (for example the component declaration at apps/roam/src/components/canvas/DgSubpageBreadcrumb.tsx:9), which the repository style guide requires for all functions.
Impact: Contributors lose the guaranteed, self-documenting signatures the project standardises on, making accidental signature changes easier to miss.
Affected declarations and the rule
Root AGENTS.md → TypeScript Guidelines: "Use explicit return types for functions".
Missing return types in this PR:
DgSubpageBreadcrumb(apps/roam/src/components/canvas/DgSubpageBreadcrumb.tsx:9) and thegohelper (:17)NestedPageHelperButtons(apps/roam/src/components/canvas/DgSubpageBreadcrumb.tsx:109)enterPage(apps/roam/src/components/canvas/nestedPageNavigation.ts:24)DgSubpageUtil.readPreviewModel/component/indicator(apps/roam/src/components/canvas/DgSubpageUtil.tsx:269,:275,:707)
Existing code in the same area follows the rule (e.g. SyncModeMenuSwitchItem returns ReactElement in apps/roam/src/components/canvas/uiOverrides.tsx:85-96).
| const DgSubpageBreadcrumb = () => { | |
| const DgSubpageBreadcrumb = (): React.ReactElement | null => { |
Was this helpful? React with 👍 or 👎 to provide feedback.
A plugin version without the nested-pages feature must still read a canvas that contains portals — an unknown custom shape type makes loadSnapshot throw, which blanks the entire canvas there. Portals are now plain geo rectangles carrying meta.dgSubpage (with a visible "⤵ name" label for old clients), rendered by DgSubpageGeoUtil, a GeoShapeUtil subclass that draws the portal chrome for meta-carrying shapes and defers to stock geo behavior otherwise. Old clients see a movable, bindable, labeled rectangle and keep sequential page-menu navigation; Cloudflare rooms no longer carry a custom record type at all, so mixed-version collaboration works. Verified against tldraw 2.4.6 and pinned by nestedPagesCompat.test.ts: geo-with-meta and unknown migration sequences load fine on a default-utils store; an unknown shape type throws. The dg-subpage custom type and its migration sequence are gone; util registration now goes through combineShapeUtilsWithDefaults so a custom util can replace a stock one (registering "geo" twice throws). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
hey matt's agent, for the next round can you make sure to run through our devOps list: DG Organization handbook |
Resolves ENG-2150.
Ports the click-verified nested-pages prototype from tldraw-local (reference: the private
dg-team/dg-prototypesrepo,nested-pages/—SPEC.md+ROAM-PORT-PLAN.md) into the Roam plugin canvas (tldraw 2.4.6).What it adds
Nested sub-page portals: a portal looks like a framed sub-canvas — a colored title bar (showing the live target-page name) over a live spatial preview of another tldraw page: one scaled box per shape in its true position, discourse-type colors from the graph's own node settings, images in place. Clicking the title bar enters the target page; a breadcrumb/back bar (real UI chrome in the
HelperButtonsslot, only on nested pages) navigates back up. Nesting is fractal. A "Create sub-canvas portal" context-menu action creates a child page + portal in one step.Data model — built for backward compatibility
A portal is a native
georectangle carryingmeta.dgSubpage = { targetPageId, accent, title }, with a visible "⤵ page name" label; the page hierarchy lives inpage.meta.dgNested.parentPageId. There is no custom shape type — deliberately:loadSnapshotthrow — anduseRoamStorethen blanks the entire canvas. With meta-on-native-geo, an old client shows a labeled, movable, bindable rectangle and keeps sequential page navigation via the native page menu (they just can't click into portals). Verified empirically against 2.4.6 and pinned bynestedPagesCompat.test.ts, which simulates an old client loading a new board.Rendering:
DgSubpageGeoUtil extends GeoShapeUtildraws the portal chrome for meta-carrying geos and defers to stock geo behavior otherwise. Registration goes through a newcombineShapeUtilsWithDefaults(a custom util may now replace a stock util; registering "geo" twice throws).Interaction rules (SPEC §4)
enterPage(zoom-to-fit, inset 80, 200 ms).useValue), never a cached snapshot — zero bytes added to the document. The header shows the live page name, so renames reflect immediately.toSvgshares the classifier, projection, paint table, and label thresholds with the live render so exports can't drift. Node labels drop their format prefix (the type-code chip carries it), cap at 90 chars, and image boxes keep the full image under a max-2-line label strip.editor.options.maxPages(40) loudly before creating anything — tldraw'screatePagesilently no-ops at the cap, which would strand an orphan portal.Tests / verification
src/utils/nestedPages.ts): grammar-prefix derivation from node formats (never hardcoded codes; handles bracketed[[EVD]] -titles), cycle-guarded lineage, classifier skip rules + count==boxes-drawn, layout, label thresholds, maxPages guard — plus 2 backward-compat tests (nestedPagesCompat.test.ts) simulating an old client. Full suite: 130/130.check-types,eslint, and the extension build all pass.plugin-testing-akamatsulab2still pending — draft until then.dg-subpageshapes) are not readable by this build — delete those test portals / reset the canvas State before loading. No released version ever wrote that type.Follow-ups (deliberately not in this PR)
ConvertToDialogprecedent)canvas_*tool support for creating/linking portals (linkSubpagePortalis already exported for it)props.textupdates on create/link only)Scope check
$scope-checkagainst ENG-2150 and the final diff.Done When: (1)linkSubpagePortalis exported with no UI caller yet; (2) portal titles live-sync to target-page renames; (3) the backward-compatibility data model (native geo + meta instead of a custom shape type).dg-prototypes/nested-pages/ROAM-PORT-PLAN.md, the ticket's Solution section, anddg-prototypes/nested-pages/LOG-eng-2150-roam-port.md.Review guide (PR exceeds the 400-line guideline)
Size: ~1,900 insertions across 10 files, of which ~500 are unit tests and ~700 the single self-contained portal renderer.
Why it isn't split: the feature is one closed loop — the portal's header calls
enterPage, the breadcrumb walks the meta the creation action writes, and the pure utils exist only for these consumers. A foundational-first split (utils → renderer+registration → chrome+action) would make the first two PRs unreviewable in isolation. Happy to restack into dependent PRs if a reviewer prefers.Read in this order:
src/utils/__tests__/nestedPagesCompat.test.ts— the backward-compat contract, smallest file, explains the data model.src/utils/nestedPages.ts+ its test file — meta schema and all classifier/lineage/layout decisions, documented inline.src/components/canvas/DgSubpageUtil.tsx—DgSubpageGeoUtil: the portal/native branch, the two render paths (they intentionally share model, projection, paint table, and label thresholds).src/components/canvas/nestedPageNavigation.ts— create/enter/link semantics (maxPages guard, lineage meta, explicitparentId).useCanvasStoreAdapterArgs.ts(notecombineShapeUtilsWithDefaults+ the customShapeTypes filter),useRoamStore.ts,TldrawCanvasCloudflareSync.tsx,Tldraw.tsx,uiOverrides.tsx,DgSubpageBreadcrumb.tsx— registration + chrome diffs.Testing path:
pnpm test && pnpm check-types && pnpm buildinapps/roam, then dev-loadapps/roam/distinto a test graph and: right-click → "Create sub-canvas portal" → click the title bar (first click, with something else selected) → add nodes/images on the child page → back via breadcrumb → rename the child page (header follows) → copy as PNG (preview must render) → delete the child page (placeholder, no navigation). For the compat claim: open the same canvas page with the production extension — the portal shows as a "⤵ name" rectangle and the board still loads.🤖 Generated with Claude Code