ハコヅミを作る #22:見出しと一覧の見た目を、サンプル画像を見てから直す

前回(#21)でフォントを変えたら、今度は一覧の見た目が気になってきました。機能は足りているのに、どこか事務的です。

今回は、見た目の手直しを3つの Issue(#104・#106・#108)で進めた話です。どれも、Issue を作る前に、サンプル画像を見て決める進め方にしました。


進め方:先に画像、あとで Issue

見た目の変更は、言葉で仕様を書いても、できあがりが想像と違うことがよくあります。#14では、PR を出してから見た目を2回直しました。

今回は、/issue のメモに「サンプル画像を提示して」と書いて、順番を入れ替えました。

  1. メモを渡す(「こうしたい」と、参考の画像があればそれも)
  2. Claude Code が、今の画面に見た目だけを仮に当てて撮った画像を、案ごとに並べて出す
  3. 画像を見て、案を選ぶ
  4. 選んだ内容で Issue を作る → /implement

2 の画像は、手元で動かしたアプリのページに、スタイルや HTML を一時的に差し込んで撮ったものです。コードは変えていません。作るのは数分で、捨てても惜しくありません。

この記事の「今/案A/案B」の画像は、そのときのサンプルです。データは、#7で取り込んだ実際のギアと、記事用の架空のキャンプ場・消耗品です。


見出しを、線の上に乗ったタブにする(#104)

カテゴリやコンテナの見出しは、緑の文字と下の線だけでした。項目の文字に埋もれて、区切りが分かりにくい。参考にしたい見出しの画像(線の上に、色を塗った札が乗っている形)を渡して、サンプルを作ってもらいました。

見出しのサンプル(案A)。左はギアの一覧で、「椅子・テーブル」「火器」の見出しが、緑の背景に白い文字のタブになり、まっすぐな線の左端に乗っている。右は持ち物チェックで、「カーゴ 30L-1」のタブの中に白いチェックボックスが入り、右端に「積み込み済み」と出ている

案A:タブ+今と同じまっすぐな線(左:ギアの一覧、右:持ち物チェック)

見出しのサンプル(案B)。案Aと同じタブで、線の両端が少し下に折れている

案B:タブ+両端が下がった線(渡した参考の画像に近い形)

案B は参考の画像に近い形ですが、項目がたくさん並ぶ一覧では、少しにぎやかです。案A にしました。

サンプルを作る段階で、気づいたことが1つあります。持ち物チェックでは、コンテナの見出しの左に、一括チェックのチェックボックスがあります(#9)。緑のタブの中に入れると、緑のチェックボックスが背景に溶けて見えなくなります。タブの中だけ、白いチェックボックスにしました。言葉だけで仕様を書いていたら、実装してから気づいたはずです。

実装は、見出しの見た目を1つの部品(SectionHeading)にまとめただけです。これまでの見出しの部品(Section)の中身を差し替えたので、それを使っている画面は、すべて同じ見た目に変わりました。プランの画面のカテゴリの見出しは別の作りだったので、同じ部品を使うように直しています。


見た目の微修正・その1(#106)

タブを本番の前に開発環境で使ってみると、今度はタブが目立ちすぎるのが気になりました。あわせて、ギアとキャンプのリストも直します。

タブを少し小さく

タブの大きさのサンプル。左から、今(文字 18px)、案A(文字 16px・上下の余白を半分)、案B(文字 14px・上下の余白を半分)

タブの大きさ(左から、今/案A 16px/案B 14px)

「若干小さく」とメモに書きましたが、どれくらいかは、見ないと決められません。項目の文字と同じ大きさになる 案A(16px) にしました。

ギアのリスト:★をうしろへ

ギアのリストの並びのサンプル。左は今で、名称のすぐあとに★があり、そのあとにメーカーとコンテナのバッジ。右は修正案で、名称、メーカー、コンテナのあとに★が並ぶ

ギアのリストの並び(左:今、右:名称 → メーカー → コンテナ → ★)

おすすめ度の★(#13)は、名称のすぐうしろに置いていました。★の数で行の長さが変わり、メーカーやコンテナのバッジの位置がそろいません。★をいちばんうしろに回しました。

キャンプのリスト:1件を1行にまとめる

いちばん変わったのは、キャンプのリストです。

キャンプのリストのサンプル。左は今で、日付・キャンプ場・プラン・実績・進み具合が、下線付きの1行の文字で並ぶ。中央の案Aは、キャンプ場の名前、曜日と泊数付きの日付、プランのバッジの3段と、右に進み具合の数と棒。右の案Bは、案Aの左に、月と日を大きく出した札が付く

キャンプのリスト(左から、今/案A:2〜3行にまとめる/案B:左に日付の札)

左の「今」は、1行に全部の情報を詰めた、下線付きのリンクです。メモには「ちょっと事務的な感じなので、もう少し洗練したイメージにして」としか書いていません。それでも、案が2つ並べば、選ぶことはできます。

どちらの案も、次のように変えています。

  • キャンプ場の名前・日付・プランを、3段に分ける
  • 日付に、曜日と泊数を足す(「2026/10/10(土)〜10/11(日)・1泊」)
  • プランは、バッジで出す
  • 進み具合は、右に数と細い棒。全部済んだら「積み込み済み」
  • 行全体を押せるようにする

左に月日の札が付く 案B にしました。スマホの幅では、札の分だけキャンプ場の名前が折れやすくなりますが、いつのキャンプかが一目で分かります。

落とし穴:同じ名前のラベル

実装では、これまでの E2E が1件落ちました。一覧からキャンプを押して、キャンプの画面で進み具合を確かめるテストです。エラーは、こうでした。

Error: strict mode violation: getByLabel('進み具合') resolved to 182 elements

キャンプの画面の進み具合には、aria-label="進み具合" を付けています。新しいリストの行の進み具合にも、同じラベルを付けました。テストは、行を押したあと、画面が切り替わるのを待たずに「進み具合」を探します。ふだんは、キャンプの画面に1つだけ見つかるまで待ってくれます。ところが、一覧にも同じ名前の要素がたくさんあるので、画面が切り替わる前の一覧のほうで見つかってしまい、「1つに決まらない」というエラーになりました。

一覧の行のほうは、ラベルではなく、読み上げ用の文字(画面には出ない「進み具合」)に変えました。読み上げでは今までどおり「進み具合 15 / 45」と読まれ、テストがラベルで探したときには、キャンプの画面の1つだけが見つかります。

同じ名前を2つの画面に付けると、画面をまたぐテストが取り違える。見た目の変更でも、こういう形でテストに響きます。


見た目の微修正・その2(#108)

続けて、コンテナとプランの一覧、消耗品の残りの表示です。

コンテナ一覧・プラン一覧

コンテナ一覧のサンプル。左は今で、コンテナの名前のリンクが縦に並ぶだけ。中央の案Aは、アイコン・名前・「ギア 16 件・消耗品 2 件」を1行にまとめた行。右の案Bは、アイコンを大きく上に置いた、2列のタイル

コンテナ一覧(左から、今/案A:アイコン付きの行/案B:アイコン付きのタイル)

名前のリンクが並ぶだけだった一覧に、#19で用意したアイコンと、中身の件数を足しました。キャンプのリストと作りがそろう 案A にしています。プラン一覧も同じ形です。

消耗品の残りの表示

消耗品一覧のサンプル。左は今で、「残りなし」「残りあり」「毎回用意」が名前のすぐうしろに小さな文字で出ている。中央の案Aは、色付きの丸い札が行の右端にそろう(残りなしは赤、残りありは淡い緑、毎回用意は枠線だけ)。右の案Bは、色の丸と文字が右端にそろう

消耗品の残りの表示(左から、今/案A:色付きの札/案B:色の丸+文字)

「残りなし」を見落とさないことが目的なので、はっきり出る 案A にしました。どちらの案でも効いているのは、右端にそろえたことです。今は名前のすぐうしろにあるので、行ごとに位置がばらばらで、目で追えません。

持ち物チェックの「残りなし」(#16)は、変えていません。持ち物チェックの行は、チェックボックスやバッジで込み合っていて、札を足すとうるさくなるためです。

コンテナの編集画面

コンテナの編集画面。左は今で、「中に入っているギア」「中に入っている消耗品」が、ページの背景の上にそのまま出ている。右は修正案で、2つのまとまりが白いカードの中に入っている

コンテナの編集画面(左:今、右:中身も白いカードの中に入れる)

これは、タブにしたことで目立つようになった不ぞろいです。この画面だけ、中身の一覧が白いカードの外に出ていました。

落とし穴:件数が、全部 0 になる

コンテナ一覧に出す件数は、1回の問い合わせで数えたいので、サブクエリで書きました。

gearCount: sql<number>`(select count(*) from ${gears} where ${gears.containerId} = ${containers.id} ...)`

ところが、足した E2E が落ちます。ギアを2つ入れたコンテナが、「ギア 0 件」と出ていました。

原因は、Drizzle が出す SQL です。join のない問い合わせでは、Drizzle は列をテーブル名なし("id")で出します。上の ${containers.id} も、SQL の中では "id" になります。サブクエリの中で "id" と書くと、SQLite は、まず中のテーブル(ギア)の id として読みます。つまり「ギアのコンテナの id が、ギア自身の id と同じ」という、決して成り立たない条件になっていました。

外側の列は、テーブル名を付けて書くように直しました。

// 外側のコンテナの id は、テーブル名を付けて書く
const containerId = sql`${containers}.${sql.identifier(containers.id.name)}`;

型チェックも通り、エラーも出ず、ただ数が 0 になる。画面を見ても「まだ何も入っていないコンテナ」に見えるだけなので、テストがなければ気づきにくい不具合です。#18でキャンプ一覧の進み具合を数えたときは、たまたま join のある問い合わせだったので、踏んでいませんでした。CLAUDE.md に注意を書き足しています。


できあがり

実装後の3つの画面。左からコンテナ一覧(アイコン・名前・件数の行)、プラン一覧(同じ形)、消耗品一覧(タブの見出しと、右端にそろった残りの札)

実装後のコンテナ一覧・プラン一覧・消耗品一覧(スマホの幅)


おわりに

  • 見た目の変更は、Issue の前にサンプル画像を出してもらう。案が並べば、「事務的な感じ」のようなあいまいなメモからでも選べる
  • サンプルを作る段階で、仕様の穴(緑の背景の上のチェックボックス)が見つかる
  • 同じ名前のラベルを2つの画面に付けると、画面をまたぐテストが取り違える
  • Drizzle のサブクエリから外側の列を指すときは、テーブル名を付ける

3つの Issue で、あとから見た目の直しが出たのは、タブの大きさ(使ってみて、目立ちすぎると分かった)だけでした。サンプルを作る手間は、十分に元が取れています。

次回は、カード表示を作り直した話です。こちらは、サンプルを見てもなお、受入のあとに直しが出ました。

← トップページに戻る