プロジェクトタイムラインビューアの構築
ブログの記事一覧をカレンダーで眺めていて、「どのプロジェクトに何日使ったか」が見えないことに気づいた。カレンダーは日付軸しかない。プロジェクト軸を横に並べたガントチャート風のビューがあれば、時間の使い方が一目で分かる。そう思って /project-timeline ページを丸ごと作った。
やったこと
1. project-timeline.vue の新規作成
apps/web/app/pages/project-timeline.vue を1ファイルで構築した。横軸にプロジェクト名、縦軸に日付を並べるテーブルレイアウト。project_name フロントマターが設定されている記事を queryCollection で引き、プロジェクト別・日付別に自動集計する仕組みにした。
データ取得のクエリはシンプルで、project_name IS NOT NULL の記事だけを publishedAt DESC で取得する。
const { data: articles } = await useAsyncData('timeline-articles', () =>
queryCollection('pages')
.select('title', 'path', 'publishedAt', 'project_name')
.where('project_name', 'IS NOT NULL')
.order('publishedAt', 'DESC')
.all()
)
プロジェクト一覧は記事数の多い順にソートし、12色のカラーパレットを順番に割り当てる。セルの背景色でどのプロジェクトが活発かが視覚的に分かる。
2. 日次・週次・月次の3ビューモード
タブ切り替えで3つの粒度を選べるようにした。
- 日次: 1行=1日。月ヘッダー行を挟んで区切る
- 週次: 1行=1週間(月曜始まり)。記事を週単位で集約
- 月次: 1行=1ヶ月。3件以上は折りたたみ、「他N件」ボタンで展開
週次ビューの getMonday 関数では、(d.getDay() + 6) % 7 で月曜を0とするインデックスに変換し、その分だけ日付を戻す。ISO週の開始日を正しく算出するために、この計算が要になる。
月次ビューでは1セルあたりの記事数が多くなるため、expandedMonthCells という Set で展開状態を管理し、デフォルトで3件まで表示する制御を入れた。
3. stickyヘッダーとプロジェクト列の固定
ここで一番ハマったのがCSSだった。thead を position: sticky; top: 0 にしてもスクロール時にヘッダーが追従しない。原因を追ったところ、Nuxtのリセットスタイルが table の display を block に書き換えていた。
/* Nuxtリセット対策: display: table を明示 */
.timeline-table {
display: table;
border-collapse: separate;
border-spacing: 0;
}
display: table を明示した瞬間、stickyが効き始めた。テーブル要素の display が block になると、sticky positioningのコンテキストが壊れるという挙動を初めて体感した。
さらに、左端の日付列を position: sticky; left: 0 で横スクロール時にも固定した。左上のコーナーセルは top: 0 と left: 0 の両方を設定し、z-index: 20 で最前面に配置。ヘッダー行が z-index: 10、日付列が z-index: 5 という3層構造にした。
4. ブログindex.vueへのタイムラインボタン追加
apps/web/app/pages/blog/index.vue の view-toggle エリアに、タイムラインページへの NuxtLink ボタンを追加した。リスト表示・カレンダー表示と並んで「タイムライン」が選べるようになった。SVGアイコンはガントチャートをイメージした横棒グラフ風のデザインにした。
5. Chrome DevTools MCPでのブラウザ確認
実装中の見た目の確認にChrome DevTools MCPを使った。リモートデバッグポート9223でChromeを起動し、new_page で localhost:3000/project-timeline を開いてスクリーンショットを取得。sticky挙動やカラーパレットの視認性をブラウザ上で直接確認しながら調整を進めた。
6. Codex(GPT-5.3)によるプランレビュー
実装前にCodexへプランレビューを投げた。返ってきた指摘の中で、ISO週の境界処理に関するものが的を射ていた。年末年始に週が年をまたぐケースで、月ヘッダーの表示がズレる可能性を指摘された。週の開始日(月曜)の月を基準にすることで対処した。
他にも「toISOString() がUTC変換するため日本時間で前日になるバグ」を指摘され、ローカル日付フォーマット関数 formatDateKey を用意して回避した。
7. 2026-02-21日記の生成
前日の開発日記を生成した。4本の詳細記事(認証基盤、ダッシュボード方針転換、インフラ整備、テスト環境)と、それらを統合した日記を作成。日記のタイトル「ダッシュボードを作って壊し、1ドメイン統合型へ舵を切った日」は、当日の最大の意思決定を凝縮したものになった。
8. 設計方針変更ドキュメントの追記
eurekapu-nuxt4のドメイン統合方針について、設計変更ドキュメントに追記した。独立型ダッシュボード(サブドメイン分離)を廃止し、1ドメイン統合型(/admin/ ルート)へ移行する判断の根拠と、移行時に活きる知見(SSR時のCookie転送問題など)を記録した。
設計判断メモ
なぜテーブルレイアウトか
CSS Gridやdivベースのレイアウトも検討したが、横軸・縦軸ともに動的に増減するデータにはHTMLテーブルが素直だった。テーブルなら border-collapse やセルの自動揃えが効くし、stickyも(displayさえ正しければ)期待通りに動く。
なぜ1ファイルか
project-timeline.vue はロジックとテンプレートとスタイルで約650行。コンポーネント分割も考えたが、ビューモードごとのデータ構造(dailyRows, weekRows, monthRows)が密結合しており、分割してもprops/emitのやり取りが増えるだけだと判断した。
カラーパレット
12色を固定配列で持ち、プロジェクトのインデックスでモジュロ割り当てする方式。プロジェクト数が12を超えると色が循環するが、現時点のプロジェクト数(16前後)なら十分区別できる。将来的にはハッシュベースの色生成に切り替える余地がある。
学んだこと
- NuxtのリセットCSSが
tableのdisplayをblockに変えていた。display: tableを明示しないとstickyが死ぬ。CSSリセットの影響は、デベロッパーツールで computed styles を開いて初めて気づいた toISOString()はUTC変換するため、JST環境では日付が前日に巻き戻る。ローカル日付を扱うときはgetFullYear()/getMonth()/getDate()で自前フォーマットする- Codexのレビューは、ISO週の年またぎなど自分が見落とすエッジケースを突いてくる。プラン段階で投げる価値がある
position: stickyはtopとleftを同時に設定できる。コーナーセルの固定はこの組み合わせで実現できた
関連ファイル
apps/web/app/pages/project-timeline.vue- タイムラインビューア本体apps/web/app/pages/blog/index.vue- タイムラインボタンの追加apps/web/content.config.ts-project_nameenum定義