Skip to content

name: tier2-notion-engine-2026-06-18 description: Scoping plan , CreatorTrack Tier 2 (Notion engine): block editor depth, page chrome, database-view completion, on top of the finished component library + property type system type: project


CreatorTrack Tier 2 , The Notion Engine (Scoping Plan)

URL: https://mkdocs.justinsforge.com/memory/plans/tier2-notion-engine-2026-06-18/

Status: SCOPING. 2026-06-18. Repo: /home/justinwieb/forge-suite. Stands on: Tier 1 component library (21 components) + Section 0 primitives + the property type system (23 types, one registry). Those are DONE. This is the layer that turns the parts into the actual Notion-class product.

The finding: scaffolded but uneven (NOT greenfield)

What already works: - Database VIEWS , all 5 real (components/workspace/database/Views.tsx): Table, Board (group-by), Gallery, List, Calendar. Per-type filter operators + calcColumn exist. - Page shell + chrome partial: PageHeader (renders cover + icon), EditableTitle, PageTree, WorkspaceShell, Topbar, AccountMenu, NewPageButton. - Block editor , shallow: BlockRenderer + BlockList + block API render paragraph, heading, callout, code, divider, numbered, quote, to_do, toggle; editing is Enter-makes-block + backspace-deletes-empty only.

The three real gaps, in rough size order: block editor depth >> page chrome >> view completion.

Workstream A , Block editor (the big one)

Today: 9 block types, no rich editing. Add, all on Section 0 primitives: - Block roster to Notion parity: proper H1/H2/H3, bulleted list, image (URL until files backend lands), bookmark/link-preview, embed, table-of-contents, equation (KaTeX), simple table, columns; synced block + video/file/audio deferred (need files backend). - Slash menu ("/" to insert) , Section 0 floating + shortcut registry. - Block drag handle (six-dot reorder + nest) , Section 0 DnD. - Inline formatting toolbar (bold / italic / link / code / color on text selection). - Turn-into / convert between block types. - Nesting + indent (Tab / Shift-Tab) for lists & toggles; markdown shortcuts (#, -, [], > …). Files: components/workspace/BlockList.tsx, BlockRenderer.tsx, new components/workspace/blocks/*, new SlashMenu + InlineToolbar, app/api/block/*.

Workstream B , Page chrome (your shell wishlist lands here)

Today: cover/icon render, no pickers. Add: - Icon / emoji picker + cover picker (Unsplash + URL now; upload when files backend lands) , wire into PageHeader. - Breadcrumbs in the topbar (use the Tier 1 Breadcrumbs component). - Share / permissions menu , the core.node_shares table already exists; wire UI to it. - Last-edited indicator (from nodes.updated_at/updated_by, now populated) + page-info. - Page actions "…" menu: rename, duplicate, move to, delete, copy link, open-as-peek. - Your earlier shell-polish list (folded in here): profile section to sidebar bottom + its menus, sidebar/toolbar hover sizing, expand-toolbar hover alignment, toolbar scroll gradient, remove the JustinsForge.com link, and drag-drop pages in PageTree (Section 0 DnD). Files: components/workspace/PageHeader.tsx, PageTree.tsx, Topbar.tsx, AccountMenu.tsx, WorkspaceShell.tsx, new picker/share components.

Workstream C , Database view completion

Today: 5 views + per-type filter operators + calcColumn. Add: - AND/OR filter group builder (composes the per-type operators from the property system). - Aggregate footer row wired in TableView (the calcColumn engine exists; surface it). - Sub-groups / group-by beyond board; sort builder UI. - Linked databases (one collection shown in multiple places) + database/page templates. Files: components/workspace/database/* only.

Workstream D , Property editor popover polish (small)

Ensure all 23 types have a clean config + value editor popover (config editors exist from the prop-types work); fill gaps, no new architecture.

Execution , parallel-safe boundaries

A, B, C, D are mostly file-isolated (A: blocks, B: page chrome, C: database/, D: property popovers), so a small capped fan-out works , the lesson from Tier 1 holds: one owner per file area, no shared-file edits, merge serially. The one shared-file collision risk: PageView.tsx / WorkspaceShell.tsx are rendered by both A (block editor) and B (page chrome). Mitigation: B owns the page shell files; A exposes its editor as a self-contained component B mounts, and the final mount wiring is the integrator step (same pattern as the studio registry).

Recommended sequence (respecting the no-RAM cap, ~3 workers max): 1. Block editor (A) as one focused Opus session, HIGH thinking , it's the largest and most architectural (block model, slash, DnD, inline toolbar). Do this first/most-carefully. 2. In parallel with A: page chrome (B) + view completion (C) as two more workers (file-isolated from A and each other). D folds into C or a quick pass. 3. Integrator merges each serially + wires the editor mount.

DECISIONS LOCKED (2026-06-18, Justin: "3 worker, full parity, fold it in")

  1. 3-worker capped fan-out (A block-editor + B page-chrome + C views; D folds into C).
  2. Full Notion block parity now (incl. simple table, columns, equation/KaTeX, toc, bookmark, embed). Only synced-block + video/file/audio defer (need the files backend).
  3. Shell-polish wishlist folded into Workstream B.

File ownership (HARD , prevents collision; one owner per area)

Worker OWNS (only edits these) MUST NOT touch
A , block-editor components/workspace/BlockList.tsx, BlockRenderer.tsx, NEW components/workspace/blocks/*, NEW SlashMenu + InlineToolbar (under components/workspace/), app/api/block/* PageView, WorkspaceShell, PageHeader, PageTree, database/*
B , page-chrome+shell components/workspace/PageHeader.tsx, PageTree.tsx, Topbar.tsx, AccountMenu.tsx, WorkspaceShell.tsx, PageView.tsx, NEW picker/share/page-actions components, theme.css shell-polish (hover/scroll-gradient) BlockList/BlockRenderer/blocks/, database/
C , views+prop-popovers components/workspace/database/* ONLY everything else

The PageView seam: A keeps BlockList's export signature backward-compatible so B's PageView import keeps working. If A must change the editor's mount API, it STOPS and notes it for the integrator (who updates the single import in PageView at merge). No worker edits another's files; the studio-registry pattern (self-contained export + integrator wires) applies.

Verification reality (Tier 2 is interaction-heavy)

Worktree Turbopack panics + no RAM for 3 dev servers, so workers verify with tsc --noEmit AND write explicit manual-test steps for each feature. RICH interaction verification (slash menu, drag, inline toolbar, pickers) happens at MERGE on the main systemd :3060 server (not a worktree). Integrator (home-base) does that live pass per branch before the next merge.

Out of scope

  • files upload/storage backend (deferred by Justin; image/cover use URL/Unsplash until then).
  • button block actions (needs the automation surface).
  • Apps (finance re-skin, clips, fitness) , roadmap step 4, after Tier 2.

Worker B , page-chrome + shell polish (DONE 2026-06-18)

Branch t2-chrome in worktree forge-suite-wt/t2-chrome. 4 commits, tsc --noEmit CLEAN. NOT run live (Turbopack panics in worktree) , each commit body has explicit manual-test steps for the integrator's :3060 pass.

Shipped (all Workstream B + the shell-polish wishlist): - Icon/emoji picker (searchable emoji grid + image-URL tab) + cover picker (Unsplash gallery + URL), wired into PageHeader with Notion-style hover "Add icon/Add cover" controls + a "Change cover" overlay. Persist via PATCH /api/node/[id] (icon col + props.cover). - Last-edited indicator (LastEdited, live relative time off node.updatedAt). - Page "…" actions menu (PageActionsMenu): rename / copy link / open in new tab / duplicate / move to… / delete. Share menu (ShareMenu) wired to core.node_shares. - Shell polish: removed JustinsForge pill; topbar scroll-gradient; icon-btn hover; PageTree drag-reorder of top-level pages (Section 0 SortableList). - Server: new lib/node-actions.ts (setNodeProps/duplicate/move/trash/reorder + share CRUD), new routes app/api/node/[id]/action + /share, PATCH extended for cover.

Built on (did NOT re-roll): kit Popover/Menu/Avatar/Icon/useToast, primitives SortableList/SortableHandleProps. var(--token) only.

Seam notes for the INTEGRATOR (read before merge): 1. PageHeader signature changed from {node} to explicit {nodeId,title,initialIcon,initialCover,updatedAt}. I updated both call sites I could reach: PageView.tsx (mine) and DatabasePage.tsx (unowned page-chrome, root of components/workspace/, NOT in C's database/ subdir). If C or A also render PageHeader, fix those call sites at merge. 2. lib/node-actions.ts is a NEW file (not an edit to lib/mutations.ts) to avoid collisions with A/C. PATCH route app/api/node/[id]/route.ts got a small additive edit (cover) — low conflict risk. 3. Verify live: core.node_shares writes go through asUser() (RLS). If an RLS policy blocks member INSERT/SELECT on the share table, the Share menu will read empty / fail silently — confirm policy on the :3060 pass (tsc can't catch this). 4. Breadcrumbs: left the existing custom topbar crumb (icon+trail, already working) rather than swapping to the Tier 1 Breadcrumbs component — intentional, no churn. 5. Pickers stay open after a pick (kit Popover exposes no close-to-children); outside-click dismisses. Fine for now; wire a close if desired later. 6. No logout/auth route exists, so AccountMenu items remain display-only (auth backend out of B's scope).

Worker C , views + property-popover polish (DONE 2026-06-18)

Branch t2-views in worktree forge-suite-wt/t2-views. 4 commits, tsc --noEmit CLEAN after each. NOT run live (Turbopack panics in worktree) , each commit body has explicit manual-test steps for the integrator's :3060 pass. All 9 changed files are under components/workspace/database/ ONLY , zero edits to lib/collection, app/api, theme.css, workspace.css, PageView, or WorkspaceShell. DatabaseClient's export signature is unchanged, so NO integrator mount-API change is needed for this branch.

Key architectural decision: rather than re-roll the base engine or edit lib/collection.ts (not in C's ownership), I added an ADDITIVE extended engine NEW components/workspace/database/viewModel.ts. It stores its extra keys in the SAME collection_views.config JSON (verified lib/mutations.updateView persists config wholesale, so unknown keys round-trip cleanly) and reuses the registry's matchesFilter / PROP_TYPES[type].compare/isEmpty (no hand-rolled matching). ExtViewConfig = base ViewConfig + { filterGroup, sorts[], subgroup, collapsed[], calc{}, templates[], source }. effectiveFilterGroup/effectiveSorts migrate a legacy single filter/sort on READ (old views keep working; the first edit upgrades them). The only persist seam is patchExt(patchView, partial) (one cast through the Partial<ViewConfig>-typed action) + cfgOf(config).

Shipped (Workstream C + D): 1. AND/OR filter GROUP builder + ordered MULTI-sort builder (FilterSort.tsx); the column header's Sort/Filter quick-actions (PropertyMenu.tsx) rewired to the engine. 2. Table via applyViewEx + COLLAPSIBLE ROW GROUPING (collapse persists) + PERSISTED aggregate footer (calc selection saved in config); all Views.tsx layouts via applyViewEx + BOARD SUB-GROUPS (secondary group inside each column). 3. LINKED DATABASES (NEW LinkedView.tsx, read-through of another collection) + ROW TEMPLATES (NEW Templates.tsx: split "New ▾" + TemplateManager defaults) + NEW ViewOptions.tsx toolbar gear (table grouping picker + linked source picker + templates). 4. Workstream D: audited , all 23 types already have a config editor (where configurable) + a value editor (Cell.tsx dispatch); fixed the only gap (OptionsEditor now uses the shared OptChip so the config preview matches rendered chips).

Seam notes for the INTEGRATOR / API owner (NOT blockers): 1. Linked DB is read-only (titles + open links). Existing /api/collection/[id]/rows returns only {id,title}. Full linked cells/filters need a collection-SNAPSHOT endpoint (props+rows+VALUES for an arbitrary db id); the picker + render seam are already in place , swap LinkedView's fetch for a snapshot fetch when that endpoint lands. 2. Templates = DATA defaults only (title seed + cell values). Page-BODY templates (default blocks) need Worker A's block editor + a block-clone path , out of C's lane. 3. Extended config persists via a cast through patchView. If the engine owner wants these first-class later, lift the ExtViewConfig fields into lib/collection's ViewConfig and drop the cast in patchExt , no behavior change, just types. 4. Live-pass priority on :3060: filter groups (And/Or), multi-sort reorder, table row-grouping collapse-persist, footer calc persist, board sub-groups, new-from-template, linked source switch. (tsc can't catch the persistence round-trip , confirm config keys survive reload.)

Worker A , block editor (DONE 2026-06-18)

Branch t2-block in worktree /home/justinwieb/forge-suite-wt/t2-block. 3 commits (a1f5070 foundation, c261ac2 API+mutations, 1ed4c87 editor). npx tsc --noEmit clean. Did NOT run next dev (Turbopack panics in worktree, per plan).

Delivered (full parity for Workstream A): rich inline text (bold/italic/underline/ strike/code/link/color via InlineToolbar); slash menu insert + turn-into; drag-handle reorder + per-row add(+) and block menu (duplicate/delete/turn-into); nesting via props.indent (Tab/Shift-Tab) with toggle-collapse hiding deeper children; markdown shortcuts (#, ##, ###, -, 1., [], [x], >, ```, ---); Enter split+continuation, Backspace convert-to-text / outdent / merge-into-previous; numbered-list numbering + TOC from headings. Full roster: paragraph, h1-3, bulleted/numbered/to_do/toggle, quote, callout, code, divider, image(URL), bookmark, embed, table_of_contents, equation(KaTeX), table, columns. (synced/video/file/audio/button deferred per plan.)

Files (all within OWNS): BlockList.tsx + BlockRenderer.tsx (rewritten), NEW SlashMenu.tsx + InlineToolbar.tsx, NEW blocks/ (types, richtext sanitizer, markdown, SpecialBlocks, blocks.module.css, katex.d.ts), app/api/block/ (POST+PATCH props/position).

Decisions / notes for INTEGRATOR: 1. NO schema migration , reused existing blocks.props jsonb + position. Nesting is a depth integer in props.indent (not a parent_id tree). True block-tree containment, if ever needed, is a later migration. 2. SEAM (one-line PageView change needed): PageView.tsx (Worker B) maps blocks to { id, type, content } , add props: b.props so rich state hydrates on first paint. lib/data.ts nodeBlocks already returns the full row incl. props. BlockList's signature is backward-compatible (props optional) so PageView still compiles unchanged; without the one-liner, blocks render without their stored props (indent/checked/url/latex/…). 3. Shared-infra touch (NOT another worker's file): extended lib/mutations.ts createBlock/updateBlock with optional props/position (block-specific funcs only, additive, no overlap with B/C). Flagging for merge awareness. 4. Added dep: [email protected] (+ local katex.d.ts shim, no @types) for the equation block , locked decision "equation (KaTeX)". package.json/lock touched. 5. Styling: all new styles in scoped blocks/blocks.module.css (var(--token) only). Did NOT edit workspace.css/theme.css (Worker B owns shell-polish there). Basic blocks no longer depend on the legacy global ws-bl/ws-nl/ws-tdl markers (now rendered in-module). 6. Rich text storage: content now holds sanitized inline HTML (was plain text). sanitizeHtml runs on save (client), in the API (server), and at every render boundary. 7. Inline toolbar uses document.execCommand (pragmatic for contenteditable) + manual wrap for inline-code; works inside table cells & columns too (data-rt="1").

Needs the integrator live pass on :3060 (interaction-heavy: slash/drag/toolbar/markdown/ pickers) , manual steps are in each commit body.