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")¶
- 3-worker capped fan-out (A block-editor + B page-chrome + C views; D folds into C).
- 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).
- 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¶
filesupload/storage backend (deferred by Justin; image/cover use URL/Unsplash until then).buttonblock 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.