7段階の幅で横スクロールを順に潰した
/lessons/financial-statements/cockpit-00-summary をスマホで開いたら、BS の右列がはみ出して、画面の外に消えた数字を確認するために横スクロールを2回引っ張った。「狭い画面で動くこと」を後回しにしてきたツケを、ここで払うことに決めた。
検証幅は 1920 / 1366 / 1280 / 1100 / 900 / 768 / 375 の7段階。Chrome DevTools MCP の emulate で各幅に切り替えながら、横スクロールバーが画面下に出ない状態を順に作った。
最初は resize_page で幅だけ変える方針で進めたが、window.innerWidth は変わるのに CSS のメディアクエリが効かない瞬間があって、レイアウトが古い幅のまま固まる挙動が出た。emulate でデバイスエミュレーションごと切り替える形に変更したら、メディアクエリも素直に追従した。
1920px — wide-section を1480pxで頭打ちにする
24インチ以上のディスプレイで開くと、wide-section が画面いっぱいに広がって、左右のテーブルが遠く離れて視線が振れた。「広い画面なら何でも広く使えばいい」訳ではなく、読む距離を一定に保つ方針に切り替える。
.wide-section {
max-width: 1480px;
margin-inline: auto;
}
1480px で頭打ちにして、それ以上の幅では中央に寄せる。1920px のディスプレイでは左右に220pxずつ余白が出るが、テーブルの数字が視野角に収まる側を優先した。
375px — BSの折り返しがオーバーラップした
モバイル幅で BS を開くと、資産側と負債純資産側が横並びのまま縮んで、テーブル内のテキストが折り返して隣の列に重なった。「狭い画面では2列を諦めて1列にする」が正解。
@media (max-width: 480px) {
.bs-grid {
grid-template-columns: 1fr;
}
}
資産明細 → 負債純資産明細 → 合計行、の順に縦に積み重ねる構造に変更。スマホで開いたとき、上から下にスクロールすれば全数字が見える。横スクロールは消えた。
statements-grid に S/S が入っていなかった
狭幅版(1280px以下)の statements-grid の grid-template-areas を確認したら、bs / pl / cs の3エリアしか定義されていない。S/S を追加した時に狭幅側だけ追従漏れしていた。
広幅版では4表が見えるのに、狭幅版に切り替えた瞬間 S/S が消える挙動が出ていた。grid-template-areas に ss エリアを追加し、各幅でレイアウト先を再定義。
モーダル拡大時、左ペインの下に空白が伸びていた
「拡大ボタン」でモーダルを開くと、左ペイン(テーブル側)の下に画面1〜2画面分の空白が伸びて、右ペイン(解説側)と高さが揃わなかった。
調べると、左ペイン側に min-height: 100dvh が常時掛かっていた。デスクトップでは右ペインに合わせて高さを揃えたいが、モバイルでは内容に応じた自然高で良い。
@media (min-width: 769px) {
.modal-left {
min-height: 100dvh;
}
}
デスクトップだけに条件付けしたら、モバイルの空白が消えた。
ヘッダー横に拡大/縮小ボタンを追加
モーダル拡大中、テーブルの数字を細かく見たい時にブラウザのズーム(Ctrl++)を使うと、モーダル外の背景までズームしてレイアウトが崩れる。モーダル内だけでズームを完結させたい。
ヘッダー右側、× 閉じるボタンの隣に − と + のボタンを並べた。クリックで modalZoom の値を 0.1 刻みで上下させ、CSS の zoom プロパティに反映する。
const modalZoom = ref(1)
const zoomIn = () => modalZoom.value = Math.min(2, modalZoom.value + 0.1)
const zoomOut = () => modalZoom.value = Math.max(0.5, modalZoom.value - 0.1)
開いた瞬間、ビューポートに自動でフィットさせる
+/− ボタンで手調整できる前提を作った後、「最初に開いた瞬間にビューポート全体が画面に収まる」初期値が欲しくなった。手で何回も − を押させるのは負け筋だ。
applyFitZoom 関数を追加して、モーダルを開いた瞬間とウィンドウリサイズ時に呼び出す。コンテンツの実寸とビューポートサイズから、縦横どちらも収まる倍率を計算して modalZoom に代入する。
const applyFitZoom = () => {
const content = modalContentRef.value
if (!content) return
const ratioW = window.innerWidth / content.scrollWidth
const ratioH = window.innerHeight / content.scrollHeight
modalZoom.value = Math.min(ratioW, ratioH, 1)
}
onMounted(() => {
applyFitZoom()
window.addEventListener('resize', applyFitZoom)
})
最初は縦方向のフィットだけ計算していたが、横長コンテンツで画面右にはみ出す事象が出たので、ratioW も足して Math.min で小さい方を採る形に直した。
CSS zoom 下で SVG オーバーレイがずれた
ここで一番ハマった問題。zoom: 0.7 を当てた状態で BS と CS を結ぶ SVG コネクターを描くと、線の端がテーブルの数字から数十px ずれる。zoom が等倍(1.0)に戻すと、ずれは消える。
原因を追ったら、SVG の path 座標は getBoundingClientRect() で取得したアンカー座標から計算している。getBoundingClientRect() は CSS zoom 後の実画面座標を返すのに対し、SVG の viewBox を未指定にしていたため、SVG 側は内部的に等倍前提の座標系で描画していた。両者の座標系がズレて、線の端が数字から離れた。
解決策は SVG に 動的な viewBox を付けて、コンテナの実寸(CSS zoom 前のサイズ)に座標系を揃えること。
// コンテナの「論理サイズ」(zoom無視)でviewBoxを設定
const svgViewBox = computed(() => {
const w = containerRef.value?.offsetWidth ?? 0
const h = containerRef.value?.offsetHeight ?? 0
return `0 0 ${w} ${h}`
})
offsetWidth は zoom の影響を受けない論理サイズを返すので、これを viewBox の幅として渡せば SVG 側の座標系と path 計算側の座標系が一致する。zoom がいくつでも、線の端が数字に正確に当たる。
modalZoom 変更時にオーバーレイ再計算
+/− ボタンで modalZoom を変えた瞬間、SVG オーバーレイが古い座標のまま残って、線が数字からずれて見えた。
オーバーレイの再計算 trigger に modalZoom を含めて、ズーム変更ごとに座標を再評価する形に変更。
watch([entryId, modalZoom], () => {
recalcOverlayPositions()
})
これで +/− を押すたびに線が追従するようになった。
コミット
担当分のみ4ファイル、162行追加でコミット。
1eea1fbレスポンシブ調整・S/S狭幅版バグ修正・左ペイン min-height 条件付け4a67b35モーダル±ズーム・applyFitZoom・SVG viewBox 動的化
作業ログは memo/2026-05-09/cockpit-responsive-and-modal-zoom-worklog.md に残した。
学びメモ
resize_page だけだとメディアクエリが追従しないことがあった。 Chrome DevTools MCP で幅を変えた時、window.innerWidth の値は更新されるのに @media (max-width: 480px) のスタイルが古い幅のまま固まる瞬間が出た。emulate でデバイスエミュレーションごと切り替えに変えたら、メディアクエリも一緒に追従した。次から狭幅検証では最初から emulate を使う。
CSS zoom と座標計算ライブラリの相性は悪い。 getBoundingClientRect() は zoom 後の座標を返すのに、SVG の path は viewBox なしでは等倍前提で描画される。両者の座標系を揃える方法は SVG 側に動的 viewBox を当てるのが一番素直だった。transform: scale() で代替する案も検討したが、子要素のレイアウト幅が変わらない問題が出るので zoom を維持した。
「最初に開いた瞬間に最適な状態」を作るのは ±ボタン2個分の手間を消す。 applyFitZoom を入れる前は、ユーザーが毎回 − を3〜4回押してフィットさせていた。自動フィット1行が、ユーザーの指の動きを毎回4回ずつ削った。