Skip to content

fix: app-tab Rule-8 sweep — roster/stats/pipeline/scorecard/qualifier become real phone layouts#801

Merged
njrini99-code merged 1 commit into
mainfrom
fix/app-mobile-rule8
Jul 10, 2026
Merged

fix: app-tab Rule-8 sweep — roster/stats/pipeline/scorecard/qualifier become real phone layouts#801
njrini99-code merged 1 commit into
mainfrom
fix/app-mobile-rule8

Conversation

@njrini99-code

Copy link
Copy Markdown
Owner

Problem

The Bridge mobile sweep's scouts (#800) found the same defect classes on the app side: min-w tables and unwrapped control rows forcing horizontal scroll at 390px on phone-primary surfaces (golf calendar hero, baseball roster wall / stats record book / pipeline list, golf round scorecard, qualifier round-by-round, dev-plan filter tabs).

Fix / Outcome

Every surface gets a hand-composed phone treatment below md (or lg for pipeline, mirroring Academics); desktop stays byte-identical:

  • Calendar hero: grid nav row + full-width primary CTA in the thumb zone.
  • Roster wall / stats record book: doctrine-8 card rows with the 2-3 stats a coach actually triages by (AVG/OPS, ERA/WHIP); leaders per column set.
  • Pipeline list: card rows below lg; plus a real desktop bug fixed — the list row's avatar stacked above the name (Button single-child contract).
  • Round scorecard: a real phone scorecard (per-hole strips, tone-colored to-par pills, nine totals) — not a squeezed table. Qualifier breakdown: rank/name/total cards with per-round chips.
  • Dev-plan goal tabs: contained edge-bled scroller below md.

Verification

5 packets each fixer→adversarial-verify (all correct/correct-with-nits) + a re-scout of baseball player surfaces. tsc ✓ eslint ✓ vitest 975 ✓. Local prod build died on ENOSPC (disk, not code) — CI build is the gate here.

Git Activity Timeline note

App-side Rule-8 mobile sweep: 6 phone-primary surfaces get real phone layouts; 1 desktop render bug fixed.

🤖 Generated with Claude Code

https://claude.ai/code/session_01J2E46URHkhCnEQSYDtZLJX

… tables become real phone layouts

The Bridge-sweep scouts found the same disease on the app side: min-w
tables and unwrapped control rows forcing horizontal scroll at 390px on
phone-primary surfaces. Each fix keeps md+ byte-identical:

- Golf calendar hero: phone composition = grid nav row (prev/today/next)
  + full-width primary action in the thumb zone (was a single unwrapping
  4-control row that overflowed the Surface at 390px).
- Baseball roster wall (default surface): full-width PlayerRowPlate rows
  with AVG/OPS below md; the 5-column min-w-[680px] wall stays at md+.
- Baseball stats center record book: 2-stat self-labeled card rows below
  md (AVG/OPS hitting, ERA/WHIP pitching), leaders computed per column
  set; ghost ready-rows get matching placeholders.
- Baseball pipeline list view: card rows below lg mirroring the
  Academics split; desktop table unchanged except the list row's avatar
  no longer stacks above the name (Button single-child contract — real
  desktop render bug, wrapped in one flex child).
- Golf round scorecard: real phone scorecard — per-hole strips (hole
  chip · par · tone-colored score + to-par pill · putts · FW/GIR marks)
  + nine-total rows; the min-w table stays md+. Qualifier round-by-round
  becomes rank/name/total card rows with per-round chips.
- Baseball dev-plan goal filter tabs ride a contained edge-bled
  scroller below md (4 nowrap triggers exceeded 390px content width).

Verification: every packet adversarially verified (all correct /
correct-with-nits); tsc, eslint --max-warnings 0, vitest 975 tests green.
Local production build hit ENOSPC (disk, not code) — CI build is the
authoritative gate on this PR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J2E46URHkhCnEQSYDtZLJX
@cursor

cursor Bot commented Jul 10, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@vercel

vercel Bot commented Jul 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
helmv3 Ignored Ignored Preview Jul 10, 2026 4:12pm

Request Review

@supabase

supabase Bot commented Jul 10, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project qmnssrrolpinvwjjnufo because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Rule-8 mobile layouts for calendar, roster/stats, pipeline, scorecard & qualifiers

🐞 Bug fix ✨ Enhancement 🕐 40+ Minutes

Grey Divider

AI Description

• Replace overflow-x tables with true phone-first layouts below md/lg.
• Keep md+ / lg+ desktop render byte-identical while improving 390px usability.
• Fix Pipeline list row avatar/name stacking caused by Button single-child wrapping.
Diagram

graph TD
  A["Rule-8 responsive split"] --> B["Calendar hero"] --> C["Roster wall"] --> D["Stats record book"] --> E["Pipeline list"] --> F["Round scorecard"] --> G["Qualifier breakdown"] --> H["Dev-plan tabs"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Generic ResponsiveTableToCards wrapper component
  • ➕ Reduces repeated md:hidden/md:block split boilerplate across surfaces
  • ➕ Centralizes accessibility/spacing conventions for card-row lists
  • ➕ Easier to roll out Rule-8 to future tables consistently
  • ➖ Hard to generalize: each surface has different “headline” fields and interactions
  • ➖ Risk of over-abstraction while the team is still discovering the doctrine patterns
  • ➖ May constrain bespoke phone compositions (e.g., scorecard hole strips)
2. Keep tables + overflow-x scrolling (status quo)
  • ➕ Minimal code churn
  • ➕ Lowest risk to semantics/interaction parity
  • ➖ Explicitly violates Rule 8 on reading surfaces
  • ➖ Still creates horizontal scroll at 390px and poor tap targets
  • ➖ Doesn't address table min-w / control-row overflow defects

Recommendation: The PR’s explicit breakpoint split (phone-first composed layouts + byte-identical desktop tables) is the right call for Rule-8 compliance and minimizes desktop regression risk. Consider extracting a small helper only after 2–3 more Rule-8 conversions prove a stable shared pattern (e.g., a tiny utility for “phone card list + desktop table” scaffolding), but avoid forcing the scorecard/qualifier layouts into a premature generic abstraction.

Files changed (7) +785 / -199

Enhancement (5) +720 / -195
PipelineClient.tsxAdd <lg pipeline card rows + fix Button children layout bug +192/-7

Add <lg pipeline card rows + fix Button children layout bug

• Introduces a Rule-8 card-row rendering below lg (select, status, updated date, notes edit, actions) while preserving the lg+ table. Also fixes a desktop rendering bug where Avatar + name stacked vertically inside Button by enforcing a single flex child wrapper consistent with Button’s children contract.

src/app/baseball/(dashboard)/dashboard/pipeline/PipelineClient.tsx

RosterFairway.tsxPhone roster wall: full-width rows with AVG/OPS headline stats +57/-17

Phone roster wall: full-width rows with AVG/OPS headline stats

• Adds a mobile-specific stat builder (AVG/OPS) and renders a md:hidden full-width list of PlayerRowPlate rows for phone. Keeps the existing md+ five-column roster wall unchanged behind md:block.

src/app/baseball/(dashboard)/dashboard/roster/RosterFairway.tsx

StatsCenterClient.tsxPhone record book: 2-stat self-labeled card rows + mobile leaders +107/-12

Phone record book: 2-stat self-labeled card rows + mobile leaders

• Splits StatSpread into md+ (existing 6-col plate) vs <md (full-width rows carrying only headline stats). Adds MOBILE_* column specs plus separate leader-set computation so mobile OPS/ERA/WHIP highlighting remains correct, and updates ghost rows to match self-labeled mobile cards.

src/components/baseball/stats-center/StatsCenterClient.tsx

FairwayQualifierDetail.tsxQualifier round breakdown: phone card rows + shared rank computation +160/-80

Qualifier round breakdown: phone card rows + shared rank computation

• Reworks the coach-only round-by-round breakdown into md:hidden card rows (rank, player link, per-round chips, total/to-par) while preserving the original md+ matte table. Moves ranking into a single precomputed map so phone and desktop renders stay consistent without shared mutable counters.

src/components/fairway/pages/qualifiers/FairwayQualifierDetail.tsx

FairwayRoundDetail.tsxPhone scorecard: per-hole strips with to-par pills (md+ table preserved) +204/-79

Phone scorecard: per-hole strips with to-par pills (md+ table preserved)

• Introduces a md:hidden per-hole strip layout (hole, par, score, to-par pill, putts, FW/GIR marks) plus a nine-total row, matching golfer expectations on mobile. Retains the md+ matte table as-is and adds cn() usage for tone/pill styling in the new phone row component.

src/components/fairway/pages/rounds/FairwayRoundDetail.tsx

Bug fix (2) +65 / -4
DevPlanClient.tsxMake dev-plan goal tabs horizontally scrollable on phone +9/-2

Make dev-plan goal tabs horizontally scrollable on phone

• Wraps the TabsList in a contained, edge-bled overflow-x scroller below md to prevent trigger overflow at ~390px. Keeps md+ behavior unchanged (overflow visible, no bleed).

src/app/baseball/(dashboard)/dashboard/dev-plan/DevPlanClient.tsx

FairwayCalendarHero.tsxPhone calendar hero: grid nav row + full-width primary CTA +56/-2

Phone calendar hero: grid nav row + full-width primary CTA

• Keeps the existing md+ single-row control cluster intact, but adds a md:hidden phone composition with a centered Prev/Today/Next row and a separate full-width primary action in the thumb zone. Prevents control-row overflow on phone-primary surfaces.

src/components/fairway/pages/calendar/FairwayCalendarHero.tsx

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

Summary by CodeRabbit

  • Enhancements
    • Improved mobile and tablet layouts across development plans, pipelines, rosters, stats, calendars, qualifiers, and round details.
    • Added mobile-friendly pipeline cards with selection, status updates, notes, profile viewing, and removal actions.
    • Added compact mobile stat views for batting, pitching, and roster summaries.
    • Added phone-optimized calendar navigation and primary actions.
    • Added responsive mobile layouts for qualifier standings and nine-hole scorecards.
    • Preserved full desktop table and scorecard experiences on larger screens.

Walkthrough

Responsive mobile layouts were added across baseball dashboards and Fairway pages. Desktop presentations remain available at larger breakpoints, while mobile views use horizontally scrollable tabs, cards, compact stat rows, phone-specific controls, qualifier cards, and per-hole scorecard strips.

Changes

Responsive UI surfaces

Layer / File(s) Summary
Baseball dashboard mobile views
src/app/baseball/(dashboard)/dashboard/dev-plan/DevPlanClient.tsx, src/app/baseball/(dashboard)/dashboard/pipeline/PipelineClient.tsx
Dev-plan tabs gain horizontal scrolling, while pipeline list entries gain mobile cards with selection, stage updates, notes, profile, and removal actions.
Baseball roster and stats layouts
src/app/baseball/(dashboard)/dashboard/roster/RosterFairway.tsx, src/components/baseball/stats-center/StatsCenterClient.tsx
Roster and Stats Center pages add mobile-specific stat rows, leader calculations, ghost states, and responsive record-book layouts.
Calendar mobile actions
src/components/fairway/pages/calendar/FairwayCalendarHero.tsx
Phone layouts separate calendar navigation from the full-width primary action while retaining the desktop action cluster.
Qualifier responsive breakdown
src/components/fairway/pages/qualifiers/FairwayQualifierDetail.tsx
Round breakdowns use phone cards and desktop tables, with shared precomputed positions for both render paths.
Mobile scorecard strips
src/components/fairway/pages/rounds/FairwayRoundDetail.tsx
Phone scorecards render per-hole strips with par, score, to-par, putts, and FW/GIR indicators; desktop tables remain unchanged at md+.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested labels: design-system

🚥 Pre-merge checks | ✅ 4 | ❌ 8

❌ Failed checks (1 warning, 7 inconclusive)

Check name Status Explanation Resolution
Title check ⚠️ Warning Uses a Conventional Commits type, but omits any required scope like baseball or golf, so it does not satisfy the title rule. Rewrite as fix(): , choosing the primary scope such as baseball or golf.
No Service-Role In Client Bundles ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
Rls Coverage On New Tables ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
Auth Check In Server Actions ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
Sport-Prefixed Table Names ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
No Destructive Writes ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
No Edits To Historical Migrations ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
Conventional Commits ❓ Inconclusive Repository clone failed, so this custom check could not run with code access. Retry the review run. If this persists, inspect pre-merge custom-check logs for infrastructure or agent runtime failures.
✅ Passed checks (4 passed)
Check name Status Explanation
Description check ✅ Passed The description clearly matches the UI/layout changes in the patch and is on-topic.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/app-mobile-rule8
  • 🛠️ helm safety pass
  • 🛠️ dashboard ux pass
  • 🛠️ rls test pass

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ast-grep (0.44.1)

ast-grep could not parse rule config: /ast-grep-rules/../git/.coderabbit/ast-grep/no-explicit-any.yml

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

src/app/baseball/(dashboard)/dashboard/dev-plan/DevPlanClient.tsx

ESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox.

src/app/baseball/(dashboard)/dashboard/pipeline/PipelineClient.tsx

ESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.

src/app/baseball/(dashboard)/dashboard/roster/RosterFairway.tsx

ESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.

  • 4 others

Comment @coderabbitai help to get the list of available commands.

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📜 Skill insights (1)

Context used
✅ Compliance rules (platform): 98 rules

Grey Divider


Action required

1. Select-all set mismatch 🐞 Bug ≡ Correctness
Description
PipelinePage’s new mobile select-all/deselect-all row decides “all selected” via
selectedPlayers.size === filteredWatchlist.length, which can be true even when the selected IDs
differ from the filtered IDs. This can show the wrong label and cause toggleSelectAll to clear or
replace unrelated selections when filters change.
Code

src/app/baseball/(dashboard)/dashboard/pipeline/PipelineClient.tsx[R1337-1339]

+                      <Button variant="ghost" size="sm" onClick={toggleSelectAll}>
+                        {selectedPlayers.size === filteredWatchlist.length && filteredWatchlist.length > 0 ? 'Deselect all' : 'Select all'}
+                      </Button>
Relevance

⭐⭐⭐ High

PipelineClient correctness issues (selection state) have been addressed/accepted recently in PR
#550.

PR-#550

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
selectedPlayers is independent state and is not reset when filters change, while
filteredWatchlist is derived from the current filters. The new mobile header uses a size-equality
comparison to decide the label, and toggleSelectAll uses the same comparison to clear/replace the
selection, which is incorrect unless the selected set is exactly the filtered set.

src/app/baseball/(dashboard)/dashboard/pipeline/PipelineClient.tsx[780-838]
src/app/baseball/(dashboard)/dashboard/pipeline/PipelineClient.tsx[958-976]
src/app/baseball/(dashboard)/dashboard/pipeline/PipelineClient.tsx[1333-1340]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The new mobile select-all/deselect-all UI uses a cardinality comparison (`selectedPlayers.size === filteredWatchlist.length`) to determine whether all filtered players are selected. Because `selectedPlayers` persists independently of filtering, this can misclassify the state (same size, different elements) and the action can clear/overwrite selections that are not part of the current filtered list.

### Issue Context
- `selectedPlayers` is maintained as a Set and is not cleared when `filterTab`, `positionFilter`, or `gradYearFilter` changes.
- `filteredWatchlist` is recomputed from those filters.
- The mobile header label and the desktop header checkbox both use the same size-based check.

### Fix Focus Areas
- src/app/baseball/(dashboard)/dashboard/pipeline/PipelineClient.tsx[970-976]
- src/app/baseball/(dashboard)/dashboard/pipeline/PipelineClient.tsx[1333-1340]

### What to change
1. Compute an `allFilteredSelected` boolean via set inclusion, e.g.:
  - `filteredWatchlist.length > 0 && filteredWatchlist.every(i => selectedPlayers.has(i.id))`
2. Update the mobile button label (and desktop checkbox checked state) to use `allFilteredSelected`.
3. Update `toggleSelectAll` to act on the filtered IDs set-wise:
  - If `allFilteredSelected`, remove only the filtered IDs from `selectedPlayers` (don’t necessarily clear everything).
  - Else, add missing filtered IDs to `selectedPlayers` (don’t necessarily replace the entire set).
  This avoids wiping selections that are outside the current filtered subset.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Hole number hidden 🐞 Bug ≡ Correctness
Description
ScorecardHoleRow renders the hole number with aria-hidden="true" and does not provide any other
accessible hole identifier. Screen-reader users will hear the row’s stats but not which hole they
correspond to.
Code

src/components/fairway/pages/rounds/FairwayRoundDetail.tsx[R758-763]

+      <span
+        aria-hidden="true"
+        className="grid h-8 w-8 shrink-0 place-items-center rounded-full bg-surface-sunken font-fw-mono text-body-sm font-semibold tabular-nums text-text-secondary"
+      >
+        {hole.hole_number}
+      </span>
Relevance

⭐⭐ Medium

A11y fixes sometimes accepted (PR #209) but similar UI/a11y nits often rejected (PR #290).

PR-#209
PR-#290

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Within ScorecardHoleRow, {hole.hole_number} appears only inside a span marked
aria-hidden="true", and the remaining visible labels are Par/Putts/FW/GIR—none identify the hole
number for assistive tech.

src/components/fairway/pages/rounds/FairwayRoundDetail.tsx[729-770]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
In the new phone scorecard layout, the hole number is the primary identifier for each row, but it is explicitly hidden from assistive technologies (`aria-hidden="true"`). There is no alternative accessible label like “Hole 3”, so the row loses critical context for screen-reader navigation.

### Issue Context
`ScorecardHoleRow` is used to render the phone-only per-hole strips inside a `<ul>`. The only place the hole number is rendered is in the `aria-hidden` span.

### Fix Focus Areas
- src/components/fairway/pages/rounds/FairwayRoundDetail.tsx[733-770]

### What to change
Pick one of these approaches:
1. Remove `aria-hidden` from the hole number span so it is announced.
2. Keep the decorative styling but add an explicit accessible label, e.g.:
  - Add `<span className="sr-only">Hole {hole.hole_number}</span>` at the start of the row, and/or
  - Add `aria-label={`Hole ${hole.hole_number}`}` to the `<li>` (or `aria-labelledby` wiring).
Ensure the hole identifier is announced before the stat values.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

3. Non-scale spacing in scorecard 📜 Skill insight ⚙ Maintainability
Description
New Tailwind spacing utilities use off-scale values (e.g., gap-2.5, py-2.5, px-1.5, py-0.5,
gap-1.5) that do not match the required spacing scale. This violates the spacing-token compliance
requirement and can reduce consistency and layout rhythm across components.
Code

src/components/fairway/pages/rounds/FairwayRoundDetail.tsx[R757-778]

+    <li className="flex items-center gap-2.5 py-2.5 first:pt-0 last:pb-0">
+      <span
+        aria-hidden="true"
+        className="grid h-8 w-8 shrink-0 place-items-center rounded-full bg-surface-sunken font-fw-mono text-body-sm font-semibold tabular-nums text-text-secondary"
+      >
+        {hole.hole_number}
+      </span>
+      <div className="w-8 shrink-0 leading-tight">
+        <p className="font-fw-display text-eyebrow font-medium uppercase tracking-[0.06em] text-text-tertiary">
+          Par
+        </p>
+        <p className="font-fw-mono text-body-sm font-medium tabular-nums text-text-secondary">
+          {p ?? '—'}
+        </p>
+      </div>
+      <div className="flex min-w-0 flex-1 items-center gap-2">
+        <span className={cn('font-fw-mono text-h3 font-semibold tabular-nums', scoreTone)}>
+          {s ?? '—'}
+        </span>
+        <span
+          className={cn(
+            'shrink-0 rounded-full px-1.5 py-0.5 font-fw-mono text-caption font-medium tabular-nums',
Relevance

⭐ Low

Token/spacing-scale enforcement suggestions have been rejected before (e.g., token-based spacing in
PR #274).

PR-#274

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1519417 restricts padding/margin/gap to the defined spacing scale
(4/8/12/16/24/32/48/64px). The updated scorecard row/pill styles add gap-2.5 and py-2.5 (10px)
plus px-1.5/gap-1.5 (6px) and py-0.5 (2px), which are explicitly called out as off-scale and
therefore non-compliant with the rule.

src/components/fairway/pages/rounds/FairwayRoundDetail.tsx[757-778]
src/components/fairway/pages/qualifiers/FairwayQualifierDetail.tsx[438-473]
Skill: modern-saas-ui

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`FairwayRoundDetail.tsx` and `FairwayQualifierDetail.tsx` introduce multiple Tailwind spacing utilities (e.g., `gap-2.5`, `py-2.5`, `px-1.5`, `py-0.5`, `gap-1.5`) that are not on the approved spacing scale, violating the spacing-token compliance requirement.

## Issue Context
Rule / PR Compliance ID 1519417 requires all spacing values (padding/margin/gap) to use the defined spacing scale tokens: 4/8/12/16/24/32/48/64px. The current implementations include 10px (`2.5`), 6px (`1.5`), and 2px (`0.5`) values, which are off-scale and can lead to inconsistent layout rhythm across the UI.

## Fix Focus Areas
- src/components/fairway/pages/rounds/FairwayRoundDetail.tsx[757-778]
- src/components/fairway/pages/qualifiers/FairwayQualifierDetail.tsx[438-473]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment on lines +1337 to +1339
<Button variant="ghost" size="sm" onClick={toggleSelectAll}>
{selectedPlayers.size === filteredWatchlist.length && filteredWatchlist.length > 0 ? 'Deselect all' : 'Select all'}
</Button>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Select-all set mismatch 🐞 Bug ≡ Correctness

PipelinePage’s new mobile select-all/deselect-all row decides “all selected” via
selectedPlayers.size === filteredWatchlist.length, which can be true even when the selected IDs
differ from the filtered IDs. This can show the wrong label and cause toggleSelectAll to clear or
replace unrelated selections when filters change.
Agent Prompt
### Issue description
The new mobile select-all/deselect-all UI uses a cardinality comparison (`selectedPlayers.size === filteredWatchlist.length`) to determine whether all filtered players are selected. Because `selectedPlayers` persists independently of filtering, this can misclassify the state (same size, different elements) and the action can clear/overwrite selections that are not part of the current filtered list.

### Issue Context
- `selectedPlayers` is maintained as a Set and is not cleared when `filterTab`, `positionFilter`, or `gradYearFilter` changes.
- `filteredWatchlist` is recomputed from those filters.
- The mobile header label and the desktop header checkbox both use the same size-based check.

### Fix Focus Areas
- src/app/baseball/(dashboard)/dashboard/pipeline/PipelineClient.tsx[970-976]
- src/app/baseball/(dashboard)/dashboard/pipeline/PipelineClient.tsx[1333-1340]

### What to change
1. Compute an `allFilteredSelected` boolean via set inclusion, e.g.:
   - `filteredWatchlist.length > 0 && filteredWatchlist.every(i => selectedPlayers.has(i.id))`
2. Update the mobile button label (and desktop checkbox checked state) to use `allFilteredSelected`.
3. Update `toggleSelectAll` to act on the filtered IDs set-wise:
   - If `allFilteredSelected`, remove only the filtered IDs from `selectedPlayers` (don’t necessarily clear everything).
   - Else, add missing filtered IDs to `selectedPlayers` (don’t necessarily replace the entire set).
   This avoids wiping selections that are outside the current filtered subset.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +758 to +763
<span
aria-hidden="true"
className="grid h-8 w-8 shrink-0 place-items-center rounded-full bg-surface-sunken font-fw-mono text-body-sm font-semibold tabular-nums text-text-secondary"
>
{hole.hole_number}
</span>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

3. Hole number hidden 🐞 Bug ≡ Correctness

ScorecardHoleRow renders the hole number with aria-hidden="true" and does not provide any other
accessible hole identifier. Screen-reader users will hear the row’s stats but not which hole they
correspond to.
Agent Prompt
### Issue description
In the new phone scorecard layout, the hole number is the primary identifier for each row, but it is explicitly hidden from assistive technologies (`aria-hidden="true"`). There is no alternative accessible label like “Hole 3”, so the row loses critical context for screen-reader navigation.

### Issue Context
`ScorecardHoleRow` is used to render the phone-only per-hole strips inside a `<ul>`. The only place the hole number is rendered is in the `aria-hidden` span.

### Fix Focus Areas
- src/components/fairway/pages/rounds/FairwayRoundDetail.tsx[733-770]

### What to change
Pick one of these approaches:
1. Remove `aria-hidden` from the hole number span so it is announced.
2. Keep the decorative styling but add an explicit accessible label, e.g.:
   - Add `<span className="sr-only">Hole {hole.hole_number}</span>` at the start of the row, and/or
   - Add `aria-label={`Hole ${hole.hole_number}`}` to the `<li>` (or `aria-labelledby` wiring).
Ensure the hole identifier is announced before the stat values.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/app/baseball/`(dashboard)/dashboard/dev-plan/DevPlanClient.tsx:
- Around line 562-570: Update the responsive tab scroller wrapper inside the
Goals Tabs section of DevPlanClient by adding sm:-mx-6 sm:px-6 alongside the
existing -mx-4 px-4 classes, preserving the md overrides. Apply the same
responsive padding adjustment to the related wrapper at the additionally
referenced location.

In `@src/app/baseball/`(dashboard)/dashboard/pipeline/PipelineClient.tsx:
- Around line 718-723: Replace the arbitrary min-h-[44px] classes on the Save,
Cancel, and controls referenced around the note actions in PipelineClient with
the existing shared Button/IconButton size variants or contract. Remove one-off
control height styling while preserving the intended mobile touch target and
layout.
- Around line 643-649: The PaperCard in the pipeline item is a clickable div
without keyboard semantics, conflicting with its nested controls. Remove
onClick={onFocus} from PaperCard and invoke onFocus from the relevant
interactive controls, or convert the container to an accessible interactive
element with Enter/Space handling and visible focus styling while preserving
nested-control behavior.
- Around line 1333-1339: Update toggleSelectAll in PipelineClient.tsx to scope
selection changes to filteredWatchlist: count selected IDs that are currently
visible, deselect only those visible IDs when all visible rows are selected,
otherwise add all visible IDs while preserving hidden selections. Update the
button label condition to use the visible selected count rather than
selectedPlayers.size.
- Around line 655-678: Update the document-level keyboard handler near the list
navigation logic to return early when the event target is inside a button, link,
element with role="button", or an editable control, in addition to the existing
input, textarea, and select checks. Use this guard before handling Enter/Space
so nested controls in the PipelineClient button and checkbox sections retain
their native keyboard activation.

In `@src/components/fairway/pages/calendar/FairwayCalendarHero.tsx`:
- Around line 178-183: The Today navigation buttons in FairwayCalendarHero are
incorrectly marked with aria-pressed, which is only for toggle controls. Remove
aria-pressed from both the desktop and mobile Today buttons, or replace it with
an appropriate accessible label such as “Today, current date.”

In `@src/components/fairway/pages/qualifiers/FairwayQualifierDetail.tsx`:
- Around line 450-480: The phone totals in the qualifier row are unlabeled for
screen readers. In the mobile totals block near the player link, add visually
hidden “Total” and “To par” labels associated with the respective values, or
restructure the block as a labeled definition list while preserving the existing
visual layout and conditional score formatting.
- Around line 450-480: Replace the `rounded-fw-sm` classes in the player `Link`
and round-score `<span>` within the qualifier detail rendering with an approved
radius token: `rounded-2xl`, `rounded-xl`, or `rounded-lg`, consistently
following the path instructions.

In `@src/components/fairway/pages/rounds/FairwayRoundDetail.tsx`:
- Around line 628-640: Replace the non-approved rounded-fw-md and rounded-full
classes in the total display block and the corresponding section around the
related FairwayRoundDetail markup with approved rounded-2xl, rounded-xl, or
rounded-lg utilities, preserving the intended visual hierarchy and avoiding any
other radius tokens.
- Around line 756-807: The hole row currently hides its identity and exposes
score metadata only through visual abbreviations and indicators. In the
row-rendering JSX around the hole number, add an accessible “Hole N” label and
explicit accessible labels for the score, to-par value, fairway status, and GIR
status; update or augment the existing `HitMark` usage as needed so assistive
technology receives text rather than relying on visual dots, while preserving
the current visual layout.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 23a923b7-ee4a-461c-acdb-4a6c0292e895

📥 Commits

Reviewing files that changed from the base of the PR and between 1035e6b and b05d262.

📒 Files selected for processing (7)
  • src/app/baseball/(dashboard)/dashboard/dev-plan/DevPlanClient.tsx
  • src/app/baseball/(dashboard)/dashboard/pipeline/PipelineClient.tsx
  • src/app/baseball/(dashboard)/dashboard/roster/RosterFairway.tsx
  • src/components/baseball/stats-center/StatsCenterClient.tsx
  • src/components/fairway/pages/calendar/FairwayCalendarHero.tsx
  • src/components/fairway/pages/qualifiers/FairwayQualifierDetail.tsx
  • src/components/fairway/pages/rounds/FairwayRoundDetail.tsx

Comment on lines +562 to +570
{/* Goals with Tabs. The four icon+label+badge triggers are
whitespace-nowrap and sum past the ~338px content width at
390px, and TabsList has no scroll handling of its own — so
below md the list rides a contained edge-bled scroller
(matching the page's px-4 shell) instead of overflowing the
page. md+ is untouched (overflow-visible, no bleed). */}
<Tabs defaultValue="active" value={activeTab} onChange={setActiveTab}>
<TabsList className="mb-4">
<div className="-mx-4 mb-4 overflow-x-auto px-4 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden md:mx-0 md:overflow-visible md:px-0">
<TabsList>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Match the tab scroller to the responsive page shell (DevPlanClient.tsx:569).

The page switches to sm:px-6, but this wrapper remains -mx-4 px-4 until md. At 640–767px, the tabs therefore do not align with the shell edge. Add sm:-mx-6 sm:px-6.

As per path instructions, mobile screens must use consistent page padding and spacing.

Also applies to: 584-584

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/baseball/`(dashboard)/dashboard/dev-plan/DevPlanClient.tsx around
lines 562 - 570, Update the responsive tab scroller wrapper inside the Goals
Tabs section of DevPlanClient by adding sm:-mx-6 sm:px-6 alongside the existing
-mx-4 px-4 classes, preserving the md overrides. Apply the same responsive
padding adjustment to the related wrapper at the additionally referenced
location.

Source: Path instructions

Comment on lines +643 to +649
<PaperCard
onClick={onFocus}
className={cn(
'p-4',
pressableClass({ ink: 'pursuit', lift: true }),
focused && 'ring-2 ring-inset ring-pursuit',
)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not make the card container a keyboardless clickable <div> (PipelineClient.tsx:643-649).

PaperCard defaults to a div, yet it receives onClick={onFocus} and has no keyboard semantics. Remove the wrapper click and invoke onFocus from the relevant controls, or provide an accessible interaction model that does not conflict with the nested controls.

As per path instructions, clickable non-button elements must support Enter/Space keyboard handling and visible focus behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/baseball/`(dashboard)/dashboard/pipeline/PipelineClient.tsx around
lines 643 - 649, The PaperCard in the pipeline item is a clickable div without
keyboard semantics, conflicting with its nested controls. Remove
onClick={onFocus} from PaperCard and invoke onFocus from the relevant
interactive controls, or convert the container to an accessible interactive
element with Enter/Space handling and visible focus styling while preserving
nested-control behavior.

Source: Path instructions

Comment on lines +655 to +678
<Button
variant="ghost"
onClick={onOpenPeek}
rightIcon={<IconChevronRight size={16} aria-hidden className="text-text-tertiary" />}
className="h-auto min-h-0 flex-1 justify-between gap-3 rounded-fw-sm px-2 py-1 text-left font-normal"
>
{/* Fairway's <Button> renders non-icon children inside a single bare
<span> (no className) as a direct child of its inline-flex
container — that span gets blockified, and an inline Avatar
followed by a block-level name/school div inside it triggers
anonymous-block-box generation (Avatar stacks above the name
instead of beside it). An explicit flex wrapper here establishes
its own flex formatting context so Avatar + text lay out as a
real identity row, matching the Avatar+name idiom used elsewhere
(MessagesClient.tsx, AcademicsClient.tsx) instead of relying on
the Button's auto-wrapping children slot. */}
<div className="flex min-w-0 flex-1 items-center gap-3">
<Avatar src={item.player?.avatar_url} name={name} size="md" />
<div className="min-w-0 flex-1">
<span className="block truncate font-annual text-body-lg text-text-primary">{name}</span>
<span className="block truncate text-eyebrow text-text-tertiary">{item.player?.high_school_name || 'No school'}</span>
</div>
</div>
</Button>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Let nested controls own Enter/Space (PipelineClient.tsx:655-678,738-750).

The document handler at PipelineClient.tsx:1028-1035 excludes only input, textarea, and select elements. It therefore intercepts Enter/Space on these new buttons and checkboxes, opening the peek panel or toggling row selection instead of activating the focused control. Ignore events inside button, a, [role="button"], and editable elements before handling list navigation.

As per path instructions, components handling user input must preserve normal keyboard activation for interactive controls.

Also applies to: 738-750

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/baseball/`(dashboard)/dashboard/pipeline/PipelineClient.tsx around
lines 655 - 678, Update the document-level keyboard handler near the list
navigation logic to return early when the event target is inside a button, link,
element with role="button", or an editable control, in addition to the existing
input, textarea, and select checks. Use this guard before handling Enter/Space
so nested controls in the PipelineClient button and checkbox sections retain
their native keyboard activation.

Source: Path instructions

Comment on lines +718 to +723
<Button size="sm" onClick={onSaveNote} className="min-h-[44px] flex-1">
Save
</Button>
<Button variant="ghost" size="sm" onClick={onCancelNote} className="min-h-[44px] flex-1">
Cancel
</Button>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use shared touch-target sizes instead of arbitrary heights (PipelineClient.tsx:718-723,738-750).

Replace min-h-[44px] with the existing shared Button/IconButton size contract so mobile controls do not introduce one-off control heights.

As per path instructions, do not introduce one-off spacing, radius, icon sizes, or control heights.

Also applies to: 738-750

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/baseball/`(dashboard)/dashboard/pipeline/PipelineClient.tsx around
lines 718 - 723, Replace the arbitrary min-h-[44px] classes on the Save, Cancel,
and controls referenced around the note actions in PipelineClient with the
existing shared Button/IconButton size variants or contract. Remove one-off
control height styling while preserving the intended mobile touch target and
layout.

Source: Path instructions

Comment on lines +1333 to +1339
<div className="flex items-center justify-between gap-3 lg:hidden">
<span className="font-annual text-eyebrow uppercase tracking-[0.14em] text-text-tertiary">
{filteredWatchlist.length} player{filteredWatchlist.length !== 1 ? 's' : ''}
</span>
<Button variant="ghost" size="sm" onClick={toggleSelectAll}>
{selectedPlayers.size === filteredWatchlist.length && filteredWatchlist.length > 0 ? 'Deselect all' : 'Select all'}
</Button>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Scope “select all” to visible filtered rows (PipelineClient.tsx:1333-1339).

toggleSelectAll() at PipelineClient.tsx:970-975 compares the global selection size with filteredWatchlist.length, then replaces the selection with only visible IDs. With selections retained across filters, the button can show the wrong action and silently drop hidden selections. Compare the visible selected count and add/remove only the currently filtered IDs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/baseball/`(dashboard)/dashboard/pipeline/PipelineClient.tsx around
lines 1333 - 1339, Update toggleSelectAll in PipelineClient.tsx to scope
selection changes to filteredWatchlist: count selected IDs that are currently
visible, deselect only those visible IDs when all visible rows are selected,
otherwise add all visible IDs while preserving hidden selections. Update the
button label condition to use the visible selected count rather than
selectedPlayers.size.

Comment on lines +178 to +183
<Button
variant={focusIsToday ? 'secondary' : 'ghost'}
size="sm"
className="justify-self-center"
onClick={() => onNavigate('today')}
aria-pressed={focusIsToday}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fix the Today button semantics in src/components/fairway/pages/calendar/FairwayCalendarHero.tsx:178-183.

aria-pressed describes a toggle button, but this control performs navigation. Remove it or use an accessible label such as “Today, current date”; apply the same correction to the desktop button at Line 140.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/fairway/pages/calendar/FairwayCalendarHero.tsx` around lines
178 - 183, The Today navigation buttons in FairwayCalendarHero are incorrectly
marked with aria-pressed, which is only for toggle controls. Remove aria-pressed
from both the desktop and mobile Today buttons, or replace it with an
appropriate accessible label such as “Today, current date.”

Comment on lines 450 to +480
<Link
href={`/golf/dashboard/stats?player=${playerId}`}
className="rounded-fw-sm underline-offset-2 outline-none hover:underline focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2 focus-visible:ring-offset-canvas"
className="min-w-0 flex-1 truncate rounded-fw-sm font-fw-sans text-body font-medium text-text-primary underline-offset-2 outline-none hover:underline focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2 focus-visible:ring-offset-canvas"
>
{data.playerName}
</Link>
</td>
{roundColumns.map((n) => {
const round = data.rounds.find((r) => r.roundNumber === n);
return (
<td key={n} className="px-2 py-2.5 text-center">
{round ? (
<div className="flex flex-col items-center">
<span className="font-fw-mono text-body-sm font-medium text-text-primary tabular-nums">
{round.score ?? '—'}
</span>
<span className={cn('font-fw-mono text-caption tabular-nums', toParToneClass(round.toPar))}>
{formatToPar(round.toPar)}
</span>
</div>
) : (
<span className="text-caption text-text-tertiary">—</span>
)}
</td>
);
})}
<td className="py-2.5 pl-3 text-right font-fw-mono font-medium text-text-primary tabular-nums">
{hasRounds ? data.totalScore : '—'}
</td>
<td
</div>
<div className="shrink-0 text-right">
<p className="font-fw-mono text-body font-semibold tabular-nums text-text-primary">
{hasRounds ? data.totalScore : '—'}
</p>
<p
className={cn(
'font-fw-mono text-caption tabular-nums',
hasRounds ? toParToneClass(data.totalToPar) : 'text-text-tertiary',
)}
>
{hasRounds ? formatToPar(data.totalToPar) : '—'}
</p>
</div>
</div>

{hasRounds ? (
<div className="flex flex-wrap gap-1.5">
{roundColumns.map((n) => {
const round = data.rounds.find((r) => r.roundNumber === n);
if (!round) return null;
return (
<span
key={n}
className="inline-flex items-center gap-1 rounded-fw-sm bg-surface-sunken px-2 py-1 font-fw-mono text-caption tabular-nums"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Add accessible labels to phone totals in src/components/fairway/pages/qualifiers/FairwayQualifierDetail.tsx:450-480.

The desktop table has Total and To par headers, but the phone row exposes two unlabeled values to screen readers. Add visually hidden labels or render these values as a labeled definition list.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/fairway/pages/qualifiers/FairwayQualifierDetail.tsx` around
lines 450 - 480, The phone totals in the qualifier row are unlabeled for screen
readers. In the mobile totals block near the player link, add visually hidden
“Total” and “To par” labels associated with the respective values, or
restructure the block as a labeled definition list while preserving the existing
visual layout and conditional score formatting.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use approved radius tokens in src/components/fairway/pages/qualifiers/FairwayQualifierDetail.tsx:450-480.

The new rounded-fw-sm utilities violate the path instruction permitting only rounded-2xl, rounded-xl, or rounded-lg. Replace them, or explicitly update the rule if rounded-fw-sm is the canonical Fairway token.

As per path instructions, “Reject inline hex values, ad-hoc spacing, or radius outside rounded-2xl/rounded-xl/rounded-lg.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/fairway/pages/qualifiers/FairwayQualifierDetail.tsx` around
lines 450 - 480, Replace the `rounded-fw-sm` classes in the player `Link` and
round-score `<span>` within the qualifier detail rendering with an approved
radius token: `rounded-2xl`, `rounded-xl`, or `rounded-lg`, consistently
following the path instructions.

Source: Path instructions

Comment on lines +628 to +640
<div className="mt-1 flex items-center justify-between rounded-fw-md bg-surface-tint px-3 py-2.5">
<span className="font-fw-display text-caption font-medium uppercase tracking-[0.1em] text-text-tertiary">
{label} total
</span>
<div className="flex items-baseline gap-3">
<span className="font-fw-mono text-caption tabular-nums text-text-secondary">
{puttTotal || '—'} putts
</span>
<span className="font-fw-mono text-h3 font-semibold tabular-nums text-text-primary">
{scoreTotal || '—'}
</Td>
</tr>
{/* Putts */}
<tr>
<RowLabel>Putts</RowLabel>
{holes.map((h) => (
<Td key={h.hole_number} className="text-text-secondary">
{finite(h.putts) ?? '—'}
</Td>
))}
<Td className="bg-surface-tint text-text-secondary">{puttTotal || '—'}</Td>
</tr>
{/* Fairway hit — par-3s have no fairway (fairway_hit NULL → blank dot) */}
<tr>
<RowLabel>FW</RowLabel>
{holes.map((h) => (
<Td key={h.hole_number}>
<HitMark value={h.fairway_hit} />
</Td>
))}
<Td className="bg-surface-tint" />
</tr>
{/* GIR */}
<tr>
<RowLabel>GIR</RowLabel>
{holes.map((h) => (
<Td key={h.hole_number}>
<HitMark value={h.gir} />
</span>
</div>
</div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use approved radius tokens in src/components/fairway/pages/rounds/FairwayRoundDetail.tsx:628-640,757-780.

The new rounded-fw-md and rounded-full utilities violate the path instruction permitting only rounded-2xl, rounded-xl, or rounded-lg. Replace them, or update the rule if these are intended canonical Fairway tokens.

As per path instructions, “Reject inline hex values, ad-hoc spacing, or radius outside rounded-2xl/rounded-xl/rounded-lg.”

Also applies to: 757-780

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/fairway/pages/rounds/FairwayRoundDetail.tsx` around lines 628
- 640, Replace the non-approved rounded-fw-md and rounded-full classes in the
total display block and the corresponding section around the related
FairwayRoundDetail markup with approved rounded-2xl, rounded-xl, or rounded-lg
utilities, preserving the intended visual hierarchy and avoiding any other
radius tokens.

Source: Path instructions

Comment on lines +756 to +807
return (
<li className="flex items-center gap-2.5 py-2.5 first:pt-0 last:pb-0">
<span
aria-hidden="true"
className="grid h-8 w-8 shrink-0 place-items-center rounded-full bg-surface-sunken font-fw-mono text-body-sm font-semibold tabular-nums text-text-secondary"
>
{hole.hole_number}
</span>
<div className="w-8 shrink-0 leading-tight">
<p className="font-fw-display text-eyebrow font-medium uppercase tracking-[0.06em] text-text-tertiary">
Par
</p>
<p className="font-fw-mono text-body-sm font-medium tabular-nums text-text-secondary">
{p ?? '—'}
</p>
</div>
<div className="flex min-w-0 flex-1 items-center gap-2">
<span className={cn('font-fw-mono text-h3 font-semibold tabular-nums', scoreTone)}>
{s ?? '—'}
</span>
<span
className={cn(
'shrink-0 rounded-full px-1.5 py-0.5 font-fw-mono text-caption font-medium tabular-nums',
pillTone,
)}
>
{formatToPar(toPar)}
</span>
</div>
{/* Label-over-value, mirroring the Par block — one micro-pattern
across the row's small data blocks. */}
<div className="w-10 shrink-0 text-right leading-tight">
<p className="font-fw-display text-eyebrow font-medium uppercase tracking-[0.06em] text-text-tertiary">
Putts
</p>
<p className="font-fw-mono text-body-sm tabular-nums text-text-secondary">
{finite(hole.putts) ?? '—'}
</p>
</div>
<div className="flex shrink-0 flex-col items-end gap-1 pl-1">
<span className="inline-flex items-center gap-1.5">
<span className="font-fw-display text-eyebrow font-medium uppercase tracking-[0.06em] text-text-tertiary">
FW
</span>
<HitMark value={hole.fairway_hit} />
</span>
<span className="inline-flex items-center gap-1.5">
<span className="font-fw-display text-eyebrow font-medium uppercase tracking-[0.06em] text-text-tertiary">
GIR
</span>
<HitMark value={hole.gir} />
</span>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Keep hole identity and score metadata accessible in src/components/fairway/pages/rounds/FairwayRoundDetail.tsx:756-807.

aria-hidden="true" removes the hole number, so assistive technology cannot associate the row’s values with a hole. Add an accessible “Hole N” label and explicit labels for score, to-par, fairway, and GIR instead of relying on visual abbreviations and dots.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/fairway/pages/rounds/FairwayRoundDetail.tsx` around lines 756
- 807, The hole row currently hides its identity and exposes score metadata only
through visual abbreviations and indicators. In the row-rendering JSX around the
hole number, add an accessible “Hole N” label and explicit accessible labels for
the score, to-par value, fairway status, and GIR status; update or augment the
existing `HitMark` usage as needed so assistive technology receives text rather
than relying on visual dots, while preserving the current visual layout.

@greptile-apps

greptile-apps Bot commented Jul 10, 2026

Copy link
Copy Markdown

Greptile Summary

This PR implements Rule-8 mobile layouts across 6 phone-primary surfaces — dev-plan goal tabs, recruiting pipeline, roster wall, stats center, golf calendar hero, qualifier breakdown, and round scorecard — using the established hidden lg:block / lg:hidden dual-render pattern. Desktop layouts are byte-identical; no data paths, server actions, or RLS-touching code change.

  • Pipeline: adds PipelineListCard (below lg) mirroring the AcademicsClient split; statusOptions sourced from PIPELINE_STAGES so only the 5 canonical enum values are writable; also fixes the desktop avatar-stacking bug (Button single-child contract).
  • Qualifier breakdown: pre-computes rank positions into a Map before the dual render pass — correctly prevents the old let scoredSeen = 0; ++scoredSeen mutable counter from producing wrong ranks on the second render.
  • Round scorecard: introduces ScorecardHoleRow with tone-coded score pills; parTotal is computed and shown in the nine header but omitted from the nine-total strip, leaving the nine-to-par unavailable at a glance on phone (see inline comment).

Confidence Score: 4/5

Safe to merge — all changes are layout-only with no data, auth, or business-logic paths touched.

Every changed file is pure UI: dual-render splits (phone card / desktop table), a contained tab scroller, and a new per-hole strip scorecard. The qualifier breakdown correctly pre-computes rank positions in a Map before the dual render, avoiding a shared mutable counter. Pipeline stage options flow from the canonical PIPELINE_STAGES array, so the card Select cannot write an invalid enum value. The one gap worth a follow-up: the round scorecard nine-total strip omits nine-to-par on phone even though parTotal is already computed.

FairwayRoundDetail.tsx — the phone nine-total strip has parTotal in scope but doesn't expose nine-to-par. PipelineClient.tsx — confirm the bulk-action toolbar is outside the desktop-only block so multi-select works on phone.

Important Files Changed

Filename Overview
src/app/baseball/(dashboard)/dashboard/dev-plan/DevPlanClient.tsx Wraps TabsList in a contained edge-bled horizontal scroller below md; one line of change with accurate comment explaining the overflow math. No logic changes.
src/app/baseball/(dashboard)/dashboard/pipeline/PipelineClient.tsx Adds PipelineListCard for the phone layout (below lg) and fixes the desktop avatar-stacking bug. statusOptions sourced from the canonical PIPELINE_STAGES array — only the 5 valid enum values are writable. Follows the AcademicsClient hidden/lg:block split pattern exactly.
src/app/baseball/(dashboard)/dashboard/roster/RosterFairway.tsx Adds buildWallStatsMobile (AVG/OPS only, consistent leader handling) and splits RosterWall into md:hidden / hidden md:block dual renders. Clean, no data-path changes.
src/components/baseball/stats-center/StatsCenterClient.tsx Adds MOBILE_BATTING_COLS (AVG + OPS) and MOBILE_PITCHING_COLS (ERA + WHIP), independent leader sets, and a dual-render in StatSpread. OPS added inline via b.ops (tsc ✓). buildMobileStats callbacks memoized correctly.
src/components/fairway/pages/calendar/FairwayCalendarHero.tsx Phone nav row (grid-cols-3 for centered Today) + full-width CTA below md; desktop hidden via md:flex / md:hidden split. Consistent aria-pressed on Today across both renders.
src/components/fairway/pages/qualifiers/FairwayQualifierDetail.tsx Refactors RoundBreakdownTable to pre-compute rank positions in a Map before the dual render (phone ul + md+ table) — correctly prevents a shared mutable counter from producing wrong ranks on the second pass. Card chips correctly show per-round score + to-par.
src/components/fairway/pages/rounds/FairwayRoundDetail.tsx Introduces ScorecardHoleRow and splits ScorecardNine into per-hole strip (phone) and original table (md+). parTotal is computed and shown in the header but omitted from the nine-total strip, leaving no at-a-glance nine-to-par for coaches on phone.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    subgraph Breakpoint["Responsive Split (Rule 8)"]
        BP{Viewport}
        BP -->|"< md / < lg"| PHONE[Phone card layout]
        BP -->|"≥ md / ≥ lg"| DESKTOP[Desktop table — unchanged]
    end

    subgraph Baseball["Baseball surfaces"]
        PHONE --> DPC["DevPlanClient\n— contained tab scroller"]
        PHONE --> PLC["PipelineClient\n— PipelineListCard\n  (Checkbox + Stage Select + Note)"]
        PHONE --> RFC["RosterFairway\n— PlayerRowPlate\n  AVG + OPS only"]
        PHONE --> SCC["StatsCenterClient\n— StatSpread mobile view\n  AVG+OPS / ERA+WHIP"]
    end

    subgraph Golf["Golf / Fairway surfaces"]
        PHONE --> CAL["FairwayCalendarHero\n— grid-cols-3 nav + full-width CTA"]
        PHONE --> QUAL["FairwayQualifierDetail\n— rank/name/total cards\n  + per-round chips"]
        PHONE --> RND["FairwayRoundDetail\n— ScorecardHoleRow strips\n  (hole · par · pill · putts · FW/GIR)"]
    end

    PLC --> STAGES["PIPELINE_STAGES\n(5 canonical enum values)"]
    QUAL --> POSMAP["positions Map\n(pre-computed rank\nbefore dual render)"]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    subgraph Breakpoint["Responsive Split (Rule 8)"]
        BP{Viewport}
        BP -->|"< md / < lg"| PHONE[Phone card layout]
        BP -->|"≥ md / ≥ lg"| DESKTOP[Desktop table — unchanged]
    end

    subgraph Baseball["Baseball surfaces"]
        PHONE --> DPC["DevPlanClient\n— contained tab scroller"]
        PHONE --> PLC["PipelineClient\n— PipelineListCard\n  (Checkbox + Stage Select + Note)"]
        PHONE --> RFC["RosterFairway\n— PlayerRowPlate\n  AVG + OPS only"]
        PHONE --> SCC["StatsCenterClient\n— StatSpread mobile view\n  AVG+OPS / ERA+WHIP"]
    end

    subgraph Golf["Golf / Fairway surfaces"]
        PHONE --> CAL["FairwayCalendarHero\n— grid-cols-3 nav + full-width CTA"]
        PHONE --> QUAL["FairwayQualifierDetail\n— rank/name/total cards\n  + per-round chips"]
        PHONE --> RND["FairwayRoundDetail\n— ScorecardHoleRow strips\n  (hole · par · pill · putts · FW/GIR)"]
    end

    PLC --> STAGES["PIPELINE_STAGES\n(5 canonical enum values)"]
    QUAL --> POSMAP["positions Map\n(pre-computed rank\nbefore dual render)"]
Loading

Comments Outside Diff (1)

  1. src/app/baseball/(dashboard)/dashboard/pipeline/PipelineClient.tsx, line 229-263 (link)

    P2 Bulk action toolbar visibility on phone

    When players are selected via the card Checkbox, bulk operations (stage change, remove) are powered by selectedPlayers. The desktop layout renders the bulk-action toolbar inline above the table — it's worth confirming that toolbar is visible when the card list is active (i.e., it's not hidden inside the hidden lg:block PaperCard wrapper). If the bulk action bar is a sibling of the entire list/table block and outside the lg:hidden / hidden lg:block split, it should work. But if it's rendered inside the hidden lg:block PaperCard, selected cards would have no bulk-action affordance on phone.

    Is the bulk-action toolbar (stage change dropdown + remove button for multi-select) rendered outside the hidden lg:block PaperCard, or is it inside the desktop-only block?

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/app/baseball/(dashboard)/dashboard/pipeline/PipelineClient.tsx
    Line: 229-263
    
    Comment:
    **Bulk action toolbar visibility on phone**
    
    When players are selected via the card `Checkbox`, bulk operations (stage change, remove) are powered by `selectedPlayers`. The desktop layout renders the bulk-action toolbar inline above the table — it's worth confirming that toolbar is visible when the card list is active (i.e., it's not hidden inside the `hidden lg:block` PaperCard wrapper). If the bulk action bar is a sibling of the entire list/table block and outside the `lg:hidden` / `hidden lg:block` split, it should work. But if it's rendered inside the `hidden lg:block` `PaperCard`, selected cards would have no bulk-action affordance on phone.
    
     Is the bulk-action toolbar (stage change dropdown + remove button for multi-select) rendered outside the `hidden lg:block` PaperCard, or is it inside the desktop-only block?
    
    How can I resolve this? If you propose a fix, please make it concise.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

    Fix in Claude Code

Fix All in Claude Code

Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
src/components/fairway/pages/rounds/FairwayRoundDetail.tsx:628-640
**Nine-total strip missing to-par on phone**

`parTotal` is already computed (line 604) and shown in the header (`Par {parTotal}`), but the nine-total chip only shows `scoreTotal` and `puttTotal`. On desktop a coach reads nine-to-par by comparing the Par row total with the Score row total side by side; on phone there's no equivalent — the only way to know the front/back nine was +2 is to mentally sum the per-hole chips. Adding a to-par pill next to `scoreTotal` (`scoreTotal - parTotal` when both are non-zero, using `formatToPar` + `toParToneClass`) would give coaches the same at-a-glance context and is a single-expression addition using helpers already in scope.

### Issue 2 of 2
src/app/baseball/(dashboard)/dashboard/pipeline/PipelineClient.tsx:229-263
**Bulk action toolbar visibility on phone**

When players are selected via the card `Checkbox`, bulk operations (stage change, remove) are powered by `selectedPlayers`. The desktop layout renders the bulk-action toolbar inline above the table — it's worth confirming that toolbar is visible when the card list is active (i.e., it's not hidden inside the `hidden lg:block` PaperCard wrapper). If the bulk action bar is a sibling of the entire list/table block and outside the `lg:hidden` / `hidden lg:block` split, it should work. But if it's rendered inside the `hidden lg:block` `PaperCard`, selected cards would have no bulk-action affordance on phone.

Reviews (1): Last reviewed commit: "fix: app-tab Rule-8 sweep — roster/stats..." | Re-trigger Greptile

Comment on lines +628 to +640
<div className="mt-1 flex items-center justify-between rounded-fw-md bg-surface-tint px-3 py-2.5">
<span className="font-fw-display text-caption font-medium uppercase tracking-[0.1em] text-text-tertiary">
{label} total
</span>
<div className="flex items-baseline gap-3">
<span className="font-fw-mono text-caption tabular-nums text-text-secondary">
{puttTotal || '—'} putts
</span>
<span className="font-fw-mono text-h3 font-semibold tabular-nums text-text-primary">
{scoreTotal || '—'}
</Td>
</tr>
{/* Putts */}
<tr>
<RowLabel>Putts</RowLabel>
{holes.map((h) => (
<Td key={h.hole_number} className="text-text-secondary">
{finite(h.putts) ?? '—'}
</Td>
))}
<Td className="bg-surface-tint text-text-secondary">{puttTotal || '—'}</Td>
</tr>
{/* Fairway hit — par-3s have no fairway (fairway_hit NULL → blank dot) */}
<tr>
<RowLabel>FW</RowLabel>
{holes.map((h) => (
<Td key={h.hole_number}>
<HitMark value={h.fairway_hit} />
</Td>
))}
<Td className="bg-surface-tint" />
</tr>
{/* GIR */}
<tr>
<RowLabel>GIR</RowLabel>
{holes.map((h) => (
<Td key={h.hole_number}>
<HitMark value={h.gir} />
</span>
</div>
</div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Nine-total strip missing to-par on phone

parTotal is already computed (line 604) and shown in the header (Par {parTotal}), but the nine-total chip only shows scoreTotal and puttTotal. On desktop a coach reads nine-to-par by comparing the Par row total with the Score row total side by side; on phone there's no equivalent — the only way to know the front/back nine was +2 is to mentally sum the per-hole chips. Adding a to-par pill next to scoreTotal (scoreTotal - parTotal when both are non-zero, using formatToPar + toParToneClass) would give coaches the same at-a-glance context and is a single-expression addition using helpers already in scope.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/components/fairway/pages/rounds/FairwayRoundDetail.tsx
Line: 628-640

Comment:
**Nine-total strip missing to-par on phone**

`parTotal` is already computed (line 604) and shown in the header (`Par {parTotal}`), but the nine-total chip only shows `scoreTotal` and `puttTotal`. On desktop a coach reads nine-to-par by comparing the Par row total with the Score row total side by side; on phone there's no equivalent — the only way to know the front/back nine was +2 is to mentally sum the per-hole chips. Adding a to-par pill next to `scoreTotal` (`scoreTotal - parTotal` when both are non-zero, using `formatToPar` + `toParToneClass`) would give coaches the same at-a-glance context and is a single-expression addition using helpers already in scope.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code

@njrini99-code njrini99-code merged commit d1cbc58 into main Jul 10, 2026
47 checks passed
@njrini99-code njrini99-code deleted the fix/app-mobile-rule8 branch July 10, 2026 19:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant