feat(fairway): the premium facelift — every coach screen as a field sheet - #1933
Open
njrini99-code wants to merge 174 commits into
Open
feat(fairway): the premium facelift — every coach screen as a field sheet#1933njrini99-code wants to merge 174 commits into
njrini99-code wants to merge 174 commits into
Conversation
…roup, StatMatrix, floating dock, capture tooling Phase 1 of the Fairway premium facelift (docs/design/fairway-facelift/BRIEF.md): - design-tokens: semantic depth ladder (--fw-depth-1..4), --fw-shadow-control, one frost recipe (tint, saturate, blur tiers, backgrounds, rim, edge light, contact shadows, selection) with dark-scheme values. - globals: .fw-frost + subtle/floating/modal/bar/selection tiers, .fw-dock-fade, phone blur downshift, reduced-transparency / no-backdrop / forced-colors fallbacks; --fw-mobile-nav-height now reserves the 60px dock + 10px float. - GlassSurface: renders the shared frost tiers (tier, edgeHighlight, tint, interactive) instead of a private recipe; exports GlassSurfaceTier. - New surfaces/InsetGroup (grouped rows with seams) and modules/StatMatrix (one object for related values) to replace nested mini-cards. - Button gains shape="block" (14px CTA); Sheet gains material="frost". - FairwayBottomNav becomes the floating dock: 60px capsule, 16px side inset, floats above the safe area, contact fade, compact tinted active island. - scripts/ui-intelligence/capture-golf-facelift.mjs: coach + player, phone + desktop, fold + full, tabs and sheets, code graph per route (npm run ui:facelift:capture). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
The "which component solves this?" map (brief §22): bestFor / avoidFor / replaces / mobile / desktop per component, plus the question → visual table. registry.test.ts keeps every name a real Fairway export. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
…en specs - --fw-mobile-nav-height now reserves the dock's real box (10px top pad + 60px capsule + 10px float gap + safe area) and AppShell's shell offset matches; the old max() under-reserved 10px on notched phones. - next.config: logging.serverFunctions=false — the dev server echoed every Server Function's arguments, including the password passed to loginAction, into its log. - Capture script: resumable manifest, hydration-safe login with retries, re-login on session lapse, dev badge hidden. - docs/design/fairway-facelift: AUDIT.md (competing surfaces) and the first screen specs (coach home, roster, rounds library, player dossier, tasks, qualifiers, team stats, round detail, calendar desktop). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
…gacy glass chrome; audit + specs Next 16 defaults experimental.staleTimes.dynamic to 0, so every dock or rail tab tap refetched the (cookie-dynamic) dashboard page and repainted its loading skeleton, even hopping between two tabs. 60s of client router cache serves the hop back; revalidatePath and router.refresh() still purge. .fw-glass-chrome is now paint-only alias of the Frost subtle tier so its 19 consumers stop carrying a second recipe. AUDIT.md gains the hydration, dead-code and performance sections; two more screen specs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
…ss, Meter, ResizableWorkspace, frost Toolbar Menu and Tooltip wrap the Radix primitives already in the tree and float on the frost tiers; ScrollArea is CSS/DOM over the existing scroll-fade hook; Progress (process) and Meter (measure) are two distinct feedback primitives; ResizableWorkspace is a CSS-grid three-pane shell with pointer and keyboard resizing and persisted layout. Toolbar gains material="frost" (default path unchanged). Registry entries added. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
…toolbar, seam rows Five KPI cards, floating trend pill and per-group cards become ViewHeader, StatStrip (delta chips fold the trend in), a TickerStrip of the last 15 scored rounds on desktop, one Toolbar (search, player, round type, month/ week) and ONE surface with sticky seam headers and hairline rows. Group sparklines only at 6+ points; null to-par rounds no longer plot as even par. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
…h inline drill Overdue banner, Templates card and quick-stats rail are gone. StatMatrix (Open · Active · Completed · Overdue, the overdue cell jumps to Active), Toolbar with a status Segmented and one multi-select Category filter, and one surface of rows: title, assignee progress, due, category, overflow. Row click expands an inline DrillPanel on desktop or a Sheet on phone; From template lives in the header menu and opens a Sheet. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
Per-qualifier card galleries become one matte surface of rows (name · dates · spots · course · status) under Active and Concluded seams; the live or next qualifier is one elevated block above the toolbar, derived from the unfiltered list so the filters below it cannot hide it. Search and status filters live in the Toolbar. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
… MatrixBoard rows Eight tall player cards become ViewHeader, a header surface (StatMatrix + who needs attention), one Toolbar (search, sort, needs-attention, export) and a MatrixBoard with one row per player (name · SG total · trend sparkline · focus · signal), row click expands the detail. The page derives recent_scores from data it already fetched; no new query. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
FairwaySidebar and FairwayShellSkeleton take tone='dark' | 'cream'; the golf dashboard shell passes cream so the rail sits beside the cream canvas as one instrument, with the active row as a frost-selection capsule. Baseball and admin keep the warm-black default; nav-* tokens are untouched. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
loading.tsx and page.tsx's own loading branch still shape-matched the retired filter-pills + card-list + Templates-rail composition. Reshape both skeletons to mirror FairwayTasks: masthead -> StatMatrix -> toolbar -> one matte Surface of seam rows, so the swap into real data has no layout shift. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
… pulse board, recent rounds Twelve stacked cards become a ViewHeader with one primary action (New event) and an overflow menu, then Today beside Team performance (window Segmented, StatMatrix, trend), Team pulse as a MatrixBoard beside the latest notifications, and Recent rounds as a TickerStrip over seam rows. The skeleton mirrors the new layout. Invite notice only below 3 players. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
The Suspense fallback still modeled the pre-facelift five-tile grid and a per-group Surface. Reuse the real StatStrip primitive (RSC-safe, no hooks) for the hero so phone gets its rail shape and desktop its grid, and collapse the ledger placeholder into ONE Surface with a couple of seam-header shells and 8 hairline-divided rows, matching FairwayRoundsLibrary's live layout. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
Labeled bar rows (You · Team · Tour) with values, diverging from zero for signed strokes-gained metrics, a cold-start explanation instead of a missing marker, and a visually hidden table for assistive tech. Swapped into the standing drills, insight and evidence panels, goal and focus-area cards and the filmstrip review; StandingTrack loses its redundant pin so the Spine and SG instrument draw a plain fill bar. Legacy StandingBar Card/Inline/Hero and StandingStrip carry deprecation headers. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
…l 44px touch target MatrixBoard: add an optional per-row action slot (MatrixBoardRow.actions) rendered as a sibling of the row's press-target button, never nested inside it, so a caller-supplied overflow Menu/IconButton stays a real, independently-focusable control. Add external row-expand control (expandedRowId + onExpandedRowChange on MatrixBoardProps) so a consumer can drive which row is expanded from its own state; omitting expandedRowId preserves today's fully independent per-row internal useState exactly as before. All existing MatrixBoard tests still pass; 5 new tests cover the actions slot (present/absent, sibling not nested) and both the controlled and uncontrolled expand paths. FilterPill: the sm/md pill's 30px/36px min-height fails the WCAG 2.2 AA (2.5.8) 44px touch-target minimum on phones. Expand the hit area to 44px under `(pointer: coarse)` only, mirroring the identical fix already applied to Button `sm` and Segmented `sm`/`md` — the pill's visual height on a mouse-driven desktop is unchanged. Applies to both the button and link (FilterPillLink) forms. New filter-pill.test.tsx covers the coarse-pointer expansion plus baseline toggle/disabled/count behavior. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
…Sheet Adopts MatrixBoard's row action slot and controlled expand: the player actions menu is a sibling control on every row, desktop keeps the inline expand, below sm a tap opens a bottom Sheet with the same detail body. Trend sparkline pinned to the 96px track. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
`stickyTop` was a plain number, so no consumer could stick a Toolbar under the app top bar, whose real offset is a CSS calc() baking in `env(safe-area-inset-top)` (`--golf-mobile-header-offset`) plus an optional hub sub-nav strip (`--fw-hub-subnav-offset`). Accept `stickyTop?: number | string`: a number keeps today's plain-px behavior unchanged; a string (e.g. "calc(var(--golf-mobile-header-offset) + var(--fw-hub-subnav-offset, 0px))") is used verbatim as the CSS `top` value. The stuck-detection IntersectionObserver's `rootMargin` offset is negated via a wrapping `calc()` for the string path (a valid CSS <length-percentage>) instead of the numeric `+1px` arithmetic, which only applies to the number path. Two new tests: a calc() string is applied verbatim to `top`, and the 0px numeric default is unchanged when `stickyTop` is omitted. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
…d frost when stuck Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
Round detail (FairwayRoundDetail.tsx): collapse the three-pill masthead to
one primary "Open full review" plus an overflow Menu ("All stats", "Change
round type"); replace the 3-panel InstrumentCluster hero with one green
InstrumentPanel (score, grade dots, course · date · player, ai_recap
caption) followed by Filmstrip + a 5-up StatMatrix when hole data exists, or
a single InlineNotice when it doesn't; replace the "Areas to work on"
card+EmptyState+duplicate-CTA with an RxCard rendered only when the review
has real focus areas; add a phone-only sticky "Open full review" CTA
(Button shape="block"). Scorecard/ScoringDistribution/Pulse are unchanged —
they aren't in the removal list, have pinned regression tests, and already
self-hide with no hole data.
RoundTypeEditor.tsx: convert to a parent-controlled open/onOpenChange API so
FairwayRoundDetail's overflow menu can be its trigger, instead of the
component managing its own closed-state button. Add RoundTypeEditorTrigger,
a small self-toggling wrapper preserving the old default behavior for the
one other consumer with no menu of its own to open it from
(rounds/continue/[id]/page.tsx, a server component rendering into a client
child with no surrounding chrome) — that route's chrome is out of scope
here.
Review page (rounds/[id]/review/page.tsx): both "Refresh" buttons become an
icon-only IconButton in the header actions; the bottom "All Stats" block
button becomes a quiet ghost text link.
Updated the two Suspense loading.tsx skeletons under rounds/[id] to match
the new masthead/hero shapes, and the round-type mounting test to open the
overflow menu (Radix menuitem role, portalled to document.body) instead of
querying a standalone button.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
…rd + one analysis Bento Standalone StatMatrix header (sticky on desktop), MatrixBoard with no internal KPI band, and one Bento (tornado, fundamentals, putts by distance, approach proximity) replace six boxes. One primary action; export and Ask CoachHelm live in the overflow Menu. Trajectory cell shows InsufficientData until a player clears the 8-round gate. Loading skeleton matches the new composition. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
A page whose body text is under 40 chars is recorded as status "blank" so a silent login or render failure is not mistaken for a finished capture. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
…EF §15.10)
Five static guards in src/test/static/fairway-facelift-ratchets.test.ts,
following the existing readFileSync-scan pattern in that directory (e.g.
golf-conditional-redirect.test.ts). Each guard ships an explicit
allowlist of every violation that exists today, so the suite is green
now; any NEW violation anywhere not already on a list fails the build.
Every failure prints the offending file:line plus a one-line fix hint.
1. No `@/components/ui/{button,input,confirm-dialog,skeleton,
dropdown-menu,row-actions-menu}` import under src/components/fairway
or the golf dashboard tree — 25 current imports across 21 files.
2. No JSX use of the legacy dot-on-a-rail standing family (StandingStrip,
StandingTrack, the v3 StandingBar Card/Inline/Hero variants) outside
its own deprecated file/test — 1 remaining consumer
(spine-stage/StandingDrill.tsx still renders <StandingTrack>).
StandingStrip and the v3 Card/Inline/Hero variants already have zero
external consumers.
3. No hand-rolled `backdrop-filter`/`backdropFilter` outside
globals.css and the overlays CSS — repo-wide (not fairway-scoped,
matching AUDIT.md's "six bespoke recipes" being a repo-wide finding):
18 files (8 CSS/module.css, 10 .tsx inline styles), spanning golf auth,
baseball onboarding, marketing and admin surfaces in addition to
Fairway's own Toolbar stuck-glass recipe.
4. No `position: sticky` element whose className also carries a
blur-tier class (fw-glass-chrome / bare fw-frost / fw-glass-strong /
fw-glass-regular) — 10 files, mostly sticky calendar headers/footers
on fw-glass-chrome, plus Toolbar's own tested sticky+frost combo.
5. Every fairway barrel's component-shaped export (PascalCase,
function/forwardRef/memo-shaped) is named in registry.ts — the
reverse of registry.test.ts's existing "every registry name is a
real export" check, so it lives in this file instead of duplicating
that one. 103 unregistered component exports today, mostly the
several barrels (view-header, calendar, forms, command,
notifications, cards-insight, data-table) whose own doc comments
already say "ADDITIVE ONLY — nothing existing imports this," staged
ahead of registry entries.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
…s note Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
…te stage, continuous date rail Golf mobile lane of the Fairway facelift (docs/design/fairway-facelift/ screens/calendar.mobile.md and dock.mobile.md, written first). Calendar (phone): - Masthead is one matte sticky bar (bg-surface + foot hairline) on the --fw-z-sticky tier; the legacy .fw-glass-chrome + arbitrary box-shadow are gone. Matte, not frost, on purpose: a backdrop-filter over the scrolling stage is re-blurred every scroll frame on a phone. - FairwayDayStrip is one continuous sunken rail with a tinted matte island for the selected day; density dots come from the canonical eventPresentation.ts tone table (the strip's and the drawer's private copies are removed); selection haptic fires only on an actual change. - The agenda renders the whole period inside ONE matte Surface with hairline day sections and opaque pinned day headings (no per-day cards, no blur inside the stage, no z-[8]/rounded-xl/inline shadows); the member rail is a hairline row; the month sits on the same stage; the skeleton mirrors the new first paint. - The event sheet is Sheet material="frost" (bottom on phone; the desktop right inspector stays matte by the primitive's own rule): matte sticky header, Your response as one InsetGroup, metadata InsetGroup, the coach's responses as StatMatrix (its first consumer), People / Files / Attendance as InsetGroup rows, sticky Sheet.Footer block CTA. Performance (owner requirement): - People and Files mount after the sheet settles (onAnimationEnd, 360 ms fallback for reduced motion), so the open translate animates a light tree; the only blur is the settled frost panel. - One getEventRSVP per open: EventPeopleSection accepts the coach's attendees as a prop and fetches for itself only for a player or as its Retry path. - The agenda's minute clock lives in today's rows only (TodayRows); BucketRows and FairwayEventCard are memoized; visibleBuckets is memoized with a Set. No framer layout under pages/calendar. Dock and More sheet: - FairwayBottomNav imports matchActive from more-nav.ts (one matcher), More is a native control sharing the tabs' class and selection haptic, the haptic is skipped when re-tapping the active tab, the active island is capped at a third of the dock. - MoreNavSheet is Sheet material="frost" (no fw-glass-chrome); each rail section is one InsetGroup of seam rows with bare glyphs, no icon tiles. - MoreSheetFooter drops the legacy ui/button; Sign out is a quiet danger text row. Verified: vitest app-shell (79) + pages/calendar + mobile audit (303), eslint on every changed component, tsc --noEmit clean. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NGiUYKH3fXJrVnxPxH2h8Z
…e sticky tier, one RSVP fetch per open - The phone FAB moves from z-[19] to z-[var(--fw-z-sticky)] and from an inline box-shadow to the shadow-raise token; its bottom offset still follows --fw-mobile-nav-height. - The coach's getEventRSVP result now also feeds the drawer's People section (attendees prop): null while in flight, the list once loaded, undefined on failure so the section's own Retry path takes over. Kept separate from the calendar/dock commit so the desktop calendar work can rebase over this file alone. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NGiUYKH3fXJrVnxPxH2h8Z
New round entry uses the Fairway Input. The three ui/confirm-dialog consumers (general settings, notification settings, classes) move to ModalShell-based confirms. ChartTooltip uses the glass blur utility and the strong glass background token instead of hardcoded values. The command palette's glass surface becomes a thin wrapper over the canonical GlassSurface (exported as CommandGlassSurface; the command barrel keeps the GlassSurface alias). InstrumentPanel's headers describe the matte recipe it actually renders. Left in place: four ui/button uses in new round entry that pass haptic="none", which the Fairway Button cannot express yet; the onboarding/join/admin loading screens, which the design-system rule keeps on the legacy skeleton. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
The screen was a masthead over a stack of rounded cards. It is now the four regions round-review.v3.md asks for, and it reads as a field sheet: a bare masthead with the verdict typeset large, ONE stage carrying a new page-local instrument, three ledger columns divided by vertical hairlines, and a dense hole-by-hole table. RoundShape is the instrument, and it is genuinely new work rather than a chart dropped in a box: percentage-geometry columns that draw the round's shape from `momentumData` — a bar per hole above or below the par line, amber over, green under, deep green for a hole two or more under — with the cumulative running total drawn across them as one non-scaling polyline and the finishing figure labelled where the line ends. Its column model serves the degraded stage too: a scorecard-only round plots its neighbouring rounds through the same instrument instead of a different one. Every number, bar and clause is sourced. The verdict names the best stretch and the worst three-hole window from the hole deltas and drops either clause when the holes cannot support it; a scorecard-only round says so in a sentence and grows no invented series. Readout deltas come from `getStatAverages`, the player's own recent rounds, because the trend row carries only score. `round-shape.ts` holds all of it as pure functions so the arithmetic is testable without a DOM. `RoundSGSummary` and `ReviewBreakdown` keep their data behind the masthead's "Full breakdown" rather than being deleted for the sake of the new composition. The retired `FilmstripReview`/`ReviewHero` pair left one audit test reading a file that no longer exists; it now asserts the affordance that replaced the hint copy. tsc clean, eslint clean over the changed files, 204 tests passing across the round review and review page scopes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
… trend The stage's ScoreField row pairs a window-filtered avg/bars series with a recent_trend delta that is deliberately never re-derived per window (it has to match the Attention ledger/table/masthead, which are all all-time) — so a narrowed window could show one or two rounds beside a trend describing a much longer history, with nothing on screen saying so. Flagged by the facelift lead against their own Home-page fix for the same aggregation- mismatch class (headline number vs. the series next to it). Recomputing trend per window was considered and rejected: the canonical five-vs-five classifier needs ~8 in-window rounds for a signal, so most players would read "no read" at this screen's 90D default, and the stage's own trend could then disagree with the ledger/table for the same player in the same viewport — reintroducing the exact contradiction the all-time choice was made to prevent. Fixed by stating the split in the stage's caption instead: no data or classification changes. Also re-verified the ledger's xl split against LANGUAGE.md's breakpoint rule, rewritten as a floor while this screen was mid-build (00a73b9): captured 768/1024/1280/1440 and confirmed no player name clips in either ledger column or the table at any width. roster.v3.md's Result section is updated to match the current rule text and records both fixes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
`loading.tsx` is the first thing a navigation to this screen paints, and it was reproducing the old page: a `max-w-6xl` shell, a centred bordered card with an icon stack, a 3-up mini-stat row and two stacked rows. The v3 field sheet has no cards and a different shell, so every visit flashed a layout the page never renders and then reflowed. It now mirrors the page's own loading branch exactly — the same shell, the masthead silhouette, one stage block — so the route fallback hands off to the page with no shape change. Also records in round-review.v3.md that removing this page's framer-motion wrappers left `golf/dashboard/premium-components.tsx` reachable from no route. It was that file's only importer. Deleting a module in the shared dashboard tree is outside this screen's ownership, so it is reported here rather than removed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
The screen was a hero card above a toolbar above one Surface holding two seam-lists: four stacked containers that said nothing a table would not. It now follows the page language. A bare masthead on canvas states the verdict, ONE Surface holds the stage, a bare ledger row of hairline columns sits under it, and the dense table closes the page. The stage is QualifyingField: every qualifier on one shared date axis, each bar running from the entry deadline through the last day of play. The pale half is the waiting period and is drawn only when entry_deadline was actually recorded, so a missing column reads as missing rather than as a period we invented. entry_deadline, selection_state, selection_slots_total, selection_slots_coach_pick and num_rounds have been arriving on every page load and rendering nowhere; this is where they start counting. There are no per-row rails. The ground is vertical month gridlines. A rail under every bar turns a short mark into a handle parked on a slider, which is the one shape this instrument must never produce. A one-day qualifier is a position rather than a length, so it renders as a stroke carrying its own date, and every mark states its own window in mono beside it instead of asking a coach to measure four percent against a tick strip at the bottom of the field. Production carries two qualifier rows dated year 60824, which a date column accepts. Plotted, they stretched the axis across fifty-eight thousand years, collapsed every real bar, and drove the monthly tick loop through roughly seven hundred thousand iterations. DOMAIN_LIMIT_YEARS and isPlottable keep such a row out of the domain and state its own condition in place of a bar; MAX_TICK_STEPS in ScoreField is the backstop for any caller that forgets to bound its domain. The table renders two branches, a stacked list below md and the table at md and up, both always in the DOM with CSS choosing between them. A nine-column table at 390px either scrolls sideways or truncates the one column that identifies the row. Nothing reads a breakpoint at runtime. `today` arrives as a bare YYYY-MM-DD string from the route, so no clock is read during render and the server markup and the first client paint agree. Verified: 58 tests across the qualifiers suite, the mapped src/test/coachhelm/v3/qualifying.test.ts check (15), tsc clean, eslint clean, and desktop and phone captures reviewed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
Round detail draws the same picture round review does, so the instrument stops being page-local. `RoundShape.tsx` becomes `HoleField.tsx` and is rewritten to stand on its own: it declares `HoleFieldColumn`, `HoleFieldPoint`, `HoleFieldLine` and `HoleFieldDivider`, owns `polylinePoints` and `holeFieldCap`, and imports nothing from this page. The dependency runs one way, type-only, from `round-shape.ts` into the instrument. It is still not in the fairway barrel or the registry. Its props stay a column model rather than `HoleBreakdown[]`. `HoleBreakdown` belongs to `round-review-system`, so taking it would tie every future caller to the review's view model, which is the coupling a shared instrument has to avoid. A caller maps what it has into columns and keeps captions and headings on its own side. `cap` is now optional and derives from the columns, so rendering the field needs no helper import at all. Two tests carry the facts this screen can get wrong. A round can report `holes_played: 18` while having zero `golf_holes` rows, so a regression test pairs that count with an empty `holeByHole` and asserts the scorecard-only verdict, no instrument and no table: every hole-level branch reads the rows, never the count. And `hole-field-real-round` carries the 18 real rows of round 2f343331 out of the database and asserts the whole path against them, including a genuine three-way tie for the worst window and a level run that cannot claim to be under par. tsc clean, eslint clean, 214 tests passing across the round review and review page scopes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
The stage heads a column Avg and so does the table, and they are different numbers: the stage averages only the selected window while the table averages the whole career. On the live roster that is Mason Rivers at 78.0 above and 75.6 below, three strokes apart, under one word. The stage's eyebrow scopes itself to the window but cannot scope a table eight hundred pixels further down the page. The table column is now "Avg all-time". Verified: 69 tests across the roster suite. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
Team stats asked its question in the wrong order. The roster board came
first and the team's strokes gained sat two screens below it in a Bento
cell, so a coach read nine rank columns before learning which column
mattered, then had to hold the leak in their head while scrolling back.
The page is now one instrument. CategoryField puts two registers on ONE
column grid: the team's signed strokes gained per category on top, the
roster's ranks in the same five columns below. The eye falls down the
amber column and lands on the players who own it, with no memory step in
between. Around it, a bare masthead whose verdict names the leak, its
owner and the roster's trajectory; three bare readouts; a hairline
ledger of where it leaks, who leads each and the fundamentals; a dense
category-detail table where a row opens; and the two leak maps as a bare
diptych, a different subject that keeps its charts and loses its boxes.
Deleted, not restyled: ViewHeader on the populated path, the sticky
StatMatrix band, MatrixBoard, the Bento and all four BentoCells, the
InstrumentPanel around the leak charts, the RailBars plus nested
StatMatrix pair, the duplicate sgTakeaway caption, and the
useMediaQuery('(min-width: 940px)') hook. The header label switch is now
two spans gated by one CSS breakpoint, so exactly one label is in the
accessibility tree at any width and nothing reads a breakpoint at
runtime.
Honesty, kept and tightened. A missing strokes-gained reading renders an
en dash and never a zero-length bar, which would claim the team sits
exactly on the Tour baseline. A trend count of zero states the gate
rather than "0 climbing, 0 sliding". A failed rounds fetch is no longer
able to render as a cold start: the "appears once players log rounds"
sentence is gated strictly on a real cold start, and the arrow glyph is
gone from the most-improved label.
All page-level derivations move to a pure, JSX-free team-stats-logic.ts
with its own unit tests. CategoryField is deliberately page-local and is
a promotion candidate only: modules/index.ts, modules/types.ts and
registry.ts are untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
Replaces the masthead + StatMatrix + Toolbar + Surface-of-seam-rows deck with the coach language: a bare masthead, ONE Surface holding the Due field, a bare three-column ledger, then a dense table. Due field is a new page-local instrument (not in modules/, the barrel or registry.ts): one lane per person who still owes open dated work, on a shared due-date axis with an interior Today rule. Overdue marks rise as amber bars scaled by days late; a date not yet reached is a neutral stroke. Ground is vertical gridlines, never a per-row rail. Work nobody owns gets its own terminal lane rather than borrowing a name. Every derivation moves to a pure, JSX-free tasks-field-logic.ts with 47 unit tests. The masthead's worst offender and each lane's "Worst late" figure are one function at two call sites, so they cannot disagree. Honesty and hydration: - `today` is a bare YYYY-MM-DD prop resolved once by the route; nothing in the tree reads a clock during render and date-only columns parse at local midnight. - Every count derives from `tasks` against that one day instead of the hook's fetch-time stats, which read a bare date as UTC midnight and so counted a task due TODAY as overdue in any US zone. - A task is not late on its own due day. - A load error outranks an empty list, which outranks a zero count. - An impossible due date leaves the axis and keeps its place in Load, the ledger and the table. Deletes the useMediaQuery desktop/phone fork: both table branches are in the DOM with CSS choosing. Both loading states now render one shared FairwayTasksSkeleton so neither can drift from the composition. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
The login step filled both fields and clicked Sign in. On a busy dev server the fill lands before React hydrates, and a fill that lands before hydration is discarded when React attaches: the form looks filled, the button stays disabled, and the click retries against a control that will never enable. Each attempt then burned its full thirty-second timeout, so a failed login cost two minutes and returned "0/0 surfaces captured" with no usable reason. It now checks whether Sign in actually enabled, retypes both values if it did not, gives up on an attempt in eight seconds instead of thirty, and says which of the three ways the attempt failed. Verified by capturing coach/round-review, which had just failed twice this way and succeeded on the first attempt afterwards. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
Read at full size at 1440, the first pass had six things wrong with it. "AROUND THE GREEN" wrapped over three lines in a 4.5rem category track, which is the clipping failure LANGUAGE.md's breakpoint floor exists to prevent, and no viewport width fixes it because the track is fixed. The header carries the head noun instead; the full name is still on the page in the verdict, in the leak ledger and as the sort control's accessible name, and the old "Short game" contradiction stays fixed. The readouts rail carried three items where the coach home carries four, and `justify-between` over a rail as tall as a nine-row field pushed them so far apart the column read as empty rather than as a stack. "Team SG / rd" printed its unit twice, once in the label and once inside the value string. The label keeps the unit; the value is now the number. The team register's identity line said "4 rds in 30d" beside strokes gained pooled over every round a player has logged, which captions the row with a window it is not measured over. The stage legend said "Tap", which is not what a coach at 1440 does. And every leak map printed "Leak Map" as a second heading under its own SectionHead: that is `LeakMap`'s default title, the component's own name leaking into the page. Both halves now pass their real title, kept in the DOM as the chart's accessible name and hidden visually, so each half shows one heading. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
… the rebuild Two regressions from the field-sheet rewrite, found while re-verifying against the lead's second note (both predate it — this rebuild fully replaced FairwayCoachRoster.tsx and silently dropped two earlier fixes made to the old MatrixBoard-era file): - The container's symmetric `md:px-8` lost 191162a's `md:pr-24` right clearance for the fixed "Ask CoachHelm" pill (bottom-6/right-6, h-14, 80px footprint). Restored as `md:pl-8 md:pr-24`. - The Attention column's two empty-state strings used em dashes ("Nothing to assess yet — attention flags..."), against the house style 191162a already established for this exact page (periods only). Rewritten as two sentences. Also fixes a new regression from a concurrent commit (8371a55, "Avg all-time"): at true phone width (393px) the wider header text pushed the table's own overflow-x-auto past its visible width by 77px, silently clipping the Trend value and the row's actions button — 0px page-level overflow the whole time, since the clipping is internal to the table's own scroll container (same failure shape as the earlier min-w-[720px] bug). Root cause measured directly: the Player cell's inline intent pill alone was ~110px of the ~282px cell, more than the table had left over. Fixed by hiding the intent control below `md` (same trade already made for Rounds/SG:Total/Focus) and giving the "Avg all-time" header an `aria-label` so a phone-safe "All-time" span can render without changing the accessible name the existing test asserts on. Verified at 393px with a Playwright measurement (not just the page-level overflow check that missed this the first time): table wrapper scrollWidth now equals clientWidth (0px internal overflow); Trend and Actions cells both land fully inside the visible 353px. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
…p finding At 390px the field opened with the Scoring column off the right edge and the active sort column cut in half, because the specified 3.25rem tracks plus a legible identity floor are wider than the 326px a phone leaves inside the page gutter and the Surface's padding. A swatch sliced down the middle reads as broken, not as "there is more over here". 7.5rem of identity plus five 2.5rem tracks is 320px, so every column and a whole player name fit with no scrolling; a 2.5rem track still clears the 34px rank swatch and the 36px team bar, and the scroller stays for anything narrower. The leak maps computed an honest one-line finding and then never showed it: ChartFrame takes `takeaway` for the chart's spoken label only. Both halves now print it above the plot as well. Records the build and its thirteen deviations in the screen spec, and writes the page's standing rules into the feature doc the registry names for these files: one Surface, two registers on one shared column grid, an en dash for absent, a stated gate instead of a zero trend count, a failed fetch that is never a cold start, and no breakpoint read at runtime. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
Per facelift lead: the "Ask CoachHelm" launcher is fixed bottom-6/right-6 and pinned to the viewport at every scroll position, so page- or table-local padding only clears content that happens to sit at the page's own bottom edge — a row mid-list still passes under it while scrolling. Asymmetric md:pl-8/md:pr-24 also shifted the whole masthead left of the page's true center for a fix that doesn't hold. Reverting FairwayCoachRoster.tsx's container back to symmetric md:px-8; the lead is taking the launcher's own scroll-aware/icon-only behavior upstream as a cross-page product decision. The two em-dash-to-period copy fixes in roster-parts.tsx (kept, not part of this revert) and the phone-width table fix (Avg-header aria-label + hiding the intent pill below md) both stand — neither was in question. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
…module `StandingTrack` was the instrument this pass replaced. Every consumer migrated to `StandingBars`, its ratchet allowlist reached zero, and the file then had no importer and no route, which is what `route-reachability.test.ts` failed on. `premium-components.tsx` lost its only importer when the round review stopped drawing a card. Both are deleted rather than left as files a future reader has to prove dead again. The ratchet keeps its `StandingTrack` exemption as an empty list rather than dropping it, so the scan still reads as "everything except the component's own file" if the file is ever restored. Also fixes a false positive in storage-buckets-tracked. `bucket` is not only a storage word: the notification feed's day grouping uses it for today/yesterday/earlier, and the observability program uses it for a severity classification. The option regex cannot tell either apart from a storage id, so it reported "earlier" as a bucket the app addresses and no migration creates. The four classification words are now excluded by VALUE rather than by file, so a real storage bucket referenced from the same file is still caught, and the KNOWN_MISSING_BUCKETS escape hatch the test explicitly forbids for this purpose stays untouched. route-reachability still reports three CoachHelm components orphaned by the /intelligence rebuild. That screen is being rewritten right now, so what it reaches is not yet settled and they are deliberately left alone. Verified: storage-buckets-tracked 4/4, src/test/static 42/42. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
Appends the Result section IMPLEMENTING.md requires: the files, twelve deviations with their reasons (one clock as a prop, derived counts over the hook's UTC-midnight stats, not-late-on-the-due-day, the "Load" rename off a colliding header word, no baseline rail, inline detail in place of a portalled Sheet, the 5/3/4 ledger), the spec citations that drifted, and what is not verified. Also records the screen's contract in the feature doc the knowledge map names for these paths. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
The label track was a fixed 44px. "Field Avg" at caption size needs about 55, so the reference row on every strokes-gained metric rendered "Fiel…" and hid which reference the bar was measured against, which is the only thing that row exists to say. Reported from the round review's third ledger column at its 3/12 width, but it was never width-dependent: the label was clipped everywhere the component rendered. The track now sizes to the longest label in the group. The grid's first column is already `auto`, so one width still serves every row and the bars stay aligned; a max width keeps a pathological label from eating the bar. Verified: StandingBars 25/25. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
The screen was a deep-green Spine beside a three-pane ResizableWorkspace: a queue, a dossier and a permanently docked chat panel, with a leak band above them built on a taxonomy the filter chips beside it did not share. Two client-measured breakpoints decided what rendered. It now follows the page language. A bare masthead states the verdict with real links, ONE Surface holds the stage, a bare ledger row of hairline columns sits under it, and the dense table closes the page. The stage is LeakRail: every raw signal category ranked by the strokes it is putting at risk, summed from |strokeImpact| over the per-player signals that carry one. There are no per-row rails. The ground is a vertical zero rule and a vertical across-category mean rule, both spanning every row. A rail under a bar turns a short mark into a handle parked on a slider, and a tick sitting mid-track on eight rows is eight sliders. Every row states its own figure in mono beside it instead of asking a coach to measure it against a distant axis. A roster roll-up's strokeImpact is the SUM of per-player leaks already in the same list, so the rail and the queue skip it and the table still lists it. Those are two different counts, so they are captioned apart: the Open signals readout counts everything and says how many roll-ups that includes; the Queue column counts what a coach can actually triage. A category where no signal carries a measured impact reads "Not measured", never a zero-length bar indistinguishable from a genuinely zero leak. getSignalGroups failing forces groups to [], the same shape an all-clear queue has, so every branch keyed on it is gated behind groupsError. Focus areas and Outcomes awaiting read different queries and keep rendering with their own states; blanking a read that succeeded is the same bug pointed the other way. Both useMediaQuery calls are gone with the components they gated. The table renders a stacked list and a dense table, both always in the DOM with CSS choosing, because at 390px a six-column table either scrolls sideways or truncates the column that says whose signal it is. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
Every clause in the summary is guarded on its own count being non-zero except the par clause, which printed unconditionally. A scorecard-only round carries a total score and no golf_holes rows, so its narrative read "0 pars" — a hole-by-hole fact derived from no holes at all, and the only clause in the sentence, leaving a dangling separator behind it. The par count is the right clause to print at zero for a round we did measure hole by hole: eighteen bogeys and no pars is a true sentence. So the guard is on having hole rows, not on the count. When no clause survives, the sentence ends after the score rather than trailing a stray period. The rendered page was already fixed in buildReviewViewModel; this is the same rule at the source that writes the narrative. Verified: 3 new tests, including the eighteen-bogey case that must still say "0 pars". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
…ty track `truncate` sets `white-space: nowrap`, which makes a flex item's automatic minimum size its whole unbroken line. The row already carried `min-w-0`, but the truncating child did not, so a long player name refused to shrink and spilled out of the fixed identity track instead of clipping inside it. Current rosters have short names, which is the only reason this has not shown up in a capture. Found by helmv3-20 from the other end: they traced 4px of overflow on player development at every width, 1440 included, to a truncated suggestion row whose longest line wanted 262px inside a 92px cell. The general rule is worth keeping: a column that wraps holds its content whole at any width, so `truncate` is what turns a layout question into a breakpoint question. Verified: ScoreField 5/5. Audited every other `truncate` on the facelift screens; the rest already carry `min-w-0` or sit in a table cell with an explicit max width. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
`role="row"` on the `<Link>` overrode the anchor's implicit link role, so a screen reader announced a row of cells with nothing to say the row went anywhere. The row is now a `div role="row"` with a real link on the player name, and the anchor's `after` pseudo-element stretches across the row so the whole row stays one click target without a div click handler or a second tab stop. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
`scoringTrend` is the only series on the player payload long enough to be a stage: `recentRounds` is sliced to five rows, this one covers every scored round. But the map formatted the date away, to "Sep 10" with no year, so two rounds twelve months apart produce the same label and any axis built from those strings is fabricated. `date` now carries the round's own `round_date` as a bare YYYY-MM-DD. Purely additive, one field on one map, no existing consumer changes. `label` keeps its exact current text and is documented as display-only. Asked for by helmv3-20 for the player home stage, which would otherwise have had to plot ordinal position and label its axis "rounds in order". Made here rather than there because this file is mine and the edit is two lines, so a cross-session collision is not worth the coordination. Verified: dashboard-data 24/24. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
Three CI type failures, all traceable to one decision: I withdrew the
full-project typecheck from every implementer while the machine was
under load. Vitest does not typecheck, so each of these passed its
local test run and failed only on the branch.
* `FairwayRoundRow.tsx` and its test were untracked in the working
tree while the committed `FairwayRoundsLibrary` imports them
(TS2307, and a `check:cycles` unresolved-file failure on the same
specifier). Both are restored here; the props match the committed
call site exactly, including `seasonAvgToPar`.
* `round-review-content.ts` does not declare `RoundData` or
`HoleBreakdown` — it imports them, type-only, from the server-action
module, so re-importing them from there was TS2459. The test now
names their real home. `import type` is erased at compile, so this
still does not pull `'use server'` into the test.
* `dashboard-data.ts` had a SECOND `scoringTrend` construction site —
the monthly team series at line 713 — that I missed when `date` was
added to the interface. A month bucket has no single round date, so
its `date` is the first of the bucket month: enough to place the
point on a real axis, which the year-less `label` cannot do. The
field doc now says so instead of claiming every point carries its
own round's date.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
The v3 rebuild had unified both personas onto one dense <table>,
deleting FairwayRoundRow.tsx and losing the player's MicroBar, FIR/GIR
quick stats, and Best-of-period badge. That row belongs to a different
session's byte-identical-player contract, so it stays. The coach-only
dense table is the actual v3 deliverable; RoundsTable now takes only
`groups`/`onNavigate` (no `userRole` branch) and RoundsTableGroup grew
`bestId`/`hasMultiple`, used solely by the restored player row's
"Best" badge — a COACH ONLY comment on RoundsTable warns against
re-merging the two row presentations; extract a shared date/formatter
helper instead if duplication grows.
Also, two shared-code fixes surfaced while reworking this screen:
* Zero-rounds EmptyState carried its own filled primary button
("Log your first round") alongside the header's own primary ("New
round"), for both personas. Demoted the empty-state button to
secondary rather than removing it — it's the player's only forward
path on a truly empty screen, and this is a defect fix, not a
v3-scope change.
* The ledger row's two-column split had `xl:divide-x` but nothing
for the earlier breakpoints where it stacks to one column on
phone, leaving no hairline between Score scatter and the grouped
list. Added `divide-y` as the base state, cancelled at `md`.
The coach table is bare per LANGUAGE.md's "one Surface" rule (the
scatter stage is the page's only Surface); the player ledger keeps its
pre-facelift Surface wrapper unchanged.
Verification: eslint clean and vitest 80/80 (7 files) on this screen's
directory, both under the shared machine's vitest lock. Full-project
tsc was withdrawn from implementers this pass per instruction; skipped
here. Deviations and their reasons are recorded in this screen's spec
Result section, including two honestly-unresolved items: no fresh
player-persona capture was obtained today (dev-server crashes, lock
contention, and login races each cost an attempt), and one desktop
capture showed an empty scatter chart once, not reproduced on an
identical phone capture — investigated, not conclusively resolved.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
…Focus column Undoes the `hidden md:block` on the row's intent control from the prior commit. It was the wrong fix for the phone table-overflow bug: pill, name, avatar, and actions checked out for reachability, but hiding the control removed a coach's only way to set intent on a phone entirely — FairwayPlayerCard (its other render site) no longer renders anywhere now that the roster is a table, and FairwayPlayerProfile has no intent control either. Rebuilt the same way QualifiersTable already does this (qualifiers-parts.tsx, commit db18ee5): a `md:hidden` stacked `<ul>` row (name + year badge, trend, a mono avg/rounds/SG meta line, then the intent control and actions menu each with a real, untruncated touch target) alongside the existing dense table wrapped `hidden md:block`. Both branches stay in the DOM with CSS choosing, so nothing reads a breakpoint at runtime. The table only renders at `md` and up now, so the phone-only "All-time" header span and its `aria-label` workaround come back out — "Avg all-time" is always plain text with room to spare there. Also records the lead's decision to decline the stage's Focus column outright (duplicates the table/ledger, costs the rounds strip real width, answers a different question than the stage asks) in roster.v3.md's Result section, replacing the earlier "blocked pending lead" framing — it was reviewed and declined, not left unanswered. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
…abel Read at 768, 1024, 1280 and 1440, this screen failed at exactly 1024. The three ledger columns and the stage's two-column split both engaged at a width that cannot hold them: "SG: Approach" broke mid-word to "Appro / ach", the leak categories truncated to "Co…" and "Sh…", and the instrument was squeezed into roughly 290px. The stage split, the ledger grid, the readouts column and the table's Drive and Approach columns move from `lg:` to `xl:`, so 1024 gets one full-width column and the splits arrive at 1280 with room to stand. That also retired the "Fiel…" truncation in the third ledger column without touching StandingBars: at an xl third it prints "Field Avg" in full. The par row had the same disease one level down. "Par 4" in column one truncated to "PA…" at 768 and "P…" at 1024, because a column is an eighteenth of the width. The word is now a row label in the instrument's right lane beside "Hole", and the columns carry bare numerals the way a scorecard prints them. The lane is a real flex track mirroring the column stack, so each label aligns with its row by structure rather than by an offset, and it renders only when it has something to hold. The season view uses it for "Date" and "To par". tsc clean, eslint clean, 214 tests passing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
…the sweep
The dead-code sweep deleted files the navigation docs still name, and it
deleted enough of them to move two generated counts. Neither shows up in
a test run; both fail the branch.
DOCS THE SWEEP INVALIDATED
* `memory/projects/golfhelm.md` — regenerated. The server-action count
was 205 and is 196. Its hand-written hook table also still listed
`use-auto-save-round` and `use-offline-sync`, both long gone, under a
heading that claimed twelve hooks; that is now ten and accurate.
* `memory/features/admin-platform.md` — `admin-people-data.ts` and
`admin-bi-data.ts` went with the unreachable admin pages that called
them, so the feature doc no longer lists them.
* `memory/features/shot-tracking.md` — the line said round persistence
was `use-offline-sync.ts`. It was already a correction of an earlier
dead path, and the correction is now dead too. It names
`src/lib/offline/sync-engine.ts`, which is what actually runs.
* `docs/generated/contracts/admin_platform.md`, `WORLD_MODEL.*` and
`retrieval-bench.json` — regenerated from those sources.
Result: `docs:path-drift` 15 unresolved → 0, and `docs:dead-refs` 22 →
4, which is the baseline exactly. The 4 that remain are the selfheal
and admin-page references that predate this branch.
LINT
* `capture-golf-facelift.mjs` — eight `no-undef` errors, all inside
`page.evaluate` callbacks, whose bodies are serialised and run in
the browser. The globals are real there and absent here. Declared by
name rather than widening the file's whole env to `browser`, which
would also silence a genuine mistake in the Node half.
* `Menu.test.tsx`, `Tooltip.test.tsx`, `boards.test.tsx` — twelve
`helm/no-raw-button` warnings against a baseline of zero. The raw
`<button>` in each is a FIXTURE: these cases assert that the wrapper
accepts an arbitrary element and does not reach into it. Swapping in
the design-system `<Button>` would test that component's markup
instead of this one's contract. Disabled with that reason; the rule
stands everywhere a user can actually see the button.
* `.supabase-error-baseline.json` — 1030 → 1006. The branch paid down
24 unchecked reads and the audit refuses to leave that gap open,
because an unlocked paydown is how #1326 got silently reverted.
Not fixed here: `helm/no-empty-collection-on-error` is 48 against a
baseline of 47. The new site is `getRoundReviewTrend`, which answers a
failed read with `[]` — the same value that means "this player has no
scored rounds." That is a real defect and the fix ends on the round
review sheet, so it is assigned to the agent holding that file rather
than papered over with a disable that would have to claim empty is
right here. It isn't.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
`getRoundReviewTrend` returned `[]` from its `if (error)` branch and its `catch`, which is the same value that means "this player has no scored rounds". A coach shown a blank season trend concludes the player has no history, and nothing on the page contradicts that. The two ERROR paths now return `null`. The three DENIAL paths keep `[]`, because a malformed id, a viewer without access and a round that is not this player's all honestly mean "nothing to plot for you". The doc comment, which promised `[]` on any failure, now says which value means which. The page turns `null` into its own `trendUnavailable` state rather than folding it into the empty case, and an unexpected throw sets it too and clears the rows, so one player's season can never sit under another player's name. The stage gets a fourth branch: "Season trend unavailable" in the same quiet register as the other absences on this sheet, with the caption dropping its promise of a trajectory it could not read. Not an empty chart, which would be the lie, and not an error banner, which would make a secondary absence the loudest thing on the page. eslint clean on the four files, 207 tests passing across the review page and round review scopes, including a new case for the null path. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
The facelift specs added 867 violations against a baseline of 18711, all
of them in `docs/design/fairway-facelift/`. Nothing here changes what a
spec says.
* 592 were MD013 line-length. These are prose paragraphs, wide ASCII
layout diagrams and inline Tailwind class strings; a hard 80-column
wrap makes an ASCII diagram unreadable rather than tidier. One
`.markdownlint.jsonc` at the root of the spec tree turns MD013 off
for it and nothing else, which is what AGENTS.md already declares
for itself on its own first line. Every other rule still applies —
that is the point of scoping it to one rule in one directory rather
than updating the baseline.
* 226 were mechanical and are now fixed rather than excused:
blanks around headings, lists and fences, list indentation, ordered
prefixes, trailing newline, bullet style, and compact table pipes.
* Six `.v2.md` specs opened with a title and then a second H1 five
lines below it. The second is a section, so it is now an H2.
* Nine fenced blocks had no language; they are layout sketches, so
`text`.
* One prose line in `home.v2.md` began with "- " mid-sentence and
parsed as a stray list; reflowed. One `<date>` placeholder in
`player-development.v2.md` parsed as an HTML tag; it is a
placeholder, so it is code now.
867 → 33. The 33 that remain are all in `roster-player.v3.md`, which is
uncommitted in another agent's working tree and is theirs to fix; it was
deliberately left out of this pass so the fix would not land on top of
their unfinished Result section.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
The Fairway premium facelift for the coach side of GolfHelm. Every coach screen was a masthead sitting on a deck of rounded cream boxes. The owner's words were that it looked basic and like the product was going backwards. This branch replaces that idiom with a written page language and rebuilds each screen against it.
Do not merge yet. This is opened for review at the owner's request.
The page language
docs/design/fairway-facelift/LANGUAGE.mdis binding, and every screen spec cites it. One anatomy, in DOM order:Surface, holding one real instrument.Green is ink. Amber means over par or urgent. Nothing else is coloured.
Two rules earned their place the hard way and are worth reading before reviewing:
No dot-on-a-rail. A mark sitting on a full-width rail reads as a handle parked on a slider. Instruments use vertical ground, or a zero rule that marks genuinely rise above and fall below, or no ground at all. Every mark states its own value in mono beside it rather than asking the reader to measure it against a distant axis. The component that embodied the old pattern,
StandingTrack, is deleted here.The breakpoint rule is a floor, not a number. A split is legal only at the width where every column still holds its content whole. A fixed rail beside a flexible instrument may not split before
xl. A fractional split shares the loss and may sit lower, but must be captured there. Verified at 768, 1024, 1280 and 1440, and on a phone.Screens rebuilt
Coach home, calendar, roster, player detail, rounds library, round review, qualifiers, team stats, tasks and intelligence. Each has a spec under
docs/design/fairway-facelift/screens/*.v3.mdwith a## Resultsection recording every deviation and why.Correctness fixes found along the way
These are not cosmetic. Each was a number or a sentence that lied.
StandingBarsclipped its own reference label to "Fiel…" everywhere it rendered, hiding which reference the bar was measured against.ScoreField's identity track.truncatesetswhite-space: nowrap, which makes a flex item's minimum size its whole unbroken line.Defence against real production data
Two
golf_qualifiersrows carry astart_datein the year 60824, which adatecolumn accepts. Plotted, they stretched the axis across roughly fifty-eight thousand years, collapsed every real bar to a hairline, and drove a monthly tick loop through about seven hundred thousand iterations.DOMAIN_LIMIT_YEARSandisPlottablekeep such a row out of the domain and render it as "Dates outside the plotted range" instead of a bar, andMAX_TICK_STEPSinScoreFieldis the backstop for any caller that forgets to bound its own domain. The rows themselves are still wrong and still there.Mobile
Mobile is in scope. A wide table at 390px either scrolls sideways or truncates the column that identifies the row. Screens render two branches, a stacked list and the table, both always in the DOM with CSS choosing between them, so no breakpoint is read at runtime and the server render and first client paint agree.
Two things a reviewer should know before reading the history
02a9266fdand82ce0ab49swept up files because staging was too broad.02a9266fddeleted the player's round row component and its test, which is a real mistake: an implementer then built on a tree where the player's row had vanished, so the coach's dense table briefly rendered for players too. Both the component and its test are restored, and the role branch is fixed. A third,83d72a570, looks alarming at twenty-nine files but is the intended dead-code sweep, recorded afterwards in77927200e.Verification
Per-screen: scoped vitest, eslint on changed files, and desktop and phone captures read at full size at each of the four desktop widths and on a phone. Feature docs updated through
npm run knowledge:map. Repo-wideroute-reachabilityandstorage-buckets-trackedwere red and are addressed; the remaining reachability entries are CoachHelm components orphaned by the intelligence rebuild.Full typecheck, lint and build run under the required GitHub checks on this branch.
Still landing
Several implementers are still committing into this branch, so expect further commits on the same PR.
🤖 Generated with Claude Code
https://claude.ai/code/session_011zGybqwusmeKC2jJhsU9JC