開発eurekapu-nuxt4

投資判断クイズを2カラムレイアウトへ移行:1ページ完成→コンポーネント化→横展開

午前にPart 2投資判断クイズの74問とSVG18枚を流し込み終え、午後はUX改善に切り替えた。/financial-statements-quiz/investment/01-operating-cf を開いて指を止めた瞬間、画面の上下にスクロールしないと届かない余白が広がっていた。「ここってなんでこんなにスペース空いてるんですかね」と打ち込んだのが、今日の作業の起点になった。

スペースの正体を掘る

Claude Code に CSS を追わせると、原因はすぐ出てきた。.practice-listpadding-top: 50vhpadding-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.vue18-...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秒のあいだ、画面下に「次へ進みます」のカウントダウン表示を出す