From 00e10a992fb14ee0a9e4297de9bc7b7e55c416da Mon Sep 17 00:00:00 2001 From: Joel Brock Date: Tue, 19 May 2026 16:32:50 -0700 Subject: [PATCH] Timeline: hover/focus tooltips, stage 0 lane, adaptive date axis Three improvements to the report's DateTimeline: 1. Tooltips on every dot. Each dot is now a focusable span (tabIndex, role=img, full aria-label). A small ink-tinted card appears above the dot on mouse hover or keyboard focus, showing field label, formatted date, stage rank, and an 'Opened' marker for Date_Opened. Anchor flips to left/center/right based on the dot's position so tooltips don't overflow the row at the edges. 2. Plot every date field, including stage 0. The previous version skipped Stage 0 dates (Internal_Startup_Assessment_Date, Date_Closed_Folded). Now there are six swim lanes (0-5) instead of five. Stage 0 gets bg-leaf-200 so the gradient extends one step lighter. 3. Adaptive month/year x-axis under the lanes. generateAxisTicks picks a 'nice' interval based on the visible span: 1mo / 2mo / 3mo / 6mo / 1yr / 2yr. January-bordered ticks include the year so the reader has anchors. Today gets its own labeled clay tick when it falls in range. --- components/ReportView.tsx | 215 ++++++++++++++++++++++++++++++++++---- 1 file changed, 193 insertions(+), 22 deletions(-) diff --git a/components/ReportView.tsx b/components/ReportView.tsx index 8a647d6..7819655 100644 --- a/components/ReportView.tsx +++ b/components/ReportView.tsx @@ -220,12 +220,13 @@ function StageProgress({ currentRank }: { currentRank: number }) { /** * Date-grouped timeline strip at the top of the report. Walks every - * date-type field in stage sections 1–5 (Stage 0 omitted intentionally), - * pulls each field's most-recent entered date, and plots events into - * five horizontal swim lanes — one per stage. Stage 5's `Date_Opened` - * gets visual emphasis as the journey's anchor at the right end of the - * time axis. A faint "Today" tick anchors the reader if today's date - * falls within the plotted range. + * date-type field across all stage sections (0–5), pulls each field's + * most-recent entered date, and plots events into six horizontal swim + * lanes — one per stage. Stage 5's `Date_Opened` gets visual emphasis + * as the journey's anchor at the right end of the time axis. A faint + * "Today" tick anchors the reader if today falls within the range, and + * an adaptive month/year axis runs beneath the lanes. Each dot exposes + * a tooltip on hover or keyboard focus. */ function DateTimeline({ sections, @@ -242,7 +243,7 @@ function DateTimeline({ }; const events: Event[] = []; for (const section of sections) { - if (section.rank < 1 || section.rank > 5) continue; + if (section.rank < 0 || section.rank > 5) continue; for (const f of section.fields) { if (f.type !== "date") continue; const history = fieldHistory[f.name]; @@ -262,9 +263,8 @@ function DateTimeline({ const times = events.map((e) => new Date(e.date).getTime()).filter(Number.isFinite); if (times.length === 0) return null; - const openedTime = events.find((e) => e.isOpened)?.date - ? new Date(events.find((e) => e.isOpened)!.date).getTime() - : undefined; + const openedEvent = events.find((e) => e.isOpened); + const openedTime = openedEvent ? new Date(openedEvent.date).getTime() : undefined; const minT = Math.min(...times); // If Date_Opened is set, anchor the right edge there; if any other event // is later, extend so nothing falls off the chart. @@ -274,6 +274,8 @@ function DateTimeline({ const todayPct = todayT >= minT && todayT <= maxT ? ((todayT - minT) / tRange) * 100 : null; + const ticks = generateAxisTicks(minT, maxT); + return (
    - {[1, 2, 3, 4, 5].map((rank) => { + {[0, 1, 2, 3, 4, 5].map((rank) => { const rowEvents = events.filter((e) => e.rank === rank); const isEmpty = rowEvents.length === 0; return ( @@ -315,7 +317,6 @@ function DateTimeline({ {todayPct !== null && ( @@ -324,18 +325,12 @@ function DateTimeline({ const t = new Date(e.date).getTime(); if (!Number.isFinite(t)) return null; const x = ((t - minT) / tRange) * 100; - const bg = e.isOpened ? "bg-clay-700" : stageDotBg(rank); - const size = e.isOpened ? "h-3.5 w-3.5" : "h-2.5 w-2.5"; - const ring = e.isOpened ? "ring-2" : "ring-1"; return ( - ); })} @@ -344,6 +339,65 @@ function DateTimeline({ ); })}
+ + {/* Adaptive month/year axis. Render only when we have ≥2 ticks so a + single tick doesn't dangle. Today gets its own labeled mark when + it falls within range. */} + {ticks.length >= 2 && ( +
+ +
+ + {ticks.map((tk, i) => { + const x = ((tk.t - minT) / tRange) * 100; + // Edge labels shift so they don't overflow the row. + const alignClass = + x < 6 + ? "left-0 origin-top-left" + : x > 94 + ? "right-0 origin-top-right text-right" + : "left-1/2 -translate-x-1/2 text-center"; + return ( + + + + {tk.label} + + + ); + })} + {todayPct !== null && ( + + + + Today + + + )} +
+
+ )} {/* Accessible event list — invisible to sighted users but readable by SR */}