utils/web_ui/README.md
Krzysztof kuhy Rudnicki 03bf1ed08b web_ui: shared web token + component layer (Phase 3)
The web counterpart to design_system (Dart): one :root block of frozen tokens
plus the components that were duplicated across repos. Extracted from
dufs-cloud and awesome-mcp-explorer, which both now consume it.

- tokens.css: the frozen palette, spacing/radius/type scales, shadow policy and
  focus ring, under the canonical variable names from unified-design-system.
  Also ships the global :focus-visible rule, rather than leaving each consumer
  to remember it.
- RangeSlider: reconciled from the two drifted copies. Takes the accessible DOM
  from awesome-mcp-explorer (real <button> thumbs with role="slider" and the
  aria-value* trio) and grab-the-nearer-thumb from dufs-cloud. Keyboard support
  (arrows/PageUp/PageDown/Home/End) is new: both donors were pointer-only, which
  the design system rules out, and an arrow-key-deaf role="slider" is an ARIA
  role that lies.
- quantile.ts + fuzzyMatch: the genuinely duplicated helpers.

FilterBar is deliberately absent. The two same-named components share no
structure -- a horizontal <select> strip over files vs a vertical tri-state chip
sidebar over servers, with disjoint FilterState, props and sort keys. It is the
name-collision-is-not-a-duplicate trap, so extracting it would ship a widget
nobody imports.

dist/ is committed (and un-ignored against the root rule) because Vite does not
transpile dependencies; consumers stay at zero config. Consumed via pnpm's
&path:/web_ui suffix -- npm cannot install from a subdirectory of a git repo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U4Rj3s7K2qzrLNgJqqPhno
2026-08-15 14:40:45 +02:00

95 lines
3.6 KiB
Markdown

# @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
```jsonc
// package.json — pnpm only, see "Why the &path: suffix" below.
"dependencies": {
"@kuhyx/web-ui": "github:kuhyx/utils#web_ui-v0.1.0&path:/web_ui"
}
```
```ts
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. |
`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
```bash
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
```bash
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`.