投資判断クイズを2カラムレイアウトへ移行:1ページ完成→コンポーネント化→横展開
午前にPart 2投資判断クイズの74問とSVG18枚を流し込み終え、午後はUX改善に切り替えた。/financial-statements-quiz/investment/01-operating-cf を開いて指を止めた瞬間、画面の上下にスクロールしないと届かない余白が広がっていた。「ここってなんでこんなにスペース空いてるんですかね」と打ち込んだのが、今日の作業の起点になった。
スペースの正体を掘る
Claude Code に CSS を追わせると、原因はすぐ出てきた。.practice-list に padding-top: 50vh と padding-bottom: 50vh が積まれている。矢印キー(↑↓/jk)でクイズを切り替えるとき、選択中の問題を画面中央に持ってくるためのバッファだった。
.practice-list {
padding-top: 50vh;
padding-bottom: 50vh;
}
理屈は通っている。ただ、キーボード操作前提のUXがマウス操作のユーザーには「無駄に空いた画面」として刺さる。原因が見えても、これを潰すだけでは要件が満たせないとすぐ気付いた。
本当の要件は basics ページのUX移植だった
頭の中にあった理想形は、すでに basics ページで動いていた2カラムレイアウトだった。左に解説図を固定し、右にクイズリストを独立スクロールさせる構成。投資クイズもこれに揃えたい、というのが本音だった。スペースを詰めるのではなく、画面そのものの組み方を入れ替える話だった。
QuizPage コンポーネントは basics 側ですでに動いている。これを使って 01-operating-cf.vue を書き換えるところから着手した。
1ページだけ先に仕上げる
横展開する前に、1ページを完成形にする方針で進めた。
- 左ペイン:
Part2OcfVsNetIncomeの SVG を固定 - 右ペイン: Q1〜Q4 を独立スクロールで並べる
- 選択中の問題に応じて左ペインの図がマゼンタで強調される
ここで詰まったのが highlight: null のケースだった。Q1/Q3/Q4 はハイライト対象がなく、左ペインに「問題文がない問題」を出すと違和感が残る。プレースホルダーとして薄い文字だけを出す方針に変え、CSS の :has() セレクタで対応した。
.quiz-page:has(.quiz-card[data-highlight="null"].is-active) .left-pane {
flex-basis: 30%;
}
選択中のカードが data-highlight="null" のときだけ左ペインを縮めて、右の問題文を広く読ませる。Q2 を選ぶと左ペインが元のサイズに戻り、Part2OcfVsNetIncome の C 社(粉飾サイン)にマゼンタが当たる。図と問題のリンクが視覚的につながった瞬間、自分でも「これだ」と声が出た。
「最後の問題を解いたら次のトピックへ」
1ページが固まったところで、追加の要望を出した。
最後の問題を解いたら自動で次のトピックへ遷移してほしい。あと 8-1 から 8-18 まで全部に適用したいので、コンポーネント化して。
QuizPage に自動遷移を仕込んだ。最後の問題を解いた瞬間に5秒のタイマーを張り、次トピックへ navigateTo する。
watch(isAllSolved, (solved) => {
if (!solved || !props.nextPath) return
setTimeout(() => navigateTo(props.nextPath), 5000)
})
5秒は迷った。3秒だと「もう一度見たい」が間に合わない。10秒だと止まったように感じる。Q2を解いた直後の自分の視線移動を見て、図を一度見返してから次に行く動作が約4秒だったので、5秒に落とした。
InvestmentTopicPage で18ページに敷く
横展開のために、共通ラッパー InvestmentTopicPage.vue を切り出した。トピック番号・タイトル・SVGコンポーネント・問題配列・次トピックへのパスを受け取るだけのシェルに仕上げた。
<template>
<QuizPage
:topic-id="topicId"
:title="title"
:svg-component="svgComponent"
:questions="questions"
:next-path="nextPath"
/>
</template>
各ページ(01-operating-cf.vue 〜 18-...vue)は InvestmentTopicPage に props を渡すだけになった。1ページあたり10行を切るところまで削れた。
使用制限が近づいて打ち切り
横展開を始めたところで使用制限が見えてきた。残り17ページを今日中に終わらせるのは諦めて、memo/2026-05-14/investment-quiz-2col-migration-progress.md に進捗を書き残した。どのページを差し替え済みで、どのページが未着手か、InvestmentTopicPage を使うときに気を付ける点(SVGコンポーネントの import パス、nextPath の終端処理)まで残しておけば、明日の自分が迷わない。
今日の学び
- AI 実装でも UX の違和感は人間が拾う係。「スペースが空いてる」の一言が出なければ、
padding: 50vhは仕様として生き残っていた - 原因と要件は別物。スペースを詰める修正ではなく、レイアウト構成そのものを入れ替える要件に切り替えた瞬間、作業の方向が定まった
- コンポーネント化のタイミングは1ページ完成後がよい。最初から汎用化を狙うと、
:has()セレクタやhighlight: nullのような実装ディテールが見えないまま抽象化してしまう - 自動遷移のタイマーは、自分の視線移動を計測してから決める。秒数を決めるのに「ちょうどいい」で済ませると、後で必ず触り直すことになる
明日やること
- 残り17ページ(02-... 〜 18-...)を
InvestmentTopicPageで書き換える - 最終トピック(18番)の
nextPathを「Part 2 完了画面」へ向ける - 自動遷移5秒のあいだ、画面下に「次へ進みます」のカウントダウン表示を出す