定義
Notionやwikiのように「文書の中に別の文書が入っていて、それを押してどんどん掘り下げていく」UIを作りたいときに使うパターンである。Hierarchical document navigation(階層的文書探索)は、1つの平ら(flat)な文書ではなく、文書が互いを参照するグラフを扱う。pageブロック(文書の中から別の文書を指すブロック)とnavigator(探索操作を担うインターフェース: push=中に入る、peek=プレビュー窓で覗く、back=戻る)で子文書を行き来する。breadcrumb(「ホーム > プロジェクト > 詳細」のような現在位置のパス表示)はこれまで入ってきたスタックを反映し、peek dialog(覗き見用のモーダル窓)はreadOnly editor surface(編集不可モードのエディタ画面)を再利用する。
文脈で理解する
ブログ記事1本のように文書が一層なら、URLルーティングだけで十分である。しかし「プロジェクトを開くとその中に下位文書があり、それをまた開くとさらに深い文書が出てくる」構造では、毎回ページを差し替えると文脈が切れる。このパターンは文書をidで結ばれたグラフとして見て、同じモーダルの殻の中で内容idだけ差し替えたり(peek)、スタックに積んで深く入り(push)、再びpopして出る(back)方式で探索の連続性を保つ。「ブラウザの戻る」ではなく、アプリが自分で管理する明示的なスタックが要点である。
なぜ必要か
wiki・ポートフォリオ・KBのデモは「ホーム→プロジェクト→詳細」の深さがあって初めて説得力が出る。URL routeだけではoverlay peek・in-place pushの連続性を作りにくい。
動作原理
- Page blockが
documentIdを参照する — リンク(route)と埋め込み(in-place)を区別する。 - Navigatorが
{ push, peek, back }の契約を提供する — React contextではなく注入可能なインターフェースとして置けば、Storybook・テストでin-memory graphを使える。 - Peekはmodal shellを維持し内部idだけ差し替える — readOnly editor surface再利用でmarkup driftを防ぐ。
- ArchiveはapplyPatchesのチェックポイントでサブツリーのpage idを収集しrepositoryに渡す — 「ブロック削除」と「子文書整理」を1トランザクションにまとめる。
- Document resolverはidごとのfetch dedup + (newId, oldContent)の中間フレームの遮断(render-time resetまたは
key={id})を行う。
実務での適用
- navigatorをinjectable interfaceとして置き、Storybook・テストでin-memory graphを使う。
- slash command「Page」はcapability gateの後ろに置く。
- create成功後、block patchとdocument createを1つのユーザーアクションにまとめる。
トレードオフ
- in-modal pushはroute VTとは別 —
router.back()ではなくexplicit stack pop。 - resolver cacheは性能↑だがstale bug↑ — id変更時のkey reset必須。
使ってはいけない場合
- 単一ページのブログのようにdepth=1なら、page block・peekは過剰である。
よくある間違い
- peekごとに新しいDialogをmount → animation/focusが壊れる。
- refreshがcache-firstで、reopen時に以前のrevisionを表示する。
関連概念
- focused-guest-editor — peekの編集surface
- radix-portal-scoped-css — dialogスタイルの隔離