mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-11 17:34:00 +01:00
Some checks are pending
crdt_sync_dart tests / test (push) Waiting to run
crdt_sync_flutter tests / test (push) Waiting to run
crdt-sync pre-commit / pre-commit (push) Waiting to run
crdt-sync tests / test (push) Waiting to run
dependency freshness / dependencies on newest stable (push) Waiting to run
design_system tests / test (push) Waiting to run
file length / file-length (push) Waiting to run
freedays pre-commit / pre-commit (push) Waiting to run
freedays tests / test (push) Waiting to run
gatelock pre-commit / pre-commit (push) Waiting to run
gatelock tests / test (push) Waiting to run
github_device_auth tests / test (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
music_theory tests / test (push) Waiting to run
palette drift / palette (push) Waiting to run
shared gate tests / gate unit tests (push) Waiting to run
sync_settings_ui tests / test (push) Waiting to run
web_ui tests / test (push) Waiting to run
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AwtowrowDXx6ccmWVPkCaF
125 lines
6.5 KiB
Markdown
125 lines
6.5 KiB
Markdown
# Phase 3 record — `~/src/utils/web_ui` (`@kuhyx/web-ui`)
|
||
|
||
The web counterpart to Phase 1's Dart `design_system`. Companion to
|
||
`nielsen-audit.md`, which holds the grid and the deferred clusters.
|
||
|
||
Shipped as `web_ui-v0.3.1`, consumed by **dufs-cloud** and
|
||
**awesome-mcp-explorer**.
|
||
|
||
Gated like Phase 1: `web_ui/.pre-commit-config.yaml` +
|
||
`.github/workflows/web-ui-tests.yml` run lint, the 100% coverage bar, the ramp
|
||
proof, and a **dist/-is-fresh check** — that last one caught a stale build on
|
||
its very first run, which is the one failure mode a committed `dist/`
|
||
introduces.
|
||
|
||
## What shipped
|
||
|
||
| Item | Notes |
|
||
| --- | --- |
|
||
| `tokens.css` | Full palette, spacing, radius, type, shadow tiers, focus ring, and the global `:focus-visible` rule. |
|
||
| `RangeSlider` | Reconciled from the two drifted copies; keyboard-operable. |
|
||
| `quantile.ts` | `nth`, `clamp01`, `quantileValue`, `valueQuantile`, `fractionFromPointer`. |
|
||
| `fuzzyMatch` | Was `fuzzyMatch` in dufs-cloud and `matchesFuzzy` in amx. |
|
||
| Categorical ramp | `--cat-1…6`; see `tokens.md`. New token group, see below. |
|
||
|
||
## FilterBar was NOT extracted — and should not be
|
||
|
||
The phase brief listed `FilterBar` + `filter-sort.ts` as the third component,
|
||
to be "reconciled" between dufs-cloud (174 lines) and awesome-mcp-explorer
|
||
(225). Inspection says they are a **name collision, not a duplicate** — exactly
|
||
the Phase 1 trap the brief itself warns about, one level deeper than the three
|
||
`FilterSheet`s.
|
||
|
||
They share a name and nothing else:
|
||
|
||
| | dufs-cloud | awesome-mcp-explorer |
|
||
| --- | --- | --- |
|
||
| Shape | horizontal `<div class="filterbar">` strip | vertical `<aside class="filters">` sidebar |
|
||
| Controls | search input, `<select>`s, sort key + direction | tri-state chip pickers, no search, no sort |
|
||
| Domain | files (`DirEntry` + `MetaIndex`) | MCP servers (`Server`) |
|
||
| Props | `onFilter` + `onSort` | one `onChange` |
|
||
| `FilterState` | `type`/`extIncludes`/`minSize`/`minDurationMs`/`minPixels` | `languages`/`scope`/`os`/`foss`/`cost`/`grades` |
|
||
| `SortKey` | 9 file keys | disjoint server keys |
|
||
|
||
`applyFilterSort` differs in arity and parameter types. Extracting a component
|
||
over this would mean inventing a generic that neither caller wants, then
|
||
threading both vocabularies through it — the "shipping a widget nobody imports"
|
||
failure mode, with a forced abstraction on top.
|
||
|
||
What *was* genuinely duplicated inside those files was extracted: the fuzzy
|
||
subsequence matcher (identical algorithm, renamed identifiers) and the whole
|
||
`quantile.ts` module (near-verbatim, comments aside).
|
||
|
||
**Do not re-raise FilterBar as an extraction candidate** without new evidence —
|
||
specifically, a third repo whose filter surface is structurally like one of
|
||
these two. A shared *sort-direction toggle* or *filter-chip* is a plausible
|
||
future extraction; the bar itself is not.
|
||
|
||
`passesTri`/`TriSelect` was checked as a possible third duplicate: dufs-cloud
|
||
carries `extIncludes`/`extExcludes` in its `ExtensionPicker`, but the logic is
|
||
inline and file-extension-specific, not a shared generic. One repo is not two,
|
||
so it stays in amx.
|
||
|
||
## The categorical ramp (new)
|
||
|
||
The brief flagged that the design system has one accent and therefore no answer
|
||
for "N mutually distinguishable hues", and said to raise it rather than invent
|
||
one inline. It was raised; the decision was to **design a real ramp** rather
|
||
than map grades onto `success`/`warning`/`danger` (which would have collapsed
|
||
six ordinal steps into three and made A and B identical).
|
||
|
||
Six colours, chosen by search under measured constraints, documented in
|
||
`tokens.md` and re-checkable with `scripts/ramp_check.py` (exits non-zero on a
|
||
regression). The binding constraint turned out to be theme independence: a
|
||
colour readable on both `ink` and `paper` must sit in L\* 44–59, which is why
|
||
the ramp is mid-toned throughout. A seventh hue is not available at ΔE 20 — the
|
||
best candidate reaches 17.9.
|
||
|
||
amx's grades now map onto it. **This changed those colours visibly**, and for
|
||
the better: the old hand-picked A/B pair (`#2f8f5b` green vs `#4f8f2f` olive)
|
||
was nearly indistinguishable under deuteranopia.
|
||
|
||
## Follow-ups
|
||
|
||
1. **Dart side does not have the ramp.** `tokens.md` is the source of truth for
|
||
both stacks and now documents `cat-1…6`, but `~/src/utils/design_system` ships
|
||
only the original palette. A `design_system` bump should add it, so the
|
||
"same hex values everywhere" invariant holds. Deliberately not done here:
|
||
Phase 1 is tagged with six consumers, and that retag is its own change.
|
||
2. **Python/Tk side** likewise has no ramp (no consumer needs one yet).
|
||
3. **`eslint-plugin-testing-library` is not installed here**, so the one
|
||
query-by-class in the slider test carries a prose comment instead of a
|
||
disable directive (an unknown-rule disable is itself an error). If more
|
||
component tests land, installing the plugin to match the consumers is the
|
||
tidier answer.
|
||
4. **npm cannot consume this package.** `utils` has no root `package.json`, and
|
||
npm's `github:` shorthand has no subdirectory syntax — it clones the root and
|
||
fails with `ENOENT` (verified, npm 11.16). pnpm's `&path:/web_ui` works, so
|
||
both consumers are pnpm. A future npm-only consumer needs either its own repo
|
||
or a published tarball.
|
||
|
||
## Verification performed
|
||
|
||
- `pnpm test` + 100% coverage in all three repos (web_ui 84, amx 405, dufs 224).
|
||
- Built bundles grepped: shared tokens present, every bespoke value absent —
|
||
definitions *and* `var()` references, since checking only definitions lets a
|
||
shadowed token survive.
|
||
- Real headless Chrome at 1366x768 and 1024x600: `scrollWidth <= innerWidth`,
|
||
and `:focus-visible` computed as `2px solid rgb(184,134,46)` after a real Tab
|
||
(a programmatic `.focus()` does not match `:focus-visible` and will report no
|
||
ring even when one is correctly styled).
|
||
- dufs-cloud checked against a live `dufs` backend, since a static file server
|
||
answers `501` to `PROPFIND` and the app never renders its filter bar.
|
||
|
||
## The bug jsdom could not catch
|
||
|
||
The first cut of the keyboard support stepped by *array index*. Every unit test
|
||
passed, on an evenly spaced ten-value fixture. In the browser the arrow keys did
|
||
nothing: ~1500 of amx's ~2981 star values are `0`, so a step inside that run
|
||
moved the index and left the value — and therefore the thumb, the readout and
|
||
the filter — exactly where it was.
|
||
|
||
`steppedValue` now walks to the next *distinct* value. The lesson is about
|
||
fixtures, not about arrows: an evenly-spaced fixture is not a distribution, and
|
||
the component's whole reason for existing is that real distributions are lumpy.
|