Skip to content

overhaul: 01 foundation - #15

Open
CS-5 wants to merge 5 commits into
claude/website-overhaul-plan-38czl6from
overhaul/01-foundation
Open

overhaul: 01 foundation#15
CS-5 wants to merge 5 commits into
claude/website-overhaul-plan-38czl6from
overhaul/01-foundation

Conversation

@CS-5

@CS-5 CS-5 commented Aug 28, 2026

Copy link
Copy Markdown
Member

Layer 1 of the overhaul stack, on top of the plan branch (#14). plan/01-foundation.md.

An empty-but-real Astro project with the complete final toolchain, and the old site parked for reference. No UI yet — that starts in Phase 02.

What's here

Legacy parked. src/, data/, and every Next.js config file moved to legacy/ with a README saying reference-only. public/ and functions/ stay at root. Nothing in the new tree imports from it, and every tool ignores it.

Runtime + package manager. mise.toml pins node 26.7.0 and pnpm 11.22.0, with per-platform checksums committed to mise.lock. mise tasks are thin forwarders — package.json scripts are the single source of truth. pnpm-workspace.yaml sets a 7-day release cooldown and denies install scripts except sharp and esbuild.

Astro scaffold. Astro 7 static, sitemap integration, Tailwind v4 through @tailwindcss/vite (no config file), strictest tsconfig with the explicit strictness flags from the brief, @/* path alias, and src/lib/cn.ts as the only sanctioned cn import site.

Lint + format. oxlint (type-aware, with anti-slop vendored into tools/lint/anti-slop/) and oxfmt own .ts/.js/.json/.jsonc/.css. ESLint (strictTypeChecked + eslint-plugin-astro's jsx-a11y-strict) and Prettier own .astro and .md. Import sorting is on for both halves. clsx, classnames, tailwind-merge, and any legacy/ path are banned imports.

Agent + editor config. AGENTS.md (one screen) with CLAUDE.md symlinked to it; a PostToolUse hook that routes each edited file to its owning formatter and linter, exiting 2 with findings on failure; VS Code settings matching the same split.

CI. pnpm checkpnpm build → offline lychee link check over dist/, blocking, on every PR. Deploys stay with Cloudflare Pages' git integration (D14).

Verified

  • mise install && pnpm install && pnpm check && pnpm build green.
  • Seeding import clsx from "clsx" and a chained type assertion in a .ts file fails pnpm lint on both counts — the import ban and anti-slop are live.
  • The hook formats a .ts with oxfmt and an .astro with Prettier, and returns oxlint findings to the agent with exit 2.
  • Nothing under legacy/ is read by oxlint, oxfmt, Prettier, tsc, or knip.

CI blocking on a seeded lint error is the one criterion not verifiable locally — it gets checked on this PR.

Deviations from the phase brief

  • typescript is 6.0.3, not 7.x. typescript-eslint peers <6.1.0 and @astrojs/check peers ^5 || ^6. oxlint-tsgolint bundles its own checker and is unaffected.
  • Pins are the newest version at least a week old, not the newest version — that's minimumReleaseAge doing its job, and it applies to every dependency here.
  • pnpm typecheck is astro check && tsc -p functions. functions/ gained strict and moved to moduleResolution: "bundler" (which is what Cloudflare's esbuild actually does) so its existing extensionless @/* imports resolve. That surfaced a real bug: an absent CF-Connecting-IP header was being sent to Turnstile as the string "null". The field is optional, so it's now omitted when missing.
  • Three gitignore-semantics workarounds, all documented in docs/tooling.md: oxfmt needs --ignore-path .gitignore (it reads .prettierignore by default, which would make it skip every file it owns); Prettier is invoked against an explicit **/*.{astro,md} glob (a blanket * ignore prunes directories irreversibly); ESLint scopes via files: for the same reason.
  • anti-slop is not re-registered as a local ESLint plugin for .astro frontmatter. Rationale in ADR 0001 — the rules are type-aware, frontmatter is thin by convention here, and the logic worth checking lives in .ts.
  • ADR 0002 supersedes Phase 03's icon choice: icons come from @tabler/icons (plain SVG source, inlined at build) rather than astro-icon + @iconify-json/tabler. @tabler/icons-react can't be used at all — it ships React components, which breaks the zero-framework-runtime rule.

Docs added

docs/tooling.md (setup, ownership table, cooldown behavior, the corepack footgun, env vars), docs/adr/0001-toolchain-split.md (D24 + the exact steps to collapse onto oxc later), docs/adr/0002-tabler-icons-direct.md.

README.md still describes the Next.js workflow — Phase 11 rewrites it, per the plan.


Generated by Claude Code

CS-5 and others added 2 commits August 28, 2026 00:07
…ain, CI

Park the Next.js site in legacy/ and stand up the Astro project it will be
replaced by, with the full final toolchain in place before any UI exists.

- mise pins node 26.7.0 + pnpm 11.22.0 with committed checksums; tasks forward
  to package.json scripts, which stay the single source of truth.
- pnpm-workspace.yaml enforces a 7-day release cooldown and denies install
  scripts except sharp and esbuild. Dependency pins are therefore the newest
  version at least a week old, not the newest version.
- Astro 7 static scaffold: sitemap integration, Tailwind v4 via the vite
  plugin, strictest tsconfig, cn re-exported from a single sanctioned site.
- oxlint (type-aware, with anti-slop vendored from dmmulroy/anti-slop) and
  oxfmt own .ts/.js/.json/.css; ESLint (typed + jsx-a11y-strict) and Prettier
  own .astro and .md. Ownership table and invocation quirks in docs/tooling.md,
  rationale and the oxc migration seam in docs/adr/0001.
- knip, .editorconfig, VS Code settings, AGENTS.md (CLAUDE.md symlink), and a
  PostToolUse hook that routes each edited file to its owning toolchain and
  feeds lint failures back.
- CI gates every PR on check + build + an offline link check over dist/.

Typechecking functions/ surfaced a real bug: an absent CF-Connecting-IP header
was being sent to Turnstile as the string "null". The header is optional, so it
is now omitted when missing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YRfxMh7FLjQtDbb1BEsCbR
Astro emits root-relative hrefs (/_astro/index.*.css). In --offline mode
lychee cannot resolve those against a local file tree without knowing what
the root is, so it errored on every page. CI's check and build steps passed;
this was the only failure.

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

@CS-5 CS-5 left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Code review of the Phase 01 foundation (a268397). I installed the lockfile and ran the whole gate locally to check the toolchain actually does what the docs claim: astro check, tsc -p functions, oxlint --type-aware, eslint, oxfmt --check, prettier --check, knip, and astro build all pass, and seeded-violation probes confirm the oxlint side is live (the clsx ban, anti-slop chained-assertion rules, and type-aware TS diagnostics all fire, in src/ and in functions/), the hook formats + feeds findings back with exit 2, oxfmt does own .css, and prettier's Tailwind class sorting works in .astro.

Four findings, all verified by running the tools rather than by reading:

  1. eslint.config.ts:17jsx-a11y-strict registers zero rules. eslint-plugin-jsx-a11y is an optional peer of eslint-plugin-astro and is not installed, so accessibility linting is silently off repo-wide. Highest-impact item here, given ADR 0001's "do not drop accessibility coverage".
  2. eslint.config.ts:36 — the clsx/tailwind-merge and legacy/* import bans don't apply to .astro. They exist only in oxlint, which ignores .astro.
  3. functions/tsconfig.json:12 — the Pages Functions are typechecked more loosely than src/ (no noUncheckedIndexedAccess etc.), which is backwards for request-handling code.
  4. .claude/hooks/format-lint.sh:37 — a missing binary is reported as a lint failure, so a pre-install edit is blocked with an empty error.

Nothing wrong with the one behavioral code change in the diff: the remoteip-when-null Turnstile fix in functions/util.ts is correct, and the noEmit addition to the functions tsconfig is a good catch.


Generated by Claude Code

Comment thread eslint.config.ts
// it prunes directories, and unignoring files inside them does not bring them back.
globalIgnores(["legacy/**", "dist/**", ".astro/**", "node_modules/**", "public/**"]),
astroConfigs.recommended,
astroConfigs["jsx-a11y-strict"],

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This config contributes zero rules — there is currently no accessibility linting at all.

eslint-plugin-jsx-a11y is an optional peer of eslint-plugin-astro@3.1.0 (peerDependenciesMeta.eslint-plugin-jsx-a11y.optional: true) and is not in devDependencies, so the plugin's jsx-a11y config block degrades to { rules: {} } — silently, with no install warning.

Verified on a fresh pnpm install of this branch:

$ eslint --print-config src/pages/index.astro | grep -c jsx-a11y
0

and this file passes pnpm lint with exit 0:

---
---
``&lt;img src="/x.png" /&gt;``
<div onclick="void 0">click</div>

(Typed linting itself works — no-floating-promises and no-unnecessary-condition both fire in .astro frontmatter. It is only the a11y half that is dead.)

That contradicts AGENTS.md, docs/tooling.md, docs/adr/0001 ("Do not drop accessibility coverage to finish the migration") and Phase 09's accessibility = 1.0 Lighthouse gate — the ruleset meant to catch these before Lighthouse does is inert from Phase 01 onward.

Fix: add eslint-plugin-jsx-a11y to devDependencies. Knip will then report it unused (it is only reached through the astro config), so it needs a knip.jsonc ignoreDependencies entry alongside tailwindcss.


Generated by Claude Code

Comment thread eslint.config.ts
plugins: { perfectionist },
rules: {
// Mirrors oxfmt's sortImports so the whole repo is sorted the same way.
"perfectionist/sort-imports": "error",

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The clsx / tailwind-merge and legacy/* import bans are unenforced in .astro files.

Both bans live only in .oxlintrc.json, which ignores **/*.astro. ESLint is the only linter that reads .astro, and it sets no no-restricted-imports — so the two rules AGENTS.md leads with are off in the file type the entire site is built from (Phase 03 onward).

Verified on this branch — 0 findings, exit 0:

---
import { parseColor } from "../../legacy/src/styles/theme.ts";
const c = parseColor("yellow");
---
<p>{c}</p>

eslint --print-config on an .astro file shows no no-restricted-imports entry. Note the phase acceptance check git grep -l 'from "legacy' src also misses this, since the import is relative.

Fix: restate the same no-restricted-imports config in this files: ["**/*.astro"] block. It is a copy of the oxlint one, but unlike the anti-slop rules (whose omission ADR 0001 justifies) these two bans need no plugin to work.


Generated by Claude Code

Comment thread functions/tsconfig.json Outdated
}
},
"noEmit": true,
"strict": true

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

functions/ ends up typechecked much more loosely than src/ — the opposite of what you want, since this is the only code in the repo handling untrusted request input.

The root tsconfig.json extends astro/tsconfigs/strictest and adds noUncheckedIndexedAccess, exactOptionalPropertyTypes, noImplicitOverride, noFallthroughCasesInSwitch, verbatimModuleSyntax. This one enables strict alone, so none of those apply to the Pages Functions.

Verified with the same three lines in both trees:

const parts: string[] = "a,b".split(",");
export const first: string = parts[5];
export const len = first.length;

tsc -p functions → exit 0. astro checkts(2322): Type 'string' is not assignable to type 'number'-class error on the identical code. So an out-of-range index or a missing optional field off request.json<GenericFormRequest>() typechecks clean here and throws at runtime in the Worker.

Fix: extend the same base and keep only the genuinely Worker-specific bits:

Suggested change
"strict": true
"noEmit": true,
"strict": true,
"exactOptionalPropertyTypes": true,
"noFallthroughCasesInSwitch": true,
"noImplicitOverride": true,
"noUncheckedIndexedAccess": true,
"verbatimModuleSyntax": true

Generated by Claude Code

Comment thread .claude/hooks/format-lint.sh Outdated
run() {
local tool="$1"; shift
local bin="$repo_root/node_modules/.bin/$tool"
[[ -x "$bin" ]] || { command -v pnpm >/dev/null && pnpm exec "$tool" "$@" 2>&1; return; }

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

A missing tool is indistinguishable from a lint failure here, so the hook blocks the edit with nothing useful to say.

Bare return propagates the status of the && chain. With no pnpm on PATH the chain short-circuits to 1, so run oxlint … returns 1 with empty output; the ts branch then takes the failure path, prints a blank line to stderr and exits 2. Concretely: fresh clone, before pnpm install, agent edits any .ts file → the edit is rejected with no message. With pnpm present but no node_modules, the same path surfaces pnpm's "Command not found: oxlint" as if it were lint findings.

Worth separating the two cases — e.g. have run return a sentinel status (or set a flag) when neither the local bin nor pnpm exists, and treat that as "tools not installed, exit 0" rather than as findings.


Generated by Claude Code

CS-5 commented Aug 28, 2026

Copy link
Copy Markdown
Member Author

/simplify pass — quality review (reuse · simplification · efficiency · altitude)

Quality only; correctness findings are in the separate review on this PR.

Three of the four angles came back empty for this PR, and that's a real result rather than a shrug — I'll say what was checked and cleared at the bottom. Two efficiency findings:


1. Tailwind scans legacy/, so every page ships ~32 KB of CSS generated from the retired Next.js site

src/styles/global.css:1

@import "tailwindcss";

Tailwind v4's automatic source detection walks up from the CSS file and scans the whole project, minus .gitignored paths. legacy/ is checked in, so all of the old Next.js JSX is a scan target and its classes are emitted into the one shared stylesheet that every page links.

Verified in built output further up the stack — dist/_astro/BaseLayout.*.css contains classes that exist nowhere in src/:

bg-[url(/image/metal-shavings.webp)]   from-green-900/95   bg-red-600
hover:bg-yellow-500   lg:right-[-225]   data-[active=true]:after:h-[2px]
dark:text-white   h-450   max-h-37.5

Cost, measured. Adding @source not "../../legacy"; took the shared bundle from 68,567 → 36,666 bytes (12,870 → 8,206 gzipped) — a 47% cut, 4.7 KB gzipped off every page load — plus that much less scanning and generation on every build. dist/index.html and dist/styleguide/index.html came out byte-identical in size, and every house utility (pocket, eyebrow, spec-label, ambient-pool, draw-on, text-h1) survived.

This is worth fixing here rather than later: legacy/ is reference-only per AGENTS.md, so nothing it contains should ever reach the build, and the exclusion belongs next to the import that creates the problem.

Fix: @source not "../../legacy"; beside the @import, or an explicit @source scoped to ../.


2. CI reinstalls the whole dependency tree from the network on every run

.github/workflows/ci.yml:21-29

      - name: Install mise tools
        uses: jdx/mise-action@v3
        with:
          version: 2026.8.14
          install: true
          cache: true

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

mise-action's cache: true covers mise-managed tool binaries (node, pnpm), not pnpm's content-addressable store, and there's no actions/cache step for it. Every PR run re-downloads and re-links the full tree — which by PR #17/#19 includes @tabler/icons (~12k files) and sharp's platform binaries.

Fix: cache the pnpm store keyed on pnpm-lock.yaml, install with --prefer-offline.

Separately, package.json:16pnpm check chains typecheck && lint && fmt:check && knip serially, but the last three are mutually independent. Running them concurrently uses the runner's second core for free. Worth weighing against the fact that serial output is easier to read when something fails; if you keep it serial, that's a deliberate call rather than an oversight.


Checked and cleared

  • Reuse: ignoring the renames into legacy/, none of the new content (src/lib/cn.ts, tsconfig.json, .oxlintrc.json, eslint.config.ts, knip.jsonc, the vendored anti-slop rules) re-implements anything already in the tree. The ignore lists that repeat across the four configs are each required by their own tool and have no shared mechanism to reuse.
  • Simplification: the mise.toml task list duplicates package.json scripts, but that's the stated single-source-of-truth forwarding design, not accidental duplication. tools/lint/anti-slop/ is a vendored upstream plugin (MIT, documented as copy-don't-fork in its VENDOR.md) — out of scope for restructuring.
  • Altitude: the suppressions are all at the right depth and carry their justification. .oxlintrc.json's **/*.astro ignore and ESLint's files: ["**/*.astro"] scoping are the ownership split from ADR 0001; knip.jsonc's ignoreDependencies: ["tailwindcss"] names a real tool limitation. The plan/01-foundation.md edit adds an honest "Deviations from this brief" section rather than weakening a criterion — the one unmet box stays [ ] with a reason. That's the model the rest of the stack should follow.

Generated by Claude Code

Accessibility linting was inert. `eslint-plugin-jsx-a11y` is an optional peer of
`eslint-plugin-astro`, so `astroConfigs["jsx-a11y-strict"]` registered zero rules
and an alt-less `<img>` passed `pnpm lint`. It is now a direct devDependency, and
`pnpm-workspace.yaml` allows eslint 10 against its stale `^9` peer range.

The `clsx`/`tailwind-merge` and `legacy/*` import bans lived only in
`.oxlintrc.json`, which ignores `**/*.astro` — the file type the site is built
from. `eslint.config.ts` restates them.

`functions/` enabled `strict` alone, so the only code handling untrusted request
input typechecked more loosely than `src/`. It now carries the same
`noUncheckedIndexedAccess`/`exactOptionalPropertyTypes` flags as the root.

The format-lint hook's `run()` propagated its `&&` chain status, so a missing
binary was indistinguishable from a lint failure: before `pnpm install`, any
`.ts` edit was rejected with an empty message. Missing tools now return a
sentinel and let the edit through.

Tailwind's source detection scanned the checked-in `legacy/` tree, emitting
utilities for the retired Next.js site into the stylesheet every page links
(47,119 -> 9,910 bytes here, with identical HTML). `@source not` excludes it.

CI reinstalled the whole dependency tree from the network each run; mise-action
caches tool binaries, not pnpm's store. Added an `actions/cache` step keyed on
the lockfile and `--prefer-offline`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BX5PrKuYNRLVxiEj3eejhs
CS-5 pushed a commit that referenced this pull request Aug 28, 2026
Four primitives had behavior that was declared but never ran.

Carousel's scoped `[data-carousel-track] > *` was rewritten by Astro to require
this component's own scope attribute, which slotted slides never carry — so
`scroll-snap-align`, `flex: 0 0 100%` and the `flex-basis` media rule matched
nothing. Slides collapsed to content width, nothing snapped, and `itemBasis` was
dead. `:global(*)` on the child escapes the scope while keeping `define:vars`.

FieldError's `empty:hidden` could never match: `:empty` requires no child nodes
and the `<Icon>` was unconditional, so a placeholder rendered a bare red alert
triangle. The icon is now gated on slot content.

Button spread `disabled` onto the `<a>` path, where the attribute is invalid and
`:disabled` never matches — a disabled link rendered at full opacity, fully
clickable, and typechecked cleanly. It maps to `aria-disabled` (which the
variants already style) and the `href` is dropped. `Props` also extended only
`HTMLAttributes<"button">`, so every external link button the site needs failed
`astro check`; it now picks up `download`/`hreflang`/`rel`/`target`.

Accordion's `name` became an unused `data-accordion-name` with no consumer, so
the wrapper's documented exclusive-open was a no-op — the styleguide worked only
because it repeated `name` on each item. Dropped the prop; the JSDoc and the
`shadcn-astro` worked example now say where `name` belongs.

Icon emitted `stroke`/`stroke-width` unconditionally, but Tabler's filled sources
carry no stroke, so filled glyphs inflated ~1px on every edge and thickened
narrow details.

Carousel's explicit `behavior: "smooth"` bypassed the reduced-motion
`scroll-behavior: auto !important` in global.css; the track's `scroll-smooth`
class supplies it instead. The styleguide's section numerals ran 1–6, 8, 7.

Quality: `Field.variants.ts` holds the recipe `Input` and `Textarea` were
copy-pasting — five of six lines byte-identical, which is the reuse the
sibling-variants convention exists for. `Button` gained a `pocket` variant so the
Carousel arrows and the Dialog close compose it rather than hand-building icon
buttons; the Dialog's was `p-1`, below the 44px minimum the shared recipe
enforces. `tools/checks/cn-font-size-group.mjs` makes `cn`'s font-size group
drifting from the `--text-*` tokens a `pnpm check` failure instead of a comment —
that invariant fails invisibly, and it already cost this phase a 1.3:1 contrast
bug.

The brief's carousel keyboard criterion had been rewritten in place to describe
what shipped. Restored, with the arrow decision recorded under "Deviations from
this brief" alongside the rest.

Newly-live jsx-a11y rules (see PR #15) caught two real violations: `href="#"` on
the styleguide's demo links, and `tabindex="0"` on the carousel track. The track
is a keyboard-reachable scroll container, so it is `role="region"` with a name,
and that one role is added to `no-noninteractive-tabindex`'s allowlist —
dropping the tabindex would make the slides keyboard-unreachable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BX5PrKuYNRLVxiEj3eejhs
CS-5 and others added 2 commits August 28, 2026 09:57
`oxfmt --check` was failing CI on two files: `.vscode/settings.json` (a `.json`
file, so no trailing commas) and `knip.jsonc` (a `.jsonc` file, where oxfmt keeps
them). Applied the formatter's own output.

Removing `src/lib/cn.ts` left `cnfast` with no importer, which knip's
`dependencies: "error"` rule reports. The dependency now arrives in Phase 02
alongside the first component that merges classes; `plan/01` says so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BX5PrKuYNRLVxiEj3eejhs
CS-5 pushed a commit that referenced this pull request Aug 28, 2026
Four primitives had behavior that was declared but never ran.

Carousel's scoped `[data-carousel-track] > *` was rewritten by Astro to require
this component's own scope attribute, which slotted slides never carry — so
`scroll-snap-align`, `flex: 0 0 100%` and the `flex-basis` media rule matched
nothing. Slides collapsed to content width, nothing snapped, and `itemBasis` was
dead. `:global(*)` on the child escapes the scope while keeping `define:vars`.

FieldError's `empty:hidden` could never match: `:empty` requires no child nodes
and the `<Icon>` was unconditional, so a placeholder rendered a bare red alert
triangle. The icon is now gated on slot content.

Button spread `disabled` onto the `<a>` path, where the attribute is invalid and
`:disabled` never matches — a disabled link rendered at full opacity, fully
clickable, and typechecked cleanly. It maps to `aria-disabled` (which the
variants already style) and the `href` is dropped. `Props` also extended only
`HTMLAttributes<"button">`, so every external link button the site needs failed
`astro check`; it now picks up `download`/`hreflang`/`rel`/`target`.

Accordion's `name` became an unused `data-accordion-name` with no consumer, so
the wrapper's documented exclusive-open was a no-op — the styleguide worked only
because it repeated `name` on each item. Dropped the prop; the JSDoc and the
`shadcn-astro` worked example now say where `name` belongs.

Icon emitted `stroke`/`stroke-width` unconditionally, but Tabler's filled sources
carry no stroke, so filled glyphs inflated ~1px on every edge and thickened
narrow details.

Carousel's explicit `behavior: "smooth"` bypassed the reduced-motion
`scroll-behavior: auto !important` in global.css; the track's `scroll-smooth`
class supplies it instead. The styleguide's section numerals ran 1–6, 8, 7.

Quality: `Field.variants.ts` holds the recipe `Input` and `Textarea` were
copy-pasting — five of six lines byte-identical, which is the reuse the
sibling-variants convention exists for. `Button` gained a `pocket` variant so the
Carousel arrows and the Dialog close compose it rather than hand-building icon
buttons; the Dialog's was `p-1`, below the 44px minimum the shared recipe
enforces. `tools/checks/cn-font-size-group.mjs` makes `cn`'s font-size group
drifting from the `--text-*` tokens a `pnpm check` failure instead of a comment —
that invariant fails invisibly, and it already cost this phase a 1.3:1 contrast
bug.

The brief's carousel keyboard criterion had been rewritten in place to describe
what shipped. Restored, with the arrow decision recorded under "Deviations from
this brief" alongside the rest.

Newly-live jsx-a11y rules (see PR #15) caught two real violations: `href="#"` on
the styleguide's demo links, and `tabindex="0"` on the carousel track. The track
is a keyboard-reachable scroll container, so it is `role="region"` with a name,
and that one role is added to `no-noninteractive-tabindex`'s allowlist —
dropping the tabindex would make the slides keyboard-unreachable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BX5PrKuYNRLVxiEj3eejhs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants