この記事で扱うこと
ドキュメントエディタのテンプレートを Notion 型の深さ(ドキュメントの中に下位ドキュメントが続く構造)まで拡張した一日だ。朝は命令的 shell の整理、続いて page ブロック・push/peek/breadcrumb、collection·embed ブロック、seeded gradient 表面、最後に read-only viewer バンドル分離まで一筋に合わせた。各作業がどんな状況で何を目標にどう解けて何を学んだかを、作業ごとに整理する。
一日のまとめ
| 作業 | 何をしたかったか | やったこと | 結果 |
|---|---|---|---|
| Navigator | page ブロック + push/peek/back + breadcrumb + slash "Page" | documentId を指す page ブロック、navigator が push/peek/back スタックを管理、peek は readOnly surface 再利用 | 空間的な深さのデモが可能、peek マークアップの drift なし |
| Archive | 削除 patch チェックポイントで page id サブツリーを収集 | applyPatches 前後のチェックポイントで page documentId を集めて repository に渡す | 「ブロック削除」と「子ドキュメント整理」を一つの patch にまとめる |
| Surface | cover なしでも完成したカード — タイトルハッシュ → パステル gradient | seeded gradient 表面 + モーション縮小状態 | 雑なグリッド → スクリーンショット回帰で固定されたカード |
| Viewer | @pkg/viewer + viewer.css、PM/dnd import 0 件 CI | subpath + chrome layer 除外 CSS で import graph を分離、readOnly prop で BlockRenderer を共有 | peek/embed 経路からエディタコードが抜けてバンドルが軽くなる、CI で 0 件固定 |
| Embed bridge | document の中に SDUI、SDUI の中に document — 一方向 variable bridge | 一方向 variable bridge で embed 接続 | 循環なく両側の embed が成立 |
| Shell 整理 | hook の肥大化を防ぐ — IME·clipboard 回帰の防止 | pure 関数抽出後に characterization テスト | 関連パッケージ全 suite の green を維持 |
1. 階層型ドキュメントナビゲーション
前提知識(概念)
- Page block: 本文ブロックが別の
documentIdを指す「ドキュメントの中のドキュメント」リンク。 - Peek: 全ページ遷移(route)なしにオーバーレイ窓(dialog)で子ドキュメントを開く UX。
どんな状況だったか
flat な単一 editor だけだと「ホーム → プロジェクト → 詳細」の 空間的な深さ をデモしにくい。
中心の作業
page ブロックが documentId を指し、navigator が push/peek/back スタックを管理する。peek dialog は readOnly editor surface を再利用 して viewer markup drift を防ぐ。archive は applyPatches 前後の チェックポイント で page documentId を集めて repository に渡す。async fetch は (newId, 以前のドキュメント content) 中間フレーム を残しうるので、id 変更時に render-time reset または key={documentId} で stale content を遮断する。結果として空間的な深さのデモが可能になり peek マークアップの drift がなくなった。
教訓
- navigator は injectable interface(
push/peek/back)にしておくと Storybook·テストが楽だ。 - archive は「ブロック削除」と「子ドキュメント整理」を 一つの patch チェックポイント にまとめる。
→ Hierarchical document navigation
2. Viewer バンドル分離
前提知識(概念)
- バンドル(bundle)/ import graph: 一つの入口から
importで連れてこられるモジュール全体がバンドルだ。何が何を import するかの関係網が import graph で、これが大きくなるとバンドルが重くなる。 - tree-shaking: 実際に使わない export をバンドルから取り除く最適化。import 境界を誤ると使わないコードも連れてくる。
- Viewer entry: 編集 UI(ProseMirror、dnd-kit など)なしに読み取り専用レンダーだけを露出するパッケージ入口経路(subpath)。
どんな状況だったか
gallery カード・peek プレビューが editor barrel を import すると ProseMirror·dnd-kit が embed 経路まで 付いてくる — 読むだけの画面なのにエディタコードが丸ごと連れてこられてバンドルが重くなる。
中心の作業
@pkg/viewer subpath + chrome layer 除外 CSS で import graph を分離 し、viewer module graph に PM/dnd 0 件を lint/CI で検証した。readOnly prop で BlockRenderer を共有すれば DOM は editor と同じで バンドルだけ 変わる。npm publish exports/files に CSS entry が欠けると消費者 unstyled viewer につながるので manifest 検証が必須だ。結果として peek/embed 経路からエディタコードが抜けてバンドルが軽くなり、CI で 0 件が固定された。
教訓
- ポートフォリオのデモ = ブロック + 表面 + バンドル境界 — 三つのうち一つだけ合わせても説得力が出ない。
- peek は同じ editor DOM、違う import graph — tree-shaking は export 境界の問題だ。
→ Read-only viewer bundle splitting
3. 表面・アーカイブ・embed・shell の整理
前提知識(概念)
- seeded gradient 表面: タイトルハッシュを seed にパステル gradient cover を生成する方式。cover 画像なしでもカードが完成して見えるようにする。
- characterization テスト: 既存の動作をそのまま固定してリファクタ後の回帰を捕まえるテスト。
どんな状況だったか
ブロックタイプは増えたのにカード cover が空だと 雑なグリッド のように見える。また hook に handler·patch·autoscroll ロジックが混ざっていて characterization テストなしには IME·clipboard 回帰が頻繁だ。document の中に SDUI、SDUI の中に document を互いに embed すると循環が生じうる。
中心の作業
- Surface: seeded gradient 表面 + モーション縮小状態で、雑なグリッドをスクリーンショット回帰で固定されたカードに変えた。
- Embed bridge: 一方向 variable bridge で embed を接続し、循環なく両側の embed が成立するようにした。
- Shell 整理: pure 関数抽出後に characterization テストで関連パッケージ全 suite の green を維持した。
教訓
- 表面システム(gradient cover)なしにブロックだけきれいにしてもデモの説得力が足りない。
- imperative shell の整理は pure 関数抽出 + characterization の順 — hook の肥大化をまず防ぐ。