• #Miller Columns
  • #Vue
  • #UI設計
  • #ディープリンク
  • #目次
  • #eurekapu
開発eurekapu-nuxt4メモ

MillerViewerの書籍風目次とディープリンク対応

前日のUI改善で65スライドをフラットに横断できるようになった。だがBSPLページでチャプターをクリックしてもカーソルが空振りし、目次から直接セクションに飛ぶ手段もなかった。今日はそのバグ修正から始めて、気づけば目次ページを書籍の巻頭目次のような構造に作り替えていた。

URLベースからnameベースのマッチングへ

BSPLページ(貸借対照表・損益計算書のページ)で、チャプターをクリックしても何も起きないバグが出ていた。

原因: チャプター選択のマッチングにURLを使っていたが、BSPLページでは全チャプターが同じURLを共有している。URLで比較すると、どのチャプターをクリックしても「すでに選択済み」と判定されてしまう。

修正: マッチングキーをURLから name プロパティに切り替えた。nameはチャプターごとに一意なので、同一URLのページでもクリックが正しく反応するようになった。

// Before: 同一URLだと全て「選択済み」になる
const isActive = chapter.url === currentUrl
// After: nameは一意なので正しくマッチする
const isActive = chapter.name === currentName

この修正で、TOCとmillerChaptersの間で名前が一致しないバグも見つかった。原因は末尾の「。」の有無だった。TOC側に句点がついていて、millerChapters側にはなかった。地味だが、nameベースマッチングに切り替えたからこそ表面化したバグだった。

singleSectionModeの追加

BSPLページはセクションが1つしかない。3カラム構成のColumn 3にセクション一覧を表示しても、項目が1つだけ並ぶ間抜けな見た目になる。

singleSectionMode フラグを追加し、セクションが1つしかないページでは自動的にColumn 3を非表示にした。Column 1とColumn 2だけの2カラム表示に切り替わり、ステージ領域がその分広くなる。

判定はデータ駆動にした。チャプターデータのセクション配列長が1以下なら自動でフラグが立つので、将来セクションが増えれば自然に3カラムに戻る。

チャプター説明文の書き換え

各チャプターの説明文が「〜がハイライトされています」という表示状態の説明になっていた。これはビューアの操作ガイドとしては正しいが、学習者が「このチャプターで何を学ぶのか」を把握する手がかりにならない。

説明文を1つずつ書き直して、B/SやP/Lの構造的な意味を伝える内容にした。「負債の部がハイライトされています」ではなく、「他人から調達した資金がどう記録されるか」のような、財務諸表の構造に紐づく一文に変えた。

目次ページの書籍風リデザイン

従来の目次はカード表示だった。チャプターがカードとして並ぶUIは一覧性があるが、書籍の目次が持つ「構造の見通し」が欠けていた。

カード表示を廃止し、MillerViewerのミラーカラムレイアウトに目次を統合した。書籍の巻頭目次のように、チャプターとセクションが階層構造で並ぶ。左カラムでチャプターを選ぶと、右カラムにセクション一覧とその要約が表示される。

カードUIと違って、選択したチャプターの中身がその場で展開される。ページ遷移なしに「この章には何があるか」を確認できるので、全体の構造を俯瞰してから飛び先を決められる。

クエリパラメータによるディープリンク

目次からセクション単位で直接ジャンプできるよう、クエリパラメータ ?ci=&si= を追加した。

  • ci: チャプターインデックス
  • si: セクションインデックス

目次ページでセクションをクリックすると、このパラメータ付きのURLでMillerViewerに遷移する。ページ読み込み時にパラメータを読み取って、該当するチャプターとセクションを自動選択する。ブックマークやリンク共有にもそのまま使える。

SSG(静的サイト生成)環境なのでサーバー側のルーティングは不要で、クライアントサイドの useRoute().query だけで完結する。パラメータが不正値の場合はデフォルト(先頭チャプター)にフォールバックする。

非選択項目のグレーアウト

Column 1/2/3で、選択されていない項目をグレーアウトした。選択中の項目だけが通常の色で表示され、それ以外は薄くなる。

視線が選択中の項目に自然に集まるようになった。特にチャプター数が10を超える場面で、「いま自分がどこにいるか」が一瞬でわかる。

実装はCSSの opacity 切り替えだけ。選択中は opacity: 1、非選択は opacity: 0.4 にしている。JavaScriptでの制御は不要で、computed経由のクラスバインドで済んだ。

セクション単位の要約データ追加

目次にセクション単位の要約を追加した。もともとチャプター単位の要約はあったが、チャプターの中にセクションが5つも6つもあると、「このセクションで何を扱っていたか」が思い出せない。

セクションごとに1〜2行の要約を書き、目次ページでセクション名の下に表示するようにした。学習者が「あの話はどこだっけ」と探すとき、セクション名と要約を眺めれば目的地が見つかる。

要約データはチャプター定義のJSONに直接埋め込んだ。CMSやデータベースを介さず、コードと同じリポジトリで管理できるので、内容を変えたいときはテキストエディタで書き換えてデプロイするだけで済む。

コンポーネントのリファクタリング

スタイルの上書きに :deep() セレクタを多用していた箇所を、propsでの制御に切り替えた。親コンポーネントから :deep() でスタイルを注入するのではなく、子コンポーネントがpropsに応じてスタイルを切り替える。

合わせて、複数箇所で繰り返していたロジックをcomputed化した。同じ条件分岐が3箇所にコピーされていたのを、1つのcomputedに集約して参照する形に書き直した。

:deep() は親が子の内部構造を知っている前提になるので、コンポーネントの独立性が壊れる。propsで渡すようにしたことで、子コンポーネント側で「何が変わるか」が型で明示されるようになった。

振り返り

「URLが同じだからクリックが効かない」という報告を受けて調べ始めたら、nameベースへの切り替え、TOCとの名前不一致修正、singleSectionMode、説明文書き換え、目次の構造化、ディープリンクと、芋づる式に作業が広がった。

カードUIを捨ててミラーカラムに目次を統合した瞬間、画面を見て「これは書籍の目次だ」と声に出た。UIの形が変わると、同じデータでもコンテンツの見え方まで変わる。

nameベースマッチングへの切り替えで末尾の句点バグが浮上した件は、マッチングキーを変えるとそれまで隠れていた不整合が表に出てくるという教訓だった。URLという「たまたま一致していたキー」に依存していたから見えなかっただけで、不整合自体は最初から存在していた。

今回の変更は前日の3/13記事(Miller Columns UIコンポーネントの進化)の直接的な続きにあたる。2日がかりで、MillerViewerがナビゲーションツールから「書籍のような学習体験」に一歩近づいた。