• #nuxt3
  • #vue
  • #タイムライン
  • #ガントチャート
  • #CSS
  • #sticky
  • #codex-review
開発mdx-playgroundメモ

プロジェクトタイムラインビューアの構築

ブログの記事一覧をカレンダーで眺めていて、「どのプロジェクトに何日使ったか」が見えないことに気づいた。カレンダーは日付軸しかない。プロジェクト軸を横に並べたガントチャート風のビューがあれば、時間の使い方が一目で分かる。そう思って /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だった。theadposition: sticky; top: 0 にしてもスクロール時にヘッダーが追従しない。原因を追ったところ、Nuxtのリセットスタイルが tabledisplayblock に書き換えていた。

/* Nuxtリセット対策: display: table を明示 */
.timeline-table {
  display: table;
  border-collapse: separate;
  border-spacing: 0;
}

display: table を明示した瞬間、stickyが効き始めた。テーブル要素の displayblock になると、sticky positioningのコンテキストが壊れるという挙動を初めて体感した。

さらに、左端の日付列を position: sticky; left: 0 で横スクロール時にも固定した。左上のコーナーセルは top: 0left: 0 の両方を設定し、z-index: 20 で最前面に配置。ヘッダー行が z-index: 10、日付列が z-index: 5 という3層構造にした。

4. ブログindex.vueへのタイムラインボタン追加

apps/web/app/pages/blog/index.vueview-toggle エリアに、タイムラインページへの NuxtLink ボタンを追加した。リスト表示・カレンダー表示と並んで「タイムライン」が選べるようになった。SVGアイコンはガントチャートをイメージした横棒グラフ風のデザインにした。

5. Chrome DevTools MCPでのブラウザ確認

実装中の見た目の確認にChrome DevTools MCPを使った。リモートデバッグポート9223でChromeを起動し、new_pagelocalhost: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が tabledisplayblock に変えていた。display: table を明示しないとstickyが死ぬ。CSSリセットの影響は、デベロッパーツールで computed styles を開いて初めて気づいた
  • toISOString() はUTC変換するため、JST環境では日付が前日に巻き戻る。ローカル日付を扱うときは getFullYear()/getMonth()/getDate() で自前フォーマットする
  • Codexのレビューは、ISO週の年またぎなど自分が見落とすエッジケースを突いてくる。プラン段階で投げる価値がある
  • position: stickytopleft を同時に設定できる。コーナーセルの固定はこの組み合わせで実現できた

関連ファイル

  • apps/web/app/pages/project-timeline.vue - タイムラインビューア本体
  • apps/web/app/pages/blog/index.vue - タイムラインボタンの追加
  • apps/web/content.config.ts - project_name enum定義