What this covers
A day spent extending a document-editor template all the way to Notion-style depth (a structure where sub-documents continue inside a document). In the morning I cleaned up the imperative shell, then aligned it as one thread: page block · push/peek/breadcrumb, collection/embed blocks, seeded gradient surface, and finally read-only viewer bundle splitting. I organize this task by task — for each, the situation, the goal, how it was solved, and what I learned.
Day summary
| Task | What I wanted | What I did | Result |
|---|---|---|---|
| Navigator | page block + push/peek/back + breadcrumb + slash "Page" | page block pointing at a documentId, navigator manages push/peek/back stack, peek reuses readOnly surface | Spatial-depth demo possible, no peek markup drift |
| Archive | Collect the page-id subtree at the delete-patch checkpoint | Gather page documentIds at the checkpoint before/after applyPatches, pass to repository | "block delete" and "child-document cleanup" bundled into one patch |
| Surface | A finished card even without a cover — title hash → pastel gradient | seeded gradient surface + reduced-motion state | shoddy grid → a card pinned by screenshot regression |
| Viewer | @pkg/viewer + viewer.css, PM/dnd 0 imports in CI | Split the import graph via subpath + chrome-layer-excluded CSS, share BlockRenderer via readOnly prop | Editor code dropped from peek/embed paths so bundle got lighter, 0 pinned by CI |
| Embed bridge | SDUI inside document, document inside SDUI — one-way variable bridge | Connect embeds via a one-way variable bridge | Embeds on both sides without a cycle |
| Shell cleanup | Prevent hook bloat — guard against IME/clipboard regressions | Extract pure functions then add characterization tests | Kept the whole related-package suite green |
1. Hierarchical document navigation
Background (concepts)
- Page block: a "document within a document" link where a body block points at another
documentId. - Peek: a UX that opens a child document in an overlay window (dialog) without a full page navigation (route).
What the situation was
With only a flat single editor, it's hard to demo the spatial depth of "home → project → detail."
Core work
A page block points at a documentId, and the navigator manages the push/peek/back stack. The peek dialog reuses the readOnly editor surface to prevent viewer markup drift. Archive collects page documentIds at the checkpoint before/after applyPatches and hands them to the repository. Async fetch can leave a (newId, previous document content) intermediate frame, so on id change it blocks stale content with a render-time reset or key={documentId}. As a result the spatial-depth demo became possible and peek markup drift went away.
Lesson
- Keep the navigator as an injectable interface (
push/peek/back) and Storybook/tests get easy. - Archive bundles "block delete" and "child-document cleanup" into one patch checkpoint.
→ Hierarchical document navigation
2. Viewer bundle splitting
Background (concepts)
- bundle / import graph: the bundle is all the modules pulled in by
importfrom one entry point. The web of what imports what is the import graph, and when it grows the bundle gets heavy. - tree-shaking: an optimization that strips exports you don't actually use from the bundle. Put the import boundary in the wrong place and unused code gets pulled in too.
- Viewer entry: a package entry path (subpath) that exposes only read-only rendering, without the editing UI (ProseMirror, dnd-kit, etc.).
What the situation was
If gallery cards and peek previews import the editor barrel, ProseMirror and dnd-kit follow all the way to the embed path — a read-only screen, yet the whole editor code gets pulled in and the bundle gets heavy.
Core work
I split the import graph via a @pkg/viewer subpath + chrome-layer-excluded CSS, and verified 0 PM/dnd in the viewer module graph via lint/CI. Sharing BlockRenderer via a readOnly prop keeps the DOM the same as the editor and changes only the bundle. A missing CSS entry in the npm publish exports/files leads to a consumer unstyled viewer, so manifest verification is essential. As a result editor code dropped from the peek/embed paths so the bundle got lighter, and 0 was pinned by CI.
Lesson
- A portfolio demo = blocks + surface + bundle boundary — get only one of the three right and it isn't persuasive.
- peek is the same editor DOM, a different import graph — tree-shaking is an export-boundary problem.
→ Read-only viewer bundle splitting
3. Surface, archive, embed, and shell cleanup
Background (concepts)
- seeded gradient surface: generating a pastel gradient cover with the title hash as seed. Makes a card look finished even without a cover image.
- characterization test: a test that pins existing behavior as-is to catch regressions after a refactor.
What the situation was
Block types grew, but an empty card cover makes it look like a shoddy grid. Also, with handler/patch/autoscroll logic mixed into the hook, IME/clipboard regressions are frequent without characterization tests. Embedding SDUI inside a document and a document inside SDUI can create a cycle.
Core work
- Surface: with a seeded gradient surface + reduced-motion state, I turned a shoddy grid into a card pinned by screenshot regression.
- Embed bridge: connected embeds via a one-way variable bridge so embeds hold on both sides without a cycle.
- Shell cleanup: extracted pure functions then kept the whole related-package suite green with characterization tests.
Lesson
- Pretty blocks without a surface system (gradient cover) aren't persuasive enough as a demo.
- Imperative shell cleanup goes in the order: extract pure functions + characterization — prevent hook bloat first.