From 6017b9e11c17aa09450c1d8b5eca6f7bbdf13e2b Mon Sep 17 00:00:00 2001 From: Nima <74723240+nimkimi@users.noreply.github.com> Date: Thu, 6 Aug 2026 16:36:20 +0200 Subject: [PATCH 1/2] chore: restore react-hooks/set-state-in-effect to error The rule was demoted to a warning to land the Next.js 16 upgrade, with the six offending components tracked as follow-up work. All six are now fixed at the pattern level rather than silenced: the media-query reads go through a shared useSyncExternalStore hook, the arrival skip path no longer sets state synchronously in its layout effect, the contact toast is derived from the action state, and the nav closes on route change during render. Deleting the override restores eslint-config-next 16's default. npm run lint reports 0 errors and 0 warnings, and a deliberate setState-in-effect now fails the build as an error rather than a warning. --- eslint.config.mjs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 2960c8c..92ec6cb 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -18,19 +18,6 @@ export default defineConfig([ ]), { extends: [...nextCoreWebVitals] }, - // `react-hooks/set-state-in-effect` is newly an error in eslint-config-next - // 16. It fires on six components that set state from an effect: the - // SSR-safe `prefers-reduced-motion` reads (Reveal, Preloader), the toast - // enter/exit transition, and the route-change menu close in SiteNav. Each is - // a real (small) cascading-render cost, but fixing them means reworking the - // arrival animation and motion internals — a behaviour change that does not - // belong in a version upgrade. Demoted to a warning so the signal stays - // visible; fix tracked separately, and it should land before React Compiler - // is enabled, since that is the same class of code the compiler reasons about. - { - rules: { 'react-hooks/set-state-in-effect': 'warn' }, - }, - // Test fixtures render a bare on purpose — the LCP/bandwidth advice // behind `no-img-element` is about shipped pages, not assertions. { From 46f5832bd18e6fd1f481e01c731d14b2baf20617 Mon Sep 17 00:00:00 2001 From: Nima <74723240+nimkimi@users.noreply.github.com> Date: Thu, 6 Aug 2026 16:39:08 +0200 Subject: [PATCH 2/2] docs: close out the post-upgrade debt cleanup Ticks every acceptance criterion with the evidence behind it, records the two screenshot differences the spec's 'or every difference is explained' clause allows, and moves the feature to the Done table in the roadmap. The two explained differences: the original contact-page baseline is not reproducible because it was captured with a reCAPTCHA site key in the environment and no .env file exists in the repo, so the comparison was re-based on a fresh same-environment capture of pre-change main; and the two reduced-motion first-paint shots changed on purpose, which is the arrival fix. --- docs/ROADMAP.md | 1 + docs/features/post-upgrade-debt-cleanup.md | 52 ++++++++++++---------- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 8ee9588..11b1d4a 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -60,3 +60,4 @@ Raw ideas, no commitment. See `docs/ideas.md` for even rougher captures. | Projects section | 2025 | | Dark mode (system preference) | 2025 | | `.claude/` project config + prettier hook | 2026-05 | +| Post-upgrade debt cleanup | 2026-08 | diff --git a/docs/features/post-upgrade-debt-cleanup.md b/docs/features/post-upgrade-debt-cleanup.md index 7b1ab7c..d6713f3 100644 --- a/docs/features/post-upgrade-debt-cleanup.md +++ b/docs/features/post-upgrade-debt-cleanup.md @@ -1,6 +1,6 @@ # Feature: Post-Upgrade Debt Cleanup -_Status: spec approved 2026-08-05. Supersedes the four "follow-ups" listed in `.claude/HANDOFF.md`._ +_Status: **SHIPPED 2026-08-06** (PRs #9-#15). Spec approved 2026-08-05. Superseded the four "follow-ups" that were listed in `.claude/HANDOFF.md`._ ## Context @@ -35,38 +35,44 @@ A portfolio whose primary audience is recruiters and hiring managers is judged p ### Global (the whole feature is done when all of these hold on `main`) -- [ ] `npm run check` exits 0. -- [ ] `npm run lint` reports **0 errors and 0 warnings**. -- [ ] `eslint.config.mjs` no longer contains a `react-hooks/set-state-in-effect` override. -- [ ] `npx tsc --noEmit` is clean. -- [ ] `npm test` passes, with **more** tests than the 240 baseline (every fix ships a test). -- [ ] Playwright passes 16/16 (plus new tests) against **both** a production build and the Turbopack dev server. -- [ ] `eslint-config-prettier` does not appear in `package.json`. -- [ ] CI runs the format check and fails the build when formatting drifts. -- [ ] The 10 full-page screenshots (5 routes × desktop 1440 / mobile 390) are **byte-identical** to the ones captured from `5c5ef55`, or every difference is explained and approved. +- [x] `npm run check` exits 0. +- [x] `npm run lint` reports **0 errors and 0 warnings**. Verified armed, not merely un-overridden: a deliberate `setState` in a `useEffect` fails as `1 problem (1 error, 0 warnings)`. +- [x] `eslint.config.mjs` no longer contains a `react-hooks/set-state-in-effect` override. +- [x] `npx tsc --noEmit` is clean. +- [x] `npm test` passes with **308 tests / 31 files**, up from the 243 baseline. +- [x] Playwright passes 16/16 against a production build **and** 16/16 against the Turbopack dev server. No new e2e specs were needed — every fix is unit-covered. +- [x] `eslint-config-prettier` does not appear in `package.json`. +- [x] CI runs the format check (`.github/workflows/ci.yml`, `Format check` → `npm run check`). +- [x] The 10 full-page screenshots are byte-identical, **with two differences explained and approved**, per the "or every difference is explained" clause: + 1. **The contact page could not be compared against the original baseline.** The `5c5ef55`-era capture rendered the real reCAPTCHA widget; it ran with `NEXT_PUBLIC_RECAPTCHA_SITE_KEY` set, and no `.env*` file exists in the repo now, so that environment is irreproducible (contact was 1440x931 then, 1440x900 now). The comparison was re-based on a fresh same-environment capture of pre-change `main`, against which **contact is byte-identical at both viewports**. + 2. **The two reduced-motion first-paint shots changed on purpose** — that is `#D`'s fix. Before, that frame painted a dark full-screen overlay plus an unplaced lime monogram stranded in the top-left, with no nav and no hero. After, it paints the real page. Approved on review of the before/after images. + + Three further shots (`home-full-1440`, `home-settled-1440`, `home-settled-390`) move within a **separately measured run-to-run noise envelope** — established by capturing unchanged `main` twice — because they are arrival-animation and grain-overlay frames and are timing-dependent by nature. Every deterministic shot is byte-identical. + +- [x] The signature arrival was recorded before and after at both viewports and reviewed on taste, per the Design Notes. Approved. ### Batch 1 — Tooling (must merge before Batch 2 branches) -- [ ] `package.json` pins `"prettier": "3.8.3"` exactly — no `^`, no `~`. -- [ ] The reformat is a **standalone commit** containing nothing but Prettier output. -- [ ] `eslint-config-prettier` removed from `devDependencies` and absent from `package-lock.json`. -- [ ] `.github/workflows/ci.yml` gains a `Format check` step running `npm run check`, placed immediately after `Lint`. -- [ ] A deliberate formatting violation on a branch fails CI (verified, not assumed). +- [x] `package.json` pins `"prettier": "3.8.3"` exactly — no `^`, no `~`. +- [x] The reformat is a **standalone commit** containing nothing but Prettier output. +- [x] `eslint-config-prettier` removed from `devDependencies` and absent from `package-lock.json`. +- [x] `.github/workflows/ci.yml` gains a `Format check` step running `npm run check`, placed immediately after `Lint`. +- [x] A deliberate formatting violation on a branch fails CI (verified, not assumed). ### Batch 2A — `accent-contrast` guard -- [ ] `assertDarkOnLime()` fails with a clear message when `getComputedStyle(node).color` yields fewer than 3 numeric channels, instead of silently producing `Math.max(...[]) === -Infinity`. -- [ ] A test proves the guard rejects an empty computed color — **written first, and observed failing against the current implementation.** -- [ ] The two existing accent-contrast tests still pass in dev and prod mode. +- [x] `assertDarkOnLime()` fails with a clear message when `getComputedStyle(node).color` yields fewer than 3 numeric channels, instead of silently producing `Math.max(...[]) === -Infinity`. +- [x] A test proves the guard rejects an empty computed color — **written first, and observed failing against the current implementation.** +- [x] The two existing accent-contrast tests still pass in dev and prod mode. ### Batch 2B–2E — `set-state-in-effect` Each of the four component issues: -- [ ] Removes its violation without an `eslint-disable`. -- [ ] Ships a test written **first** that fails against the current implementation and passes after. -- [ ] Leaves every existing test in its file passing, unmodified where possible; any modified test must be justified in the PR body as testing an implementation detail rather than behaviour. -- [ ] Produces no hydration warning in the browser console on a production build. +- [x] Removes its violation without an `eslint-disable`. +- [x] Ships a test written **first** that fails against the current implementation and passes after. +- [x] Leaves every existing test in its file passing, unmodified where possible; any modified test must be justified in the PR body as testing an implementation detail rather than behaviour. +- [x] Produces no hydration warning in the browser console on a production build. Checked in a real browser against `next start`, across all five routes at both `prefers-reduced-motion` settings: no hydration warning, and in fact no console error or warning of any kind. Per-group specifics: @@ -77,7 +83,7 @@ Per-group specifics: ### Batch 3 — Closer -- [ ] `eslint.config.mjs` restores `react-hooks/set-state-in-effect` to `'error'` (by deleting the override block and its comment), and `npm run lint` is clean. +- [x] `eslint.config.mjs` restores `react-hooks/set-state-in-effect` to `'error'` (by deleting the override block and its comment), and `npm run lint` is clean. ## Design Notes