awesome-mcp-explorer needs six mutually distinguishable colours for its A-F
grades, and the design system had no answer -- one accent, and semantic roles
that deliberately cluster in one hue wedge. Raised as its own decision (per
PHASE3-PROMPT) and resolved by designing the ramp rather than mapping grades
onto success/warning/danger, which would have collapsed six steps into three.
Chosen by search, not by eye, under four hard constraints:
- pairwise CIE dE >= 20 under normal vision AND deuteranopia, protanopia and
tritanopia (worst observed 26.0)
- >= 3:1 against BOTH backgrounds, since the ramp does not re-theme. This is
the binding constraint: such colours only exist in L* 44-59.
- monotonic lightness 58 -> 44, so hue and lightness encode redundantly and
the ramp survives greyscale as an ordinal scale
- muted saturation, to sit with the rest of the palette
scripts/ramp_check.py re-runs the whole proof and exits non-zero on a
regression, so this is a gate rather than a paragraph. A seventh hue is not
available (best candidate reaches dE 17.9); group the tail into "other".
This ramp is the only token group that leaves the warm wedge, deliberately:
hue separation is the entire point of a categorical scale.
Also: tokens.test.ts guards every frozen value against drift, and
tokens.md was split (screen-size.md) to hold the 250-line cap.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U4Rj3s7K2qzrLNgJqqPhno
4.5 KiB
@kuhyx/web-ui
The shared token + component layer for kuhy's web apps — the web counterpart to
~/utils/design_system (Dart). Owns the frozen palette, the spacing/radius/type
scales, and the components that were each reimplemented in two or more repos.
Consumers import the tokens rather than transcribing the token table by hand.
Source of truth for the values is ~/utils/unified-design-system/tokens.md;
this package is its executable form for the web.
Install
// package.json — pnpm only, see "Why the &path: suffix" below.
"dependencies": {
"@kuhyx/web-ui": "github:kuhyx/utils#web_ui-v0.2.0&path:/web_ui"
}
import "@kuhyx/web-ui/tokens.css"; // once, at the app entry point
import "@kuhyx/web-ui/range-slider.css"; // only if you use RangeSlider
import { RangeSlider, fuzzyMatch, quantileValue } from "@kuhyx/web-ui";
Why the &path:/web_ui suffix
utils is a monorepo with no root package.json, and npm cannot install from
a subdirectory of a git repo — github:user/repo#tag clones the root, looks
for package.json there, and fails with ENOENT (verified against npm 11.16).
pnpm supports the subdirectory via &path:/<dir>, which is why both consumers
are pnpm. This is the only difference from the Dart (git: {url, ref, path}) and
Python (#subdirectory=) conventions the rest of utils follows.
Why dist/ is committed
Vite does not transpile dependencies, so shipping raw .tsx would force
per-repo bundler config on every consumer. The tag carries a prebuilt dist/
(ESM + .d.ts, JSX already compiled, .ts specifiers rewritten to .js), so
consumers stay at zero config. pnpm build before tagging, always — a tag
whose dist/ is stale ships old code to every consumer with no error.
What's here
| Export | Notes |
|---|---|
tokens.css |
The :root block: palette, spacing, radius, type, shadows, focus ring. Also sets :focus-visible globally. |
RangeSlider |
Dual-thumb slider over a value distribution, not a linear ramp. |
range-slider.css |
Its styling, entirely in tokens. |
quantile.ts helpers |
nth, clamp01, quantileValue, valueQuantile, fractionFromPointer. |
fuzzyMatch |
Case-insensitive subsequence match. |
The categorical ramp
--cat-1 … --cat-6 are for charts, tags and category dots — N mutually
distinguishable colours with no inherent meaning. Never use them for
semantic state; --success/--warning/--danger own that.
They deliberately leave the warm wedge the rest of the palette sits in, because
hue separation is the whole point of a categorical scale. The values were chosen
by search under a ≥20 CIE ΔE floor across normal vision and deuteranopia,
protanopia and tritanopia, ≥3:1 contrast on both backgrounds, and a
monotonic lightness staircase so the ramp survives greyscale. Re-run the proof
with python3 ~/utils/unified-design-system/scripts/ramp_check.py — it exits
non-zero if an edit breaks any of it. A seventh hue is not available: the best
candidate reaches only ΔE 17.9. Group the tail into "other" instead.
fractionFromPointer is deliberately pure geometry: it makes the slider's
behaviour testable without layout, which jsdom does not implement.
RangeSlider is keyboard-operable
Both donor copies were pointer-only. The design system requires that every
action be reachable with the keyboard alone, and a role="slider" that ignores
arrow keys is an ARIA role that lies — so the shared version adds:
| Key | Effect |
|---|---|
← ↓ / → ↑ |
Step one sample through the distribution |
PageDown / PageUp |
Step ten samples |
Home / End |
Jump to the distribution's bounds |
Pass label and format to get the built-in head (name + value readout);
omit them for a bare track when the caller draws its own chrome, as
dufs-cloud's SizeRange/DurationRange wrappers do.
Development
pnpm install
pnpm test # vitest
pnpm coverage # 100% branches/functions/lines/statements, enforced
pnpm lint # tsc --noEmit && eslint
pnpm build # tsc -b + copy CSS into dist/
The 100% bar matches Phase 1's design_system: a shared layer two repos depend
on does not get to be the least-tested code in the tree.
Releasing
pnpm test && pnpm build
git add web_ui && git commit -m "web_ui: <what changed>"
git tag web_ui-v<major>.<minor>.<patch> && git push origin main --tags
Then bump the tag in each consumer's package.json and re-run pnpm install.