diff --git a/web_ui/.gitignore b/web_ui/.gitignore
new file mode 100644
index 0000000..3110c21
--- /dev/null
+++ b/web_ui/.gitignore
@@ -0,0 +1,10 @@
+# dist/ is deliberately NOT ignored here: consumers pull this package as a git
+# dep and Vite does not transpile dependencies, so the built ESM + .d.ts must be
+# committed in the tag. See README.md. The repo root ignores `dist/` globally,
+# so this un-ignores it for this subtree only — without it, `git add web_ui`
+# silently skips the build and the tag ships a package with no code in it.
+!dist/
+!dist/**
+node_modules/
+coverage/
+*.tsbuildinfo
diff --git a/web_ui/README.md b/web_ui/README.md
new file mode 100644
index 0000000..079ff01
--- /dev/null
+++ b/web_ui/README.md
@@ -0,0 +1,94 @@
+# @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:/
`, 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: "
+git tag web_ui-v.. && git push origin main --tags
+```
+
+Then bump the tag in each consumer's `package.json` and re-run `pnpm install`.
diff --git a/web_ui/dist/index.d.ts b/web_ui/dist/index.d.ts
new file mode 100644
index 0000000..698bcea
--- /dev/null
+++ b/web_ui/dist/index.d.ts
@@ -0,0 +1,10 @@
+/**
+ * The shared web token + component layer.
+ *
+ * `tokens.css` is not re-exported here — CSS is imported by path:
+ * `import "@kuhyx/web-ui/tokens.css";`
+ */
+export { clamp01, fractionFromPointer, nth, quantileValue, valueQuantile } from "./quantile.ts";
+export { RangeSlider, steppedValue, type RangeSliderProps } from "./range-slider.tsx";
+export { fuzzyMatch } from "./text.ts";
+//# sourceMappingURL=index.d.ts.map
\ No newline at end of file
diff --git a/web_ui/dist/index.d.ts.map b/web_ui/dist/index.d.ts.map
new file mode 100644
index 0000000..6f893b9
--- /dev/null
+++ b/web_ui/dist/index.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,OAAO,EAAE,mBAAmB,EAAE,GAAG,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAChG,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,KAAK,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtF,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC"}
\ No newline at end of file
diff --git a/web_ui/dist/index.js b/web_ui/dist/index.js
new file mode 100644
index 0000000..8afe078
--- /dev/null
+++ b/web_ui/dist/index.js
@@ -0,0 +1,10 @@
+/**
+ * The shared web token + component layer.
+ *
+ * `tokens.css` is not re-exported here — CSS is imported by path:
+ * `import "@kuhyx/web-ui/tokens.css";`
+ */
+export { clamp01, fractionFromPointer, nth, quantileValue, valueQuantile } from "./quantile.js";
+export { RangeSlider, steppedValue } from "./range-slider.js";
+export { fuzzyMatch } from "./text.js";
+//# sourceMappingURL=index.js.map
\ No newline at end of file
diff --git a/web_ui/dist/index.js.map b/web_ui/dist/index.js.map
new file mode 100644
index 0000000..9eac3c7
--- /dev/null
+++ b/web_ui/dist/index.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,OAAO,EAAE,mBAAmB,EAAE,GAAG,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAChG,OAAO,EAAE,WAAW,EAAE,YAAY,EAAyB,MAAM,mBAAoB,CAAC;AACtF,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC"}
\ No newline at end of file
diff --git a/web_ui/dist/quantile.d.ts b/web_ui/dist/quantile.d.ts
new file mode 100644
index 0000000..88e517f
--- /dev/null
+++ b/web_ui/dist/quantile.d.ts
@@ -0,0 +1,43 @@
+/**
+ * Distribution-aware slider maths.
+ *
+ * A linear min..max track is useless on skewed data, which is the normal case
+ * in both consumers: dufs-cloud's file sizes span bytes to gigabytes, and
+ * awesome-mcp-explorer's star counts run 0..166,785 with a median of 8, so a
+ * linear midpoint sits above 99.9% of all rows. Mapping the track through the
+ * actual distribution puts the middle of the track at the median, which makes
+ * equal track distance mean roughly equal numbers of items.
+ *
+ * This was duplicated in dufs-cloud and awesome-mcp-explorer before it moved
+ * here; both now import it.
+ */
+/**
+ * Bounds-checked array access (callers guarantee the index is valid). Keeps the
+ * quantile helpers free of scattered non-null assertions or dead undefined
+ * guards while still satisfying `noUncheckedIndexedAccess`.
+ */
+export declare function nth(values: readonly number[], index: number): number;
+/** Clamp to the unit interval. */
+export declare function clamp01(x: number): number;
+/**
+ * The value at quantile `f` (0..1) of an ascending, non-empty array, with linear
+ * interpolation between the two straddling samples. This is what makes the
+ * sliders distribution-aware: f = 0.5 returns the *median*, so the middle of the
+ * track sits where half the items are — not the arithmetic midpoint of the
+ * range. Empty input yields 0 (callers hide the slider in that case).
+ */
+export declare function quantileValue(sorted: readonly number[], f: number): number;
+/**
+ * The quantile (0..1) of `value` within `sorted` — the inverse of
+ * {@link quantileValue}, used to place a thumb at the right spot on the track.
+ * Values at or beyond the ends map to 0/1.
+ */
+export declare function valueQuantile(sorted: readonly number[], value: number): number;
+/**
+ * Pointer x to a 0..1 fraction of a track.
+ *
+ * Extracted as pure geometry so slider behaviour is testable without layout,
+ * which jsdom does not implement.
+ */
+export declare function fractionFromPointer(rect: DOMRect, clientX: number): number;
+//# sourceMappingURL=quantile.d.ts.map
\ No newline at end of file
diff --git a/web_ui/dist/quantile.d.ts.map b/web_ui/dist/quantile.d.ts.map
new file mode 100644
index 0000000..a9122bd
--- /dev/null
+++ b/web_ui/dist/quantile.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"quantile.d.ts","sourceRoot":"","sources":["../src/quantile.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH;;;;GAIG;AACH,wBAAgB,GAAG,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAMpE;AAED,kCAAkC;AAClC,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAEzC;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAS1E;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAa9E;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAE1E"}
\ No newline at end of file
diff --git a/web_ui/dist/quantile.js b/web_ui/dist/quantile.js
new file mode 100644
index 0000000..b41f2ac
--- /dev/null
+++ b/web_ui/dist/quantile.js
@@ -0,0 +1,79 @@
+/**
+ * Distribution-aware slider maths.
+ *
+ * A linear min..max track is useless on skewed data, which is the normal case
+ * in both consumers: dufs-cloud's file sizes span bytes to gigabytes, and
+ * awesome-mcp-explorer's star counts run 0..166,785 with a median of 8, so a
+ * linear midpoint sits above 99.9% of all rows. Mapping the track through the
+ * actual distribution puts the middle of the track at the median, which makes
+ * equal track distance mean roughly equal numbers of items.
+ *
+ * This was duplicated in dufs-cloud and awesome-mcp-explorer before it moved
+ * here; both now import it.
+ */
+/**
+ * Bounds-checked array access (callers guarantee the index is valid). Keeps the
+ * quantile helpers free of scattered non-null assertions or dead undefined
+ * guards while still satisfying `noUncheckedIndexedAccess`.
+ */
+export function nth(values, index) {
+ const value = values[index];
+ if (value === undefined) {
+ throw new RangeError(`index ${index} out of range`);
+ }
+ return value;
+}
+/** Clamp to the unit interval. */
+export function clamp01(x) {
+ return Math.min(1, Math.max(0, x));
+}
+/**
+ * The value at quantile `f` (0..1) of an ascending, non-empty array, with linear
+ * interpolation between the two straddling samples. This is what makes the
+ * sliders distribution-aware: f = 0.5 returns the *median*, so the middle of the
+ * track sits where half the items are — not the arithmetic midpoint of the
+ * range. Empty input yields 0 (callers hide the slider in that case).
+ */
+export function quantileValue(sorted, f) {
+ const n = sorted.length;
+ if (n === 0)
+ return 0;
+ const pos = clamp01(f) * (n - 1);
+ const lo = Math.floor(pos);
+ const hi = Math.ceil(pos);
+ const a = nth(sorted, lo);
+ const b = nth(sorted, hi);
+ return Math.round(a + (b - a) * (pos - lo));
+}
+/**
+ * The quantile (0..1) of `value` within `sorted` — the inverse of
+ * {@link quantileValue}, used to place a thumb at the right spot on the track.
+ * Values at or beyond the ends map to 0/1.
+ */
+export function valueQuantile(sorted, value) {
+ const n = sorted.length;
+ if (n <= 1)
+ return 0;
+ if (value <= nth(sorted, 0))
+ return 0;
+ if (value >= nth(sorted, n - 1))
+ return 1;
+ // value strictly inside the range, so there is an index hi >= 1 with
+ // sorted[hi] >= value and sorted[hi - 1] < value.
+ const hi = sorted.findIndex((v) => v >= value);
+ const lo = hi - 1;
+ const a = nth(sorted, lo);
+ const b = nth(sorted, hi);
+ // a < value <= b here, so b - a > 0 (no divide-by-zero).
+ return (lo + (value - a) / (b - a)) / (n - 1);
+}
+/**
+ * Pointer x to a 0..1 fraction of a track.
+ *
+ * Extracted as pure geometry so slider behaviour is testable without layout,
+ * which jsdom does not implement.
+ */
+export function fractionFromPointer(rect, clientX) {
+ return rect.width === 0 ? 0 : clamp01((clientX - rect.left) / rect.width);
+}
+//# sourceMappingURL=quantile.js.map
\ No newline at end of file
diff --git a/web_ui/dist/quantile.js.map b/web_ui/dist/quantile.js.map
new file mode 100644
index 0000000..51c4e24
--- /dev/null
+++ b/web_ui/dist/quantile.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"quantile.js","sourceRoot":"","sources":["../src/quantile.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH;;;;GAIG;AACH,MAAM,UAAU,GAAG,CAAC,MAAyB,EAAE,KAAa;IAC1D,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAC5B,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,MAAM,IAAI,UAAU,CAAC,SAAS,KAAK,eAAe,CAAC,CAAC;IACtD,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,kCAAkC;AAClC,MAAM,UAAU,OAAO,CAAC,CAAS;IAC/B,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AACrC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,MAAyB,EAAE,CAAS;IAChE,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC;IACxB,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IACtB,MAAM,GAAG,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IACjC,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC3B,MAAM,EAAE,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC1B,MAAM,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAC1B,MAAM,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAC1B,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,GAAG,EAAE,CAAC,CAAC,CAAC;AAC9C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,MAAyB,EAAE,KAAa;IACpE,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC;IACxB,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,CAAC,CAAC;IACrB,IAAI,KAAK,IAAI,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;QAAE,OAAO,CAAC,CAAC;IACtC,IAAI,KAAK,IAAI,GAAG,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC;QAAE,OAAO,CAAC,CAAC;IAC1C,qEAAqE;IACrE,kDAAkD;IAClD,MAAM,EAAE,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC;IAC/C,MAAM,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;IAClB,MAAM,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAC1B,MAAM,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAC1B,yDAAyD;IACzD,OAAO,CAAC,EAAE,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;AAChD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAa,EAAE,OAAe;IAChE,OAAO,IAAI,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC;AAC5E,CAAC"}
\ No newline at end of file
diff --git a/web_ui/dist/range-slider.css b/web_ui/dist/range-slider.css
new file mode 100644
index 0000000..0cb3d6e
--- /dev/null
+++ b/web_ui/dist/range-slider.css
@@ -0,0 +1,68 @@
+/*
+ * Styling for the shared RangeSlider. Imported separately from tokens.css:
+ * import "@kuhyx/web-ui/tokens.css";
+ * import "@kuhyx/web-ui/range-slider.css";
+ *
+ * Every value routes through a token — no literals.
+ */
+
+.slider-head {
+ display: flex;
+ justify-content: space-between;
+ color: var(--text-muted);
+ font-size: var(--text-caption);
+ letter-spacing: var(--tracking-caption);
+}
+
+.slider-value {
+ /* Tabular figures so a dragging readout does not reflow the row. */
+ font-variant-numeric: tabular-nums;
+}
+
+.slider-track {
+ position: relative;
+ height: 22px;
+ margin-top: var(--space-1);
+ /* Let the element own the gesture instead of the page scrolling under it. */
+ touch-action: none;
+}
+
+.slider-track::before {
+ content: "";
+ position: absolute;
+ top: 9px;
+ right: 0;
+ left: 0;
+ height: 4px;
+ border-radius: 2px;
+ background: var(--border);
+}
+
+.slider-fill {
+ position: absolute;
+ top: 9px;
+ height: 4px;
+ border-radius: 2px;
+ background: var(--accent);
+}
+
+.slider-thumb {
+ position: absolute;
+ top: 4px;
+ width: 14px;
+ height: 14px;
+ margin-left: -7px;
+ padding: 0;
+ border: 2px solid var(--accent);
+ border-radius: 50%;
+ background: var(--bg);
+ cursor: pointer;
+}
+
+/* The thumbs are the keyboard target, so the ring must be unmistakable. The
+ * global :focus-visible rule in tokens.css already colours it; the wider offset
+ * here keeps it clear of the 14px circle. */
+.slider-thumb:focus-visible {
+ outline: 2px solid var(--focus-ring);
+ outline-offset: 2px;
+}
diff --git a/web_ui/dist/range-slider.d.ts b/web_ui/dist/range-slider.d.ts
new file mode 100644
index 0000000..2e633c6
--- /dev/null
+++ b/web_ui/dist/range-slider.d.ts
@@ -0,0 +1,23 @@
+export interface RangeSliderProps {
+ /**
+ * Formats a value for the head readout. Omit it (with `label`) to render a
+ * bare track — dufs-cloud's wrappers draw their own labels and value notes,
+ * so a built-in head would duplicate them.
+ */
+ readonly format?: (value: number) => string;
+ /** Current upper bound; equal to the distribution max when unconstrained. */
+ readonly hi: number;
+ /** Accessible name. Falls back to a generic one when omitted. */
+ readonly label?: string;
+ /** Current lower bound; equal to the distribution min when unconstrained. */
+ readonly lo: number;
+ /** Reports both thumb values (raw, clamped so lo <= hi) on every change. */
+ readonly onChange: (lo: number, hi: number) => void;
+ /** The full ascending distribution — not just its endpoints. */
+ readonly values: readonly number[];
+}
+/** Steps one thumb by `delta` positions through the distribution. Pure, so the
+ * keyboard contract is testable without layout. */
+export declare function steppedValue(values: readonly number[], current: number, delta: number): number;
+export declare function RangeSlider({ format, hi, label, lo, onChange, values, }: RangeSliderProps): null | React.JSX.Element;
+//# sourceMappingURL=range-slider.d.ts.map
\ No newline at end of file
diff --git a/web_ui/dist/range-slider.d.ts.map b/web_ui/dist/range-slider.d.ts.map
new file mode 100644
index 0000000..ac62093
--- /dev/null
+++ b/web_ui/dist/range-slider.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"range-slider.d.ts","sourceRoot":"","sources":["../src/range-slider.tsx"],"names":[],"mappings":"AAyBA,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IAC5C,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,iEAAiE;IACjE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,4EAA4E;IAC5E,QAAQ,CAAC,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACpD,gEAAgE;IAChE,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;CACpC;AAID;mDACmD;AACnD,wBAAgB,YAAY,CAC1B,MAAM,EAAE,SAAS,MAAM,EAAE,EACzB,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,GACZ,MAAM,CAMR;AAED,wBAAgB,WAAW,CAAC,EAC1B,MAAM,EACN,EAAE,EACF,KAAK,EACL,EAAE,EACF,QAAQ,EACR,MAAM,GACP,EAAE,gBAAgB,GAAG,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CAyG7C"}
\ No newline at end of file
diff --git a/web_ui/dist/range-slider.js b/web_ui/dist/range-slider.js
new file mode 100644
index 0000000..40a4240
--- /dev/null
+++ b/web_ui/dist/range-slider.js
@@ -0,0 +1,109 @@
+import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
+/**
+ * A dual-thumb slider whose track maps through a value *distribution* rather
+ * than a linear min..max ramp (see `./quantile.ts` for why).
+ *
+ * Reconciled from the two drifted copies in dufs-cloud and
+ * awesome-mcp-explorer. It takes the accessible DOM from awesome-mcp-explorer
+ * (real `` thumbs carrying `role="slider"` and the `aria-value*` trio,
+ * so the control is a focus stop) and the grab-the-nearer-thumb pointer
+ * behaviour from dufs-cloud (pressing anywhere on the track moves the closer
+ * handle, instead of requiring a direct hit on an
+ * 8px target).
+ *
+ * Keyboard support is new in the shared version. Both donors were pointer-only,
+ * which the design system rules out outright: every action must be reachable
+ * with the keyboard alone, and a `role="slider"` that does not respond to arrow
+ * keys is an ARIA role that lies. Arrow keys step by one item through the
+ * distribution, Home/End jump to the bounds.
+ *
+ * The component deals only in raw values; callers translate the extremes into
+ * "no filter".
+ */
+import { useCallback, useState } from "react";
+import { fractionFromPointer, nth, quantileValue, valueQuantile } from "./quantile.js";
+/** Steps one thumb by `delta` positions through the distribution. Pure, so the
+ * keyboard contract is testable without layout. */
+export function steppedValue(values, current, delta) {
+ const last = values.length - 1;
+ // Round to a real sample index so a thumb sitting between two samples still
+ // advances by exactly one item rather than stalling on a fractional step.
+ const index = Math.round(valueQuantile(values, current) * last);
+ return nth(values, Math.min(last, Math.max(0, index + delta)));
+}
+export function RangeSlider({ format, hi, label, lo, onChange, values, }) {
+ const [drag, setDrag] = useState(null);
+ const apply = useCallback((thumb, value) => {
+ if (thumb === "lo")
+ onChange(Math.min(value, hi), hi);
+ else
+ onChange(lo, Math.max(value, lo));
+ }, [hi, lo, onChange]);
+ // Reads the rect off the event's own target rather than a ref: the pointer
+ // handlers are bound to the track, so currentTarget *is* the track. A ref
+ // would add a null branch that cannot happen once the element is mounted.
+ const valueAt = useCallback((track, clientX) => quantileValue(values, fractionFromPointer(track.getBoundingClientRect(), clientX)), [values]);
+ const onKeyDown = useCallback((thumb, event) => {
+ const current = thumb === "lo" ? lo : hi;
+ const step = (delta) => {
+ event.preventDefault();
+ apply(thumb, steppedValue(values, current, delta));
+ };
+ if (event.key === "ArrowLeft" || event.key === "ArrowDown")
+ step(-1);
+ else if (event.key === "ArrowRight" || event.key === "ArrowUp")
+ step(1);
+ else if (event.key === "PageDown")
+ step(-10);
+ else if (event.key === "PageUp")
+ step(10);
+ else if (event.key === "Home") {
+ event.preventDefault();
+ apply(thumb, nth(values, 0));
+ }
+ else if (event.key === "End") {
+ event.preventDefault();
+ apply(thumb, nth(values, values.length - 1));
+ }
+ }, [apply, hi, lo, values]);
+ // Fewer than two values means there is nothing to range over.
+ if (values.length < 2)
+ return null;
+ const lowest = nth(values, 0);
+ const highest = nth(values, values.length - 1);
+ const loFraction = valueQuantile(values, lo) * 100;
+ const hiFraction = valueQuantile(values, hi) * 100;
+ const name = label ?? "Range";
+ const thumbProps = (thumb) => ({
+ "aria-valuemax": highest,
+ "aria-valuemin": lowest,
+ "aria-valuenow": thumb === "lo" ? lo : hi,
+ className: `slider-thumb slider-${thumb}`,
+ onKeyDown: (event) => {
+ onKeyDown(thumb, event);
+ },
+ role: "slider",
+ style: { left: `${String(thumb === "lo" ? loFraction : hiFraction)}%` },
+ tabIndex: 0,
+ type: "button",
+ });
+ return (_jsxs("div", { className: "slider", children: [label !== undefined && format !== undefined && (_jsxs("div", { className: "slider-head", children: [_jsx("span", { children: label }), _jsxs("span", { className: "slider-value", children: [format(lo), " \u2013 ", format(hi)] })] })), _jsxs("div", { className: "slider-track", onPointerDown: (event) => {
+ const value = valueAt(event.currentTarget, event.clientX);
+ // Grab whichever thumb is nearer, so a press anywhere on the track
+ // does something sensible instead of requiring a direct hit.
+ const thumb = Math.abs(value - lo) <= Math.abs(value - hi) ? "lo" : "hi";
+ setDrag(thumb);
+ event.currentTarget.setPointerCapture(event.pointerId);
+ apply(thumb, value);
+ }, onPointerMove: (event) => {
+ if (drag === null)
+ return;
+ apply(drag, valueAt(event.currentTarget, event.clientX));
+ }, onPointerUp: (event) => {
+ if (drag === null)
+ return;
+ setDrag(null);
+ event.currentTarget.releasePointerCapture(event.pointerId);
+ }, children: [_jsx("div", { className: "slider-fill", style: { left: `${String(loFraction)}%`, width: `${String(hiFraction - loFraction)}%` } }), _jsx("button", { "aria-label": `${name} minimum`, ...thumbProps("lo") }), _jsx("button", { "aria-label": `${name} maximum`, ...thumbProps("hi") })] })] }));
+}
+//# sourceMappingURL=range-slider.js.map
\ No newline at end of file
diff --git a/web_ui/dist/range-slider.js.map b/web_ui/dist/range-slider.js.map
new file mode 100644
index 0000000..d76b440
--- /dev/null
+++ b/web_ui/dist/range-slider.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"range-slider.js","sourceRoot":"","sources":["../src/range-slider.tsx"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAE9C,OAAO,EAAE,mBAAmB,EAAE,GAAG,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAuBvF;mDACmD;AACnD,MAAM,UAAU,YAAY,CAC1B,MAAyB,EACzB,OAAe,EACf,KAAa;IAEb,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC;IAC/B,4EAA4E;IAC5E,0EAA0E;IAC1E,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC;IAChE,OAAO,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;AACjE,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,EAC1B,MAAM,EACN,EAAE,EACF,KAAK,EACL,EAAE,EACF,QAAQ,EACR,MAAM,GACW;IACjB,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAe,IAAI,CAAC,CAAC;IAErD,MAAM,KAAK,GAAG,WAAW,CACvB,CAAC,KAAY,EAAE,KAAa,EAAQ,EAAE;QACpC,IAAI,KAAK,KAAK,IAAI;YAAE,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;;YACjD,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC;IACzC,CAAC,EACD,CAAC,EAAE,EAAE,EAAE,EAAE,QAAQ,CAAC,CACnB,CAAC;IAEF,2EAA2E;IAC3E,0EAA0E;IAC1E,0EAA0E;IAC1E,MAAM,OAAO,GAAG,WAAW,CACzB,CAAC,KAAkB,EAAE,OAAe,EAAU,EAAE,CAC9C,aAAa,CAAC,MAAM,EAAE,mBAAmB,CAAC,KAAK,CAAC,qBAAqB,EAAE,EAAE,OAAO,CAAC,CAAC,EACpF,CAAC,MAAM,CAAC,CACT,CAAC;IAEF,MAAM,SAAS,GAAG,WAAW,CAC3B,CAAC,KAAY,EAAE,KAA0B,EAAQ,EAAE;QACjD,MAAM,OAAO,GAAG,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACzC,MAAM,IAAI,GAAG,CAAC,KAAa,EAAQ,EAAE;YACnC,KAAK,CAAC,cAAc,EAAE,CAAC;YACvB,KAAK,CAAC,KAAK,EAAE,YAAY,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC;QACrD,CAAC,CAAC;QACF,IAAI,KAAK,CAAC,GAAG,KAAK,WAAW,IAAI,KAAK,CAAC,GAAG,KAAK,WAAW;YAAE,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;aAChE,IAAI,KAAK,CAAC,GAAG,KAAK,YAAY,IAAI,KAAK,CAAC,GAAG,KAAK,SAAS;YAAE,IAAI,CAAC,CAAC,CAAC,CAAC;aACnE,IAAI,KAAK,CAAC,GAAG,KAAK,UAAU;YAAE,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC;aACxC,IAAI,KAAK,CAAC,GAAG,KAAK,QAAQ;YAAE,IAAI,CAAC,EAAE,CAAC,CAAC;aACrC,IAAI,KAAK,CAAC,GAAG,KAAK,MAAM,EAAE,CAAC;YAC9B,KAAK,CAAC,cAAc,EAAE,CAAC;YACvB,KAAK,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC;QAC/B,CAAC;aAAM,IAAI,KAAK,CAAC,GAAG,KAAK,KAAK,EAAE,CAAC;YAC/B,KAAK,CAAC,cAAc,EAAE,CAAC;YACvB,KAAK,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC;QAC/C,CAAC;IACH,CAAC,EACD,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,CACxB,CAAC;IAEF,8DAA8D;IAC9D,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAEnC,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IAC9B,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC/C,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC;IACnD,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC;IACnD,MAAM,IAAI,GAAG,KAAK,IAAI,OAAO,CAAC;IAE9B,MAAM,UAAU,GAAG,CAAC,KAAY,EAAkC,EAAE,CAAC,CAAC;QACpE,eAAe,EAAE,OAAO;QACxB,eAAe,EAAE,MAAM;QACvB,eAAe,EAAE,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE;QACzC,SAAS,EAAE,uBAAuB,KAAK,EAAE;QACzC,SAAS,EAAE,CAAC,KAAK,EAAQ,EAAE;YACzB,SAAS,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QAC1B,CAAC;QACD,IAAI,EAAE,QAAQ;QACd,KAAK,EAAE,EAAE,IAAI,EAAE,GAAG,MAAM,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,EAAE;QACvE,QAAQ,EAAE,CAAC;QACX,IAAI,EAAE,QAAQ;KACf,CAAC,CAAC;IAEH,OAAO,CACL,eAAK,SAAS,EAAC,QAAQ,aACpB,KAAK,KAAK,SAAS,IAAI,MAAM,KAAK,SAAS,IAAI,CAC9C,eAAK,SAAS,EAAC,aAAa,aAC1B,yBAAO,KAAK,GAAQ,EACpB,gBAAM,SAAS,EAAC,cAAc,aAC3B,MAAM,CAAC,EAAE,CAAC,cAAK,MAAM,CAAC,EAAE,CAAC,IACrB,IACH,CACP,EACD,eACE,SAAS,EAAC,cAAc,EACxB,aAAa,EAAE,CAAC,KAAK,EAAQ,EAAE;oBAC7B,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,aAAa,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;oBAC1D,mEAAmE;oBACnE,6DAA6D;oBAC7D,MAAM,KAAK,GAAU,IAAI,CAAC,GAAG,CAAC,KAAK,GAAG,EAAE,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;oBAChF,OAAO,CAAC,KAAK,CAAC,CAAC;oBACf,KAAK,CAAC,aAAa,CAAC,iBAAiB,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;oBACvD,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;gBACtB,CAAC,EACD,aAAa,EAAE,CAAC,KAAK,EAAQ,EAAE;oBAC7B,IAAI,IAAI,KAAK,IAAI;wBAAE,OAAO;oBAC1B,KAAK,CAAC,IAAI,EAAE,OAAO,CAAC,KAAK,CAAC,aAAa,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;gBAC3D,CAAC,EACD,WAAW,EAAE,CAAC,KAAK,EAAQ,EAAE;oBAC3B,IAAI,IAAI,KAAK,IAAI;wBAAE,OAAO;oBAC1B,OAAO,CAAC,IAAI,CAAC,CAAC;oBACd,KAAK,CAAC,aAAa,CAAC,qBAAqB,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;gBAC7D,CAAC,aAED,cACE,SAAS,EAAC,aAAa,EACvB,KAAK,EAAE,EAAE,IAAI,EAAE,GAAG,MAAM,CAAC,UAAU,CAAC,GAAG,EAAE,KAAK,EAAE,GAAG,MAAM,CAAC,UAAU,GAAG,UAAU,CAAC,GAAG,EAAE,GACvF,EACF,+BAAoB,GAAG,IAAI,UAAU,KAAM,UAAU,CAAC,IAAI,CAAC,GAAI,EAC/D,+BAAoB,GAAG,IAAI,UAAU,KAAM,UAAU,CAAC,IAAI,CAAC,GAAI,IAC3D,IACF,CACP,CAAC;AACJ,CAAC"}
\ No newline at end of file
diff --git a/web_ui/dist/text.d.ts b/web_ui/dist/text.d.ts
new file mode 100644
index 0000000..8c5ee3e
--- /dev/null
+++ b/web_ui/dist/text.d.ts
@@ -0,0 +1,16 @@
+/**
+ * Small text helpers shared by the consumers' filter layers.
+ *
+ * Hand-rolled rather than pulling in Fuse.js, matching the house posture of no
+ * runtime dependencies for things this small.
+ */
+/**
+ * Case-insensitive subsequence match: every character of `query` appears in
+ * `target`, in order, though not necessarily adjacently. An empty query matches
+ * everything, so callers can pass the raw search box value straight through.
+ *
+ * dufs-cloud called this `fuzzyMatch` and awesome-mcp-explorer `matchesFuzzy`;
+ * the implementations were identical apart from identifier names.
+ */
+export declare function fuzzyMatch(query: string, target: string): boolean;
+//# sourceMappingURL=text.d.ts.map
\ No newline at end of file
diff --git a/web_ui/dist/text.d.ts.map b/web_ui/dist/text.d.ts.map
new file mode 100644
index 0000000..61b157b
--- /dev/null
+++ b/web_ui/dist/text.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../src/text.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CASjE"}
\ No newline at end of file
diff --git a/web_ui/dist/text.js b/web_ui/dist/text.js
new file mode 100644
index 0000000..91f9794
--- /dev/null
+++ b/web_ui/dist/text.js
@@ -0,0 +1,27 @@
+/**
+ * Small text helpers shared by the consumers' filter layers.
+ *
+ * Hand-rolled rather than pulling in Fuse.js, matching the house posture of no
+ * runtime dependencies for things this small.
+ */
+/**
+ * Case-insensitive subsequence match: every character of `query` appears in
+ * `target`, in order, though not necessarily adjacently. An empty query matches
+ * everything, so callers can pass the raw search box value straight through.
+ *
+ * dufs-cloud called this `fuzzyMatch` and awesome-mcp-explorer `matchesFuzzy`;
+ * the implementations were identical apart from identifier names.
+ */
+export function fuzzyMatch(query, target) {
+ if (query === "")
+ return true;
+ const q = query.toLowerCase();
+ const t = target.toLowerCase();
+ let index = 0;
+ for (let scan = 0; scan < t.length && index < q.length; scan++) {
+ if (t[scan] === q[index])
+ index++;
+ }
+ return index === q.length;
+}
+//# sourceMappingURL=text.js.map
\ No newline at end of file
diff --git a/web_ui/dist/text.js.map b/web_ui/dist/text.js.map
new file mode 100644
index 0000000..9efdd79
--- /dev/null
+++ b/web_ui/dist/text.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"text.js","sourceRoot":"","sources":["../src/text.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;;;;;GAOG;AACH,MAAM,UAAU,UAAU,CAAC,KAAa,EAAE,MAAc;IACtD,IAAI,KAAK,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC;IAC9B,MAAM,CAAC,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC;IAC9B,MAAM,CAAC,GAAG,MAAM,CAAC,WAAW,EAAE,CAAC;IAC/B,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,IAAI,GAAG,CAAC,CAAC,MAAM,IAAI,KAAK,GAAG,CAAC,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC;QAC/D,IAAI,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC;YAAE,KAAK,EAAE,CAAC;IACpC,CAAC;IACD,OAAO,KAAK,KAAK,CAAC,CAAC,MAAM,CAAC;AAC5B,CAAC"}
\ No newline at end of file
diff --git a/web_ui/dist/tokens.css b/web_ui/dist/tokens.css
new file mode 100644
index 0000000..f0bcbb4
--- /dev/null
+++ b/web_ui/dist/tokens.css
@@ -0,0 +1,112 @@
+/*
+ * The frozen design tokens, shared by every one of kuhy's web repos.
+ *
+ * Source of truth: ~/utils/unified-design-system/tokens.md (values) and
+ * README.md (the CSS custom-property names). The same numbers ship for Dart in
+ * ~/utils/design_system, so a change here is a change there — do not edit one
+ * side alone, and do not introduce a value that is not on a scale below.
+ *
+ * Dark is the default; light is an override, matching the README.
+ */
+
+:root {
+ /* Neutrals ------------------------------------------------------------- */
+ --bg: #211d1b;
+ --surface-1: #2b2624;
+ --surface-2: #38312e;
+ --border: #463e3a;
+ --text: #eceae9;
+ --text-muted: #aaa09a;
+
+ /* Accent + semantic roles ---------------------------------------------- */
+ --accent: #b8862e;
+ --info: #b8862e; /* = accent */
+ --success: #8a9a3c;
+ --warning: #e0a63c;
+ --danger: #e2585f;
+
+ /* Text/icons ON a filled accent/success/warning/danger surface. Never
+ * --text: every semantic fill sits in the same mid-light band, where
+ * near-white measures 2.2:1-3.6:1 against a 4.5:1 floor. */
+ --on-fill: #211d1b;
+ /* Text/icons on a dark translucent scrim/overlay — fixed, not theme-tied,
+ * because the scrim itself is always dark. */
+ --on-scrim: #eceae9;
+
+ /* Corner radius --------------------------------------------------------- */
+ --radius-sm: 8px; /* buttons, inputs, chips */
+ --radius-md: 12px; /* cards */
+ --radius-lg: 16px; /* sheets, dialogs */
+
+ /* Spacing (4px base) ---------------------------------------------------- */
+ --space-1: 4px;
+ --space-2: 8px;
+ --space-3: 16px;
+ --space-4: 24px;
+ --space-5: 32px;
+ --space-6: 48px;
+
+ /* Typography ------------------------------------------------------------ */
+ --font-sans: -apple-system, "Segoe UI", Roboto, system-ui, sans-serif;
+ --font-mono: ui-monospace, "Cascadia Code", monospace;
+
+ --text-display: 32px;
+ --text-title: 24px;
+ --text-subtitle: 20px;
+ --text-body: 16px; /* the floor for anything a user actually reads */
+ --text-label: 14px; /* UI chrome only */
+ --text-caption: 12px; /* UI chrome only */
+
+ --tracking-display: -0.2px;
+ --tracking-title: -0.1px;
+ --tracking-label: 0.05em;
+ --tracking-caption: 0.1em;
+
+ /* Prose line-length cap (rule 21): ~65-70 characters. */
+ --measure: 40rem;
+
+ /* Shadows: light surfaces only, and only on floating/overlay elements —
+ * never inline cards or list rows. Blur = 2x offset, tinted from ink. */
+ --shadow-1: 0 2px 4px rgba(33, 29, 27, 0.12); /* dropdown */
+ --shadow-2: 0 4px 8px rgba(33, 29, 27, 0.16); /* modal */
+
+ /* Focus ring: the platform default is black, invisible on --bg. */
+ --focus-ring: #b8862e; /* = accent */
+}
+
+@media (prefers-color-scheme: light) {
+ :root {
+ --bg: #f6f4f3;
+ --surface-1: #fcfbfb;
+ --surface-2: #fcfbfb;
+ --border: #e0dad7;
+ --text: #211d1b;
+ --text-muted: #70625b;
+ /* --on-fill / --on-scrim stay fixed — the fills and the scrim do not
+ * change with page theme. */
+ }
+}
+
+/* Dark surfaces take no shadow at all; elevation is a fill step. Light
+ * surfaces opt in explicitly via --shadow-1 / --shadow-2. */
+@media (prefers-color-scheme: dark) {
+ :root {
+ --shadow-1: none;
+ --shadow-2: none;
+ }
+}
+
+body {
+ background: var(--bg);
+ color: var(--text);
+ font-family: var(--font-sans);
+ font-size: var(--text-body);
+}
+
+/* Ships here rather than being left to each consumer: an unstyled focus ring
+ * is the accessibility regression this layer exists to prevent, and several of
+ * these apps are keyboard-only by necessity. */
+:focus-visible {
+ outline: 2px solid var(--focus-ring);
+ outline-offset: 1px;
+}
diff --git a/web_ui/package.json b/web_ui/package.json
new file mode 100644
index 0000000..a2295be
--- /dev/null
+++ b/web_ui/package.json
@@ -0,0 +1,51 @@
+{
+ "name": "@kuhyx/web-ui",
+ "version": "0.1.0",
+ "private": false,
+ "type": "module",
+ "description": "The shared token + component layer for kuhy's web apps: the frozen palette, spacing/radius/type scales as CSS custom properties, and the components that were each reimplemented in two or more repos.",
+ "license": "MIT",
+ "engines": {
+ "node": ">=24",
+ "pnpm": ">=10"
+ },
+ "packageManager": "pnpm@10.13.1",
+ "files": [
+ "dist"
+ ],
+ "main": "./dist/index.js",
+ "types": "./dist/index.d.ts",
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "default": "./dist/index.js"
+ },
+ "./tokens.css": "./dist/tokens.css",
+ "./range-slider.css": "./dist/range-slider.css"
+ },
+ "scripts": {
+ "build": "tsc -b && cp src/tokens.css src/range-slider.css dist/",
+ "lint": "tsc --noEmit && eslint src",
+ "test": "vitest run",
+ "coverage": "vitest run --coverage"
+ },
+ "peerDependencies": {
+ "react": ">=19"
+ },
+ "devDependencies": {
+ "@eslint/js": "^10.0.1",
+ "@testing-library/jest-dom": "^6.9.1",
+ "@testing-library/react": "^16.3.2",
+ "@testing-library/user-event": "^14.6.1",
+ "@types/react": "^19.2.17",
+ "@types/react-dom": "^19.2.3",
+ "@vitest/coverage-v8": "^4.1.10",
+ "eslint": "^10.7.0",
+ "jsdom": "^29.1.1",
+ "react": "^19.2.7",
+ "react-dom": "^19.2.7",
+ "typescript": "^6.0.3",
+ "typescript-eslint": "^8.64.0",
+ "vitest": "^4.1.10"
+ }
+}
diff --git a/web_ui/pnpm-lock.yaml b/web_ui/pnpm-lock.yaml
new file mode 100644
index 0000000..cd49620
--- /dev/null
+++ b/web_ui/pnpm-lock.yaml
@@ -0,0 +1,2164 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ devDependencies:
+ '@eslint/js':
+ specifier: ^10.0.1
+ version: 10.0.1(eslint@10.8.1)
+ '@testing-library/jest-dom':
+ specifier: ^6.9.1
+ version: 6.10.0(@testing-library/dom@10.4.1)
+ '@testing-library/react':
+ specifier: ^16.3.2
+ version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ '@testing-library/user-event':
+ specifier: ^14.6.1
+ version: 14.6.4(@testing-library/dom@10.4.1)
+ '@types/react':
+ specifier: ^19.2.17
+ version: 19.2.18
+ '@types/react-dom':
+ specifier: ^19.2.3
+ version: 19.2.4(@types/react@19.2.18)
+ '@vitest/coverage-v8':
+ specifier: ^4.1.10
+ version: 4.1.10(vitest@4.1.10)
+ eslint:
+ specifier: ^10.7.0
+ version: 10.8.1
+ jsdom:
+ specifier: ^29.1.1
+ version: 29.1.1
+ react:
+ specifier: ^19.2.7
+ version: 19.2.8
+ react-dom:
+ specifier: ^19.2.7
+ version: 19.2.8(react@19.2.8)
+ typescript:
+ specifier: ^6.0.3
+ version: 6.0.3
+ typescript-eslint:
+ specifier: ^8.64.0
+ version: 8.67.0(eslint@10.8.1)(typescript@6.0.3)
+ vitest:
+ specifier: ^4.1.10
+ version: 4.1.10(@vitest/coverage-v8@4.1.10)(jsdom@29.1.1)(vite@8.2.1)
+
+packages:
+
+ '@adobe/css-tools@4.5.0':
+ resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==}
+
+ '@asamuzakjp/css-color@5.1.11':
+ resolution: {integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
+
+ '@asamuzakjp/dom-selector@7.1.1':
+ resolution: {integrity: sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
+
+ '@asamuzakjp/generational-cache@1.0.1':
+ resolution: {integrity: sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
+
+ '@asamuzakjp/nwsapi@2.3.9':
+ resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==}
+
+ '@babel/code-frame@7.29.7':
+ resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-string-parser@7.29.7':
+ resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-identifier@7.29.7':
+ resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/parser@7.29.8':
+ resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ '@babel/runtime@7.29.7':
+ resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/types@7.29.8':
+ resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==}
+ engines: {node: '>=6.9.0'}
+
+ '@bcoe/v8-coverage@1.0.2':
+ resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
+ engines: {node: '>=18'}
+
+ '@bramus/specificity@2.4.2':
+ resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==}
+ hasBin: true
+
+ '@csstools/color-helpers@6.1.0':
+ resolution: {integrity: sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==}
+ engines: {node: '>=20.19.0'}
+
+ '@csstools/css-calc@3.3.0':
+ resolution: {integrity: sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==}
+ engines: {node: '>=20.19.0'}
+ peerDependencies:
+ '@csstools/css-parser-algorithms': ^4.0.0
+ '@csstools/css-tokenizer': ^4.0.0
+
+ '@csstools/css-color-parser@4.1.10':
+ resolution: {integrity: sha512-UZhQLIUyJaaMepqehrCODwCg2KW25vFvLWBmqYFaPclYvvxzj/sG8LBOhBFCp11i9uE7t1EyS+RAoV9tztPFyw==}
+ engines: {node: '>=20.19.0'}
+ peerDependencies:
+ '@csstools/css-parser-algorithms': ^4.0.0
+ '@csstools/css-tokenizer': ^4.0.0
+
+ '@csstools/css-parser-algorithms@4.0.0':
+ resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==}
+ engines: {node: '>=20.19.0'}
+ peerDependencies:
+ '@csstools/css-tokenizer': ^4.0.0
+
+ '@csstools/css-syntax-patches-for-csstree@1.1.7':
+ resolution: {integrity: sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig==}
+ peerDependencies:
+ css-tree: ^3.2.1
+ peerDependenciesMeta:
+ css-tree:
+ optional: true
+
+ '@csstools/css-tokenizer@4.0.0':
+ resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==}
+ engines: {node: '>=20.19.0'}
+
+ '@eslint-community/eslint-utils@4.10.1':
+ resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ peerDependencies:
+ eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
+
+ '@eslint-community/regexpp@4.12.2':
+ resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==}
+ engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
+
+ '@eslint/config-array@0.23.5':
+ resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ '@eslint/config-helpers@0.7.0':
+ resolution: {integrity: sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ '@eslint/core@1.2.1':
+ resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ '@eslint/js@10.0.1':
+ resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+ peerDependencies:
+ eslint: ^10.0.0
+ peerDependenciesMeta:
+ eslint:
+ optional: true
+
+ '@eslint/object-schema@3.0.5':
+ resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ '@eslint/plugin-kit@0.7.2':
+ resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ '@exodus/bytes@1.15.1':
+ resolution: {integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
+ peerDependencies:
+ '@noble/hashes': ^1.8.0 || ^2.0.0
+ peerDependenciesMeta:
+ '@noble/hashes':
+ optional: true
+
+ '@humanfs/core@0.19.2':
+ resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==}
+ engines: {node: '>=18.18.0'}
+
+ '@humanfs/node@0.16.8':
+ resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==}
+ engines: {node: '>=18.18.0'}
+
+ '@humanfs/types@0.15.0':
+ resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==}
+ engines: {node: '>=18.18.0'}
+
+ '@humanwhocodes/module-importer@1.0.1':
+ resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
+ engines: {node: '>=12.22'}
+
+ '@humanwhocodes/retry@0.4.3':
+ resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
+ engines: {node: '>=18.18'}
+
+ '@jridgewell/resolve-uri@3.1.2':
+ resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
+ engines: {node: '>=6.0.0'}
+
+ '@jridgewell/sourcemap-codec@1.5.5':
+ resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
+
+ '@oxc-project/types@0.144.0':
+ resolution: {integrity: sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg==}
+
+ '@rolldown/binding-android-arm64@1.2.4':
+ resolution: {integrity: sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [android]
+
+ '@rolldown/binding-darwin-arm64@1.2.4':
+ resolution: {integrity: sha512-Dc5mPD8F5F/FS8i01syd7FTF6yB2fVthH/TRkjwJkzUK6EpoxHtqvZQP5Zwq80/5z19TWYHIg1KOHboCgVx/aQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rolldown/binding-darwin-x64@1.2.4':
+ resolution: {integrity: sha512-fpDm4oBo6SqLvWUYCmFhdde3U9KH2fRNNMeAnAPAIwxRL345xutL0EtEUcuoxsoazdJGv/MuDBQHlCDrtbvqOg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rolldown/binding-freebsd-x64@1.2.4':
+ resolution: {integrity: sha512-rSJoreDE/HoIzoaib6MTp5jQtCTdMHKIvItAKT/ImS6Y6Ww76oUaeMyp4Vc/fAgd/ehji068IxetHXAnqUwN9A==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rolldown/binding-linux-arm-gnueabihf@1.2.4':
+ resolution: {integrity: sha512-/jm8OGHgn7oGaJu3i/qZI9spUGcJ+y/lk43ttQ/iO1tOd9NissG6o97bighBCiL+BKRngmcDuR6ikfwYdJmVuQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@rolldown/binding-linux-arm64-gnu@1.2.4':
+ resolution: {integrity: sha512-tIP06BeD9EqvECBrPZ+sqdPlYrT+aYaAiu1wYziVx5elRK/ftm33JxVDy2bXGbr6J0CrtirCkR87/X5a2euEng==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rolldown/binding-linux-arm64-musl@1.2.4':
+ resolution: {integrity: sha512-Ql1Q0EQqVThvn9VAVlwNzsUvbSFtCMGjLpRRi4pk5i7NZZ4n5ISiLMjHYtus4VQ2PvkSw24zyaCVsiS+sXPj1w==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rolldown/binding-linux-ppc64-gnu@1.2.4':
+ resolution: {integrity: sha512-GjbjXD4XXfN19D0LZNbmiCBUoDiRACsYHr0yaIbbn8aFsXjHZifcYqu/W5Er5X2X990WjHXFrxarn5chzItorQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@rolldown/binding-linux-s390x-gnu@1.2.4':
+ resolution: {integrity: sha512-p5WR0NOwaRmJ/B1b6IjEFLLivwEsf3PrdBIhRbhTCQisbo2SvHHpG4ELB/+FgQNnB88LTOF86upmJmbvZdQ2lw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [s390x]
+ os: [linux]
+
+ '@rolldown/binding-linux-x64-gnu@1.2.4':
+ resolution: {integrity: sha512-4/GyVjmhR+Tc6HLJvwc1sOhPqAZtySiSMesOZyX6JQ5XBxoTDEMKQzvo07NIK6nTon/SivlZqvhzvuVBNQhObQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+
+ '@rolldown/binding-linux-x64-musl@1.2.4':
+ resolution: {integrity: sha512-l9eeLsCNvPpmSXUej0etw/J1eqV0Jj1D5G/xG6YTijmE6dkv6E2QezgWbTfQk63v952DPqrjOCoiqxq7Bw0YUQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+
+ '@rolldown/binding-openharmony-arm64@1.2.4':
+ resolution: {integrity: sha512-e0F355MSTMm3+UOqtV3L24gFUp2N5m1f8L/7d56deik6va+AXdrt9F8LbzGpeWGWRbZEDq4m8NVnJDeBtf9DZg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@rolldown/binding-win32-arm64-msvc@1.2.4':
+ resolution: {integrity: sha512-AWLi0uBRYh6QlE7OKhiz+phZC0qwtij2QZmhmOdsLdFn64m7oMpooE9ICE3lhm9xMb4SpDo2WbHcxX1iFLFtqw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rolldown/binding-win32-x64-msvc@1.2.4':
+ resolution: {integrity: sha512-UwSDJOg3dqCAejWdxclJjCsh3Qq4vLYMDxmyHqo1btz3stK2VqgwNd3mm5tuIwzSlGIQ/1H9Hr+Zn09mrezNqQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [win32]
+
+ '@rolldown/pluginutils@1.0.1':
+ resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
+
+ '@standard-schema/spec@1.1.0':
+ resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
+
+ '@testing-library/dom@10.4.1':
+ resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==}
+ engines: {node: '>=18'}
+
+ '@testing-library/jest-dom@6.10.0':
+ resolution: {integrity: sha512-HQwu0KaB2zyT0iLzBL+8CLyZDL3KlZlZJ+2iyc9uCUnlJVskJU/UlPuVCyIPhtukjPQdT2QNoR5nCP5FqTmmDQ==}
+ engines: {node: '>=22', npm: '>=6', yarn: '>=1'}
+ deprecated: Incorrect minor release with breaking changes (Node >=22 and required @testing-library/dom peer). Use 6.9.1 for the 6.x line, or upgrade to 7.0.0.
+ peerDependencies:
+ '@testing-library/dom': '>=10 <11'
+
+ '@testing-library/react@16.3.2':
+ resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@testing-library/dom': ^10.0.0
+ '@types/react': ^18.0.0 || ^19.0.0
+ '@types/react-dom': ^18.0.0 || ^19.0.0
+ react: ^18.0.0 || ^19.0.0
+ react-dom: ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@testing-library/user-event@14.6.4':
+ resolution: {integrity: sha512-QCGwP6QrjypBLwyj5cuyfVamkaIEy/XGY+1VDehbtbQqOggYmTFpFOdWR5mPz14vX8vXLMVjDHlRNBcClyO9ew==}
+ engines: {node: '>=12', npm: '>=6'}
+ peerDependencies:
+ '@testing-library/dom': '>=7.21.4'
+
+ '@types/aria-query@5.0.4':
+ resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==}
+
+ '@types/chai@5.2.3':
+ resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
+
+ '@types/deep-eql@4.0.2':
+ resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
+
+ '@types/esrecurse@4.3.1':
+ resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==}
+
+ '@types/estree@1.0.9':
+ resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
+
+ '@types/json-schema@7.0.15':
+ resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
+
+ '@types/react-dom@19.2.4':
+ resolution: {integrity: sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==}
+ peerDependencies:
+ '@types/react': ^19.2.0
+
+ '@types/react@19.2.18':
+ resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==}
+
+ '@typescript-eslint/eslint-plugin@8.67.0':
+ resolution: {integrity: sha512-Un7Heoyj65NREbKAyIrFxeM143NZpExWmy1Nep4DLeQOeLlTeumPjoNKnBrU5D5moWXbPJgRa5Uwcdu0faVNGQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ '@typescript-eslint/parser': ^8.67.0
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/parser@8.67.0':
+ resolution: {integrity: sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/project-service@8.67.0':
+ resolution: {integrity: sha512-cvE8c7ulYeXN9fYuszhCeCsbzyVEXuhrRCybnBre7TUmqb5nRmBfQAwCj0O3WJFDeyAZt4VYv51vMCC9LHSdYw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/scope-manager@8.67.0':
+ resolution: {integrity: sha512-EgvsleTwS4E+WzzSvem8fAUubLwatMNF1B5hHSLQxcvs7q2dtRhGyujHwLJSYlG41niJ7GP24Aha2+0mb1b2kg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@typescript-eslint/tsconfig-utils@8.67.0':
+ resolution: {integrity: sha512-vV+LUSv5njUWsknE71fqKTlXUva+R76SaeORd6Zojcunk/6DvKFXONU3BrAs2H49mbygUXt6gbYunzwqNwlhdg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/type-utils@8.67.0':
+ resolution: {integrity: sha512-aVWDXbRmdXO9siTfX4ditQI1T9+zVcNazT48EJCD0v40/9RIFoUgZ05CmGEq9H2gixRpjUn/iplwvlcvutJW/Q==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/types@8.67.0':
+ resolution: {integrity: sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@typescript-eslint/typescript-estree@8.67.0':
+ resolution: {integrity: sha512-EKQBCE9yNlRJYm7jdTW5AhDacDUmSwQb0FAJAmK2EKYrNXIsa2vxcSZx6PvJ/dEdI6lS+Y9W+EXckLj0iPFGcw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/utils@8.67.0':
+ resolution: {integrity: sha512-U9D1FdwEWBwok3hxxSdhclMb0twvt9QnjIQ0VfQ1AiX2epnpSgv2ubVDsayOFyY8K6FX+AQ7E0FKWVG3iKsj1A==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ typescript: '>=4.8.4 <6.1.0'
+
+ '@typescript-eslint/visitor-keys@8.67.0':
+ resolution: {integrity: sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@vitest/coverage-v8@4.1.10':
+ resolution: {integrity: sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==}
+ peerDependencies:
+ '@vitest/browser': 4.1.10
+ vitest: 4.1.10
+ peerDependenciesMeta:
+ '@vitest/browser':
+ optional: true
+
+ '@vitest/expect@4.1.10':
+ resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==}
+
+ '@vitest/mocker@4.1.10':
+ resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==}
+ peerDependencies:
+ msw: ^2.4.9
+ vite: ^6.0.0 || ^7.0.0 || ^8.0.0
+ peerDependenciesMeta:
+ msw:
+ optional: true
+ vite:
+ optional: true
+
+ '@vitest/pretty-format@4.1.10':
+ resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==}
+
+ '@vitest/runner@4.1.10':
+ resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==}
+
+ '@vitest/snapshot@4.1.10':
+ resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==}
+
+ '@vitest/spy@4.1.10':
+ resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==}
+
+ '@vitest/utils@4.1.10':
+ resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==}
+
+ acorn-jsx@5.3.2:
+ resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
+ peerDependencies:
+ acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
+
+ acorn@8.18.0:
+ resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+
+ ajv@6.15.0:
+ resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==}
+
+ ansi-regex@5.0.1:
+ resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
+ engines: {node: '>=8'}
+
+ ansi-styles@5.2.0:
+ resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
+ engines: {node: '>=10'}
+
+ aria-query@5.3.0:
+ resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
+
+ aria-query@5.3.2:
+ resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
+ engines: {node: '>= 0.4'}
+
+ assertion-error@2.0.1:
+ resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
+ engines: {node: '>=12'}
+
+ ast-v8-to-istanbul@1.0.5:
+ resolution: {integrity: sha512-UPAgKJFSEGMWSDr3LX4tqnAb4f7KGT8O40Tyx8wbYmmZ/yn58lNCm8h3svs3eXgiGd5AXxz8NDOvXWvicq+rJA==}
+
+ balanced-match@4.0.4:
+ resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
+ engines: {node: 18 || 20 || >=22}
+
+ bidi-js@1.0.3:
+ resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==}
+
+ brace-expansion@5.0.9:
+ resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==}
+ engines: {node: 20 || >=22}
+
+ chai@6.2.2:
+ resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
+ engines: {node: '>=18'}
+
+ convert-source-map@2.0.0:
+ resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+
+ cross-spawn@7.0.6:
+ resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
+ engines: {node: '>= 8'}
+
+ css-tree@3.2.1:
+ resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==}
+ engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
+
+ css.escape@1.5.1:
+ resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==}
+
+ csstype@3.2.3:
+ resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
+
+ data-urls@7.0.0:
+ resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
+
+ debug@4.4.3:
+ resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
+ decimal.js@10.6.0:
+ resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
+
+ deep-is@0.1.4:
+ resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
+
+ dequal@2.0.3:
+ resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
+ engines: {node: '>=6'}
+
+ detect-libc@2.1.2:
+ resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
+ engines: {node: '>=8'}
+
+ dom-accessibility-api@0.5.16:
+ resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
+
+ dom-accessibility-api@0.6.3:
+ resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==}
+
+ entities@8.0.0:
+ resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==}
+ engines: {node: '>=20.19.0'}
+
+ es-module-lexer@2.3.1:
+ resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==}
+
+ escape-string-regexp@4.0.0:
+ resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
+ engines: {node: '>=10'}
+
+ eslint-scope@9.1.2:
+ resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ eslint-visitor-keys@3.4.3:
+ resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+
+ eslint-visitor-keys@5.0.1:
+ resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ eslint@10.8.1:
+ resolution: {integrity: sha512-wqA7W2jbsC/BnV9Iv1UZpKVFkO1AdNoSmYW8NWG4HNOBbkAMvIqDZ27pI2f07dqn583NcIC44ckjAcOXDL1QbQ==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+ hasBin: true
+ peerDependencies:
+ jiti: '*'
+ peerDependenciesMeta:
+ jiti:
+ optional: true
+
+ espree@11.2.0:
+ resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24}
+
+ esquery@1.7.0:
+ resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==}
+ engines: {node: '>=0.10'}
+
+ esrecurse@4.3.0:
+ resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
+ engines: {node: '>=4.0'}
+
+ estraverse@5.3.0:
+ resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
+ engines: {node: '>=4.0'}
+
+ estree-walker@3.0.3:
+ resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
+
+ esutils@2.0.3:
+ resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
+ engines: {node: '>=0.10.0'}
+
+ expect-type@1.4.0:
+ resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==}
+ engines: {node: '>=12.0.0'}
+
+ fast-deep-equal@3.1.3:
+ resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+
+ fast-json-stable-stringify@2.1.0:
+ resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
+
+ fast-levenshtein@2.0.6:
+ resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
+
+ fdir@6.5.0:
+ resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
+ engines: {node: '>=12.0.0'}
+ peerDependencies:
+ picomatch: ^3 || ^4
+ peerDependenciesMeta:
+ picomatch:
+ optional: true
+
+ file-entry-cache@8.0.0:
+ resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
+ engines: {node: '>=16.0.0'}
+
+ find-up@5.0.0:
+ resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
+ engines: {node: '>=10'}
+
+ flat-cache@4.0.1:
+ resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
+ engines: {node: '>=16'}
+
+ flatted@3.4.4:
+ resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==}
+
+ fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ glob-parent@6.0.2:
+ resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
+ engines: {node: '>=10.13.0'}
+
+ has-flag@4.0.0:
+ resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
+ engines: {node: '>=8'}
+
+ html-encoding-sniffer@6.0.0:
+ resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
+
+ html-escaper@2.0.2:
+ resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
+
+ ignore@5.3.2:
+ resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
+ engines: {node: '>= 4'}
+
+ ignore@7.0.6:
+ resolution: {integrity: sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==}
+ engines: {node: '>= 4'}
+
+ imurmurhash@0.1.4:
+ resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
+ engines: {node: '>=0.8.19'}
+
+ indent-string@4.0.0:
+ resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
+ engines: {node: '>=8'}
+
+ is-extglob@2.1.1:
+ resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
+ engines: {node: '>=0.10.0'}
+
+ is-glob@4.0.3:
+ resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
+ engines: {node: '>=0.10.0'}
+
+ is-potential-custom-element-name@1.0.1:
+ resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
+
+ isexe@2.0.0:
+ resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+
+ istanbul-lib-coverage@3.2.2:
+ resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==}
+ engines: {node: '>=8'}
+
+ istanbul-lib-report@3.0.1:
+ resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==}
+ engines: {node: '>=10'}
+
+ istanbul-reports@3.2.0:
+ resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==}
+ engines: {node: '>=8'}
+
+ js-tokens@10.0.0:
+ resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==}
+
+ js-tokens@4.0.0:
+ resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+
+ jsdom@29.1.1:
+ resolution: {integrity: sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0}
+ peerDependencies:
+ canvas: ^3.0.0
+ peerDependenciesMeta:
+ canvas:
+ optional: true
+
+ json-buffer@3.0.1:
+ resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
+
+ json-schema-traverse@0.4.1:
+ resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
+
+ json-stable-stringify-without-jsonify@1.0.1:
+ resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
+
+ keyv@4.5.4:
+ resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
+
+ levn@0.4.1:
+ resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
+ engines: {node: '>= 0.8.0'}
+
+ lightningcss-android-arm64@1.33.0:
+ resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [android]
+
+ lightningcss-darwin-arm64@1.33.0:
+ resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [darwin]
+
+ lightningcss-darwin-x64@1.33.0:
+ resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [darwin]
+
+ lightningcss-freebsd-x64@1.33.0:
+ resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [freebsd]
+
+ lightningcss-linux-arm-gnueabihf@1.33.0:
+ resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm]
+ os: [linux]
+
+ lightningcss-linux-arm64-gnu@1.33.0:
+ resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+
+ lightningcss-linux-arm64-musl@1.33.0:
+ resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+
+ lightningcss-linux-x64-gnu@1.33.0:
+ resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+
+ lightningcss-linux-x64-musl@1.33.0:
+ resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+
+ lightningcss-win32-arm64-msvc@1.33.0:
+ resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [win32]
+
+ lightningcss-win32-x64-msvc@1.33.0:
+ resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [win32]
+
+ lightningcss@1.33.0:
+ resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==}
+ engines: {node: '>= 12.0.0'}
+
+ locate-path@6.0.0:
+ resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
+ engines: {node: '>=10'}
+
+ lru-cache@11.5.2:
+ resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==}
+ engines: {node: 20 || >=22}
+
+ lz-string@1.5.0:
+ resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
+ hasBin: true
+
+ magic-string@0.30.21:
+ resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
+
+ magicast@0.5.4:
+ resolution: {integrity: sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w==}
+
+ make-dir@4.0.0:
+ resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
+ engines: {node: '>=10'}
+
+ mdn-data@2.27.1:
+ resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==}
+
+ min-indent@1.0.1:
+ resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
+ engines: {node: '>=4'}
+
+ minimatch@10.2.6:
+ resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==}
+ engines: {node: 18 || 20 || >=22}
+
+ ms@2.1.3:
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
+ nanoid@3.3.18:
+ resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
+ natural-compare@1.4.0:
+ resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
+
+ obug@2.1.4:
+ resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==}
+ engines: {node: '>=12.20.0'}
+
+ optionator@0.9.4:
+ resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
+ engines: {node: '>= 0.8.0'}
+
+ p-limit@3.1.0:
+ resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
+ engines: {node: '>=10'}
+
+ p-locate@5.0.0:
+ resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
+ engines: {node: '>=10'}
+
+ parse5@8.0.1:
+ resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==}
+
+ path-exists@4.0.0:
+ resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
+ engines: {node: '>=8'}
+
+ path-key@3.1.1:
+ resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
+ engines: {node: '>=8'}
+
+ pathe@2.0.3:
+ resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
+
+ picocolors@1.1.1:
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+ picomatch@4.0.5:
+ resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==}
+ engines: {node: '>=12'}
+
+ postcss@8.5.26:
+ resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==}
+ engines: {node: ^10 || ^12 || >=14}
+
+ prelude-ls@1.2.1:
+ resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
+ engines: {node: '>= 0.8.0'}
+
+ pretty-format@27.5.1:
+ resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
+ engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
+
+ punycode@2.3.1:
+ resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
+ engines: {node: '>=6'}
+
+ react-dom@19.2.8:
+ resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==}
+ peerDependencies:
+ react: ^19.2.8
+
+ react-is@17.0.2:
+ resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
+
+ react@19.2.8:
+ resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==}
+ engines: {node: '>=0.10.0'}
+
+ redent@3.0.0:
+ resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
+ engines: {node: '>=8'}
+
+ require-from-string@2.0.2:
+ resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
+ engines: {node: '>=0.10.0'}
+
+ rolldown@1.2.4:
+ resolution: {integrity: sha512-rSr7irW0K7QRWzjdJXqZowkcRdDtjRduh43rBltnVKd0VFq839l1lJoDvGJb6gl7+4rTTCrPWu+YfujUL8Ug7w==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+
+ saxes@6.0.0:
+ resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
+ engines: {node: '>=v12.22.7'}
+
+ scheduler@0.27.0:
+ resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
+
+ semver@7.8.5:
+ resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==}
+ engines: {node: '>=10'}
+ hasBin: true
+
+ shebang-command@2.0.0:
+ resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
+ engines: {node: '>=8'}
+
+ shebang-regex@3.0.0:
+ resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
+ engines: {node: '>=8'}
+
+ siginfo@2.0.0:
+ resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
+
+ source-map-js@1.2.1:
+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ engines: {node: '>=0.10.0'}
+
+ stackback@0.0.2:
+ resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
+
+ std-env@4.2.0:
+ resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==}
+
+ strip-indent@3.0.0:
+ resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==}
+ engines: {node: '>=8'}
+
+ supports-color@7.2.0:
+ resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
+ engines: {node: '>=8'}
+
+ symbol-tree@3.2.4:
+ resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
+
+ tinybench@2.9.0:
+ resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
+
+ tinyexec@1.3.0:
+ resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==}
+ engines: {node: '>=18'}
+
+ tinyglobby@0.2.17:
+ resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
+ engines: {node: '>=12.0.0'}
+
+ tinyrainbow@3.1.1:
+ resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==}
+ engines: {node: '>=14.0.0'}
+
+ tldts-core@7.4.10:
+ resolution: {integrity: sha512-KnQjp53ZekKgm/r3l+u8kJGGzYgrWdP8+Mql7a4vijh2WE0IrZWspQj/TpTxDho/YxO+AnOZnIjQcCD+q6iJsw==}
+
+ tldts@7.4.10:
+ resolution: {integrity: sha512-GgouD1B+sWwvkaEq8vXC15DjQitxbvs12oIXELpconwm+Tg3zfcEv4jgzq3vtKverDXsg3VI8aRgNL2Nra0Iog==}
+ hasBin: true
+
+ tough-cookie@6.0.2:
+ resolution: {integrity: sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==}
+ engines: {node: '>=16'}
+
+ tr46@6.0.0:
+ resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==}
+ engines: {node: '>=20'}
+
+ ts-api-utils@2.5.0:
+ resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==}
+ engines: {node: '>=18.12'}
+ peerDependencies:
+ typescript: '>=4.8.4'
+
+ type-check@0.4.0:
+ resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
+ engines: {node: '>= 0.8.0'}
+
+ typescript-eslint@8.67.0:
+ resolution: {integrity: sha512-S2udFs8tCKEKffuJ4TB1idGUZiXdCPGi3IPBGWXarbLQ5UPXORV8QEVzJ4gCRduURMb5EkpNCdjbk0eDIuI8Yg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ typescript: '>=4.8.4 <6.1.0'
+
+ typescript@6.0.3:
+ resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
+ undici@7.29.0:
+ resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==}
+ engines: {node: '>=20.18.1'}
+
+ uri-js@4.4.1:
+ resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+
+ vite@8.2.1:
+ resolution: {integrity: sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^20.19.0 || >=22.12.0
+ '@vitejs/devtools': ^0.4.0
+ esbuild: ^0.27.0 || ^0.28.0
+ jiti: '>=1.21.0'
+ less: ^4.0.0
+ sass: ^1.70.0
+ sass-embedded: ^1.70.0
+ stylus: '>=0.54.8'
+ sugarss: ^5.0.0
+ terser: ^5.16.0
+ tsx: ^4.8.1
+ yaml: ^2.4.2
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ '@vitejs/devtools':
+ optional: true
+ esbuild:
+ optional: true
+ jiti:
+ optional: true
+ less:
+ optional: true
+ sass:
+ optional: true
+ sass-embedded:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+ tsx:
+ optional: true
+ yaml:
+ optional: true
+
+ vitest@4.1.10:
+ resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==}
+ engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
+ hasBin: true
+ peerDependencies:
+ '@edge-runtime/vm': '*'
+ '@opentelemetry/api': ^1.9.0
+ '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0
+ '@vitest/browser-playwright': 4.1.10
+ '@vitest/browser-preview': 4.1.10
+ '@vitest/browser-webdriverio': 4.1.10
+ '@vitest/coverage-istanbul': 4.1.10
+ '@vitest/coverage-v8': 4.1.10
+ '@vitest/ui': 4.1.10
+ happy-dom: '*'
+ jsdom: '*'
+ vite: ^6.0.0 || ^7.0.0 || ^8.0.0
+ peerDependenciesMeta:
+ '@edge-runtime/vm':
+ optional: true
+ '@opentelemetry/api':
+ optional: true
+ '@types/node':
+ optional: true
+ '@vitest/browser-playwright':
+ optional: true
+ '@vitest/browser-preview':
+ optional: true
+ '@vitest/browser-webdriverio':
+ optional: true
+ '@vitest/coverage-istanbul':
+ optional: true
+ '@vitest/coverage-v8':
+ optional: true
+ '@vitest/ui':
+ optional: true
+ happy-dom:
+ optional: true
+ jsdom:
+ optional: true
+
+ w3c-xmlserializer@5.0.0:
+ resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
+ engines: {node: '>=18'}
+
+ webidl-conversions@8.0.1:
+ resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==}
+ engines: {node: '>=20'}
+
+ whatwg-mimetype@5.0.0:
+ resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==}
+ engines: {node: '>=20'}
+
+ whatwg-url@16.0.1:
+ resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
+
+ which@2.0.2:
+ resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
+ engines: {node: '>= 8'}
+ hasBin: true
+
+ why-is-node-running@2.3.0:
+ resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
+ engines: {node: '>=8'}
+ hasBin: true
+
+ word-wrap@1.2.5:
+ resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
+ engines: {node: '>=0.10.0'}
+
+ xml-name-validator@5.0.0:
+ resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
+ engines: {node: '>=18'}
+
+ xmlchars@2.2.0:
+ resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
+
+ yocto-queue@0.1.0:
+ resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
+ engines: {node: '>=10'}
+
+snapshots:
+
+ '@adobe/css-tools@4.5.0': {}
+
+ '@asamuzakjp/css-color@5.1.11':
+ dependencies:
+ '@asamuzakjp/generational-cache': 1.0.1
+ '@csstools/css-calc': 3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-color-parser': 4.1.10(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-tokenizer': 4.0.0
+
+ '@asamuzakjp/dom-selector@7.1.1':
+ dependencies:
+ '@asamuzakjp/generational-cache': 1.0.1
+ '@asamuzakjp/nwsapi': 2.3.9
+ bidi-js: 1.0.3
+ css-tree: 3.2.1
+ is-potential-custom-element-name: 1.0.1
+
+ '@asamuzakjp/generational-cache@1.0.1': {}
+
+ '@asamuzakjp/nwsapi@2.3.9': {}
+
+ '@babel/code-frame@7.29.7':
+ dependencies:
+ '@babel/helper-validator-identifier': 7.29.7
+ js-tokens: 4.0.0
+ picocolors: 1.1.1
+
+ '@babel/helper-string-parser@7.29.7': {}
+
+ '@babel/helper-validator-identifier@7.29.7': {}
+
+ '@babel/parser@7.29.8':
+ dependencies:
+ '@babel/types': 7.29.8
+
+ '@babel/runtime@7.29.7': {}
+
+ '@babel/types@7.29.8':
+ dependencies:
+ '@babel/helper-string-parser': 7.29.7
+ '@babel/helper-validator-identifier': 7.29.7
+
+ '@bcoe/v8-coverage@1.0.2': {}
+
+ '@bramus/specificity@2.4.2':
+ dependencies:
+ css-tree: 3.2.1
+
+ '@csstools/color-helpers@6.1.0': {}
+
+ '@csstools/css-calc@3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
+ dependencies:
+ '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-tokenizer': 4.0.0
+
+ '@csstools/css-color-parser@4.1.10(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
+ dependencies:
+ '@csstools/color-helpers': 6.1.0
+ '@csstools/css-calc': 3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
+ '@csstools/css-tokenizer': 4.0.0
+
+ '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)':
+ dependencies:
+ '@csstools/css-tokenizer': 4.0.0
+
+ '@csstools/css-syntax-patches-for-csstree@1.1.7(css-tree@3.2.1)':
+ optionalDependencies:
+ css-tree: 3.2.1
+
+ '@csstools/css-tokenizer@4.0.0': {}
+
+ '@eslint-community/eslint-utils@4.10.1(eslint@10.8.1)':
+ dependencies:
+ eslint: 10.8.1
+ eslint-visitor-keys: 3.4.3
+
+ '@eslint-community/regexpp@4.12.2': {}
+
+ '@eslint/config-array@0.23.5':
+ dependencies:
+ '@eslint/object-schema': 3.0.5
+ debug: 4.4.3
+ minimatch: 10.2.6
+ transitivePeerDependencies:
+ - supports-color
+
+ '@eslint/config-helpers@0.7.0':
+ dependencies:
+ '@eslint/core': 1.2.1
+
+ '@eslint/core@1.2.1':
+ dependencies:
+ '@types/json-schema': 7.0.15
+
+ '@eslint/js@10.0.1(eslint@10.8.1)':
+ optionalDependencies:
+ eslint: 10.8.1
+
+ '@eslint/object-schema@3.0.5': {}
+
+ '@eslint/plugin-kit@0.7.2':
+ dependencies:
+ '@eslint/core': 1.2.1
+ levn: 0.4.1
+
+ '@exodus/bytes@1.15.1': {}
+
+ '@humanfs/core@0.19.2':
+ dependencies:
+ '@humanfs/types': 0.15.0
+
+ '@humanfs/node@0.16.8':
+ dependencies:
+ '@humanfs/core': 0.19.2
+ '@humanfs/types': 0.15.0
+ '@humanwhocodes/retry': 0.4.3
+
+ '@humanfs/types@0.15.0': {}
+
+ '@humanwhocodes/module-importer@1.0.1': {}
+
+ '@humanwhocodes/retry@0.4.3': {}
+
+ '@jridgewell/resolve-uri@3.1.2': {}
+
+ '@jridgewell/sourcemap-codec@1.5.5': {}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ dependencies:
+ '@jridgewell/resolve-uri': 3.1.2
+ '@jridgewell/sourcemap-codec': 1.5.5
+
+ '@oxc-project/types@0.144.0': {}
+
+ '@rolldown/binding-android-arm64@1.2.4':
+ optional: true
+
+ '@rolldown/binding-darwin-arm64@1.2.4':
+ optional: true
+
+ '@rolldown/binding-darwin-x64@1.2.4':
+ optional: true
+
+ '@rolldown/binding-freebsd-x64@1.2.4':
+ optional: true
+
+ '@rolldown/binding-linux-arm-gnueabihf@1.2.4':
+ optional: true
+
+ '@rolldown/binding-linux-arm64-gnu@1.2.4':
+ optional: true
+
+ '@rolldown/binding-linux-arm64-musl@1.2.4':
+ optional: true
+
+ '@rolldown/binding-linux-ppc64-gnu@1.2.4':
+ optional: true
+
+ '@rolldown/binding-linux-s390x-gnu@1.2.4':
+ optional: true
+
+ '@rolldown/binding-linux-x64-gnu@1.2.4':
+ optional: true
+
+ '@rolldown/binding-linux-x64-musl@1.2.4':
+ optional: true
+
+ '@rolldown/binding-openharmony-arm64@1.2.4':
+ optional: true
+
+ '@rolldown/binding-win32-arm64-msvc@1.2.4':
+ optional: true
+
+ '@rolldown/binding-win32-x64-msvc@1.2.4':
+ optional: true
+
+ '@rolldown/pluginutils@1.0.1': {}
+
+ '@standard-schema/spec@1.1.0': {}
+
+ '@testing-library/dom@10.4.1':
+ dependencies:
+ '@babel/code-frame': 7.29.7
+ '@babel/runtime': 7.29.7
+ '@types/aria-query': 5.0.4
+ aria-query: 5.3.0
+ dom-accessibility-api: 0.5.16
+ lz-string: 1.5.0
+ picocolors: 1.1.1
+ pretty-format: 27.5.1
+
+ '@testing-library/jest-dom@6.10.0(@testing-library/dom@10.4.1)':
+ dependencies:
+ '@adobe/css-tools': 4.5.0
+ '@testing-library/dom': 10.4.1
+ aria-query: 5.3.2
+ css.escape: 1.5.1
+ dom-accessibility-api: 0.6.3
+ picocolors: 1.1.1
+ redent: 3.0.0
+
+ '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
+ dependencies:
+ '@babel/runtime': 7.29.7
+ '@testing-library/dom': 10.4.1
+ react: 19.2.8
+ react-dom: 19.2.8(react@19.2.8)
+ optionalDependencies:
+ '@types/react': 19.2.18
+ '@types/react-dom': 19.2.4(@types/react@19.2.18)
+
+ '@testing-library/user-event@14.6.4(@testing-library/dom@10.4.1)':
+ dependencies:
+ '@testing-library/dom': 10.4.1
+
+ '@types/aria-query@5.0.4': {}
+
+ '@types/chai@5.2.3':
+ dependencies:
+ '@types/deep-eql': 4.0.2
+ assertion-error: 2.0.1
+
+ '@types/deep-eql@4.0.2': {}
+
+ '@types/esrecurse@4.3.1': {}
+
+ '@types/estree@1.0.9': {}
+
+ '@types/json-schema@7.0.15': {}
+
+ '@types/react-dom@19.2.4(@types/react@19.2.18)':
+ dependencies:
+ '@types/react': 19.2.18
+
+ '@types/react@19.2.18':
+ dependencies:
+ csstype: 3.2.3
+
+ '@typescript-eslint/eslint-plugin@8.67.0(@typescript-eslint/parser@8.67.0(eslint@10.8.1)(typescript@6.0.3))(eslint@10.8.1)(typescript@6.0.3)':
+ dependencies:
+ '@eslint-community/regexpp': 4.12.2
+ '@typescript-eslint/parser': 8.67.0(eslint@10.8.1)(typescript@6.0.3)
+ '@typescript-eslint/scope-manager': 8.67.0
+ '@typescript-eslint/type-utils': 8.67.0(eslint@10.8.1)(typescript@6.0.3)
+ '@typescript-eslint/utils': 8.67.0(eslint@10.8.1)(typescript@6.0.3)
+ '@typescript-eslint/visitor-keys': 8.67.0
+ eslint: 10.8.1
+ ignore: 7.0.6
+ natural-compare: 1.4.0
+ ts-api-utils: 2.5.0(typescript@6.0.3)
+ typescript: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/parser@8.67.0(eslint@10.8.1)(typescript@6.0.3)':
+ dependencies:
+ '@typescript-eslint/scope-manager': 8.67.0
+ '@typescript-eslint/types': 8.67.0
+ '@typescript-eslint/typescript-estree': 8.67.0(typescript@6.0.3)
+ '@typescript-eslint/visitor-keys': 8.67.0
+ debug: 4.4.3
+ eslint: 10.8.1
+ typescript: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/project-service@8.67.0(typescript@6.0.3)':
+ dependencies:
+ '@typescript-eslint/tsconfig-utils': 8.67.0(typescript@6.0.3)
+ '@typescript-eslint/types': 8.67.0
+ debug: 4.4.3
+ typescript: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/scope-manager@8.67.0':
+ dependencies:
+ '@typescript-eslint/types': 8.67.0
+ '@typescript-eslint/visitor-keys': 8.67.0
+
+ '@typescript-eslint/tsconfig-utils@8.67.0(typescript@6.0.3)':
+ dependencies:
+ typescript: 6.0.3
+
+ '@typescript-eslint/type-utils@8.67.0(eslint@10.8.1)(typescript@6.0.3)':
+ dependencies:
+ '@typescript-eslint/types': 8.67.0
+ '@typescript-eslint/typescript-estree': 8.67.0(typescript@6.0.3)
+ '@typescript-eslint/utils': 8.67.0(eslint@10.8.1)(typescript@6.0.3)
+ debug: 4.4.3
+ eslint: 10.8.1
+ ts-api-utils: 2.5.0(typescript@6.0.3)
+ typescript: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/types@8.67.0': {}
+
+ '@typescript-eslint/typescript-estree@8.67.0(typescript@6.0.3)':
+ dependencies:
+ '@typescript-eslint/project-service': 8.67.0(typescript@6.0.3)
+ '@typescript-eslint/tsconfig-utils': 8.67.0(typescript@6.0.3)
+ '@typescript-eslint/types': 8.67.0
+ '@typescript-eslint/visitor-keys': 8.67.0
+ debug: 4.4.3
+ minimatch: 10.2.6
+ semver: 7.8.5
+ tinyglobby: 0.2.17
+ ts-api-utils: 2.5.0(typescript@6.0.3)
+ typescript: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/utils@8.67.0(eslint@10.8.1)(typescript@6.0.3)':
+ dependencies:
+ '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.1)
+ '@typescript-eslint/scope-manager': 8.67.0
+ '@typescript-eslint/types': 8.67.0
+ '@typescript-eslint/typescript-estree': 8.67.0(typescript@6.0.3)
+ eslint: 10.8.1
+ typescript: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/visitor-keys@8.67.0':
+ dependencies:
+ '@typescript-eslint/types': 8.67.0
+ eslint-visitor-keys: 5.0.1
+
+ '@vitest/coverage-v8@4.1.10(vitest@4.1.10)':
+ dependencies:
+ '@bcoe/v8-coverage': 1.0.2
+ '@vitest/utils': 4.1.10
+ ast-v8-to-istanbul: 1.0.5
+ istanbul-lib-coverage: 3.2.2
+ istanbul-lib-report: 3.0.1
+ istanbul-reports: 3.2.0
+ magicast: 0.5.4
+ obug: 2.1.4
+ std-env: 4.2.0
+ tinyrainbow: 3.1.1
+ vitest: 4.1.10(@vitest/coverage-v8@4.1.10)(jsdom@29.1.1)(vite@8.2.1)
+
+ '@vitest/expect@4.1.10':
+ dependencies:
+ '@standard-schema/spec': 1.1.0
+ '@types/chai': 5.2.3
+ '@vitest/spy': 4.1.10
+ '@vitest/utils': 4.1.10
+ chai: 6.2.2
+ tinyrainbow: 3.1.1
+
+ '@vitest/mocker@4.1.10(vite@8.2.1)':
+ dependencies:
+ '@vitest/spy': 4.1.10
+ estree-walker: 3.0.3
+ magic-string: 0.30.21
+ optionalDependencies:
+ vite: 8.2.1
+
+ '@vitest/pretty-format@4.1.10':
+ dependencies:
+ tinyrainbow: 3.1.1
+
+ '@vitest/runner@4.1.10':
+ dependencies:
+ '@vitest/utils': 4.1.10
+ pathe: 2.0.3
+
+ '@vitest/snapshot@4.1.10':
+ dependencies:
+ '@vitest/pretty-format': 4.1.10
+ '@vitest/utils': 4.1.10
+ magic-string: 0.30.21
+ pathe: 2.0.3
+
+ '@vitest/spy@4.1.10': {}
+
+ '@vitest/utils@4.1.10':
+ dependencies:
+ '@vitest/pretty-format': 4.1.10
+ convert-source-map: 2.0.0
+ tinyrainbow: 3.1.1
+
+ acorn-jsx@5.3.2(acorn@8.18.0):
+ dependencies:
+ acorn: 8.18.0
+
+ acorn@8.18.0: {}
+
+ ajv@6.15.0:
+ dependencies:
+ fast-deep-equal: 3.1.3
+ fast-json-stable-stringify: 2.1.0
+ json-schema-traverse: 0.4.1
+ uri-js: 4.4.1
+
+ ansi-regex@5.0.1: {}
+
+ ansi-styles@5.2.0: {}
+
+ aria-query@5.3.0:
+ dependencies:
+ dequal: 2.0.3
+
+ aria-query@5.3.2: {}
+
+ assertion-error@2.0.1: {}
+
+ ast-v8-to-istanbul@1.0.5:
+ dependencies:
+ '@jridgewell/trace-mapping': 0.3.31
+ estree-walker: 3.0.3
+ js-tokens: 10.0.0
+
+ balanced-match@4.0.4: {}
+
+ bidi-js@1.0.3:
+ dependencies:
+ require-from-string: 2.0.2
+
+ brace-expansion@5.0.9:
+ dependencies:
+ balanced-match: 4.0.4
+
+ chai@6.2.2: {}
+
+ convert-source-map@2.0.0: {}
+
+ cross-spawn@7.0.6:
+ dependencies:
+ path-key: 3.1.1
+ shebang-command: 2.0.0
+ which: 2.0.2
+
+ css-tree@3.2.1:
+ dependencies:
+ mdn-data: 2.27.1
+ source-map-js: 1.2.1
+
+ css.escape@1.5.1: {}
+
+ csstype@3.2.3: {}
+
+ data-urls@7.0.0:
+ dependencies:
+ whatwg-mimetype: 5.0.0
+ whatwg-url: 16.0.1
+ transitivePeerDependencies:
+ - '@noble/hashes'
+
+ debug@4.4.3:
+ dependencies:
+ ms: 2.1.3
+
+ decimal.js@10.6.0: {}
+
+ deep-is@0.1.4: {}
+
+ dequal@2.0.3: {}
+
+ detect-libc@2.1.2: {}
+
+ dom-accessibility-api@0.5.16: {}
+
+ dom-accessibility-api@0.6.3: {}
+
+ entities@8.0.0: {}
+
+ es-module-lexer@2.3.1: {}
+
+ escape-string-regexp@4.0.0: {}
+
+ eslint-scope@9.1.2:
+ dependencies:
+ '@types/esrecurse': 4.3.1
+ '@types/estree': 1.0.9
+ esrecurse: 4.3.0
+ estraverse: 5.3.0
+
+ eslint-visitor-keys@3.4.3: {}
+
+ eslint-visitor-keys@5.0.1: {}
+
+ eslint@10.8.1:
+ dependencies:
+ '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.1)
+ '@eslint-community/regexpp': 4.12.2
+ '@eslint/config-array': 0.23.5
+ '@eslint/config-helpers': 0.7.0
+ '@eslint/core': 1.2.1
+ '@eslint/plugin-kit': 0.7.2
+ '@humanfs/node': 0.16.8
+ '@humanwhocodes/module-importer': 1.0.1
+ '@humanwhocodes/retry': 0.4.3
+ '@types/estree': 1.0.9
+ ajv: 6.15.0
+ cross-spawn: 7.0.6
+ debug: 4.4.3
+ escape-string-regexp: 4.0.0
+ eslint-scope: 9.1.2
+ eslint-visitor-keys: 5.0.1
+ espree: 11.2.0
+ esquery: 1.7.0
+ esutils: 2.0.3
+ fast-deep-equal: 3.1.3
+ file-entry-cache: 8.0.0
+ find-up: 5.0.0
+ glob-parent: 6.0.2
+ ignore: 5.3.2
+ imurmurhash: 0.1.4
+ is-glob: 4.0.3
+ json-stable-stringify-without-jsonify: 1.0.1
+ minimatch: 10.2.6
+ natural-compare: 1.4.0
+ optionator: 0.9.4
+ transitivePeerDependencies:
+ - supports-color
+
+ espree@11.2.0:
+ dependencies:
+ acorn: 8.18.0
+ acorn-jsx: 5.3.2(acorn@8.18.0)
+ eslint-visitor-keys: 5.0.1
+
+ esquery@1.7.0:
+ dependencies:
+ estraverse: 5.3.0
+
+ esrecurse@4.3.0:
+ dependencies:
+ estraverse: 5.3.0
+
+ estraverse@5.3.0: {}
+
+ estree-walker@3.0.3:
+ dependencies:
+ '@types/estree': 1.0.9
+
+ esutils@2.0.3: {}
+
+ expect-type@1.4.0: {}
+
+ fast-deep-equal@3.1.3: {}
+
+ fast-json-stable-stringify@2.1.0: {}
+
+ fast-levenshtein@2.0.6: {}
+
+ fdir@6.5.0(picomatch@4.0.5):
+ optionalDependencies:
+ picomatch: 4.0.5
+
+ file-entry-cache@8.0.0:
+ dependencies:
+ flat-cache: 4.0.1
+
+ find-up@5.0.0:
+ dependencies:
+ locate-path: 6.0.0
+ path-exists: 4.0.0
+
+ flat-cache@4.0.1:
+ dependencies:
+ flatted: 3.4.4
+ keyv: 4.5.4
+
+ flatted@3.4.4: {}
+
+ fsevents@2.3.3:
+ optional: true
+
+ glob-parent@6.0.2:
+ dependencies:
+ is-glob: 4.0.3
+
+ has-flag@4.0.0: {}
+
+ html-encoding-sniffer@6.0.0:
+ dependencies:
+ '@exodus/bytes': 1.15.1
+ transitivePeerDependencies:
+ - '@noble/hashes'
+
+ html-escaper@2.0.2: {}
+
+ ignore@5.3.2: {}
+
+ ignore@7.0.6: {}
+
+ imurmurhash@0.1.4: {}
+
+ indent-string@4.0.0: {}
+
+ is-extglob@2.1.1: {}
+
+ is-glob@4.0.3:
+ dependencies:
+ is-extglob: 2.1.1
+
+ is-potential-custom-element-name@1.0.1: {}
+
+ isexe@2.0.0: {}
+
+ istanbul-lib-coverage@3.2.2: {}
+
+ istanbul-lib-report@3.0.1:
+ dependencies:
+ istanbul-lib-coverage: 3.2.2
+ make-dir: 4.0.0
+ supports-color: 7.2.0
+
+ istanbul-reports@3.2.0:
+ dependencies:
+ html-escaper: 2.0.2
+ istanbul-lib-report: 3.0.1
+
+ js-tokens@10.0.0: {}
+
+ js-tokens@4.0.0: {}
+
+ jsdom@29.1.1:
+ dependencies:
+ '@asamuzakjp/css-color': 5.1.11
+ '@asamuzakjp/dom-selector': 7.1.1
+ '@bramus/specificity': 2.4.2
+ '@csstools/css-syntax-patches-for-csstree': 1.1.7(css-tree@3.2.1)
+ '@exodus/bytes': 1.15.1
+ css-tree: 3.2.1
+ data-urls: 7.0.0
+ decimal.js: 10.6.0
+ html-encoding-sniffer: 6.0.0
+ is-potential-custom-element-name: 1.0.1
+ lru-cache: 11.5.2
+ parse5: 8.0.1
+ saxes: 6.0.0
+ symbol-tree: 3.2.4
+ tough-cookie: 6.0.2
+ undici: 7.29.0
+ w3c-xmlserializer: 5.0.0
+ webidl-conversions: 8.0.1
+ whatwg-mimetype: 5.0.0
+ whatwg-url: 16.0.1
+ xml-name-validator: 5.0.0
+ transitivePeerDependencies:
+ - '@noble/hashes'
+
+ json-buffer@3.0.1: {}
+
+ json-schema-traverse@0.4.1: {}
+
+ json-stable-stringify-without-jsonify@1.0.1: {}
+
+ keyv@4.5.4:
+ dependencies:
+ json-buffer: 3.0.1
+
+ levn@0.4.1:
+ dependencies:
+ prelude-ls: 1.2.1
+ type-check: 0.4.0
+
+ lightningcss-android-arm64@1.33.0:
+ optional: true
+
+ lightningcss-darwin-arm64@1.33.0:
+ optional: true
+
+ lightningcss-darwin-x64@1.33.0:
+ optional: true
+
+ lightningcss-freebsd-x64@1.33.0:
+ optional: true
+
+ lightningcss-linux-arm-gnueabihf@1.33.0:
+ optional: true
+
+ lightningcss-linux-arm64-gnu@1.33.0:
+ optional: true
+
+ lightningcss-linux-arm64-musl@1.33.0:
+ optional: true
+
+ lightningcss-linux-x64-gnu@1.33.0:
+ optional: true
+
+ lightningcss-linux-x64-musl@1.33.0:
+ optional: true
+
+ lightningcss-win32-arm64-msvc@1.33.0:
+ optional: true
+
+ lightningcss-win32-x64-msvc@1.33.0:
+ optional: true
+
+ lightningcss@1.33.0:
+ dependencies:
+ detect-libc: 2.1.2
+ optionalDependencies:
+ lightningcss-android-arm64: 1.33.0
+ lightningcss-darwin-arm64: 1.33.0
+ lightningcss-darwin-x64: 1.33.0
+ lightningcss-freebsd-x64: 1.33.0
+ lightningcss-linux-arm-gnueabihf: 1.33.0
+ lightningcss-linux-arm64-gnu: 1.33.0
+ lightningcss-linux-arm64-musl: 1.33.0
+ lightningcss-linux-x64-gnu: 1.33.0
+ lightningcss-linux-x64-musl: 1.33.0
+ lightningcss-win32-arm64-msvc: 1.33.0
+ lightningcss-win32-x64-msvc: 1.33.0
+
+ locate-path@6.0.0:
+ dependencies:
+ p-locate: 5.0.0
+
+ lru-cache@11.5.2: {}
+
+ lz-string@1.5.0: {}
+
+ magic-string@0.30.21:
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+
+ magicast@0.5.4:
+ dependencies:
+ '@babel/parser': 7.29.8
+ '@babel/types': 7.29.8
+ source-map-js: 1.2.1
+
+ make-dir@4.0.0:
+ dependencies:
+ semver: 7.8.5
+
+ mdn-data@2.27.1: {}
+
+ min-indent@1.0.1: {}
+
+ minimatch@10.2.6:
+ dependencies:
+ brace-expansion: 5.0.9
+
+ ms@2.1.3: {}
+
+ nanoid@3.3.18: {}
+
+ natural-compare@1.4.0: {}
+
+ obug@2.1.4: {}
+
+ optionator@0.9.4:
+ dependencies:
+ deep-is: 0.1.4
+ fast-levenshtein: 2.0.6
+ levn: 0.4.1
+ prelude-ls: 1.2.1
+ type-check: 0.4.0
+ word-wrap: 1.2.5
+
+ p-limit@3.1.0:
+ dependencies:
+ yocto-queue: 0.1.0
+
+ p-locate@5.0.0:
+ dependencies:
+ p-limit: 3.1.0
+
+ parse5@8.0.1:
+ dependencies:
+ entities: 8.0.0
+
+ path-exists@4.0.0: {}
+
+ path-key@3.1.1: {}
+
+ pathe@2.0.3: {}
+
+ picocolors@1.1.1: {}
+
+ picomatch@4.0.5: {}
+
+ postcss@8.5.26:
+ dependencies:
+ nanoid: 3.3.18
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
+ prelude-ls@1.2.1: {}
+
+ pretty-format@27.5.1:
+ dependencies:
+ ansi-regex: 5.0.1
+ ansi-styles: 5.2.0
+ react-is: 17.0.2
+
+ punycode@2.3.1: {}
+
+ react-dom@19.2.8(react@19.2.8):
+ dependencies:
+ react: 19.2.8
+ scheduler: 0.27.0
+
+ react-is@17.0.2: {}
+
+ react@19.2.8: {}
+
+ redent@3.0.0:
+ dependencies:
+ indent-string: 4.0.0
+ strip-indent: 3.0.0
+
+ require-from-string@2.0.2: {}
+
+ rolldown@1.2.4:
+ dependencies:
+ '@oxc-project/types': 0.144.0
+ '@rolldown/pluginutils': 1.0.1
+ optionalDependencies:
+ '@rolldown/binding-android-arm64': 1.2.4
+ '@rolldown/binding-darwin-arm64': 1.2.4
+ '@rolldown/binding-darwin-x64': 1.2.4
+ '@rolldown/binding-freebsd-x64': 1.2.4
+ '@rolldown/binding-linux-arm-gnueabihf': 1.2.4
+ '@rolldown/binding-linux-arm64-gnu': 1.2.4
+ '@rolldown/binding-linux-arm64-musl': 1.2.4
+ '@rolldown/binding-linux-ppc64-gnu': 1.2.4
+ '@rolldown/binding-linux-s390x-gnu': 1.2.4
+ '@rolldown/binding-linux-x64-gnu': 1.2.4
+ '@rolldown/binding-linux-x64-musl': 1.2.4
+ '@rolldown/binding-openharmony-arm64': 1.2.4
+ '@rolldown/binding-win32-arm64-msvc': 1.2.4
+ '@rolldown/binding-win32-x64-msvc': 1.2.4
+
+ saxes@6.0.0:
+ dependencies:
+ xmlchars: 2.2.0
+
+ scheduler@0.27.0: {}
+
+ semver@7.8.5: {}
+
+ shebang-command@2.0.0:
+ dependencies:
+ shebang-regex: 3.0.0
+
+ shebang-regex@3.0.0: {}
+
+ siginfo@2.0.0: {}
+
+ source-map-js@1.2.1: {}
+
+ stackback@0.0.2: {}
+
+ std-env@4.2.0: {}
+
+ strip-indent@3.0.0:
+ dependencies:
+ min-indent: 1.0.1
+
+ supports-color@7.2.0:
+ dependencies:
+ has-flag: 4.0.0
+
+ symbol-tree@3.2.4: {}
+
+ tinybench@2.9.0: {}
+
+ tinyexec@1.3.0: {}
+
+ tinyglobby@0.2.17:
+ dependencies:
+ fdir: 6.5.0(picomatch@4.0.5)
+ picomatch: 4.0.5
+
+ tinyrainbow@3.1.1: {}
+
+ tldts-core@7.4.10: {}
+
+ tldts@7.4.10:
+ dependencies:
+ tldts-core: 7.4.10
+
+ tough-cookie@6.0.2:
+ dependencies:
+ tldts: 7.4.10
+
+ tr46@6.0.0:
+ dependencies:
+ punycode: 2.3.1
+
+ ts-api-utils@2.5.0(typescript@6.0.3):
+ dependencies:
+ typescript: 6.0.3
+
+ type-check@0.4.0:
+ dependencies:
+ prelude-ls: 1.2.1
+
+ typescript-eslint@8.67.0(eslint@10.8.1)(typescript@6.0.3):
+ dependencies:
+ '@typescript-eslint/eslint-plugin': 8.67.0(@typescript-eslint/parser@8.67.0(eslint@10.8.1)(typescript@6.0.3))(eslint@10.8.1)(typescript@6.0.3)
+ '@typescript-eslint/parser': 8.67.0(eslint@10.8.1)(typescript@6.0.3)
+ '@typescript-eslint/typescript-estree': 8.67.0(typescript@6.0.3)
+ '@typescript-eslint/utils': 8.67.0(eslint@10.8.1)(typescript@6.0.3)
+ eslint: 10.8.1
+ typescript: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ typescript@6.0.3: {}
+
+ undici@7.29.0: {}
+
+ uri-js@4.4.1:
+ dependencies:
+ punycode: 2.3.1
+
+ vite@8.2.1:
+ dependencies:
+ lightningcss: 1.33.0
+ picomatch: 4.0.5
+ postcss: 8.5.26
+ rolldown: 1.2.4
+ tinyglobby: 0.2.17
+ optionalDependencies:
+ fsevents: 2.3.3
+
+ vitest@4.1.10(@vitest/coverage-v8@4.1.10)(jsdom@29.1.1)(vite@8.2.1):
+ dependencies:
+ '@vitest/expect': 4.1.10
+ '@vitest/mocker': 4.1.10(vite@8.2.1)
+ '@vitest/pretty-format': 4.1.10
+ '@vitest/runner': 4.1.10
+ '@vitest/snapshot': 4.1.10
+ '@vitest/spy': 4.1.10
+ '@vitest/utils': 4.1.10
+ es-module-lexer: 2.3.1
+ expect-type: 1.4.0
+ magic-string: 0.30.21
+ obug: 2.1.4
+ pathe: 2.0.3
+ picomatch: 4.0.5
+ std-env: 4.2.0
+ tinybench: 2.9.0
+ tinyexec: 1.3.0
+ tinyglobby: 0.2.17
+ tinyrainbow: 3.1.1
+ vite: 8.2.1
+ why-is-node-running: 2.3.0
+ optionalDependencies:
+ '@vitest/coverage-v8': 4.1.10(vitest@4.1.10)
+ jsdom: 29.1.1
+ transitivePeerDependencies:
+ - msw
+
+ w3c-xmlserializer@5.0.0:
+ dependencies:
+ xml-name-validator: 5.0.0
+
+ webidl-conversions@8.0.1: {}
+
+ whatwg-mimetype@5.0.0: {}
+
+ whatwg-url@16.0.1:
+ dependencies:
+ '@exodus/bytes': 1.15.1
+ tr46: 6.0.0
+ webidl-conversions: 8.0.1
+ transitivePeerDependencies:
+ - '@noble/hashes'
+
+ which@2.0.2:
+ dependencies:
+ isexe: 2.0.0
+
+ why-is-node-running@2.3.0:
+ dependencies:
+ siginfo: 2.0.0
+ stackback: 0.0.2
+
+ word-wrap@1.2.5: {}
+
+ xml-name-validator@5.0.0: {}
+
+ xmlchars@2.2.0: {}
+
+ yocto-queue@0.1.0: {}
diff --git a/web_ui/src/index.ts b/web_ui/src/index.ts
new file mode 100644
index 0000000..7c6f421
--- /dev/null
+++ b/web_ui/src/index.ts
@@ -0,0 +1,9 @@
+/**
+ * The shared web token + component layer.
+ *
+ * `tokens.css` is not re-exported here — CSS is imported by path:
+ * `import "@kuhyx/web-ui/tokens.css";`
+ */
+export { clamp01, fractionFromPointer, nth, quantileValue, valueQuantile } from "./quantile.ts";
+export { RangeSlider, steppedValue, type RangeSliderProps } from "./range-slider.tsx";
+export { fuzzyMatch } from "./text.ts";
diff --git a/web_ui/src/quantile.test.ts b/web_ui/src/quantile.test.ts
new file mode 100644
index 0000000..0d76b39
--- /dev/null
+++ b/web_ui/src/quantile.test.ts
@@ -0,0 +1,108 @@
+import { describe, expect, it } from "vitest";
+
+import {
+ clamp01,
+ fractionFromPointer,
+ nth,
+ quantileValue,
+ valueQuantile,
+} from "./quantile.ts";
+
+describe("nth", () => {
+ it("reads a value", () => {
+ expect(nth([5, 6, 7], 1)).toBe(6);
+ });
+
+ it("throws for an index out of range", () => {
+ expect(() => nth([1], 5)).toThrow(RangeError);
+ });
+});
+
+describe("clamp01", () => {
+ it("passes a value already in range", () => {
+ expect(clamp01(0.5)).toBe(0.5);
+ });
+
+ it("clamps both ends", () => {
+ expect(clamp01(-1)).toBe(0);
+ expect(clamp01(2)).toBe(1);
+ });
+});
+
+describe("quantileValue", () => {
+ it("returns 0 for an empty distribution", () => {
+ expect(quantileValue([], 0.5)).toBe(0);
+ });
+
+ it("returns the ends at f=0 and f=1", () => {
+ expect(quantileValue([1, 2, 100], 0)).toBe(1);
+ expect(quantileValue([1, 2, 100], 1)).toBe(100);
+ });
+
+ it("returns the MEDIAN at f=0.5, not the linear midpoint", () => {
+ // The whole point of the quantile mapping, on a distribution shaped like
+ // the real star counts (1 .. 59k, long tail). The linear midpoint of that
+ // range is ~29,500 — above every server but one, which is exactly why a
+ // plain min..max slider is useless here. The median is 3.
+ expect(quantileValue([1, 2, 3, 4, 59_000], 0.5)).toBe(3);
+ });
+
+ it("interpolates between samples", () => {
+ expect(quantileValue([0, 10], 0.5)).toBe(5);
+ });
+
+ it("clamps an out-of-range fraction", () => {
+ expect(quantileValue([1, 5], -1)).toBe(1);
+ expect(quantileValue([1, 5], 9)).toBe(5);
+ });
+});
+
+describe("valueQuantile", () => {
+ it("returns 0 for a distribution with fewer than two samples", () => {
+ expect(valueQuantile([], 5)).toBe(0);
+ expect(valueQuantile([5], 5)).toBe(0);
+ });
+
+ it("maps the ends to 0 and 1", () => {
+ expect(valueQuantile([1, 2, 100], 1)).toBe(0);
+ expect(valueQuantile([1, 2, 100], 100)).toBe(1);
+ });
+
+ it("clamps values beyond the ends", () => {
+ expect(valueQuantile([1, 2, 100], -5)).toBe(0);
+ expect(valueQuantile([1, 2, 100], 1000)).toBe(1);
+ });
+
+ it("inverts quantileValue", () => {
+ const sorted = [1, 4, 9, 16, 25];
+ for (const f of [0.25, 0.5, 0.75]) {
+ const value = quantileValue(sorted, f);
+ expect(valueQuantile(sorted, value)).toBeCloseTo(f, 1);
+ }
+ });
+
+ it("interpolates strictly inside the range", () => {
+ expect(valueQuantile([0, 10], 5)).toBeCloseTo(0.5);
+ });
+});
+
+/** jsdom has no layout, so the rect is supplied rather than measured. */
+const rect = (over: Partial = {}): DOMRect =>
+ ({ left: 100, width: 200, ...over }) as DOMRect;
+
+describe("fractionFromPointer", () => {
+ it("maps the pointer to a fraction of the track", () => {
+ expect(fractionFromPointer(rect(), 200)).toBe(0.5);
+ });
+
+ it("clamps outside the track", () => {
+ expect(fractionFromPointer(rect(), 0)).toBe(0);
+ expect(fractionFromPointer(rect(), 9999)).toBe(1);
+ });
+
+ it("returns 0 for a zero-width track rather than dividing by zero", () => {
+ // jsdom reports zero-sized rects for everything, so this is the common
+ // case in tests rather than an exotic one.
+ expect(fractionFromPointer(rect({ width: 0 }), 150)).toBe(0);
+ });
+});
diff --git a/web_ui/src/quantile.ts b/web_ui/src/quantile.ts
new file mode 100644
index 0000000..72882ed
--- /dev/null
+++ b/web_ui/src/quantile.ts
@@ -0,0 +1,79 @@
+/**
+ * Distribution-aware slider maths.
+ *
+ * A linear min..max track is useless on skewed data, which is the normal case
+ * in both consumers: dufs-cloud's file sizes span bytes to gigabytes, and
+ * awesome-mcp-explorer's star counts run 0..166,785 with a median of 8, so a
+ * linear midpoint sits above 99.9% of all rows. Mapping the track through the
+ * actual distribution puts the middle of the track at the median, which makes
+ * equal track distance mean roughly equal numbers of items.
+ *
+ * This was duplicated in dufs-cloud and awesome-mcp-explorer before it moved
+ * here; both now import it.
+ */
+
+/**
+ * Bounds-checked array access (callers guarantee the index is valid). Keeps the
+ * quantile helpers free of scattered non-null assertions or dead undefined
+ * guards while still satisfying `noUncheckedIndexedAccess`.
+ */
+export function nth(values: readonly number[], index: number): number {
+ const value = values[index];
+ if (value === undefined) {
+ throw new RangeError(`index ${index} out of range`);
+ }
+ return value;
+}
+
+/** Clamp to the unit interval. */
+export function clamp01(x: number): number {
+ return Math.min(1, Math.max(0, x));
+}
+
+/**
+ * The value at quantile `f` (0..1) of an ascending, non-empty array, with linear
+ * interpolation between the two straddling samples. This is what makes the
+ * sliders distribution-aware: f = 0.5 returns the *median*, so the middle of the
+ * track sits where half the items are — not the arithmetic midpoint of the
+ * range. Empty input yields 0 (callers hide the slider in that case).
+ */
+export function quantileValue(sorted: readonly number[], f: number): number {
+ const n = sorted.length;
+ if (n === 0) return 0;
+ const pos = clamp01(f) * (n - 1);
+ const lo = Math.floor(pos);
+ const hi = Math.ceil(pos);
+ const a = nth(sorted, lo);
+ const b = nth(sorted, hi);
+ return Math.round(a + (b - a) * (pos - lo));
+}
+
+/**
+ * The quantile (0..1) of `value` within `sorted` — the inverse of
+ * {@link quantileValue}, used to place a thumb at the right spot on the track.
+ * Values at or beyond the ends map to 0/1.
+ */
+export function valueQuantile(sorted: readonly number[], value: number): number {
+ const n = sorted.length;
+ if (n <= 1) return 0;
+ if (value <= nth(sorted, 0)) return 0;
+ if (value >= nth(sorted, n - 1)) return 1;
+ // value strictly inside the range, so there is an index hi >= 1 with
+ // sorted[hi] >= value and sorted[hi - 1] < value.
+ const hi = sorted.findIndex((v) => v >= value);
+ const lo = hi - 1;
+ const a = nth(sorted, lo);
+ const b = nth(sorted, hi);
+ // a < value <= b here, so b - a > 0 (no divide-by-zero).
+ return (lo + (value - a) / (b - a)) / (n - 1);
+}
+
+/**
+ * Pointer x to a 0..1 fraction of a track.
+ *
+ * Extracted as pure geometry so slider behaviour is testable without layout,
+ * which jsdom does not implement.
+ */
+export function fractionFromPointer(rect: DOMRect, clientX: number): number {
+ return rect.width === 0 ? 0 : clamp01((clientX - rect.left) / rect.width);
+}
diff --git a/web_ui/src/range-slider.css b/web_ui/src/range-slider.css
new file mode 100644
index 0000000..0cb3d6e
--- /dev/null
+++ b/web_ui/src/range-slider.css
@@ -0,0 +1,68 @@
+/*
+ * Styling for the shared RangeSlider. Imported separately from tokens.css:
+ * import "@kuhyx/web-ui/tokens.css";
+ * import "@kuhyx/web-ui/range-slider.css";
+ *
+ * Every value routes through a token — no literals.
+ */
+
+.slider-head {
+ display: flex;
+ justify-content: space-between;
+ color: var(--text-muted);
+ font-size: var(--text-caption);
+ letter-spacing: var(--tracking-caption);
+}
+
+.slider-value {
+ /* Tabular figures so a dragging readout does not reflow the row. */
+ font-variant-numeric: tabular-nums;
+}
+
+.slider-track {
+ position: relative;
+ height: 22px;
+ margin-top: var(--space-1);
+ /* Let the element own the gesture instead of the page scrolling under it. */
+ touch-action: none;
+}
+
+.slider-track::before {
+ content: "";
+ position: absolute;
+ top: 9px;
+ right: 0;
+ left: 0;
+ height: 4px;
+ border-radius: 2px;
+ background: var(--border);
+}
+
+.slider-fill {
+ position: absolute;
+ top: 9px;
+ height: 4px;
+ border-radius: 2px;
+ background: var(--accent);
+}
+
+.slider-thumb {
+ position: absolute;
+ top: 4px;
+ width: 14px;
+ height: 14px;
+ margin-left: -7px;
+ padding: 0;
+ border: 2px solid var(--accent);
+ border-radius: 50%;
+ background: var(--bg);
+ cursor: pointer;
+}
+
+/* The thumbs are the keyboard target, so the ring must be unmistakable. The
+ * global :focus-visible rule in tokens.css already colours it; the wider offset
+ * here keeps it clear of the 14px circle. */
+.slider-thumb:focus-visible {
+ outline: 2px solid var(--focus-ring);
+ outline-offset: 2px;
+}
diff --git a/web_ui/src/range-slider.test.tsx b/web_ui/src/range-slider.test.tsx
new file mode 100644
index 0000000..4cc75b5
--- /dev/null
+++ b/web_ui/src/range-slider.test.tsx
@@ -0,0 +1,209 @@
+import { fireEvent, render, screen } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+
+import { RangeSlider, steppedValue } from "./range-slider.tsx";
+
+/** Ten evenly spaced values, so a quantile fraction maps to a predictable value. */
+const VALUES = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90];
+
+/** The track width the jsdom rect stub reports (see src/test/setup.ts). */
+const TRACK_WIDTH = 1000;
+
+function renderSlider(
+ properties: Partial> = {},
+): { onChange: ReturnType } {
+ const onChange = vi.fn();
+ render(
+ ,
+ );
+ return { onChange };
+}
+
+function thumbs(): HTMLElement[] {
+ return screen.getAllByRole("slider");
+}
+
+describe("steppedValue", () => {
+ it("advances one sample at a time", () => {
+ expect(steppedValue(VALUES, 30, 1)).toBe(40);
+ });
+
+ it("moves backwards for a negative delta", () => {
+ expect(steppedValue(VALUES, 30, -1)).toBe(20);
+ });
+
+ it("clamps at the top", () => {
+ expect(steppedValue(VALUES, 90, 5)).toBe(90);
+ });
+
+ it("clamps at the bottom", () => {
+ expect(steppedValue(VALUES, 0, -5)).toBe(0);
+ });
+
+ it("rounds a between-samples value to a real sample", () => {
+ expect(steppedValue(VALUES, 34, 1)).toBe(40);
+ });
+});
+
+describe("RangeSlider", () => {
+ it("renders nothing when there is nothing to range over", () => {
+ const { container } = render( );
+ expect(container).toBeEmptyDOMElement();
+ });
+
+ it("exposes both thumbs as sliders with the value trio", () => {
+ renderSlider();
+ const [low, high] = thumbs();
+ expect(low).toHaveAttribute("aria-valuemin", "0");
+ expect(low).toHaveAttribute("aria-valuemax", "90");
+ expect(low).toHaveAttribute("aria-valuenow", "0");
+ expect(high).toHaveAttribute("aria-valuenow", "90");
+ });
+
+ it("names the thumbs from the label", () => {
+ renderSlider({ label: "Stars" });
+ expect(screen.getByLabelText("Stars minimum")).toBeInTheDocument();
+ expect(screen.getByLabelText("Stars maximum")).toBeInTheDocument();
+ });
+
+ it("falls back to a generic name without a label", () => {
+ renderSlider();
+ expect(screen.getByLabelText("Range minimum")).toBeInTheDocument();
+ });
+
+ it("draws the head only when both label and format are given", () => {
+ renderSlider({ format: (v): string => `${String(v)}*`, label: "Stars" });
+ expect(screen.getByText("Stars")).toBeInTheDocument();
+ expect(screen.getByText("0* – 90*")).toBeInTheDocument();
+ });
+
+ it("omits the head when the caller draws its own chrome", () => {
+ renderSlider({ label: "Stars" });
+ expect(screen.queryByText("Stars")).not.toBeInTheDocument();
+ });
+
+ it("is focusable, so the control is a keyboard stop", () => {
+ renderSlider();
+ const [low] = thumbs();
+ low?.focus();
+ expect(low).toHaveFocus();
+ });
+
+ describe("keyboard", () => {
+ it.each([
+ ["ArrowRight", 10],
+ ["ArrowUp", 10],
+ ])("%s steps the low thumb up", (key, expected) => {
+ const { onChange } = renderSlider();
+ fireEvent.keyDown(thumbs()[0] as HTMLElement, { key });
+ expect(onChange).toHaveBeenCalledWith(expected, 90);
+ });
+
+ it.each([
+ ["ArrowLeft", 80],
+ ["ArrowDown", 80],
+ ])("%s steps the high thumb down", (key, expected) => {
+ const { onChange } = renderSlider();
+ fireEvent.keyDown(thumbs()[1] as HTMLElement, { key });
+ expect(onChange).toHaveBeenCalledWith(0, expected);
+ });
+
+ it("PageUp jumps ten samples", () => {
+ const { onChange } = renderSlider({ lo: 0 });
+ fireEvent.keyDown(thumbs()[0] as HTMLElement, { key: "PageUp" });
+ expect(onChange).toHaveBeenCalledWith(90, 90);
+ });
+
+ it("PageDown jumps ten samples down", () => {
+ const { onChange } = renderSlider({ hi: 90 });
+ fireEvent.keyDown(thumbs()[1] as HTMLElement, { key: "PageDown" });
+ expect(onChange).toHaveBeenCalledWith(0, 0);
+ });
+
+ it("Home sends a thumb to the floor", () => {
+ const { onChange } = renderSlider({ lo: 50 });
+ fireEvent.keyDown(thumbs()[0] as HTMLElement, { key: "Home" });
+ expect(onChange).toHaveBeenCalledWith(0, 90);
+ });
+
+ it("End sends a thumb to the ceiling", () => {
+ const { onChange } = renderSlider({ hi: 50 });
+ fireEvent.keyDown(thumbs()[1] as HTMLElement, { key: "End" });
+ expect(onChange).toHaveBeenCalledWith(0, 90);
+ });
+
+ it("ignores keys it does not handle", () => {
+ const { onChange } = renderSlider();
+ fireEvent.keyDown(thumbs()[0] as HTMLElement, { key: "a" });
+ expect(onChange).not.toHaveBeenCalled();
+ });
+
+ it("does not let the low thumb cross the high one", () => {
+ const { onChange } = renderSlider({ hi: 10, lo: 10 });
+ fireEvent.keyDown(thumbs()[0] as HTMLElement, { key: "ArrowRight" });
+ expect(onChange).toHaveBeenCalledWith(10, 10);
+ });
+
+ it("does not let the high thumb cross the low one", () => {
+ const { onChange } = renderSlider({ hi: 10, lo: 10 });
+ fireEvent.keyDown(thumbs()[1] as HTMLElement, { key: "ArrowLeft" });
+ expect(onChange).toHaveBeenCalledWith(10, 10);
+ });
+ });
+
+ describe("pointer", () => {
+ function track(): HTMLElement {
+ // eslint-disable-next-line testing-library/no-node-access -- the track is
+ // presentational; there is no role or text to query it by.
+ return document.querySelector(".slider-track") as HTMLElement;
+ }
+
+ it("grabs the nearer thumb when pressed anywhere on the track", () => {
+ const { onChange } = renderSlider();
+ // 10% along: nearer the low thumb, which sits at 0. The track is
+ // quantile-mapped over n-1 intervals, so 0.1 lands on 9, not 10.
+ fireEvent.pointerDown(track(), { clientX: TRACK_WIDTH * 0.1, pointerId: 1 });
+ expect(onChange).toHaveBeenCalledWith(9, 90);
+ });
+
+ it("grabs the high thumb when the press is nearer to it", () => {
+ const { onChange } = renderSlider();
+ fireEvent.pointerDown(track(), { clientX: TRACK_WIDTH * 0.9, pointerId: 1 });
+ expect(onChange).toHaveBeenCalledWith(0, 81);
+ });
+
+ it("follows the pointer while dragging", () => {
+ const { onChange } = renderSlider();
+ fireEvent.pointerDown(track(), { clientX: 0, pointerId: 1 });
+ fireEvent.pointerMove(track(), { clientX: TRACK_WIDTH * 0.3, pointerId: 1 });
+ expect(onChange).toHaveBeenLastCalledWith(27, 90);
+ });
+
+ it("ignores movement when no thumb is held", () => {
+ const { onChange } = renderSlider();
+ fireEvent.pointerMove(track(), { clientX: TRACK_WIDTH * 0.3, pointerId: 1 });
+ expect(onChange).not.toHaveBeenCalled();
+ });
+
+ it("stops dragging on pointer up", () => {
+ const { onChange } = renderSlider();
+ fireEvent.pointerDown(track(), { clientX: 0, pointerId: 1 });
+ fireEvent.pointerUp(track(), { pointerId: 1 });
+ onChange.mockClear();
+ fireEvent.pointerMove(track(), { clientX: TRACK_WIDTH * 0.5, pointerId: 1 });
+ expect(onChange).not.toHaveBeenCalled();
+ });
+
+ it("ignores a pointer up that was never preceded by a press", () => {
+ const { onChange } = renderSlider();
+ fireEvent.pointerUp(track(), { pointerId: 1 });
+ expect(onChange).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/web_ui/src/range-slider.tsx b/web_ui/src/range-slider.tsx
new file mode 100644
index 0000000..ce8604d
--- /dev/null
+++ b/web_ui/src/range-slider.tsx
@@ -0,0 +1,173 @@
+/**
+ * A dual-thumb slider whose track maps through a value *distribution* rather
+ * than a linear min..max ramp (see `./quantile.ts` for why).
+ *
+ * Reconciled from the two drifted copies in dufs-cloud and
+ * awesome-mcp-explorer. It takes the accessible DOM from awesome-mcp-explorer
+ * (real `` thumbs carrying `role="slider"` and the `aria-value*` trio,
+ * so the control is a focus stop) and the grab-the-nearer-thumb pointer
+ * behaviour from dufs-cloud (pressing anywhere on the track moves the closer
+ * handle, instead of requiring a direct hit on an
+ * 8px target).
+ *
+ * Keyboard support is new in the shared version. Both donors were pointer-only,
+ * which the design system rules out outright: every action must be reachable
+ * with the keyboard alone, and a `role="slider"` that does not respond to arrow
+ * keys is an ARIA role that lies. Arrow keys step by one item through the
+ * distribution, Home/End jump to the bounds.
+ *
+ * The component deals only in raw values; callers translate the extremes into
+ * "no filter".
+ */
+import { useCallback, useState } from "react";
+
+import { fractionFromPointer, nth, quantileValue, valueQuantile } from "./quantile.ts";
+
+export interface RangeSliderProps {
+ /**
+ * Formats a value for the head readout. Omit it (with `label`) to render a
+ * bare track — dufs-cloud's wrappers draw their own labels and value notes,
+ * so a built-in head would duplicate them.
+ */
+ readonly format?: (value: number) => string;
+ /** Current upper bound; equal to the distribution max when unconstrained. */
+ readonly hi: number;
+ /** Accessible name. Falls back to a generic one when omitted. */
+ readonly label?: string;
+ /** Current lower bound; equal to the distribution min when unconstrained. */
+ readonly lo: number;
+ /** Reports both thumb values (raw, clamped so lo <= hi) on every change. */
+ readonly onChange: (lo: number, hi: number) => void;
+ /** The full ascending distribution — not just its endpoints. */
+ readonly values: readonly number[];
+}
+
+type Thumb = "hi" | "lo";
+
+/** Steps one thumb by `delta` positions through the distribution. Pure, so the
+ * keyboard contract is testable without layout. */
+export function steppedValue(
+ values: readonly number[],
+ current: number,
+ delta: number,
+): number {
+ const last = values.length - 1;
+ // Round to a real sample index so a thumb sitting between two samples still
+ // advances by exactly one item rather than stalling on a fractional step.
+ const index = Math.round(valueQuantile(values, current) * last);
+ return nth(values, Math.min(last, Math.max(0, index + delta)));
+}
+
+export function RangeSlider({
+ format,
+ hi,
+ label,
+ lo,
+ onChange,
+ values,
+}: RangeSliderProps): null | React.JSX.Element {
+ const [drag, setDrag] = useState(null);
+
+ const apply = useCallback(
+ (thumb: Thumb, value: number): void => {
+ if (thumb === "lo") onChange(Math.min(value, hi), hi);
+ else onChange(lo, Math.max(value, lo));
+ },
+ [hi, lo, onChange],
+ );
+
+ // Reads the rect off the event's own target rather than a ref: the pointer
+ // handlers are bound to the track, so currentTarget *is* the track. A ref
+ // would add a null branch that cannot happen once the element is mounted.
+ const valueAt = useCallback(
+ (track: HTMLElement, clientX: number): number =>
+ quantileValue(values, fractionFromPointer(track.getBoundingClientRect(), clientX)),
+ [values],
+ );
+
+ const onKeyDown = useCallback(
+ (thumb: Thumb, event: React.KeyboardEvent): void => {
+ const current = thumb === "lo" ? lo : hi;
+ const step = (delta: number): void => {
+ event.preventDefault();
+ apply(thumb, steppedValue(values, current, delta));
+ };
+ if (event.key === "ArrowLeft" || event.key === "ArrowDown") step(-1);
+ else if (event.key === "ArrowRight" || event.key === "ArrowUp") step(1);
+ else if (event.key === "PageDown") step(-10);
+ else if (event.key === "PageUp") step(10);
+ else if (event.key === "Home") {
+ event.preventDefault();
+ apply(thumb, nth(values, 0));
+ } else if (event.key === "End") {
+ event.preventDefault();
+ apply(thumb, nth(values, values.length - 1));
+ }
+ },
+ [apply, hi, lo, values],
+ );
+
+ // Fewer than two values means there is nothing to range over.
+ if (values.length < 2) return null;
+
+ const lowest = nth(values, 0);
+ const highest = nth(values, values.length - 1);
+ const loFraction = valueQuantile(values, lo) * 100;
+ const hiFraction = valueQuantile(values, hi) * 100;
+ const name = label ?? "Range";
+
+ const thumbProps = (thumb: Thumb): React.ComponentProps<"button"> => ({
+ "aria-valuemax": highest,
+ "aria-valuemin": lowest,
+ "aria-valuenow": thumb === "lo" ? lo : hi,
+ className: `slider-thumb slider-${thumb}`,
+ onKeyDown: (event): void => {
+ onKeyDown(thumb, event);
+ },
+ role: "slider",
+ style: { left: `${String(thumb === "lo" ? loFraction : hiFraction)}%` },
+ tabIndex: 0,
+ type: "button",
+ });
+
+ return (
+
+ {label !== undefined && format !== undefined && (
+
+ {label}
+
+ {format(lo)} – {format(hi)}
+
+
+ )}
+
{
+ const value = valueAt(event.currentTarget, event.clientX);
+ // Grab whichever thumb is nearer, so a press anywhere on the track
+ // does something sensible instead of requiring a direct hit.
+ const thumb: Thumb = Math.abs(value - lo) <= Math.abs(value - hi) ? "lo" : "hi";
+ setDrag(thumb);
+ event.currentTarget.setPointerCapture(event.pointerId);
+ apply(thumb, value);
+ }}
+ onPointerMove={(event): void => {
+ if (drag === null) return;
+ apply(drag, valueAt(event.currentTarget, event.clientX));
+ }}
+ onPointerUp={(event): void => {
+ if (drag === null) return;
+ setDrag(null);
+ event.currentTarget.releasePointerCapture(event.pointerId);
+ }}
+ >
+
+
+
+
+
+ );
+}
diff --git a/web_ui/src/test/setup.ts b/web_ui/src/test/setup.ts
new file mode 100644
index 0000000..57cf4a9
--- /dev/null
+++ b/web_ui/src/test/setup.ts
@@ -0,0 +1,38 @@
+import "@testing-library/jest-dom/vitest";
+
+// jsdom implements no layout engine (every rect is zero-sized) and no pointer
+// capture. The slider needs a non-zero track width for fractionFromPointer to
+// mean anything, and needs the capture calls to not throw.
+const TRACK_WIDTH = 1000;
+
+Object.defineProperty(Element.prototype, "getBoundingClientRect", {
+ configurable: true,
+ value(): DOMRect {
+ return {
+ bottom: 20,
+ height: 20,
+ left: 0,
+ right: TRACK_WIDTH,
+ toJSON: () => ({}),
+ top: 0,
+ width: TRACK_WIDTH,
+ x: 0,
+ y: 0,
+ };
+ },
+});
+
+Object.defineProperties(Element.prototype, {
+ releasePointerCapture: {
+ configurable: true,
+ value: (): void => {
+ // No pointer capture in jsdom; the slider only needs the call to not throw.
+ },
+ },
+ setPointerCapture: {
+ configurable: true,
+ value: (): void => {
+ // As above.
+ },
+ },
+});
diff --git a/web_ui/src/text.test.ts b/web_ui/src/text.test.ts
new file mode 100644
index 0000000..cb3d033
--- /dev/null
+++ b/web_ui/src/text.test.ts
@@ -0,0 +1,33 @@
+import { describe, expect, it } from "vitest";
+
+import { fuzzyMatch } from "./text.ts";
+
+describe("fuzzyMatch", () => {
+ it("matches everything on an empty query", () => {
+ expect(fuzzyMatch("", "anything")).toBe(true);
+ });
+
+ it("matches an exact substring", () => {
+ expect(fuzzyMatch("cat", "concatenate")).toBe(true);
+ });
+
+ it("matches a non-adjacent subsequence", () => {
+ expect(fuzzyMatch("cne", "concatenate")).toBe(true);
+ });
+
+ it("ignores case on both sides", () => {
+ expect(fuzzyMatch("CaT", "concatenate")).toBe(true);
+ });
+
+ it("rejects characters that appear out of order", () => {
+ expect(fuzzyMatch("tac", "cat")).toBe(false);
+ });
+
+ it("rejects a query with a character the target lacks", () => {
+ expect(fuzzyMatch("dog", "concatenate")).toBe(false);
+ });
+
+ it("rejects a query longer than the target", () => {
+ expect(fuzzyMatch("catalogue", "cat")).toBe(false);
+ });
+});
diff --git a/web_ui/src/text.ts b/web_ui/src/text.ts
new file mode 100644
index 0000000..9324ca5
--- /dev/null
+++ b/web_ui/src/text.ts
@@ -0,0 +1,25 @@
+/**
+ * Small text helpers shared by the consumers' filter layers.
+ *
+ * Hand-rolled rather than pulling in Fuse.js, matching the house posture of no
+ * runtime dependencies for things this small.
+ */
+
+/**
+ * Case-insensitive subsequence match: every character of `query` appears in
+ * `target`, in order, though not necessarily adjacently. An empty query matches
+ * everything, so callers can pass the raw search box value straight through.
+ *
+ * dufs-cloud called this `fuzzyMatch` and awesome-mcp-explorer `matchesFuzzy`;
+ * the implementations were identical apart from identifier names.
+ */
+export function fuzzyMatch(query: string, target: string): boolean {
+ if (query === "") return true;
+ const q = query.toLowerCase();
+ const t = target.toLowerCase();
+ let index = 0;
+ for (let scan = 0; scan < t.length && index < q.length; scan++) {
+ if (t[scan] === q[index]) index++;
+ }
+ return index === q.length;
+}
diff --git a/web_ui/src/tokens.css b/web_ui/src/tokens.css
new file mode 100644
index 0000000..f0bcbb4
--- /dev/null
+++ b/web_ui/src/tokens.css
@@ -0,0 +1,112 @@
+/*
+ * The frozen design tokens, shared by every one of kuhy's web repos.
+ *
+ * Source of truth: ~/utils/unified-design-system/tokens.md (values) and
+ * README.md (the CSS custom-property names). The same numbers ship for Dart in
+ * ~/utils/design_system, so a change here is a change there — do not edit one
+ * side alone, and do not introduce a value that is not on a scale below.
+ *
+ * Dark is the default; light is an override, matching the README.
+ */
+
+:root {
+ /* Neutrals ------------------------------------------------------------- */
+ --bg: #211d1b;
+ --surface-1: #2b2624;
+ --surface-2: #38312e;
+ --border: #463e3a;
+ --text: #eceae9;
+ --text-muted: #aaa09a;
+
+ /* Accent + semantic roles ---------------------------------------------- */
+ --accent: #b8862e;
+ --info: #b8862e; /* = accent */
+ --success: #8a9a3c;
+ --warning: #e0a63c;
+ --danger: #e2585f;
+
+ /* Text/icons ON a filled accent/success/warning/danger surface. Never
+ * --text: every semantic fill sits in the same mid-light band, where
+ * near-white measures 2.2:1-3.6:1 against a 4.5:1 floor. */
+ --on-fill: #211d1b;
+ /* Text/icons on a dark translucent scrim/overlay — fixed, not theme-tied,
+ * because the scrim itself is always dark. */
+ --on-scrim: #eceae9;
+
+ /* Corner radius --------------------------------------------------------- */
+ --radius-sm: 8px; /* buttons, inputs, chips */
+ --radius-md: 12px; /* cards */
+ --radius-lg: 16px; /* sheets, dialogs */
+
+ /* Spacing (4px base) ---------------------------------------------------- */
+ --space-1: 4px;
+ --space-2: 8px;
+ --space-3: 16px;
+ --space-4: 24px;
+ --space-5: 32px;
+ --space-6: 48px;
+
+ /* Typography ------------------------------------------------------------ */
+ --font-sans: -apple-system, "Segoe UI", Roboto, system-ui, sans-serif;
+ --font-mono: ui-monospace, "Cascadia Code", monospace;
+
+ --text-display: 32px;
+ --text-title: 24px;
+ --text-subtitle: 20px;
+ --text-body: 16px; /* the floor for anything a user actually reads */
+ --text-label: 14px; /* UI chrome only */
+ --text-caption: 12px; /* UI chrome only */
+
+ --tracking-display: -0.2px;
+ --tracking-title: -0.1px;
+ --tracking-label: 0.05em;
+ --tracking-caption: 0.1em;
+
+ /* Prose line-length cap (rule 21): ~65-70 characters. */
+ --measure: 40rem;
+
+ /* Shadows: light surfaces only, and only on floating/overlay elements —
+ * never inline cards or list rows. Blur = 2x offset, tinted from ink. */
+ --shadow-1: 0 2px 4px rgba(33, 29, 27, 0.12); /* dropdown */
+ --shadow-2: 0 4px 8px rgba(33, 29, 27, 0.16); /* modal */
+
+ /* Focus ring: the platform default is black, invisible on --bg. */
+ --focus-ring: #b8862e; /* = accent */
+}
+
+@media (prefers-color-scheme: light) {
+ :root {
+ --bg: #f6f4f3;
+ --surface-1: #fcfbfb;
+ --surface-2: #fcfbfb;
+ --border: #e0dad7;
+ --text: #211d1b;
+ --text-muted: #70625b;
+ /* --on-fill / --on-scrim stay fixed — the fills and the scrim do not
+ * change with page theme. */
+ }
+}
+
+/* Dark surfaces take no shadow at all; elevation is a fill step. Light
+ * surfaces opt in explicitly via --shadow-1 / --shadow-2. */
+@media (prefers-color-scheme: dark) {
+ :root {
+ --shadow-1: none;
+ --shadow-2: none;
+ }
+}
+
+body {
+ background: var(--bg);
+ color: var(--text);
+ font-family: var(--font-sans);
+ font-size: var(--text-body);
+}
+
+/* Ships here rather than being left to each consumer: an unstyled focus ring
+ * is the accessibility regression this layer exists to prevent, and several of
+ * these apps are keyboard-only by necessity. */
+:focus-visible {
+ outline: 2px solid var(--focus-ring);
+ outline-offset: 1px;
+}
diff --git a/web_ui/tsconfig.json b/web_ui/tsconfig.json
new file mode 100644
index 0000000..0ed8b47
--- /dev/null
+++ b/web_ui/tsconfig.json
@@ -0,0 +1,41 @@
+{
+ "compilerOptions": {
+ "target": "ES2023",
+ "lib": ["ESNext", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "jsx": "react-jsx",
+ "types": ["vitest/globals"],
+
+ /* Strictness: everything on, matching both consumers so a file that
+ compiles here compiles there. */
+ "strict": true,
+ "noUncheckedIndexedAccess": true,
+ "noImplicitOverride": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "exactOptionalPropertyTypes": true,
+ "useUnknownInCatchVariables": true,
+ "verbatimModuleSyntax": true,
+ "isolatedModules": true,
+ "erasableSyntaxOnly": true,
+
+ /* This package ships a prebuilt dist/ (ESM + .d.ts) because consumers use
+ Vite, which does not transpile dependencies. So unlike the consumers,
+ emit is on and .ts import specifiers must be rewritten. */
+ "rewriteRelativeImportExtensions": true,
+ "allowImportingTsExtensions": true,
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true,
+ "outDir": "dist",
+ "rootDir": "src",
+
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true
+ },
+ "include": ["src"],
+ "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx", "src/test"]
+}
diff --git a/web_ui/vitest.config.ts b/web_ui/vitest.config.ts
new file mode 100644
index 0000000..93cfcb6
--- /dev/null
+++ b/web_ui/vitest.config.ts
@@ -0,0 +1,18 @@
+import { defineConfig } from "vitest/config";
+
+export default defineConfig({
+ test: {
+ coverage: {
+ exclude: ["src/index.ts", "src/**/*.d.ts"],
+ include: ["src/**/*.{ts,tsx}"],
+ provider: "v8",
+ // The same bar Phase 1's design_system holds: a shared layer that two
+ // repos depend on does not get to be the least-tested code in the tree.
+ thresholds: { branches: 100, functions: 100, lines: 100, statements: 100 },
+ },
+ environment: "jsdom",
+ globals: true,
+ include: ["src/**/*.test.{ts,tsx}"],
+ setupFiles: ["./src/test/setup.ts"],
+ },
+});