From 21c7bcb6be5137c1228432a6f0f9ed06c83cecfb Mon Sep 17 00:00:00 2001 From: njrini99-code Date: Wed, 10 Jun 2026 12:28:58 -0400 Subject: [PATCH] fix(a11y): eliminate all jsx-a11y warnings in non-test files + lint ratchet CI gate - 575+ jsx-a11y warnings fixed across src/app and src/components (label/control wiring via useId, clickable divs -> buttons, modal backdrops -> buttons, redundant role=list removed, justified disables for drag-drop/stopPropagation/tooltip/autofocus cases) - 0 jsx-a11y warnings remain outside 2 excluded test files - scripts/lint-ratchet.mjs: per-rule warning baseline (.lint-baseline.json, 2394 warnings/11 rules); 'npm run lint:ratchet' fails CI if any rule count rises; --update re-locks after intentional burn-down - ci.yml: ratchet step added after lint Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 3 + .lint-baseline.json | 13 ++ package.json | 1 + scripts/lint-ratchet.mjs | 164 ++++++++++++++++++ .../complete-signup/CompleteSignupClient.tsx | 2 +- .../baseball/(auth)/forgot-password/page.tsx | 1 + .../baseball/(auth)/reset-password/page.tsx | 1 + .../(coach-dashboard)/coach/layout.tsx | 1 + .../(dashboard)/dashboard/discover/page.tsx | 6 +- .../(dashboard)/dashboard/events/page.tsx | 15 +- .../(dashboard)/dashboard/pipeline/page.tsx | 18 +- .../(dashboard)/dashboard/program/page.tsx | 12 +- .../(dashboard)/dashboard/roster/page.tsx | 9 +- .../(dashboard)/dashboard/settings/page.tsx | 6 +- .../stats/games/new/NewGameClient.tsx | 13 +- .../dashboard/team/TeamDashboardClient.tsx | 1 + .../(dashboard)/dashboard/teams/page.tsx | 15 +- .../(dashboard)/dashboard/videos/page.tsx | 9 +- .../dashboard/watchlist/WatchlistClient.tsx | 15 +- .../(onboarding)/coach-onboarding/page.tsx | 1 + src/app/baseball/(onboarding)/player/page.tsx | 1 + .../(player-dashboard)/player/layout.tsx | 1 + src/app/global-error.tsx | 2 +- src/app/golf/(auth)/forgot-password/page.tsx | 1 + src/app/golf/(auth)/reset-password/page.tsx | 1 + src/app/golf/(auth)/signup/page.tsx | 1 + .../dashboard/components/CoachDashboard.tsx | 2 + .../dashboard/components/PlayerDashboard.tsx | 3 + .../development/development-client.tsx | 21 ++- .../dashboard/documents/documents-client.tsx | 48 +++-- .../(dashboard)/dashboard/messages/page.tsx | 1 + .../my-development/LogProgressButton.tsx | 5 +- .../dashboard/rounds/new/new-round-client.tsx | 4 +- .../settings/coaching-intelligence/page.tsx | 6 +- .../(dashboard)/dashboard/settings/page.tsx | 20 ++- .../golf/(dashboard)/dashboard/team/page.tsx | 2 + .../dashboard/team/team-settings-client.tsx | 16 +- .../dashboard/travel/travel-client.tsx | 45 +++-- src/app/golf/(onboarding)/coach/page.tsx | 1 + src/app/golf/(onboarding)/player/page.tsx | 1 + .../components/HealthScoreBreakdownModal.tsx | 1 + .../golf/admin/components/UserDetailPanel.tsx | 7 +- .../components/tracer/DataQualityIssueRow.tsx | 13 +- .../admin/crm/components/AddCoachModal.tsx | 36 ++-- .../admin/crm/components/BulkEmailModal.tsx | 10 +- .../admin/crm/components/CalendarView.tsx | 44 +++-- .../admin/crm/components/CoachDetailPanel.tsx | 2 + .../golf/admin/crm/components/CoachTable.tsx | 14 +- .../admin/crm/components/ContactLogModal.tsx | 12 +- .../admin/crm/components/EventDetailModal.tsx | 14 +- .../golf/admin/crm/components/ImportModal.tsx | 12 +- .../admin/crm/components/PipelineView.tsx | 1 + .../crm/components/QuickActionsPanel.tsx | 46 +++-- .../crm/components/QuickActionsToolbar.tsx | 2 + .../crm/components/ScheduleEventModal.tsx | 32 ++-- .../automations/AutomationEditor.tsx | 5 +- .../crm/components/notes/AddNoteDialog.tsx | 5 +- .../components/resend/ResendActivityView.tsx | 64 +++---- .../components/segments/SaveSegmentDialog.tsx | 5 +- .../components/segments/SavedSegmentsRail.tsx | 1 + .../sequences/EnrollSegmentDialog.tsx | 1 + .../crm/components/sequences/SequenceCard.tsx | 1 + .../components/sequences/SequencesList.tsx | 4 +- .../crm/components/tasks/CreateTaskDialog.tsx | 5 +- .../admin/crm/components/tasks/TaskCard.tsx | 1 + .../crm/components/tasks/TasksDueWidget.tsx | 1 + src/app/golf/admin/page.tsx | 4 +- src/app/golf/join/page.tsx | 1 + src/app/help/page.tsx | 6 +- .../announcements/CreateAnnouncementFlow.tsx | 4 +- .../baseball/announcements/PlayerSelector.tsx | 4 +- .../baseball/announcements/UrgencyPicker.tsx | 4 +- .../command-center/BaseballInviteButton.tsx | 11 +- .../baseball/documents/DocumentCard.tsx | 9 +- .../baseball/documents/DocumentPreview.tsx | 2 + .../documents/UploadNewVersionModal.tsx | 10 +- .../player-profile/PlayerProfileClient.tsx | 1 + .../position-planner/PositionPlanner.tsx | 4 +- .../baseball/profile/CollegeProfileEditor.tsx | 64 ++++--- .../PositionPriorityRanker.tsx | 1 + .../RecruitingWeightDistributor.tsx | 1 + src/components/baseball/roster/PlayerCard.tsx | 3 + .../baseball/roster/RosterToolbar.tsx | 6 +- .../settings/PhilosophySettingsClient.tsx | 9 +- .../baseball/stats/StatsUploadClient.tsx | 11 +- .../baseball/tasks/CreateTaskModal.tsx | 15 +- .../baseball/tasks/ReminderPicker.tsx | 14 +- .../baseball/team/BatchVideoUpload.tsx | 6 +- .../baseball/travel/CreateItineraryModal.tsx | 3 +- .../baseball/travel/ExpenseForm.tsx | 7 +- .../baseball/travel/ExpenseList.tsx | 3 + .../baseball/travel/TravelClient.tsx | 3 + src/components/coach/CreateCampModal.tsx | 24 ++- src/components/coach/CreateDevPlanModal.tsx | 23 ++- src/components/coach/InviteModal.tsx | 9 +- src/components/coach/PlayerDetailModal.tsx | 9 +- src/components/coach/discover/FilterPanel.tsx | 114 ++++++------ src/components/coach/discover/PlayerCard.tsx | 15 +- .../coach/discover/PlayerCardGrid.tsx | 1 + .../coach/discover/PlayerHoverPreview.tsx | 7 +- src/components/coach/lineup/LineupBuilder.tsx | 3 + .../fairway/data-table/data-table.tsx | 2 + .../pages/calendar/FairwayEventEditor.tsx | 1 + .../fairway/pages/coachhelm/FairwayBrief.tsx | 1 + .../coachhelm/FairwayCoachHelmSignals.tsx | 1 + .../pages/coachhelm/FairwayEffectiveness.tsx | 1 + .../pages/coachhelm/FairwayMyDevelopment.tsx | 9 +- .../coachhelm/FairwayPlayerCoachHelm.tsx | 2 + .../pages/coachhelm/FairwayPlayerInsight.tsx | 1 + .../pages/coachhelm/FairwayPlayerStats.tsx | 1 + .../pages/coachhelm/GenomeCompareView.tsx | 1 + .../pages/coachhelm/GenomeDetailView.tsx | 2 + .../pages/coachhelm/PlayersGridView.tsx | 4 + .../pages/messages/MessageThreadPane.tsx | 1 + .../pages/rounds-new/FairwayRecentCourses.tsx | 2 +- .../pages/tasks/FairwayCreateTaskModal.tsx | 1 + src/components/features/profile-editor.tsx | 70 +++++--- src/components/features/video-player.tsx | 1 + src/components/features/video-upload.tsx | 7 +- src/components/golf/CommandPalette.tsx | 1 + .../golf/ShotTrackingComprehensive.tsx | 6 + .../announcements/CreateAnnouncementFlow.tsx | 1 + .../golf/calendar/AvailabilityDayView.tsx | 1 + .../golf/calendar/CalendarAvatarSidebar.tsx | 2 +- .../golf/calendar/CalendarSyncButton.tsx | 4 +- .../golf/calendar/CreateFeedSection.tsx | 4 +- .../golf/calendar/EventDetailModal.tsx | 14 +- .../golf/calendar/MobileEventSheet.tsx | 6 +- .../golf/calendar/NotificationCenter.tsx | 2 +- .../golf/calendar/PremiumEventBlock.tsx | 7 +- .../golf/calendar/QuickAddEventFAB.tsx | 6 +- src/components/golf/classes/AddClassModal.tsx | 46 +++-- .../golf/classes/ConfirmClassesModal.tsx | 26 ++- .../golf/classes/UploadScheduleModal.tsx | 1 + .../coachhelm/PromoteToFocusAreaButton.tsx | 5 +- .../coachhelm/analytics/TrendIndicator.tsx | 1 + .../coachhelm/insight-card/InsightCard.tsx | 7 +- .../coachhelm/insights/InsightExportModal.tsx | 4 +- .../insights/InsightFiltersPanel.tsx | 18 +- .../coachhelm/patterns/PatternDashboard.tsx | 8 +- .../patterns/PatternValidationModal.tsx | 11 +- .../v3/Genome/GenomeComparePicker.tsx | 2 +- .../golf/coachhelm/v3/IntentDrawer/index.tsx | 5 +- .../v3/QualifyingBoard/CoachPickPanel.tsx | 2 +- .../QualifyingBoard/LeaderboardWithSlots.tsx | 2 +- .../golf/dashboard/today-timeline.tsx | 2 + .../golf/documents/DocumentPreview.tsx | 2 + .../golf/documents/ImagePreview.tsx | 1 + .../golf/documents/UploadNewVersionModal.tsx | 7 +- .../golf/messages/GolfNewMessageModal.tsx | 1 + .../golf/messages/GolfTeamBroadcastModal.tsx | 2 + .../golf/recruiting/RecruitFormSheet.tsx | 4 +- .../golf/roster/InvitePlayerButton.tsx | 13 +- .../golf/roster/PlayerStatusBadge.tsx | 6 +- .../rounds/new/RecentCoursesQuickPick.tsx | 1 - .../golf/settings/JoinTeamSection.tsx | 10 +- .../golf/stats/GolfStatsDisplay.tsx | 6 +- .../golf/stats/sections/shared-primitives.tsx | 1 + .../golf/tasks/CreateFromTemplateModal.tsx | 4 +- src/components/golf/tasks/CreateTaskModal.tsx | 5 +- src/components/golf/tasks/ReminderPicker.tsx | 21 ++- .../golf/tasks/TaskTemplateList.tsx | 15 +- src/components/golf/travel/ExpenseForm.tsx | 12 +- src/components/golf/travel/ExpenseList.tsx | 15 +- src/components/golf/travel/ExpenseSummary.tsx | 1 + src/components/messages/NewMessageModal.tsx | 2 + src/components/player/VideoShowcase.tsx | 1 + src/components/recruiting/USStateMap.tsx | 1 + src/components/ui/checkbox.tsx | 12 +- src/components/ui/confirm-dialog.tsx | 8 + src/components/ui/input.tsx | 32 ++-- src/components/ui/page-header.tsx | 1 + src/components/ui/select.tsx | 16 +- src/components/video/VideoClipper.tsx | 15 ++ 174 files changed, 1213 insertions(+), 542 deletions(-) create mode 100644 .lint-baseline.json create mode 100644 scripts/lint-ratchet.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6ca59a039..9dfbadc1f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,6 +61,9 @@ jobs: - name: Lint run: npm run lint + - name: Lint ratchet (warnings must not grow per-rule) + run: npm run lint:ratchet + - name: Unit tests run: npm run test -- --run diff --git a/.lint-baseline.json b/.lint-baseline.json new file mode 100644 index 000000000..8fb186175 --- /dev/null +++ b/.lint-baseline.json @@ -0,0 +1,13 @@ +{ + "(null)": 2, + "@typescript-eslint/no-explicit-any": 44, + "@typescript-eslint/no-unused-vars": 1, + "helm/no-arbitrary-bg-white": 1302, + "helm/no-arbitrary-radius": 38, + "helm/no-arbitrary-text-px": 165, + "helm/no-banned-color": 260, + "helm/no-raw-button": 104, + "helm/no-raw-input": 458, + "jsx-a11y/anchor-is-valid": 1, + "jsx-a11y/aria-role": 19 +} diff --git a/package.json b/package.json index dc8e3be17..1a5926848 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "build": "next build --webpack", "start": "next start", "lint": "eslint \"src/**/*.{ts,tsx}\" --max-warnings 6000", + "lint:ratchet": "node scripts/lint-ratchet.mjs", "typecheck": "tsc --noEmit", "db:types": "npx supabase gen types typescript --project-id $SUPABASE_PROJECT_ID > src/lib/types/database.ts", "db:types:check": "npm run db:types && git diff --exit-code src/lib/types/database.ts || (echo '❌ Types are out of date. Run npm run db:types and commit changes.' && exit 1)", diff --git a/scripts/lint-ratchet.mjs b/scripts/lint-ratchet.mjs new file mode 100644 index 000000000..92d14f943 --- /dev/null +++ b/scripts/lint-ratchet.mjs @@ -0,0 +1,164 @@ +#!/usr/bin/env node +/** + * lint-ratchet.mjs + * + * Runs `npx eslint src --format json`, tallies warnings per rule-id, and + * compares them against .lint-baseline.json. + * + * Exit codes: + * 0 — no regression (all rule counts <= baseline) + * 1 — regression detected (at least one rule count > baseline) + * + * Flags: + * --update Rewrite .lint-baseline.json from the current run and exit 0. + */ + +import { execFileSync } from 'node:child_process'; +import { readFileSync, writeFileSync } from 'node:fs'; +import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const ROOT = resolve(__dirname, '..'); +const BASELINE_PATH = resolve(ROOT, '.lint-baseline.json'); + +const UPDATE = process.argv.includes('--update'); + +// --------------------------------------------------------------------------- +// 1. Run ESLint and collect per-rule warning counts +// --------------------------------------------------------------------------- +let eslintOutput; +try { + // execFileSync with an explicit argv array — no shell, no injection surface. + // stderr → 'inherit' so deprecation notices print directly and don't + // pollute the JSON stdout buffer we parse below. + eslintOutput = execFileSync( + 'npx', + ['eslint', 'src', '--format', 'json', '--max-warnings', '999999'], + // maxBuffer: 64 MB — the full-repo JSON output is ~10 MB today and will + // grow; 64 MB leaves ample headroom without meaningful memory cost. + { cwd: ROOT, encoding: 'utf-8', maxBuffer: 64 * 1024 * 1024, stdio: ['ignore', 'pipe', 'inherit'] } + ); +} catch (err) { + // eslint exits non-zero when warnings/errors are present, but still writes + // valid JSON to stdout. Use that if it looks like JSON. + eslintOutput = (err.stdout || '').trim(); + if (!eslintOutput.startsWith('[')) { + console.error('ESLint failed and did not produce JSON output.'); + console.error(err.message); + process.exit(1); + } +} + +/** @type {Array<{messages: Array<{severity: number, ruleId: string|null}>}>} */ +let files; +try { + files = JSON.parse(eslintOutput); +} catch (parseErr) { + console.error('Could not parse ESLint JSON output:', parseErr.message); + process.exit(1); +} + +/** @type {Record} */ +const current = {}; +for (const file of files) { + for (const msg of file.messages) { + if (msg.severity === 1) { + // severity 1 = warning; severity 2 = error + const rule = msg.ruleId ?? '(null)'; + current[rule] = (current[rule] ?? 0) + 1; + } + } +} + +// Stable sorted copy for writing / printing +const sortedCurrent = Object.fromEntries( + Object.entries(current).sort(([a], [b]) => a.localeCompare(b)) +); + +const totalNow = Object.values(current).reduce((s, n) => s + n, 0); + +// --------------------------------------------------------------------------- +// 2. --update: overwrite baseline and exit +// --------------------------------------------------------------------------- +if (UPDATE) { + writeFileSync(BASELINE_PATH, JSON.stringify(sortedCurrent, null, 2) + '\n', 'utf-8'); + console.log( + `lint-ratchet: baseline updated — ${totalNow} warning${totalNow !== 1 ? 's' : ''} across ${Object.keys(sortedCurrent).length} rule${Object.keys(sortedCurrent).length !== 1 ? 's' : ''} locked in ${BASELINE_PATH}` + ); + process.exit(0); +} + +// --------------------------------------------------------------------------- +// 3. Load baseline +// --------------------------------------------------------------------------- +/** @type {Record} */ +let baseline; +try { + baseline = JSON.parse(readFileSync(BASELINE_PATH, 'utf-8')); +} catch { + console.error( + `lint-ratchet: baseline file not found at ${BASELINE_PATH}.\n` + + 'Run `npm run lint:ratchet -- --update` to create it.' + ); + process.exit(1); +} + +const totalBaseline = Object.values(baseline).reduce((s, n) => s + n, 0); + +// --------------------------------------------------------------------------- +// 4. Per-rule comparison +// --------------------------------------------------------------------------- +/** @type {Array<{rule: string, baseline: number, now: number, delta: number}>} */ +const regressions = []; + +// Check every rule that appears in current run +for (const [rule, nowCount] of Object.entries(current)) { + const baseCount = baseline[rule] ?? 0; + if (nowCount > baseCount) { + regressions.push({ rule, baseline: baseCount, now: nowCount, delta: nowCount - baseCount }); + } +} + +// Also check rules in baseline that have gone to 0 (not a regression, just informational) +// No action needed — fewer warnings are always fine. + +// --------------------------------------------------------------------------- +// 5. Report +// --------------------------------------------------------------------------- +if (regressions.length > 0) { + console.error('lint-ratchet: WARNING COUNT REGRESSION DETECTED\n'); + console.error( + 'The following rules have MORE warnings than the baseline.\n' + + 'Fix the new violations, or run `npm run lint:ratchet -- --update` only\n' + + 'after the net warning count has decreased.\n' + ); + + const maxRuleLen = Math.max(...regressions.map((r) => r.rule.length)); + console.error( + ` ${'Rule'.padEnd(maxRuleLen)} ${'Baseline'.padStart(8)} ${'Now'.padStart(8)} ${'Delta'.padStart(6)}` + ); + console.error(` ${'-'.repeat(maxRuleLen + 28)}`); + + for (const { rule, baseline: b, now, delta } of regressions.sort( + (a, b_) => b_.delta - a.delta + )) { + console.error( + ` ${rule.padEnd(maxRuleLen)} ${String(b).padStart(8)} ${String(now).padStart(8)} +${String(delta).padStart(5)}` + ); + } + + console.error(`\n Total: ${totalBaseline} → ${totalNow} (net ${totalNow >= totalBaseline ? '+' : ''}${totalNow - totalBaseline})`); + process.exit(1); +} + +if (totalNow < totalBaseline) { + console.log( + `lint-ratchet: warnings dropped (${totalBaseline} → ${totalNow}) — run \`npm run lint:ratchet -- --update\` to lock in the gains` + ); +} else { + // totalNow === totalBaseline (per-rule no regressions, same total) + console.log(`lint-ratchet: OK — ${totalNow} warning${totalNow !== 1 ? 's' : ''}, no regressions`); +} + +process.exit(0); diff --git a/src/app/baseball/(auth)/complete-signup/CompleteSignupClient.tsx b/src/app/baseball/(auth)/complete-signup/CompleteSignupClient.tsx index a86552c6d..9c6637ade 100644 --- a/src/app/baseball/(auth)/complete-signup/CompleteSignupClient.tsx +++ b/src/app/baseball/(auth)/complete-signup/CompleteSignupClient.tsx @@ -115,7 +115,7 @@ export default function CompleteSignupClient() {
{/* Role Selection */}
- +

I am a...