From 554201b37746492c95166efde8a9e81d431cfc16 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 11 Aug 2026 15:40:35 +0000 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20clear=20React=20Doctor=20Critical=20?= =?UTF-8?q?findings=20(46=20=E2=86=92=20100)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Install the React Doctor skill/CI hook and repair all confirmed diagnostics: pnpm supply-chain hardening, fetch/chroma safety, a11y labels, parent sync without effects, and related cleanup. --- .agents/skills/react-doctor/SKILL.md | 56 + .../skills/react-doctor/references/explain.md | 73 + .github/workflows/react-doctor.yml | 53 + .husky/pre-commit | 9 +- TODO.md | 8 + app/components/ColorPicker.tsx | 18 +- app/components/Demo.tsx | 1 + app/components/Generator.tsx | 2 +- app/components/Graphs.tsx | 28 +- app/components/Palette.tsx | 176 +- app/components/PaletteSwatchGrid.tsx | 49 + app/components/PaletteTweaks.tsx | 108 + app/components/SquareGraph.tsx | 39 +- app/components/StopSelector.tsx | 13 +- app/components/Swatch.tsx | 2 + app/components/catalyst/button.tsx | 2 +- app/components/catalyst/dropdown.tsx | 133 -- app/components/catalyst/radio.tsx | 47 - app/components/formStyles.ts | 1 + app/lib/constants.ts | 3 - app/lib/createDisplayColor.ts | 4 + app/lib/createSwatches.ts | 6 +- app/lib/generateOGImage.server.tsx | 13 +- app/lib/getGitHubData.ts | 18 +- app/lib/getSanityData.ts | 19 +- app/lib/helpers.ts | 107 +- app/lib/meta.ts | 4 +- app/lib/responses.ts | 15 +- package.json | 4 +- pnpm-lock.yaml | 2127 ++++++++++++++++- pnpm-workspace.yaml | 6 + 31 files changed, 2599 insertions(+), 545 deletions(-) create mode 100644 .agents/skills/react-doctor/SKILL.md create mode 100644 .agents/skills/react-doctor/references/explain.md create mode 100644 .github/workflows/react-doctor.yml create mode 100644 app/components/PaletteSwatchGrid.tsx create mode 100644 app/components/PaletteTweaks.tsx create mode 100644 app/components/formStyles.ts create mode 100644 pnpm-workspace.yaml diff --git a/.agents/skills/react-doctor/SKILL.md b/.agents/skills/react-doctor/SKILL.md new file mode 100644 index 0000000..d9e4c46 --- /dev/null +++ b/.agents/skills/react-doctor/SKILL.md @@ -0,0 +1,56 @@ +--- +name: react-doctor +description: Use when finishing a feature, fixing a bug, before committing React code, or when the user types `/doctor`, asks to scan, triage, or clean up React diagnostics. Covers lint, accessibility, bundle size, architecture. Includes a regression check and a full local-triage workflow that fetches the canonical playbook. +version: "1.2.0" +--- + +# React Doctor + +Scans React codebases for security, performance, correctness, and architecture issues. Outputs a 0–100 health score. + +## After making React code changes: + +Run `npx react-doctor@latest --verbose --scope changed` and check the score did not regress. + +If the score dropped, fix the regressions before committing. + +## For general cleanup or code improvement: + +Run `npx react-doctor@latest --verbose` (the default `--scope full`) to scan the full codebase. Fix issues by severity — errors first, then warnings. + +## For a focused UI design audit: + +Run `npx react-doctor@latest design --verbose`. This selects only design-tagged UI composition, typography, interaction, accessibility, and motion rules, including focused rules that remain opt-in during a general health scan. + +## /doctor — full local triage workflow + +When the user types `/doctor`, says "run react doctor", or asks for a full triage / cleanup pass (not just a regression check), fetch the canonical local-triage playbook and follow every step in it: + +```bash +curl --fail --silent --show-error \ + --header 'Cache-Control: no-cache' \ + https://www.react.doctor/prompts/react-doctor-agent.md +``` + +The playbook is the single source of truth — a scan → filter → triage → fix → validate loop that edits the working tree directly (never commits, never opens PRs). Updating the prompt at its source updates every agent on its next fetch — no skill reinstall needed. + +Pair it with the matching per-rule prompts at `https://www.react.doctor/prompts/rules//.md` (fetched on demand inside the playbook) so each fix uses the canonical, reviewer-tested recipe. + +## Configuring or explaining rules + +When the user wants to understand a rule, disagrees with one, or wants to disable / tune which rules run (not fix code), read [references/explain.md](references/explain.md) and follow it. Start with `npx react-doctor@latest rules explain `, then apply the narrowest control via `npx react-doctor@latest rules disable|set|category|ignore-tag …`, which edits your `doctor.config.*` (or `package.json#reactDoctor`). + +## Command + +```bash +npx react-doctor@latest --verbose --scope changed +``` + +| Flag | Purpose | +| ----------------- | ---------------------------------------------------------------- | +| `.` | Scan current directory | +| `--verbose` | Show affected files and line numbers per rule | +| `--scope changed` | Only report issues introduced vs the base branch (default: full) | +| `--scope lines` | Only report issues on the changed lines | +| `--score` | Output only the numeric score | +| `design` | Run only the focused UI design diagnostics | diff --git a/.agents/skills/react-doctor/references/explain.md b/.agents/skills/react-doctor/references/explain.md new file mode 100644 index 0000000..c3a1008 --- /dev/null +++ b/.agents/skills/react-doctor/references/explain.md @@ -0,0 +1,73 @@ +# Explaining and configuring rules + +Explain React Doctor rules and edit `doctor.config.*` safely. Use this when a user +wants to understand a rule or change which rules run — not for fixing diagnostics +(that is the main `react-doctor` skill / `/doctor`). + +Triggers: "why did this rule fire", "I disagree with this rule", "turn this rule off", +"stop flagging X", "too noisy", "disable design rules". + +## Workflow + +1. Identify the rule key from the diagnostic (e.g. `react-doctor/no-array-index-as-key`). +2. Explain it before changing anything: + +```bash +npx react-doctor@latest rules explain react-doctor/no-array-index-as-key +``` + +3. Pick the narrowest control that matches the user's intent (see decision guide). +4. Apply it with a `rules` subcommand (edits your `doctor.config.*` or `package.json#reactDoctor` in place, preserving other fields and formatting). +5. Validate the change did what they wanted: + +```bash +npx react-doctor@latest --verbose --diff +``` + +## Commands + +```bash +npx react-doctor@latest rules list # every rule + its effective severity +npx react-doctor@latest rules list --configured # only what your config changed +npx react-doctor@latest rules list --category Performance # filter by category +npx react-doctor@latest rules explain # why it matters + how to configure +npx react-doctor@latest rules disable # rule never runs +npx react-doctor@latest rules enable # turn back on at its recommended severity +npx react-doctor@latest rules set warn # off | warn | error +npx react-doctor@latest rules category "React Native" off # whole category +npx react-doctor@latest rules ignore-tag design # skip a rule family (design, test-noise, …) +npx react-doctor@latest rules unignore-tag design +``` + +Rule references accept the full key (`react-doctor/no-danger`), the bare id (`no-danger`), or a legacy key (`react/no-danger`). + +## Decision guide + +Match the control to the intent — prefer the narrowest one: + +- **User disagrees with one rule / it's a false positive for them** → `rules disable ` (sets `rules. = "off"`; the rule stops running everywhere). This is the default for "I don't want this rule". +- **Rule is fine but wrong severity** → `rules set warn` or `rules set error`. +- **A disabled-by-default rule they want on** → `rules enable `. +- **A whole area is unwanted** (e.g. all React Native rules) → `rules category "" off`. +- **A behavioral family is noisy** (`design`, `test-noise`, `migration-hint`) → `rules ignore-tag `. +- **Keep it locally but hide from PR comment / score / CI gate only** → do NOT disable. Edit `surfaces` in your config (`surfaces.prComment.excludeRules`, `surfaces.score.excludeTags`, `surfaces.ciFailure.excludeCategories`). The rule still shows in local `cli` output. +- **Restore test or story findings to production health** → set `surfaces.score.includeFileContexts` or `surfaces.ciFailure.includeFileContexts` to `["test"]`, `["story"]`, or both. Other surface exclusions still apply. + +How the layers combine: `ignore.tags` disables every rule carrying that tag **before** linting, so a tagged rule stays off even if `rules`/`categories` set it to `warn`/`error` (a rule-level override cannot re-enable a tag-ignored rule). For rules that aren't tag-disabled, `rules` overrides `categories` overrides the rule's default. `surfaces` is visibility-only and never changes whether a rule runs. + +## Config shape + +Config lives in `doctor.config.ts` (or `.js`/`.mjs`/`.cjs`/`.json`/`.jsonc`), or the `reactDoctor` key in `package.json`. The `rules` commands edit whichever exists — TS/JS edits preserve formatting (via magicast) — and create `doctor.config.json` when none does, stamping `$schema`: + +```ts +// doctor.config.ts +export default { + rules: { "react-doctor/no-array-index-as-key": "off" }, + categories: { "React Native": "warn" }, + ignore: { tags: ["design"] }, +}; +``` + +## Educating the user + +When explaining a rule, lead with the "Why it matters" guidance from `rules explain` and, when they want depth, the per-rule recipe at `https://www.react.doctor/prompts/rules//.md`. Only after they understand it should you offer to disable it — many "bad" rules are catching real issues. diff --git a/.github/workflows/react-doctor.yml b/.github/workflows/react-doctor.yml new file mode 100644 index 0000000..a8727fa --- /dev/null +++ b/.github/workflows/react-doctor.yml @@ -0,0 +1,53 @@ +# React Doctor — finds security, performance, correctness, accessibility, +# bundle-size, and architecture issues in React codebases. +# +# Docs: https://www.react.doctor/ci +# Source: https://github.com/millionco/react-doctor + +name: React Doctor + +on: + # Scans the PR's changed files and posts a sticky summary comment listing only the new issues introduced relative to the merge base of the target branch. + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + # Scans `main` on every push to track the health-score trend and catch regressions that slipped past PR review. + push: + branches: ["main"] + +permissions: + contents: read + pull-requests: write + issues: write + statuses: write + +# Cancels any in-flight scan for the same PR (or branch, on push) the moment a new commit arrives, so reviewers only ever see the latest run. +concurrency: + group: react-doctor-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + react-doctor: + runs-on: ubuntu-latest + steps: + # fetch-depth: 0 gives React Doctor the full git history it needs to find the merge base with the target branch. Without it a shallow checkout has no merge base, so PR runs can't compare against the base and fall back to reporting every issue in the changed files (pre-existing ones included) instead of only the ones the PR introduced. + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - uses: millionco/react-doctor@v2 + # Advisory by default: React Doctor reports findings on every PR — a + # sticky summary comment, inline review comments, and a commit status + # with the health score — but never fails the check, so it won't red-X + # a teammate's PR on day one. When your team trusts the signal, graduate + # the gate: uncomment the block below and set blocking to "error" (fail + # on new error-severity findings) or "warning" (fail on any finding). + # Full reference: https://www.react.doctor/ci + # with: + # blocking: error # Gate level: "none" (advisory, the default) | "warning" | "error" + # scope: full # On PRs, scan the whole project instead of just changed files + # comment: false # Disable the sticky PR summary comment + # review-comments: false # Disable inline review comments on changed lines + # commit-status: false # Disable the commit status (score + counts, links to the run) + # version: "0.4.0" # Pin to a specific react-doctor version instead of "latest" + # directory: apps/web # Scan a sub-directory (default: ".") + # project: "web,admin" # In a monorepo, scan specific workspace project(s) diff --git a/.husky/pre-commit b/.husky/pre-commit index 995ee37..7850c43 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,3 +1,10 @@ pnpm test pnpm format -pnpm lint \ No newline at end of file +pnpm lint + +# react-doctor: non-blocking staged scan +if [ -x "./node_modules/.bin/react-doctor" ]; then + ./node_modules/.bin/react-doctor --staged --blocking none || true +elif command -v pnpm >/dev/null 2>&1; then + pnpm dlx react-doctor@latest --staged --blocking none || true +fi diff --git a/TODO.md b/TODO.md index 8815d2f..885a487 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,13 @@ # TODO +- **React Doctor critical cleanup** + - [x] Install React Doctor skill (`pnpm dlx react-doctor@latest install --yes`) + - [x] Full scan baseline: score 46 / Critical, 50 warnings (0 errors) + - [x] Fix Security (pnpm hardening, window.open noopener) + - [x] Fix Bugs (unguarded chroma parse, fetch status, exhaustive-deps, dropdown cleanup, parent sync effect) + - [x] Fix Accessibility (Demo dark-mode toggle, Swatch button label) + - [x] Fix remaining high-confidence Maintainability/Performance findings + - [x] Rescan: score 100 / Great, 0 diagnostics - **Production outage** - [x] Confirm Workers URL returns HTTP 500 (`Unexpected Server Error`) - [x] Identify cause: Dependabot bumped `react-router` 7.17.0 → 8.3.0 while `@react-router/dev` / `@react-router/fs-routes` stayed on 7.17.0 diff --git a/app/components/ColorPicker.tsx b/app/components/ColorPicker.tsx index bac2966..0d3d2a3 100644 --- a/app/components/ColorPicker.tsx +++ b/app/components/ColorPicker.tsx @@ -5,12 +5,19 @@ import { HexColorPicker } from "react-colorful"; import { useDebounceCallback } from "usehooks-ts"; import { Button } from "~/components/catalyst/button"; -import { labelClasses } from "~/components/Palette"; +import { labelClasses } from "~/components/formStyles"; import { hexToHSL, HSLToHex, round } from "~/lib/helpers"; import { Input } from "./catalyst/input"; +const defaultButtonContent = ( + <> + + Open Color Picker + +); + // Reusable color picker content component -export function ColorPickerContent({ +function ColorPickerContent({ color, onChange, onClose, @@ -92,13 +99,6 @@ export default function ColorPicker({ panelClassName?: string; children?: React.ReactNode; }) { - const defaultButtonContent = ( - <> - - Open Color Picker - - ); - return ( diff --git a/app/components/Demo.tsx b/app/components/Demo.tsx index b0c089c..b16d433 100644 --- a/app/components/Demo.tsx +++ b/app/components/Demo.tsx @@ -46,6 +46,7 @@ export default function Demo({