Skip to content

chore: add fast-check and axe-core test dependencies - #204

Merged
pbroom merged 1 commit into
mainfrom
chore/assessment-dev-deps
Oct 10, 2026
Merged

pbroom merged 1 commit into
mainfrom
chore/assessment-dev-deps

Conversation

@pbroom

@pbroom pbroom commented Oct 6, 2026 •

Copy link
Copy Markdown
Owner

Adds fast-check (core, driver) and axe-core (react) as dev dependencies so the property-based and accessibility tests later in this stack can run.

Part of a stacked set of PRs (#204 → #221) from the color-kit assessment. Validated per branch with pnpm build and pnpm test; lint, format, typecheck, size, artifact, preprod and agents checks pass at the top of the stack.

🤖 Generated with Claude Code

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

pbroom commented Oct 6, 2026 •

Copy link
Copy Markdown
Owner Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@greptile-apps

greptile-apps Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[High risk] Adds test dependencies to the build lockfile.

The dependency-only change appears safe to merge.

Summary

Adds fast-check as a development dependency for core and driver tests, and axe-core for React tests, with corresponding pnpm lockfile entries.

Reviews (1) · Last reviewed commit: "chore: add fast-check and axe-core test ..."

pbroom added a commit that referenced this pull request Oct 10, 2026
…ction (#207)

Option-less mix() interpolated the raw hue of achromatic endpoints, so
mix(white, #0000ff) landed at h=312.03 while interpolate() (and
colorjs.io) give 264.05. Three different achromatic cutoffs were in use
(1e-4 in OKLab->OKLCH, 1e-3 in option-less interpolate, 4e-6 in the CSS
interpolation path), and the sRGB transfer function was written three
times with different sign handling.

- Add ACHROMATIC_CHROMA_THRESHOLD (0.000004, the CSS Color 4 OKLCH
  epsilon) and isAchromatic() in utils, exported from the root. Every
  8-bit/float gray converts with chroma ~1e-15 and the least chromatic
  non-gray 8-bit color has chroma ~0.00106, so the cutoff separates them.
- Option-less mix()/mixInto() now delegate to the same OKLCH default as
  interpolate()/generateScale(): a powerless endpoint borrows the other
  endpoint's hue, both powerless gives h=0, and input hues are normalized
  before the shortest-arc fix-up. mix, interpolate and generateScale now
  agree, and match colorjs.io for gray endpoints in every space.
- OKLab->OKLCH conversion keeps reporting h=0 for achromatic results
  (now at the shared threshold). Color.h stays a number: a rectangular
  source has no hue to preserve, and atan2 of float residue is noise, so
  0 is the canonical achromatic hue. Chroma is left as computed. Hues of
  OKLCH inputs are never rewritten; interpolation treats them as
  powerless when chroma <= threshold instead.
- linearToSrgbChannel()/srgbToLinearChannel() in utils are now the one
  sign-preserving, extended-range transfer pair (CSS Color 4/colorjs.io),
  used by conversion/p3, interpolation, interop kernels, srgb and gamut
  linear bounds. In-range results are unchanged.
- Property tests (fast-check): hue in [0, 360), mix(a,b,0)~a,
  mix(a,b,1)~b, option-less mix/mixInto/interpolate/generateScale
  agreement, option-less vs { space: 'oklch' } agreement, and transfer
  pair oddness/invertibility; plus colorjs.io checks for gray endpoints.

Part of a stacked set of PRs (#204 → #221) from the color-kit assessment. Validated per branch with `pnpm build` and `pnpm test`; lint, format, typecheck, size, artifact, preprod and agents checks pass at the top of the stack.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
pbroom added a commit that referenced this pull request Oct 10, 2026
relativeLuminance() and contrastAPCA() measured colors through toRgb(),
i.e. 8-bit rounded, clipped sRGB: oklch(0.6 0.1 200) vs
oklch(0.95 0.02 90) gave WCAG 3.2520 where colorjs.io gives 3.2635, and
the APCA field was stepped (5 distinct values over 200 samples spanning
dL = 0.004). Contrast regions meanwhile used unclamped luminance for
WCAG, so points just inside an AA region could still fail
contrastRatio() >= 4.5.

- Contrast metrics now measure the unrounded sRGB channels clipped to
  the display gamut. Clipping (not gamut mapping) was chosen because it
  is what toRgb()/toHex() render, it is exact and equal to colorjs.io for
  every in-gamut color, it is O(1) with no dependence on a mapping
  method, and WCAG/APCA are defined on displayable [0, 1] channels.
- New optional ContrastOptions on relativeLuminance, contrastRatio,
  contrastAPCA, meetsAA and meetsAAA: gamut ('srgb' default, or
  'display-p3' to clip in P3 and measure the extended sRGB result) and
  precision ('exact' default, or '8bit' to quantize like hex-based
  tools; reproduces color-kit 0.1 results bit for bit).
- Contrast region fields (hybrid and legacy, WCAG and APCA) evaluate the
  public contrastRatio/contrastAPCA with the region's gamut, so region
  boundaries sit on the check's own boundary. The internal
  relativeLuminanceUnclamped/contrastRatioUnclamped are removed.
- toHsl/toHsv/toHct no longer round to 8-bit first (new
  conversion/unrounded.ts; toHct uses Material's CAM16 on the unrounded
  clipped color). rgbToHsl/rgbToHsv treat channel spreads <= 1e-10 as
  gray so float noise does not produce an arbitrary hue; 8-bit inputs
  are unaffected. This touches three one-line bodies in
  conversion/index.ts.
- 8-bit inputs: default results match the old values to float precision
  and pass/fail decisions are unchanged; '8bit' mode is bit-identical.
- Tests: colorjs.io reference for unrounded/out-of-gamut OKLCH pairs
  (vs the clipped color), fast-check property that contour vertices
  nudged 0.0005 L either side split across the public check (about half
  failed with 8-bit checks), APCA/WCAG continuity, P3 gamut option, and
  HCT/HSL parity with the 8-bit results.

Part of a stacked set of PRs (#204 → #221) from the color-kit assessment. Validated per branch with `pnpm build` and `pnpm test`; lint, format, typecheck, size, artifact, preprod and agents checks pass at the top of the stack.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
pbroom added a commit that referenced this pull request Oct 10, 2026
toSrgbGamut/toP3Gamut only reduce OKLCH chroma at fixed L and h, which
under-shoots what CSS (and colorjs.io) produce: oklch(0.7 0.35 150) maps
to C=0.193 instead of the CSS Color 4 result C=0.210.

- Add GamutMapOptions { method?: 'chroma-reduction' | 'css' } to
  toSrgbGamut, toP3Gamut, toSrgbGamutInto and toP3GamutInto (types
  GamutMapMethod/GamutMapOptions exported). 'css' implements the CSS
  Color 4 binary search with local MINDE (deltaE OK JND 0.02, epsilon
  1e-4, clip in the target gamut) allocation-free on scratch objects; it
  matches colorjs.io toGamut({ method: 'css' }) to ~1e-15 dE_OK for both
  sRGB and P3.
- Default stays 'chroma-reduction': the CSS method shifts L and h, while
  contrast regions, plane geometry, slider tracks and the driver's
  displayed state rely on L/h being preserved, so it is not strictly
  better for existing tests or callers. Both methods return colors that
  pass inSrgbGamut/inP3Gamut (with GAMUT_EPSILON slack) unchanged and
  snap the lightness endpoints, so membership and mapping stay
  consistent.
- toSrgbGamut/toP3Gamut get a one-argument overload so passing them
  straight to Array.prototype.map keeps type-checking (the index
  argument is ignored at runtime).
- driver: mapDisplayedColors(requested, { gamutMapMethod }) and
  CreateColorStateOptions.gamutMapMethod forward the method; the default
  is unchanged.
- maxChromaAt reuses one scratch probe instead of allocating a Color per
  bisection step.
- The gamut-region solver's search-gap handling (oklchChromaField) is
  about maxChromaAt's bisection bracket, not the mapping method, so it
  is left as is.

Part of a stacked set of PRs (#204 → #221) from the color-kit assessment. Validated per branch with `pnpm build` and `pnpm test`; lint, format, typecheck, size, artifact, preprod and agents checks pass at the top of the stack.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
pbroom added a commit that referenced this pull request Oct 10, 2026
Controlled useColor/useMultiColor read `state` from the render closure, so
two updates in one tick (e.g. setChannel('l') then setChannel('c')) each
started from the same stale value and the second overwrote the first.

- Add an internal `useLatestSnapshot` (color-store.ts): updates write a
  pending snapshot keyed to the committed value; reads return it until the
  controlled prop (or committed uncontrolled state) changes. Refs are only
  touched in event handlers and a layout effect, never during render.
- onChange timing: both hooks now call onChange synchronously inside the
  setter, before React re-renders, in controlled and uncontrolled modes,
  once per effective update and never for no-ops. Uncontrolled useMultiColor
  previously emitted from a post-commit effect; it now computes the event
  eagerly from the latest snapshot and still commits via
  setInternalModel(prev => ...). Synchronous emission makes it StrictMode-
  safe without the post-commit sequence-dedupe queue, which is removed.
- Move single-color transitions into driver reducers (setColorRequested,
  setColorChannel, setColorActiveGamut, setColorActiveView) and use them
  from useColor; no-op semantics are unchanged.
- removeColor drops its unused `source` param (hard cut-over; removal has
  no entry left to carry a source).
- useColor hex/rgb/hsl/hsv/oklch are lazy getters cached per requested
  color, so the non-reactive <Color> provider no longer converts every render.
- Docs: document onChange timing and controlled composition.

Part of a stacked set of PRs (#204 → #221) from the color-kit assessment. Validated per branch with `pnpm build` and `pnpm test`; lint, format, typecheck, size, artifact, preprod and agents checks pass at the top of the stack.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
pbroom added a commit that referenced this pull request Oct 10, 2026
…211)

- ColorPlane, ColorArea, ColorSlider: replace inline callback refs with
  memoized callback refs (new assignRef helper honoring React 19 ref
  cleanups). The inline ColorPlane ref destroyed the WebGL program and
  render key on every commit, recompiling shaders each drag frame.
- ColorPlane: per-canvas resources live in effects keyed on the mounted
  node; handle webglcontextlost/webglcontextrestored by rendering through
  the cpu path on a fresh canvas while lost and rebuilding on restore.
  Remount the canvas whenever the required context kind changes, since a
  canvas is locked to its first context kind.
- ColorSlider: compose consumer pointer/key handlers instead of overriding
  them (respecting defaultPrevented), and cache rect + position inset per
  drag with ResizeObserver/scroll invalidation.
- ColorArea: attach window pointer/scroll listeners only while dragging.

Part of a stacked set of PRs (#204 → #221) from the color-kit assessment. Validated per branch with `pnpm build` and `pnpm test`; lint, format, typecheck, size, artifact, preprod and agents checks pass at the top of the stack.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
pbroom added a commit that referenced this pull request Oct 10, 2026
…and areas (#212)

Driver:
- Slider and area key maps add PageUp/PageDown (large step) and Home/End
  (range ends) alongside arrows. Hue wraps by default; other channels clamp
  correctly within ascending or descending ranges.
- New helpers: getColorSliderValueText, getColorAreaValueText,
  getColorAreaKeyAxis, resolveColorSliderWrap (+ key option types).

React:
- ColorSlider: stepRatio/largeStepRatio (Shift+Arrow uses the large step),
  wrap, disabled, getValueText. Human-readable aria-valuetext ("Hue 213°",
  "Lightness 60%", "Opacity 50%"); aria-valuemin/max normalized for
  descending ranges.
- Thumb: replaces shiftStepRatio with largeStepRatio (hard cut-over), adds
  getValueText. Exposes a single role="slider" with
  aria-roledescription="2D slider", aria-valuenow/min/max for the x axis,
  and aria-valuetext for both axes.
- ColorArea: disabled prop (context -> thumb aria-disabled, no tab stop,
  data-disabled on root and thumb).
- Pointer down now focuses the slider / area thumb explicitly (the
  pointerdown preventDefault suppressed default focus).
- data-focus-visible on keyboard focus via a shared modality tracker.
- Tests: keyboard matrix, hue wrap, value text, disabled, focus-on-click,
  focus-visible, and axe-core checks for slider, area, and thumb.
- Docs: API tables and accessibility notes for the new props.

Part of a stacked set of PRs (#204 → #221) from the color-kit assessment. Validated per branch with `pnpm build` and `pnpm test`; lint, format, typecheck, size, artifact, preprod and agents checks pass at the top of the stack.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
pbroom added a commit that referenced this pull request Oct 10, 2026
…ea (#213)

- Add a framework-agnostic createPointerDragController to @color-kit/driver:
  rAF coalescing, dragEpsilon filtering (both axes), and a maxUpdateHz cap
  that defers (rather than drops) over-budget frames; end() force-commits
  the pending point. Scheduling and clock are injected (defaults resolve
  requestAnimationFrame/performance.now at call time; no DOM globals at
  module scope).
- Add normalizeColorAreaPointer; ColorArea drops its inline clamp01 math.
- ColorSlider and ColorArea both drive drags through the controller.
  Slider now flushes the last pending point on pointerup/cancel like the
  area, and ignores non-finite pointer input.
- Hard cut-over: ColorSlider maxPointerRate -> maxUpdateHz (same name as
  ColorArea). Docs updated.
- Unit tests for the controller and area normalization; slider epsilon test
  now uses real pointer coordinates.

Part of a stacked set of PRs (#204 → #221) from the color-kit assessment. Validated per branch with `pnpm build` and `pnpm test`; lint, format, typecheck, size, artifact, preprod and agents checks pass at the top of the stack.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
pbroom added a commit that referenced this pull request Oct 10, 2026
Use 'display-p3' (matching CSS color(display-p3 ...)) as the only
identifier for gamma-encoded Display P3:

- InterpolationSpace: 'p3' -> 'display-p3' ('linear-p3' unchanged);
  unknown spaces now throw a TypeError instead of silently mixing in
  linear sRGB.
- PlaneModel: drop 'p3' and the 'display-p3' -> 'p3' alias; resolved
  planes keep model 'display-p3'. Unknown models throw a TypeError.
- CssColorFormat: toCss(color, 'p3') -> toCss(color, 'display-p3').
- ArraySpace (packColors/unpackColors): 'p3' -> 'display-p3'.

Update react/driver callers, tests, bench, and docs.

BREAKING CHANGE: 'p3' is no longer accepted as a space, plane model,
CSS format, or array layout; use 'display-p3'.

Part of a stacked set of PRs (#204 → #221) from the color-kit assessment. Validated per branch with `pnpm build` and `pnpm test`; lint, format, typecheck, size, artifact, preprod and agents checks pass at the top of the stack.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
pbroom added a commit that referenced this pull request Oct 10, 2026
…urface (#215)

- Remove the PlaneSenseApi type alias (use PlaneSense) and the
  PLANE_DEFAULT_RANGES constant (use PLANE_MODEL_DEFAULT_RANGES.oklch).
  PLANE_MODEL_DEFAULT_RANGES is now typed per model, so
  PLANE_MODEL_DEFAULT_RANGES.oklch.l is a defined tuple.
- Drop the internal marching-squares contour helpers and their types
  from the root barrel; nothing outside core used them.
- The compute engine (backend, scheduler, telemetry, packed ABI helpers)
  is exported only from the ./compute subpath. @color-kit/react imports
  it from @color-kit/core/compute, and the facade build rewrites core
  subpath specifiers to color-kit/<subpath>.

Root runtime exports: 197 -> 173.

BREAKING CHANGE: PlaneSenseApi, PLANE_DEFAULT_RANGES, the contour
helpers, and the compute APIs are no longer exported from the root
entry; import compute APIs from color-kit/compute.

Part of a stacked set of PRs (#204 → #221) from the color-kit assessment. Validated per branch with `pnpm build` and `pnpm test`; lint, format, typecheck, size, artifact, preprod and agents checks pass at the top of the stack.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
pbroom added a commit that referenced this pull request Oct 10, 2026
Contrast regions keep both solvers, but the caller now selects one and the
engine never switches implicitly.

- Add `engine?: 'hybrid' | 'legacy'` (default 'hybrid').
  ContrastRegionPathOptions is now a union discriminated by `engine`
  (ContrastRegionHybridOptions | ContrastRegionLegacyOptions, sharing
  ContrastRegionBaseOptions). The legacy-only options (samplingMode
  'uniform'/'adaptive', edgeInterpolation, adaptiveBaseSteps,
  adaptiveMaxDepth) require engine: 'legacy', and hybridMaxDepth /
  hybridErrorTolerance are hybrid-only. Mixing them is a type error and
  always throws a TypeError at runtime, as do unknown engines. Legacy
  samplingMode defaults to 'adaptive' when adaptive* is set, otherwise
  'uniform'; samplingMode 'hybrid' is gone.
- Plane contrast queries (PlaneContrastQueryOptions and the
  contrastBoundary/contrastRegion query types, now type aliases over the
  union), driver ColorAreaContrastRegionOptions, and inspectPlaneQuery pass
  `engine` and the engine options through, so invalid combinations are
  rejected the same way instead of being dropped by a field whitelist.
  Scheduler budgets and telemetry signatures follow the selected engine.
- The hybrid engine no longer hands failed queries to the legacy engine.
  It returns its best-effort paths and records
  trace.summary.degradedReason (ContrastHybridFallbackReason is renamed
  ContrastHybridDegradedReason). It bisects lightness intervals whose ends
  are root-free on opposite sides of the threshold, localizes branch ends
  a little past hybridMaxDepth, and keeps splitting (with the extra depth)
  while index-matched roots of neighbouring samples differ by more than
  0.03 chroma, half the branch join distance. Nearly constant-lightness
  contours previously stopped splitting there and left gaps; in a
  720-query brute-force sweep, crossings more than 0.03 from the traced
  path drop from 67 to 0 for about 1.7% more evaluations. Thin regions
  at the top of the lightness range (vertical boundary, no chroma roots)
  still degrade with 'unresolved-sign-change'; the legacy engine finds
  them.
- ContrastRegionLayer passes engine: 'legacy' explicitly (it already ran
  the legacy solver through edgeInterpolation), keeping its fill accuracy
  and speed. Its samplingMode prop is 'uniform' (default) or 'adaptive',
  and the no-op hybridMaxDepth / hybridErrorTolerance props are removed.
- Compute scheduler: only the JS backend exists, so drop the speculative
  webgpu backend kind, preferred-backend selection, warmup, baseline
  probes, circuit breakers, and their options/telemetry fields.
  PlaneComputeBackendKind is 'js' and every run reports 'default-js'.
  Also fix LRU eviction, which evicted the newly created bucket.
- Tests: engine selection and validation (runtime and type level), plane
  query pass-through, regression cases checked against a brute-force
  grid of the public checks, and degraded-reason cases. Docs and benches
  cover both engines.

BREAKING CHANGE: legacy contrast options require engine: 'legacy' and
hybrid options are rejected with it; samplingMode 'hybrid' is removed;
the hybrid solver no longer falls back to the legacy solver; the
ContrastRegionLayer hybridMaxDepth/hybridErrorTolerance props, the
'webgpu' backend kind, and the scheduler's multi-backend options and
circuitBreakers telemetry are removed.

Part of a stacked set of PRs (#204 → #221) from the color-kit assessment. Validated per branch with `pnpm build` and `pnpm test`; lint, format, typecheck, size, artifact, preprod and agents checks pass at the top of the stack.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
pbroom added a commit that referenced this pull request Oct 10, 2026
- ContrastRegionLayer scores candidate fill paths with core's
  contrastRatio / contrastAPCA, passing the region's gamut, instead of
  its own unclamped WCAG copy and gamut-less APCA call, so the score
  matches the field the contrast region was traced from.
- Export linearToSrgbChannel / srgbToLinearChannel from core and use
  them in ColorPlane's CPU path instead of a local transfer-function
  copy (GLSL shader sources keep their own implementation).
- createWebglState now deletes the shaders, program, and buffer it
  created when compilation, linking, or uniform lookup fails.

Part of a stacked set of PRs (#204 → #221) from the color-kit assessment. Validated per branch with `pnpm build` and `pnpm test`; lint, format, typecheck, size, artifact, preprod and agents checks pass at the top of the stack.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
pbroom added a commit that referenced this pull request Oct 10, 2026
- Drop the unused @base-ui/react and radix-ui peers from the color-kit
  facade (nothing in dist imports them) and stop shipping the
  `color-kit/react/color-input` subpath, which needs the unpublished
  @color-kit/control-kit. ColorInput stays in @color-kit/react and docs,
  with a note that it is not in the published package yet.
- Ship LICENSE (copied from the root at build) and THIRD_PARTY_NOTICES.md
  (Apache-2.0 notice for the bundled Material color utilities) in the
  tarball; move @material/material-color-utilities to core devDependencies
  since tsup inlines it.
- check-artifacts now asserts the license files, the absent color-input
  entry, and that dist only imports declared peers.
- Correct the "zero-dependency core" claim in DESIGN.md.
- Ignore .claude/ in ESLint and .claude/worktrees/ in git.
- Silence esbuild's expected ignored-bare-import warnings in pnpm size via a
  .size-limit.js wrapper around the existing JSON budgets.

Part of a stacked set of PRs (#204 → #221) from the color-kit assessment. Validated per branch with `pnpm build` and `pnpm test`; lint, format, typecheck, size, artifact, preprod and agents checks pass at the top of the stack.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
pbroom added a commit that referenced this pull request Oct 10, 2026
- Add @changesets/cli (v3) with `changeset` and `version-packages` scripts.
- Configure changesets so only the published `color-kit` facade is
  versioned (privatePackages: version/tag false); plain 0.x versions, with
  the `next` dist-tag still coming from publishConfig.
- Drop the two stale @color-kit/react changesets: they target a private,
  never-published package and predate the 0.1.0 release prep, so changesets
  would silently skip them.
- check:preprod now rejects changesets that target non-publishable
  packages or use a major bump while the package is pre-1.0.
- Add a Version Packages workflow (changesets/action, version PR only, no
  npm publishing or token); publishing stays manual via pnpm publish:next.
- Document the flow in CONTRIBUTING.md and README, and add a changeset for
  the facade peer/color-input/licensing changes.

Part of a stacked set of PRs (#204 → #221) from the color-kit assessment. Validated per branch with `pnpm build` and `pnpm test`; lint, format, typecheck, size, artifact, preprod and agents checks pass at the top of the stack.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
pbroom added a commit that referenced this pull request Oct 10, 2026
- Generate a TypeDoc reference for every public color-kit entry point
  (root, plane, compute, hct, interop, driver, react) from workspace
  sources via `pnpm docs:api`. The docs build runs it first and Vite
  serves the gitignored output at /reference/; the docs nav links to it
  under API Reference.
- README: engine-generated hero SVG (sRGB vs Display P3 boundaries and a
  WCAG AA region on an OKLCH L x C plane, light/dark aware) produced by
  scripts/generate-readme-hero.mjs (`pnpm readme:hero`); a subpath table
  for the color-kit package; drop the unused useColor import.
- installation.mdx: replace the disconnected useColor readout with a
  ColorStringInput bound to the same Color state.
- DESIGN.md: remove the stray brainstorm Notes section and the stray
  bold marker before useMultiColor.
- Add bug/feature issue forms, a PR template mirroring the validation
  checklist and changeset step, and a Good first contributions section.

Part of a stacked set of PRs (#204 → #221) from the color-kit assessment. Validated per branch with `pnpm build` and `pnpm test`; lint, format, typecheck, size, artifact, preprod and agents checks pass at the top of the stack.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
pbroom added a commit that referenced this pull request Oct 10, 2026
Add minor changesets for the publishable `color-kit` package covering the
assessment stack: CSS Color 4 parsing and input validation; contrast,
achromatic hue, and gamut mapping accuracy; React accessibility, stability,
and controlled state; and the breaking pre-launch API cleanup. The
packaging changes are covered by the existing facade-peers-and-licensing
changeset.

Part of a stacked set of PRs (#204 → #221) from the color-kit assessment. Validated per branch with `pnpm build` and `pnpm test`; lint, format, typecheck, size, artifact, preprod and agents checks pass at the top of the stack.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
pbroom added a commit that referenced this pull request Oct 10, 2026
The README hero's hue-150 WCAG AA split (gap between chroma 0.049 and
0.114) was an engine artifact: brute force shows one connected passing
region. It was already closed on this stack by root-jump splitting, but a
400x400 brute-force sweep of 36 hues x AA x white/black x sRGB/P3 still
found the hybrid engine splitting 13 of 144 queries (h90-100, h240-260)
into two pieces or stopping short of the gamut edge, with gaps up to
0.027 chroma.

Root cause: the greedy branch tracker only joined roots between
neighbouring lightness samples. Where the contour folds back in lightness,
roots are born or die in pairs; a fold or an exit through the axis or the
gamut edge between samples left dangling or single-point branches that
were dropped.

The hybrid engine now matches contour crossings per strip between
neighbouring samples: roots on both sides plus axis and gamut-edge
crossings (located by bisection where the zero-chroma or gamut-edge
margin changes sign), with a minimum-cost non-crossing matching, then
chains the matches into paths. After the fix every query in the sweep is
one path within 0.0001 of brute force, every vertex is a root of the
contrast margin, and paths end exactly on the axis and gamut edge. Thin
top-of-range regions that used to degrade with no paths are now traced.

Tests: brute-force agreement for the hero query (h150, AA, white/black,
sRGB/P3, both engines) and the folded sweep cases; thin-region cases now
assert the traced boundary. Regenerated the README hero SVG.



## Verification

Brute-force gate over 3,456 contrast-region queries (hues 0–350, WCAG 3/4.5/7 and APCA 0.45/0.6/0.75, 8 references, sRGB and display-p3), compared against a dense grid of the public contrast checks:

| Metric | Before | After |
|---|---|---|
| Degraded / empty where a region exists | 78 | 0 |
| Queries with a coverage gap > 0.01 | 221 | 0 |
| Queries with a split contour | 295 | 6 |
| React fill mismatch when forced to hybrid (mean / cases > 5%) | 1.79% / 9 of 180 | 0.017% / 0 of 180 |

Evaluations are unchanged (1.00x). Remaining minor APCA defects (< 0.005): a split at one fold and gamut-edge stubs. They are tracked for the follow-up that retires the legacy engine.

Part of the color-kit assessment stack (#204 → #222).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
pbroom added a commit that referenced this pull request Oct 10, 2026
Fix the last contrast-region solver defects, then remove the legacy
marching-squares engine.

Solver fixes (region-hybrid.ts):
- Place chroma roots only where the pass/fail class (margin >= 0)
  changes, and stop bisection on bracket width instead of |v| <= 1e-7.
  Value-tolerance roots broke parity with zeroSign/topSign where the
  field is flat in chroma: at the gamut edge they added a phantom root
  (path stopped short of the edge or split off a stub), and at fold tips
  they misplaced and merged the two roots (contour split at the tip).
- Insert a turning point at a fold tip when two roots on the same side
  of a strip are matched together.
- Key the maxChromaAt cache by exact lightness so gamut-edge nodes take
  the edge chroma from their own side of a maxChromaAt step.

Brute-force gate (3,456 queries): split contours 6 -> 0, max coverage
gap 0.0025 -> 0.0008, dangling path ends 12 -> 0, 0 degraded.

Legacy removal (hard cut-over):
- Delete region-legacy.ts and the legacy-only contour `interpolation`
  and `mapPoint` options.
- Remove `engine`, `samplingMode`, `edgeInterpolation`,
  `adaptiveBaseSteps`, `adaptiveMaxDepth`: type errors and a TypeError
  at runtime, including through plane queries and the driver.
- Remove ContrastRegionEngine / Base / Hybrid / Legacy option types and
  the legacy trace solver names; contrast scheduler budgets and
  telemetry keys no longer depend on an engine.
- ContrastRegionLayer runs the solver with interactive defaults
  (12 x 16 steps, depth 3, tolerance 0.003): about 0.25 ms per region
  vs 0.5 ms for the old legacy layer, fill mismatch 0.018%.
- Add a brute-force agreement test (repro cases plus a representative
  subset, ~3 s), and update docs, benches, and changesets.



## Verification

Full brute-force gate (3,456 queries; hues 0–350, WCAG 3/4.5/7 and APCA 0.45/0.6/0.75, 8 references, sRGB and display-p3):

| Metric | #222 | This PR |
|---|---|---|
| Split contours | 6 | 0 |
| Path ends off axis / gamut edge / L bounds | 12 | 0 |
| Max coverage gap | 0.0025 | 0.0008 |
| Max vertex deviation | 7.7e-5 | 3.8e-8 |
| Degraded / empty | 0 | 0 |

React `ContrastRegionLayer` (180-case fill sweep): mean mismatch 0.018% (legacy layer was 0.089%), 0 cases > 5%, about 0.22–0.25 ms per region (legacy was about 0.5 ms).

New CI gate `contrast-region-brute-force.test.ts` runs 70 cases in about 3 s, including the 8 repros that fail on the previous solver. All repo gates pass.

Part of the color-kit assessment stack (#204 → #223).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
pbroom added a commit that referenced this pull request Oct 10, 2026
Part of the color-kit assessment stack (#204 → #225). lint, build, test, format:check, typecheck and agents:check:strict pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
pbroom added a commit that referenced this pull request Oct 10, 2026
Part of the color-kit assessment stack (#204 → #225). lint, build, test, format:check, typecheck and agents:check:strict pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
pbroom added a commit that referenced this pull request Oct 10, 2026
Store the 141 distinct CSS Color 4 keywords as one alphabetical,
prefix-coded string (a digit for the letters shared with the previous
name, then the rest) plus one string of fixed-width rrggbb values,
decoded into a Map on first lookup. grey spellings still fold onto gray
and transparent is added separately.

parse + toHex + contrastRatio: 5336 B -> 5233 B gzip (-103 B)
everyday toolkit:              5380 B -> 5281 B gzip (-99 B)

Add a test that the packed table decodes to exactly the CSS Color 4
keyword list (checked against colorjs.io).



Part of the color-kit assessment stack (#204 → #227). Build, test, lint, typecheck, format, size and check:artifacts pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
pbroom added a commit that referenced this pull request Oct 10, 2026
oklab.ts destructured all four OKLab matrices into row constants at
module scope. Bundlers keep top-level destructuring even when nothing
reads it, so any bundle that needed only one direction (contrastRatio
needs OKLab -> linear sRGB; parse needs only the reverse) carried the
other two matrices too. Read the rows inside each kernel instead; the
arithmetic is unchanged, so results are bit-identical.

Gzip, size-limit (on top of #225 in parentheses):
  contrastRatio alone:            1310 -> 1096 B (1466 -> 1243 B)
  parse alone:                    4565 -> 4346 B
  parse + toHex + contrastRatio:  5233 -> 5225 B (5358 -> 5354 B)
  everyday toolkit:               5281 -> 5275 B (5412 -> 5405 B)
  packColors + unpackColors:      2317 -> 2277 B



Part of the color-kit assessment stack (#204 → #227). Build, test, lint, typecheck, format, size and check:artifacts pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
pbroom added a commit that referenced this pull request Oct 10, 2026
…n tracing (#228)

Contrast regions now have one solver, built from the chroma-column
prototype ("option C"). Pass/fail depends only on a sample's displayed
luminance, so each side of a region is one luminance level curve. The
solver traces it in columns of fixed chroma-to-lightness ratio (rays from
black), which cover the GAMUT_EPSILON slack near black where l < c: along a
ray every linear channel is r^3 times a constant, the displayed luminance
never decreases inside the gamut, and the ray's gamut edge is explicit.

- Luminance is measured with the target-gamut clipping contrastRatio /
  contrastAPCA use, the gamut edge uses the shared linear bounds, and the
  near-black strict bound is gone. Side targets come from a bisection with
  the public metric's own arithmetic, kept a hair inside the passing side,
  and piece ends are verified with the public checks, so every returned
  point passes the check and is in gamut per inSrgbGamut / inP3Gamut.
- Gamut events (where a piece starts or ends) are found on a fixed
  internal scan of the edge-visibility function, independent of the
  sampling options: 256 uniform angles plus every edge breakpoint (two
  channel bounds meeting, a channel constant changing sign, clipping
  starting, the maxChroma cap), all exact cubic roots, plus a search of
  every local extremum near zero. This replaces the prototype's probeFin /
  locateEdge, which missed fins and mixed up crossings in one bracket.
- Next to every edge corner the scan also tests one-sided slopes for a
  turn toward zero, so a gamut notch narrower than a scan step (the edge
  switching from blue > 1 to red < 0 and back near h 264.206) splits the
  contour instead of being bridged.
- Chords are checked exactly against the gamut (each channel is a cubic
  along a chord, tested at its turning points) and split while they leave
  it; sampling is clamped and spread over the hue's gamut chroma extent
  (and the angle a piece sweeps), not [0, maxChroma].
- The reference is measured unmapped, as the public checks clip it. A
  threshold equal to the contrast against white returns white as a
  one-point path, and traces count dropped pieces (droppedPieceCount).

New sampling options: initialSamples (default 32), errorTolerance (default
0.0015), maxDepth (default 6), validated like the old steps and clamped
(512, 1e-6, 12). Traces record solver 'contrast-rays' with fidelity info.
ContrastRegionLayer maps quality and drag freeze onto them, with
interactive defaults 8 / 0.004 / 3.

BREAKING CHANGE: the hybrid solver is removed. lightnessSteps, chromaSteps,
hybridMaxDepth, hybridErrorTolerance, tolerance, and maxIterations are type
errors and throw a TypeError (in contrastRegionPaths, plane queries, and
ContrastRegionLayer props). degradedReason, ContrastHybridDegradedReason,
the hybrid-only trace stages and their types, and the cusp / rootFinding /
branching timing keys are removed. ContrastRegionLayerMetrics reports
initialSamples, errorTolerance, and maxDepth.



## Verification

An independent reviewer, using its own brute-force harness built on the public contrast and gamut checks, ran three rounds. Its final verdict was safe to submit.

- Sets: main grid 3,456 queries, sRGB fold band 1,360 plus a dense 12,928, stress, cusp/polarity/slack/out-of-gamut references, more than 30k fuzz queries, and 54 constructed gamut-notch cases.
- Max gap to the true boundary: 0.00054 at core defaults and 0.0026 at React settings. There were no out-of-gamut segments and no topology errors.
- Of about 1.5M returned vertices, none fails `contrastRatio` / `contrastAPCA` or the gamut check.
- Speed: about 42× faster than the hybrid at core defaults (median 0.10 vs 4.2 ms) and 4.3× faster at the React layer setting.
- React fill mismatch: 0.023% mean, with no cases over 5%.
- Size: the plane bundle grows by about 1 kB (28.0 of 30 kB). The parse bundles are unchanged.

**Known limitation (follow-up):** the solver misses 7 tiny P3 APCA pieces against black, 6e-6 to 6e-4 long, with gaps up to 0.0022. That happens where two edge-luminance minima fall inside one internal scan step (`region-solver.ts` searches for a single extremum per scan interval).

**Breaking:** `lightnessSteps`/`chromaSteps` are replaced by `initialSamples`, `hybridErrorTolerance` by `errorTolerance`, and `hybridMaxDepth` by `maxDepth`. `tolerance`, `maxIterations` and `degradedReason` are removed, and removed options throw `TypeError`.

Part of the color-kit assessment stack (#204 → #228).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
pbroom added a commit that referenced this pull request Oct 10, 2026
… entered (#229)

Grays, black and white entered through non-OKLCH paths (hex/rgb/hsl
strings, setFromRgb/Hsl/Hsv, RGB/HSL ColorInput edits, ColorStringInput)
used to store the converted hue 0, so raising chroma afterwards jumped to
red. The state layer now keeps the latest requested hue unless the input
states an OKLCH hue (an oklch() string with a hue other than none, or an
OKLCH object). Core parse() and conversions are unchanged.

Adds driver helpers resolveIncomingRequested and hasExplicitOklchHue, and
an explicitHue option on setColorRequested, setMultiColorRequested and
SetRequestedOptions.



## Behavior

- When an achromatic color (chroma ≈ 0, including black and white) arrives through hex, rgb/hsl/hwb/lch strings, `setFromRgb`/`setFromHsl`/`setFromHsv`, or a ColorInput RGB/HSL edit, the state keeps the previous requested hue instead of resetting to 0.
- An explicit `oklch(L 0 H)` string, or an OKLCH object passed to `setRequested`, keeps its own hue. `oklch(... none)` carries the previous hue.
- Core `parse()` and the conversions are unchanged and deterministic.
- Shared driver helpers are `resolveIncomingRequested` and `hasExplicitOklchHue`. `setColorRequested` / `setMultiColorRequested` take `{ explicitHue }`, which defaults to `true`.
- Trade-off: the stored hue of a gray now depends on history, so two visually identical grays can serialize with different OKLCH hues.

Validation of the latest correction: lint, full build including DTS, test (core 888, driver 142, React 215), format:check and agents:check:strict pass locally. The control-kit dependency was built from the pinned source after installation with scripts disabled; hosted CI remains a separate check.

Multiline `oklch()` input now preserves an explicit hue consistently with the core parser. Regression coverage includes LF/CR/CRLF/tab/form-feed, zero and angle-unit hues, `none`, and alpha. The three line-break cases fail before the correction and pass after it.

Part of the color-kit assessment stack (#204 → #229).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
pbroom added a commit that referenced this pull request Oct 10, 2026
getColorAreaFallbackPoint silently mapped any unknown gamut, such as the
removed 'p3' spelling, to sRGB while every core entry point throws. It now
calls core's assertGamutTarget, so the TypeError wording is identical and
ends "'p3' is not supported; use 'display-p3'".

Validator placement: routing through the core plane fallbackPoint query
would change behaviour (it forces a finite hue and returns unclamped plane
coordinates, while the driver returns clamped, y-flipped UI coordinates
for any axes). The repo has no `./gamut` subpath, and adding one would
widen the package export map, tsup entries and facade. assertGamutTarget
is therefore exported from the existing `./plane` subpath, which already
owns the gamut-taking queries, and not from the root barrel.



lint, build, test, format:check, typecheck, size, check:artifacts, check:preprod and agents:check:strict all pass on this branch.

Part of the color-kit assessment stack (#204 → #232).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
pbroom added a commit that referenced this pull request Oct 10, 2026
…l renderer alias (#231)

Hard cut-over, no aliases left behind:

- Remove the deprecated `outOfGamut` prop and the exported
  `ColorPlaneOutOfGamutConfig` type. `edgeBehavior` is now the only way to
  choose the out-of-gamut edge treatment (default 'clamp').
- Remove `'webgl'` from `ColorPlaneRenderer` and delete the runtime mapping
  (and its one-time console warning) that resolved it to 'gpu'.
- Drop the two tests that exercised the removed options.
- Correct the docs API table, which listed the `edgeBehavior` default as
  'transparent'; the component default is 'clamp'.

ColorPlane has no runtime prop validation, so none is added; the old values
are now TypeScript errors. The `renderer="canvas2d"` alias is separate and
unchanged.

BREAKING CHANGE: ColorPlane no longer accepts `outOfGamut` or
`renderer="webgl"`.



lint, build, test, format:check, typecheck, size, check:artifacts, check:preprod and agents:check:strict all pass on this branch.

Part of the color-kit assessment stack (#204 → #232).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
pbroom added a commit that referenced this pull request Oct 10, 2026
…nt (#232)

IB-006 still described the legacy marching-squares and hybrid contrast
engines as current. Mark them as replaced with PR references (#223 retired
the legacy engine, #228 replaced the hybrid solver with the ray-based
solver in contrast/region-solver.ts), record the new options and the
removed ones, and list the two open follow-ups: 7 tiny P3 APCA pieces
missed by the single-extremum scan, and ContrastRegionLayer still
gamut-mapping its reference.

The repo-wide sweep for region-legacy, region-hybrid, hybridMaxDepth,
samplingMode and "legacy engine" found no other docs describing the removed
engines as current; remaining hits are removal notices, changelog entries or
the still-supported gamut/chroma-band samplingMode.



lint, build, test, format:check, typecheck, size, check:artifacts, check:preprod and agents:check:strict all pass on this branch.

Part of the color-kit assessment stack (#204 → #232).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
pbroom added a commit that referenced this pull request Oct 10, 2026
control-kit is a Git dependency with a `prepare` script, so pnpm prepares it
with a nested `pnpm install` in `<storeDir>/v10/tmp/_tmp_*`. The pinned
commit had no pnpm-workspace.yaml, so when the store sits inside this repo
(as on Vercel) the nested install walked up to color-kit's
pnpm-workspace.yaml, installed the whole color-kit workspace, hit
control-kit again, and recursed without end (73 levels in 4 minutes
locally; Vercel installs hung for 24+ minutes).

Pin e5aebdf, which is the previously pinned b9cd2cb plus a root
pnpm-workspace.yaml (`onlyBuiltDependencies: [esbuild]`). The nested
install now stops at control-kit's own root; no other control-kit code
changes. The same fix for control-kit main is
pbroom/control-kit#98. Moving to that line means
adopting control-kit's @pbroom/control-kit rename and newer peers, which
is left for a separate change.



## Verification

A fresh `pnpm install --frozen-lockfile` with the pnpm store **inside** the repo, which used to recurse 73+ levels, now finishes in 11 s. It runs one control-kit prepare: 211 packages, then the workspace installs once. `build` and `test` pass there. lint, build, test, format:check, typecheck, size, check:artifacts, check:preprod and agents:check:strict all pass.

Pins `e5aebdf`, which is the old pin `b9cd2cb` plus only control-kit's new `pnpm-workspace.yaml`, on the control-kit branch `fix/own-pnpm-workspace-b9cd2cb`. The same fix for control-kit `main` is in pbroom/control-kit#98. Moving to control-kit `main` later also means adopting its rename to `@pbroom/control-kit` and its new peer ranges, which is a separate upgrade.

Once this lands, #233's `--store-dir=/tmp/pnpm-store` workaround is no longer required but stays harmless.

Part of the color-kit assessment stack (#204 → #234).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
pbroom added a commit that referenced this pull request Oct 10, 2026
Slice S0 of the docs overhaul. The generated API pages need complete JSDoc, so this PR documents the whole public API and trims the public surface.

## Commits
- **`docs(core,driver,react): document the public API`** (d930462). Comments only, across 88 files; a comment-stripped comparison confirms no code changed. Every value export now has a JSDoc summary, and every function has an `@example`.
- **`refactor(react,core)!`** (251e098). Removes `BENCHMARK_SELECTED_COLOR_PLANE_RENDERER` and `COLOR_PLANE_{VERTEX,FRAGMENT}_SHADER_SOURCE` from `color-kit/react`, which is a hard cut-over of internal constants. Also exports `assertGamutTarget` from the root, adds `test-exports` assertions, and includes a minor changeset.

## Coverage (symbols with JSDoc)
256/513 → 510/510 deduplicated. Function `@example`: 82 → all.

| Entry | Value summaries | Function examples |
|---|---|---|
| root | 162/176 → 177/177 | 53/165 → 166/166 |
| plane | 29/41 → 41/41 | 0/37 → 37/37 |
| compute | 3/10 → 10/10 | 0/10 → 10/10 |
| hct | 2/3 → 3/3 | 0/3 → 3/3 |
| driver | 21/75 → 75/75 | 2/70 → 70/70 |
| react | 20/27 → 24/24 | 1/23 → 23/23 |
| interop | already complete | already complete |

## Example accuracy
A harness ran all 149 example blocks against the built package: 199 `// →` results matched and none failed. The few that couldn't be run automatically were checked by hand. One wrong existing comment, in the `mix` example, is fixed.

Bundle sizes are identical before and after the JSDoc commit, and TypeDoc reports no warnings. All repo gates pass.

Part of the color-kit assessment stack (#204 → #235).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
pbroom added a commit that referenced this pull request Oct 10, 2026
#236)

Slice S1 of the docs overhaul: the foundation for an API-first site. It replaces the shadcn component gallery with a "precision instrument" shell. Per the overhaul plan, content pages arrive in S2–S5.

## Built
- **Shell:** header with ⌘K search, theme toggle and GitHub link; a filterable sidebar (Start · Concepts · Guides · API by entry point · Map) with a Base UI mobile drawer; an "On this page" outline; a footer that reports whether the display is P3 or sRGB; skip link and landmarks.
- **Tokens:** `tokens.ts` generates `tokens.css` using color-kit itself. Achromatic neutrals, one P3 accent with sRGB fallbacks, and self-hosted Inter and JetBrains Mono. A test checks 32 contrast pairs per theme.
- **Routes:** generated from MDX frontmatter (`src/site/routes.ts`), with a hook for the generated API data (a fixture until S2).
- **Prerendering:** every route is prerendered with `react-dom/static` (`dist/<route>/index.html`) and hydrated without warnings. Search is Pagefind, loaded lazily.
- **MDX components** for later slices, documented in `src/components/ui/`: `<Example>`, `<Signature>`, `<Swatch>`/`<GamutBadge>`, `<Callout>`, `<CodeBlock>`, `<InstallLine>`, and `?highlighted` source imports.
- **`apps/docs/vercel.json`:** folds in #233's install and build settings, plus clean URLs, redirects from the old `/docs/*` and `/reference/*` paths, and immutable asset caching. It deliberately has **no SPA catch-all rewrite**: every route is prerendered, and a catch-all would serve the home page with a 200 on typos. Vercel serves `404.html` instead.

## Removed
386 files and about 48.6k lines: `vendor/sandpack` (335 files), the shadcn kit, legacy CSS, the inspector and right rail, the component-doc pages and the old demos. Old MDX moved to `src/content/_legacy/` for S4 to draw on.

Dependencies dropped: radix-ui, cva, lz-string, tw-animate-css, tailwind-merge, @tailwindcss/typography, Sandpack.

## Budgets
- Home initial JS: 86.6 kB gzip (budget 110).
- Largest chunk: `vendor-react` at 194.8 kB (limit 200).
- Prerender: 14 routes in 0.11 s.

## Verified
- Full repo gates pass, including 70 new docs tests.
- In the browser:
  - Theme toggles with no flash.
  - ⌘K opens and closes search.
  - The skip link works.
  - The drawer works at 375 px.
  - Deep links load from prerendered HTML with no hydration warnings.
  - axe reports 0 violations on the sampled pages in dark mode.

Supersedes #233 once merged.

Part of the color-kit assessment stack (#204 → #236).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
pbroom added a commit that referenced this pull request Oct 10, 2026
Slice S5, the final slice of the docs overhaul.

## Commits
- **feat(docs): native API map at `/api/map`.** A d3-hierarchy tidy tree rendered as React SVG and fed by the generated API data. There are no CDN scripts. It has its own pan/zoom (drag, pinch, ⌘/Ctrl-wheel), arrow/Home/End keyboard navigation, entry/kind/name filters and a ledger per entry point. The first view is prerendered. The chunk is 16.2 kB gzip.
- **docs(react): entry narrative, live examples and symbol notes.** `/api/react` covers the provider and hooks, requested vs displayed, controlled vs uncontrolled, accessibility and performance. It has five live examples (ColorArea + ColorPlane, ColorSlider, ColorStringInput, a controlled provider, a contrast region layer) and edge-case notes for the main components. Every claim was checked against `packages/react/src`.
- **feat(docs): polish the search dialog.** Symbols come first with kind and entry, then pages, then prose with highlighted excerpts. Recent searches are kept, and the dialog goes full-screen below 640 px.
- **fix(docs): truncate long sidebar names and collapse API domains.** The symbol tree is lazy-loaded.
- **chore(docs): point the ColorArea Playwright tools at `/api/react`.** The profile shows about 0.3 ms from pointer to thumb and no dropped frames.
- **fix(docs): keep the map ledger from widening phone pages.**
- **fix(docs): wrap long export names in symbol page headings.** The 60 longest names were checked at 375 px.

## Verified
- Gates: lint, build, test (docs 830), format:check, typecheck, size, check:artifacts and agents:check:strict all pass.
- Budgets: home initial JS is 101.7 kB gzip (limit 110), unchanged from #244. The largest chunk is 194.8 kB (limit 200).
- Browser: checked light and dark at 1440 and 375 px. No hydration warnings or console errors.

Part of the color-kit assessment stack (#204 → #245).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

This branch was successfully deployed

1 active deployment
Preview — 2221fe5f Deployed Oct 10, 2026 by vercel[bot]
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.

1 participant