mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 20:27:15 +01:00
Some checks failed
dependency freshness / dependencies on newest stable (push) Waiting to run
file length / file-length (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
palette drift / palette (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
shared gate tests / gate unit tests (push) Waiting to run
web_ui tests / test (push) Has been cancelled
RangeSlider.astro + <range-slider> replace the React component. Pure helpers, tokens.css and range-slider.css are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WzxdWYdwRAtMg7ki6TKxuU
142 lines
6.3 KiB
Markdown
142 lines
6.3 KiB
Markdown
# @kuhyx/web-ui
|
|
|
|
The shared token + component layer for kuhy's web apps — the web counterpart to
|
|
`~/src/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 `~/src/utils/unified-design-system/DOCS-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.5.0&path:/web_ui"
|
|
}
|
|
```
|
|
|
|
Astro is the only supported framework (`peerDependencies.astro >=7.4.0`).
|
|
|
|
```astro
|
|
---
|
|
// Layout / page frontmatter
|
|
import RangeSlider from "@kuhyx/web-ui/RangeSlider.astro";
|
|
import "@kuhyx/web-ui/tokens.css"; // once, in the shared layout
|
|
import "@kuhyx/web-ui/range-slider.css"; // only if you use RangeSlider
|
|
---
|
|
<RangeSlider label="Stars" />
|
|
<script>
|
|
import "@kuhyx/web-ui/range-slider"; // registers <range-slider>
|
|
const slider = document.querySelector("range-slider");
|
|
slider.values = [0, 0, 3, 40, 900]; // ascending distribution
|
|
slider.format = (value) => `${value} stars`;
|
|
slider.addEventListener("change", (event) => {
|
|
const { lo, hi } = event.detail; // bubbles; raw values, lo <= hi
|
|
});
|
|
</script>
|
|
```
|
|
|
|
Pure helpers (`fuzzyMatch`, `quantileValue`, ...) come from
|
|
`import { fuzzyMatch } from "@kuhyx/web-ui";`. That entry point touches no DOM,
|
|
so it is safe in frontmatter.
|
|
|
|
### 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, and why `.astro` ships as source
|
|
|
|
tsc cannot emit `.astro`, so `src/RangeSlider.astro` ships as it is and Astro
|
|
compiles it from the dependency. Everything TypeScript ships prebuilt: the tag
|
|
carries `dist/` (ESM + `.d.ts`, `.ts` specifiers rewritten to `.js`), and the
|
|
`.astro` file imports `../dist/...`, never the `.ts` source. **`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.astro` | Server-rendered shell for `<range-slider>`. Props: `values`, `lo`, `hi`, `label`, `format` (server-side, first paint of the head only). Its `<script>` registers the element. |
|
|
| `range-slider` | The custom element. `values` and `format` properties, `lo` / `hi` / `label` attributes, bubbling `change` event with `{ lo, hi }`. Adopts the server markup, or builds it in plain HTML. |
|
|
| `range-slider.css` | Its styling, entirely in tokens. |
|
|
| `quantile.ts` helpers | `nth`, `clamp01`, `quantileValue`, `valueQuantile`, `fractionFromPointer`. |
|
|
| `stepped-value.ts` helpers | `keyTarget`, `steppedValue`. |
|
|
| `fuzzyMatch` | Case-insensitive subsequence match. |
|
|
|
|
The element writes `lo` / `hi` itself on every change, so it works without any
|
|
wiring; set the attributes from outside to override it (e.g. a URL-hash restore).
|
|
With fewer than two `values` the slider stays hidden. `values` set before the
|
|
element upgrades is picked up on connect.
|
|
|
|
### 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 ~/src/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.
|
|
|
|
### `<range-slider>` 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 to the next **distinct** value |
|
|
| `PageDown` / `PageUp` | Step ten distinct values |
|
|
| `Home` / `End` | Jump to the distribution's bounds |
|
|
|
|
Set `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.
|
|
|
|
Stepping targets the next *distinct* value, not the next index. Real
|
|
distributions are lumpy — ~1500 of awesome-mcp-explorer's ~2981 star counts are
|
|
`0` — and an index step inside a run of equal values moves the index while
|
|
leaving the value, the thumb and the filter untouched. That reads as a dead
|
|
arrow key, and jsdom's evenly-spaced fixtures do not catch it.
|
|
|
|
## Development
|
|
|
|
```bash
|
|
pnpm install
|
|
pnpm test # vitest (jsdom for the element, AstroContainer for the .astro markup)
|
|
pnpm coverage # 100% branches/functions/lines/statements, enforced
|
|
pnpm lint # tsc --noEmit && eslint && astro check
|
|
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`.
|