ts_core 0.1.1, web_ui 0.4.0: consume @kuhyx/ts-config

The two shared packages are the first repos on the preset (link:../ts_config
inside the monorepo; consumers outside use the git tag). Each eslint.config.js
is now the defineConfig() call plus one override with its reason: a public
API name (`nextChance`, `fuzzyMatch`) the boolean-prefix rule would rename,
which is a breaking change for the three repos importing it.

Everything else the preset flagged is fixed in code, not disabled:
key order (perfectionist), hex casing, sort compare functions in tests,
Iterator#map().toArray(), explicit returns on multiline arrows.

web_ui: react-refresh/only-export-components was a real finding --
range-slider.tsx exported `steppedValue` next to the component, which breaks
Vite HMR for the module. The keyboard contract moves to stepped-value.ts as
`steppedValue` + a new pure `keyTarget(key, values, current)`, which also
resolves unicorn/no-late-event-control: the handler now decides the target
first and calls preventDefault() only when it owns the key. Its tests move
with it (the 250-line gate caught the combined file). Minor bump for the new
export. dist/ rebuilt in both packages.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnAYyNH1Sm4iJNidfYbrZJ
This commit is contained in:
Krzysztof Rudnicki 2026-09-16 21:46:41 +02:00
parent 0f9699d4f3
commit c5b3fceeae
46 changed files with 535 additions and 492 deletions

View File

@ -1 +1 @@
{"version":3,"file":"clock.d.ts","sourceRoot":"","sources":["../src/clock.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,WAAW,KAAK;IACpB,4EAA4E;IAC5E,GAAG,EAAE,MAAM,MAAM,CAAA;CAClB;AAED,2EAA2E;AAC3E,eAAO,MAAM,eAAe,QAAO,KAEjC,CAAA;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,SAAS,EAAE,KAAyB,CAAA;AAEjD,sEAAsE;AACtE,MAAM,WAAW,WAAY,SAAQ,KAAK;IACxC,mEAAmE;IACnE,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;IAElC,2DAA2D;IAC3D,GAAG,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAA;CAC9B;AAED,uEAAuE;AACvE,eAAO,MAAM,iBAAiB,GAAI,gBAAW,KAAG,WAW/C,CAAA"} {"version":3,"file":"clock.d.ts","sourceRoot":"","sources":["../src/clock.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,WAAW,KAAK;IACpB,4EAA4E;IAC5E,GAAG,EAAE,MAAM,MAAM,CAAA;CAClB;AAED,2EAA2E;AAC3E,eAAO,MAAM,eAAe,QAAO,KAAmD,CAAA;AAEtF;;;;;;;GAOG;AACH,eAAO,MAAM,SAAS,EAAE,KAAyB,CAAA;AAEjD,sEAAsE;AACtE,MAAM,WAAW,WAAY,SAAQ,KAAK;IACxC,mEAAmE;IACnE,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;IAElC,2DAA2D;IAC3D,GAAG,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAA;CAC9B;AAED,uEAAuE;AACvE,eAAO,MAAM,iBAAiB,GAAI,gBAAW,KAAG,WAW/C,CAAA"}

View File

@ -1,7 +1,5 @@
/** A {@link Clock} backed by the browser's monotonic performance timer. */ /** A {@link Clock} backed by the browser's monotonic performance timer. */
export const createRealClock = () => ({ export const createRealClock = () => ({ now: () => performance.now() });
now: () => performance.now(),
});
/** /**
* The one real clock instance UI code should pass around. * The one real clock instance UI code should pass around.
* *
@ -15,10 +13,10 @@ export const realClock = createRealClock();
export const createManualClock = (startMs = 0) => { export const createManualClock = (startMs = 0) => {
let current = startMs; let current = startMs;
return { return {
now: () => current,
advance: (deltaMs) => { advance: (deltaMs) => {
current += deltaMs; current += deltaMs;
}, },
now: () => current,
set: (timeMs) => { set: (timeMs) => {
current = timeMs; current = timeMs;
}, },

View File

@ -1 +1 @@
{"version":3,"file":"clock.js","sourceRoot":"","sources":["../src/clock.ts"],"names":[],"mappings":"AAaA,2EAA2E;AAC3E,MAAM,CAAC,MAAM,eAAe,GAAG,GAAU,EAAE,CAAC,CAAC;IAC3C,GAAG,EAAE,GAAW,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE;CACrC,CAAC,CAAA;AAEF;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,SAAS,GAAU,eAAe,EAAE,CAAA;AAWjD,uEAAuE;AACvE,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,OAAO,GAAG,CAAC,EAAe,EAAE;IAC5D,IAAI,OAAO,GAAG,OAAO,CAAA;IACrB,OAAO;QACL,GAAG,EAAE,GAAW,EAAE,CAAC,OAAO;QAC1B,OAAO,EAAE,CAAC,OAAe,EAAQ,EAAE;YACjC,OAAO,IAAI,OAAO,CAAA;QACpB,CAAC;QACD,GAAG,EAAE,CAAC,MAAc,EAAQ,EAAE;YAC5B,OAAO,GAAG,MAAM,CAAA;QAClB,CAAC;KACF,CAAA;AACH,CAAC,CAAA"} {"version":3,"file":"clock.js","sourceRoot":"","sources":["../src/clock.ts"],"names":[],"mappings":"AAaA,2EAA2E;AAC3E,MAAM,CAAC,MAAM,eAAe,GAAG,GAAU,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,GAAW,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE,EAAE,CAAC,CAAA;AAEtF;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,SAAS,GAAU,eAAe,EAAE,CAAA;AAWjD,uEAAuE;AACvE,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,OAAO,GAAG,CAAC,EAAe,EAAE;IAC5D,IAAI,OAAO,GAAG,OAAO,CAAA;IACrB,OAAO;QACL,OAAO,EAAE,CAAC,OAAe,EAAQ,EAAE;YACjC,OAAO,IAAI,OAAO,CAAA;QACpB,CAAC;QACD,GAAG,EAAE,GAAW,EAAE,CAAC,OAAO;QAC1B,GAAG,EAAE,CAAC,MAAc,EAAQ,EAAE;YAC5B,OAAO,GAAG,MAAM,CAAA;QAClB,CAAC;KACF,CAAA;AACH,CAAC,CAAA"}

View File

@ -6,6 +6,6 @@
* -- that is the coupling that makes people vendor their own copy instead, * -- that is the coupling that makes people vendor their own copy instead,
* which is how these ended up duplicated four ways to begin with. * which is how these ended up duplicated four ways to begin with.
*/ */
export { createManualClock, createRealClock, realClock, type Clock, type ManualClock, } from './clock.ts'; export { type Clock, createManualClock, createRealClock, type ManualClock, realClock, } from './clock.ts';
export { createRng, createSeededRng, nextChance, nextFloat, nextInt, pick, type Rng, type SeededRng, } from './rng.ts'; export { createRng, createSeededRng, nextChance, nextFloat, nextInt, pick, type Rng, type SeededRng, } from './rng.ts';
//# sourceMappingURL=index.d.ts.map //# sourceMappingURL=index.d.ts.map

View File

@ -1 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,SAAS,EACT,KAAK,KAAK,EACV,KAAK,WAAW,GACjB,MAAM,YAAY,CAAA;AACnB,OAAO,EACL,SAAS,EACT,eAAe,EACf,UAAU,EACV,SAAS,EACT,OAAO,EACP,IAAI,EACJ,KAAK,GAAG,EACR,KAAK,SAAS,GACf,MAAM,UAAU,CAAA"} {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EACL,KAAK,KAAK,EACV,iBAAiB,EACjB,eAAe,EACf,KAAK,WAAW,EAChB,SAAS,GACV,MAAM,YAAY,CAAA;AACnB,OAAO,EACL,SAAS,EACT,eAAe,EACf,UAAU,EACV,SAAS,EACT,OAAO,EACP,IAAI,EACJ,KAAK,GAAG,EACR,KAAK,SAAS,GACf,MAAM,UAAU,CAAA"}

View File

@ -1 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,SAAS,GAGV,MAAM,YAAY,CAAA;AACnB,OAAO,EACL,SAAS,EACT,eAAe,EACf,UAAU,EACV,SAAS,EACT,OAAO,EACP,IAAI,GAGL,MAAM,UAAU,CAAA"} {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAEL,iBAAiB,EACjB,eAAe,EAEf,SAAS,GACV,MAAM,YAAY,CAAA;AACnB,OAAO,EACL,SAAS,EACT,eAAe,EACf,UAAU,EACV,SAAS,EACT,OAAO,EACP,IAAI,GAGL,MAAM,UAAU,CAAA"}

View File

@ -54,14 +54,14 @@ export declare const pick: <T>(rng: Rng, items: readonly [T, ...T[]]) => T;
* around rather than threading mutable state through free functions. * around rather than threading mutable state through free functions.
*/ */
export interface SeededRng { export interface SeededRng {
/** True with the given probability in [0, 1]. */
chance: (probability: number) => boolean;
/** Uniform float in [0, 1). */ /** Uniform float in [0, 1). */
float: () => number; float: () => number;
/** Uniform integer in [minInclusive, maxInclusive]. */ /** Uniform integer in [minInclusive, maxInclusive]. */
int: (minInclusive: number, maxInclusive: number) => number; int: (minInclusive: number, maxInclusive: number) => number;
/** Uniform element of a non-empty array. */ /** Uniform element of a non-empty array. */
pick: <T>(items: readonly T[]) => T; pick: <T>(items: readonly T[]) => T;
/** True with the given probability in [0, 1]. */
chance: (probability: number) => boolean;
} }
/** /**
* Creates a {@link SeededRng} over the same mulberry32 core. * Creates a {@link SeededRng} over the same mulberry32 core.

View File

@ -1 +1 @@
{"version":3,"file":"rng.d.ts","sourceRoot":"","sources":["../src/rng.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,gDAAgD;AAChD,MAAM,WAAW,GAAG;IAClB,qDAAqD;IACrD,CAAC,EAAE,MAAM,CAAA;CACV;AAED,mDAAmD;AACnD,eAAO,MAAM,SAAS,GAAI,MAAM,MAAM,KAAG,GAA0B,CAAA;AAEnE;;;;;GAKG;AACH,eAAO,MAAM,SAAS,GAAI,KAAK,GAAG,KAAG,MAMpC,CAAA;AAED,uEAAuE;AACvE,eAAO,MAAM,OAAO,GAClB,KAAK,GAAG,EACR,cAAc,MAAM,EACpB,cAAc,MAAM,KACnB,MAC4E,CAAA;AAE/E,2DAA2D;AAC3D,eAAO,MAAM,UAAU,GAAI,KAAK,GAAG,EAAE,aAAa,MAAM,KAAG,OAC7B,CAAA;AAE9B;;;;;;GAMG;AACH,eAAO,MAAM,IAAI,GAAI,CAAC,EAAE,KAAK,GAAG,EAAE,OAAO,SAAS,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,KAAG,CAW/D,CAAA;AAED;;;GAGG;AACH,MAAM,WAAW,SAAS;IACxB,+BAA+B;IAC/B,KAAK,EAAE,MAAM,MAAM,CAAA;IACnB,uDAAuD;IACvD,GAAG,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,MAAM,CAAA;IAC3D,4CAA4C;IAC5C,IAAI,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,SAAS,CAAC,EAAE,KAAK,CAAC,CAAA;IACnC,iDAAiD;IACjD,MAAM,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAA;CACzC;AAED;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,GAAI,MAAM,MAAM,KAAG,SAgB9C,CAAA"} {"version":3,"file":"rng.d.ts","sourceRoot":"","sources":["../src/rng.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,gDAAgD;AAChD,MAAM,WAAW,GAAG;IAClB,qDAAqD;IACrD,CAAC,EAAE,MAAM,CAAA;CACV;AAED,mDAAmD;AACnD,eAAO,MAAM,SAAS,GAAI,MAAM,MAAM,KAAG,GAA0B,CAAA;AAEnE;;;;;GAKG;AACH,eAAO,MAAM,SAAS,GAAI,KAAK,GAAG,KAAG,MAMpC,CAAA;AAED,uEAAuE;AACvE,eAAO,MAAM,OAAO,GAClB,KAAK,GAAG,EACR,cAAc,MAAM,EACpB,cAAc,MAAM,KACnB,MAC4E,CAAA;AAE/E,2DAA2D;AAC3D,eAAO,MAAM,UAAU,GAAI,KAAK,GAAG,EAAE,aAAa,MAAM,KAAG,OAC7B,CAAA;AAE9B;;;;;;GAMG;AACH,eAAO,MAAM,IAAI,GAAI,CAAC,EAAE,KAAK,GAAG,EAAE,OAAO,SAAS,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,KAAG,CAW/D,CAAA;AAED;;;GAGG;AACH,MAAM,WAAW,SAAS;IACxB,iDAAiD;IACjD,MAAM,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAA;IACxC,+BAA+B;IAC/B,KAAK,EAAE,MAAM,MAAM,CAAA;IACnB,uDAAuD;IACvD,GAAG,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,MAAM,CAAA;IAC3D,4CAA4C;IAC5C,IAAI,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,SAAS,CAAC,EAAE,KAAK,CAAC,CAAA;CACpC;AAED;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,GAAI,MAAM,MAAM,KAAG,SAgB9C,CAAA"}

4
ts_core/dist/rng.js vendored
View File

@ -32,7 +32,7 @@ export const createRng = (seed) => ({ s: seed >>> 0 });
* of it, so they all consume exactly one step per call. * of it, so they all consume exactly one step per call.
*/ */
export const nextFloat = (rng) => { export const nextFloat = (rng) => {
rng.s = (rng.s + 0x6d_2b_79_f5) >>> 0; rng.s = (rng.s + 0x6D_2B_79_F5) >>> 0;
let t = rng.s; let t = rng.s;
t = Math.imul(t ^ (t >>> 15), t | 1); t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61); t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
@ -72,6 +72,7 @@ export const createSeededRng = (seed) => {
const state = createRng(seed); const state = createRng(seed);
const float = () => nextFloat(state); const float = () => nextFloat(state);
return { return {
chance: (probability) => float() < probability,
float, float,
int: (minInclusive, maxInclusive) => nextInt(state, minInclusive, maxInclusive), int: (minInclusive, maxInclusive) => nextInt(state, minInclusive, maxInclusive),
pick: (items) => { pick: (items) => {
@ -81,7 +82,6 @@ export const createSeededRng = (seed) => {
} }
return chosen; return chosen;
}, },
chance: (probability) => float() < probability,
}; };
}; };
//# sourceMappingURL=rng.js.map //# sourceMappingURL=rng.js.map

View File

@ -1 +1 @@
{"version":3,"file":"rng.js","sourceRoot":"","sources":["../src/rng.ts"],"names":[],"mappings":"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"} {"version":3,"file":"rng.js","sourceRoot":"","sources":["../src/rng.ts"],"names":[],"mappings":"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"}

View File

@ -1,28 +1,24 @@
// Type-aware flat config, mirroring web_ui's so the two shared TS packages // The shared preset, nothing else local except one frozen API name. This
// hold an identical bar. // package is one of the repos the bar is defined for. Framework-free, so no
import js from "@eslint/js"; // React layer.
import tseslint from "typescript-eslint"; import { defineConfig } from "@kuhyx/ts-config";
export default tseslint.config( export default defineConfig({
{ ignores: ["dist", "coverage", "eslint.config.js"] }, // Flat files under src/, no layers to fence and no alias configured.
{ linterOptions: { reportUnusedDisableDirectives: "error" } }, aliasOnly: false,
overrides: [
js.configs.recommended, {
...tseslint.configs.strictTypeChecked, files: ["src/rng.ts"],
...tseslint.configs.stylisticTypeChecked, rules: {
// `nextChance` is public API consumed by three repos; the preset's
{ // boolean-name prefixes would rename it to `isNextChance`, which is
files: ["**/*.ts"], // worse prose and a breaking change for no safety gain.
languageOptions: { "unicorn/consistent-boolean-name": "off",
parserOptions: {
project: ["./tsconfig.lint.json"],
tsconfigRootDir: import.meta.dirname,
}, },
}, },
rules: { ],
// Every exported symbol crosses a package boundary, so an explicit // tsconfig.json excludes the tests so they never land in dist/; the lint
// return type is documentation, not ceremony. // project widens the include for type-aware rules (see tsconfig.lint.json).
"@typescript-eslint/explicit-function-return-type": "error", project: ["./tsconfig.lint.json"],
}, tsconfigRootDir: import.meta.dirname,
}, });
);

View File

@ -1,6 +1,6 @@
{ {
"name": "@kuhyx/ts-core", "name": "@kuhyx/ts-core",
"version": "0.1.0", "version": "0.1.1",
"private": false, "private": false,
"type": "module", "type": "module",
"description": "Framework-free TypeScript utilities shared across kuhy's games and web apps: an injectable Clock and a seeded mulberry32 RNG. Deliberately no React and no CSS -- see @kuhyx/web-ui for the UI layer.", "description": "Framework-free TypeScript utilities shared across kuhy's games and web apps: an injectable Clock and a seeded mulberry32 RNG. Deliberately no React and no CSS -- see @kuhyx/web-ui for the UI layer.",
@ -9,7 +9,7 @@
"node": ">=24", "node": ">=24",
"pnpm": ">=10" "pnpm": ">=10"
}, },
"packageManager": "pnpm@12.4.1+sha512.2e81e399d73fe8390dab25e06aa788ab7a5908248d2f5a370f82b481147a6a7a367bf8048f9a6fdb6460f21a66f0542dedb8b94ca2c8723596741920b1656d4c", "packageManager": "pnpm@12.4.2",
"files": [ "files": [
"dist" "dist"
], ],
@ -29,11 +29,12 @@
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "10.0.1", "@eslint/js": "10.0.1",
"@kuhyx/ts-config": "link:../ts_config",
"@types/node": "26.5.1", "@types/node": "26.5.1",
"@vitest/coverage-v8": "5.0.0", "@vitest/coverage-v8": "5.0.1",
"eslint": "10.10.0", "eslint": "10.10.0",
"typescript": "6.0.3", "typescript": "6.0.3",
"typescript-eslint": "8.70.0", "typescript-eslint": "8.70.0",
"vitest": "5.0.0" "vitest": "5.0.1"
} }
} }

View File

@ -169,12 +169,15 @@ importers:
'@eslint/js': '@eslint/js':
specifier: 10.0.1 specifier: 10.0.1
version: 10.0.1(eslint@10.10.0(supports-color@7.2.0)) version: 10.0.1(eslint@10.10.0(supports-color@7.2.0))
'@kuhyx/ts-config':
specifier: link:../ts_config
version: link:../ts_config
'@types/node': '@types/node':
specifier: 26.5.1 specifier: 26.5.1
version: 26.5.1 version: 26.5.1
'@vitest/coverage-v8': '@vitest/coverage-v8':
specifier: 5.0.0 specifier: 5.0.1
version: 5.0.0(vitest@5.0.0) version: 5.0.1(vitest@5.0.1)
eslint: eslint:
specifier: 10.10.0 specifier: 10.10.0
version: 10.10.0(supports-color@7.2.0) version: 10.10.0(supports-color@7.2.0)
@ -185,8 +188,8 @@ importers:
specifier: 8.70.0 specifier: 8.70.0
version: 8.70.0(eslint@10.10.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) version: 8.70.0(eslint@10.10.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
vitest: vitest:
specifier: 5.0.0 specifier: 5.0.1
version: 5.0.0(@types/node@26.5.1)(@vitest/coverage-v8@5.0.0)(vite@8.2.1(@types/node@26.5.1)) version: 5.0.1(@types/node@26.5.1)(@vitest/coverage-v8@5.0.1)(vite@8.2.1(@types/node@26.5.1))
packages: packages:
@ -280,9 +283,6 @@ packages:
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
'@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
'@jridgewell/sourcemap-codec@1.6.0': '@jridgewell/sourcemap-codec@1.6.0':
resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==}
@ -471,11 +471,11 @@ packages:
resolution: {integrity: sha512-BoC8PiO4Hkdo0TVJh9Ntxr5MxPDI7/oFsrygN5ADelFSeXG/qgNuucIGA+L5Z6JpPTE/uRfcTWtscjbUaufepQ==} resolution: {integrity: sha512-BoC8PiO4Hkdo0TVJh9Ntxr5MxPDI7/oFsrygN5ADelFSeXG/qgNuucIGA+L5Z6JpPTE/uRfcTWtscjbUaufepQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@vitest/coverage-v8@5.0.0': '@vitest/coverage-v8@5.0.1':
resolution: {integrity: sha512-toMg6PZGCIa/lQNCDoASrfb1ly4hsUKXFtFYC9kD4t78o5Y6LyNJU7AENt8eHPr3quYdxaxK7hj2mnbFfUk9NA==} resolution: {integrity: sha512-FRC8ACiudC3dI6MTplzRSYWHDRnIv2IPfbzs4FdoJNsMal/35sWV8hwIfV8ZcqzSPy+uXHeMVONt9CEqtOU17w==}
peerDependencies: peerDependencies:
'@vitest/browser': 5.0.0 '@vitest/browser': 5.0.1
vitest: 5.0.0 vitest: 5.0.1
peerDependenciesMeta: peerDependenciesMeta:
'@vitest/browser': '@vitest/browser':
optional: true optional: true
@ -488,8 +488,8 @@ packages:
resolution: {integrity: sha512-1EOLRfsTMnyAr3+kEAsP4o9dhaDlGPpD7H5iLBBeq//YpNB1VIahkPhB+eRp9N2Dkfw8oySROjE3yf9XDeaIkQ==} resolution: {integrity: sha512-1EOLRfsTMnyAr3+kEAsP4o9dhaDlGPpD7H5iLBBeq//YpNB1VIahkPhB+eRp9N2Dkfw8oySROjE3yf9XDeaIkQ==}
engines: {node: '>=22'} engines: {node: '>=22'}
'@vitest/mocker@5.0.0': '@vitest/mocker@5.0.1':
resolution: {integrity: sha512-66PGTMIiVJP3t4a5yxU9qPtf7MdTBs8jmToMvy+HVflB3Yy13WJZTtPePdvU+wjRV02SKK5doLbSA6o9pwOmiA==} resolution: {integrity: sha512-6K1DoBNAPGvuOcSsGA4D6x+5zEEff/KmOOP3uetT2TrGpVfI+HRHRnJJfKi5ib/g1vx8IYHQD8s0pbJz8WQI7Q==}
peerDependencies: peerDependencies:
msw: ^2.4.9 msw: ^2.4.9
vite: ^6.0.0 || ^7.0.0 || ^8.0.0 vite: ^6.0.0 || ^7.0.0 || ^8.0.0
@ -499,8 +499,8 @@ packages:
vite: vite:
optional: true optional: true
'@vitest/spy@5.0.0': '@vitest/spy@5.0.1':
resolution: {integrity: sha512-uy+luWBAPw9XfthoHi5AkfHUnuPYEESjl0p/r+meoBnU8bxg5GDQ3Ey8MjcJ6sqahkL4PFyrvfMJJBw7LbU06g==} resolution: {integrity: sha512-rbto/mF/SGERxEgYOek7Xm6B9b+y+mVoo+f4b2LymYO8zM1b7uB5nHuhVMTP2hxdzgxvGiZYGxGIaMvL5y180Q==}
acorn-jsx@5.3.2: acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
@ -978,20 +978,20 @@ packages:
yaml: yaml:
optional: true optional: true
vitest@5.0.0: vitest@5.0.1:
resolution: {integrity: sha512-gpsMNoRhMjMktVxPtstOH4/PJuPyovVaMDr4oDilXaGH1EcqM2OE96SoHT2VIQ6fTGtTjqmHDrEu2X9RQiXf8Q==} resolution: {integrity: sha512-iA95lQbKEkvrtTkdAgnWbXfbipWiiWe/hDl2P5tMi6WFwD76G0NxXAGp/M9EOcYupeGJRr6wppMc7CoA41TQjg==}
engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0} engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
'@edge-runtime/vm': '*' '@edge-runtime/vm': '*'
'@opentelemetry/api': ^1.9.0 '@opentelemetry/api': ^1.9.0
'@types/node': ^22.0.0 || >=24.0.0 '@types/node': ^22.0.0 || >=24.0.0
'@vitest/browser-playwright': 5.0.0 '@vitest/browser-playwright': 5.0.1
'@vitest/browser-preview': 5.0.0 '@vitest/browser-preview': 5.0.1
'@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0 '@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0
'@vitest/coverage-istanbul': 5.0.0 '@vitest/coverage-istanbul': 5.0.1
'@vitest/coverage-v8': 5.0.0 '@vitest/coverage-v8': 5.0.1
'@vitest/ui': 5.0.0 '@vitest/ui': 5.0.1
happy-dom: '*' happy-dom: '*'
jsdom: '*' jsdom: '*'
vite: ^6.4.0 || ^7.0.0 || ^8.0.0 vite: ^6.4.0 || ^7.0.0 || ^8.0.0
@ -1118,14 +1118,12 @@ snapshots:
'@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/resolve-uri@3.1.2': {}
'@jridgewell/sourcemap-codec@1.5.5': {}
'@jridgewell/sourcemap-codec@1.6.0': {} '@jridgewell/sourcemap-codec@1.6.0': {}
'@jridgewell/trace-mapping@0.3.31': '@jridgewell/trace-mapping@0.3.31':
dependencies: dependencies:
'@jridgewell/resolve-uri': 3.1.2 '@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/sourcemap-codec': 1.6.0
'@keyv/bigmap@1.3.1(keyv@5.6.0)': '@keyv/bigmap@1.3.1(keyv@5.6.0)':
dependencies: dependencies:
@ -1289,7 +1287,7 @@ snapshots:
'@typescript-eslint/types': 8.70.0 '@typescript-eslint/types': 8.70.0
eslint-visitor-keys: 5.0.1 eslint-visitor-keys: 5.0.1
'@vitest/coverage-v8@5.0.0(vitest@5.0.0)': '@vitest/coverage-v8@5.0.1(vitest@5.0.1)':
dependencies: dependencies:
'@bcoe/v8-coverage': 1.0.2 '@bcoe/v8-coverage': 1.0.2
'@vitest/istanbul-lib-coverage': 1.0.1 '@vitest/istanbul-lib-coverage': 1.0.1
@ -1299,7 +1297,7 @@ snapshots:
obug: 2.1.4 obug: 2.1.4
std-env: 4.2.0 std-env: 4.2.0
tinyrainbow: 3.1.1 tinyrainbow: 3.1.1
vitest: 5.0.0(@types/node@26.5.1)(@vitest/coverage-v8@5.0.0)(vite@8.2.1(@types/node@26.5.1)) vitest: 5.0.1(@types/node@26.5.1)(@vitest/coverage-v8@5.0.1)(vite@8.2.1(@types/node@26.5.1))
'@vitest/istanbul-lib-coverage@1.0.1': {} '@vitest/istanbul-lib-coverage@1.0.1': {}
@ -1307,16 +1305,16 @@ snapshots:
dependencies: dependencies:
'@vitest/istanbul-lib-coverage': 1.0.1 '@vitest/istanbul-lib-coverage': 1.0.1
'@vitest/mocker@5.0.0(vite@8.2.1(@types/node@26.5.1))': '@vitest/mocker@5.0.1(vite@8.2.1(@types/node@26.5.1))':
dependencies: dependencies:
'@jridgewell/trace-mapping': 0.3.31 '@jridgewell/trace-mapping': 0.3.31
'@vitest/spy': 5.0.0 '@vitest/spy': 5.0.1
estree-walker: 3.0.3 estree-walker: 3.0.3
magic-string: 1.3.1 magic-string: 1.3.1
optionalDependencies: optionalDependencies:
vite: 8.2.1(@types/node@26.5.1) vite: 8.2.1(@types/node@26.5.1)
'@vitest/spy@5.0.0': {} '@vitest/spy@5.0.1': {}
acorn-jsx@5.3.2(acorn@8.18.0): acorn-jsx@5.3.2(acorn@8.18.0):
dependencies: dependencies:
@ -1725,10 +1723,10 @@ snapshots:
'@types/node': 26.5.1 '@types/node': 26.5.1
fsevents: 2.3.3 fsevents: 2.3.3
vitest@5.0.0(@types/node@26.5.1)(@vitest/coverage-v8@5.0.0)(vite@8.2.1(@types/node@26.5.1)): vitest@5.0.1(@types/node@26.5.1)(@vitest/coverage-v8@5.0.1)(vite@8.2.1(@types/node@26.5.1)):
dependencies: dependencies:
'@types/chai': 5.2.3 '@types/chai': 5.2.3
'@vitest/mocker': 5.0.0(vite@8.2.1(@types/node@26.5.1)) '@vitest/mocker': 5.0.1(vite@8.2.1(@types/node@26.5.1))
chai: 6.2.2 chai: 6.2.2
es-module-lexer: 2.3.2 es-module-lexer: 2.3.2
expect-type: 1.4.0 expect-type: 1.4.0
@ -1743,7 +1741,7 @@ snapshots:
why-is-node-running: 2.3.0 why-is-node-running: 2.3.0
optionalDependencies: optionalDependencies:
'@types/node': 26.5.1 '@types/node': 26.5.1
'@vitest/coverage-v8': 5.0.0(vitest@5.0.0) '@vitest/coverage-v8': 5.0.1(vitest@5.0.1)
transitivePeerDependencies: transitivePeerDependencies:
- msw - msw

View File

@ -1,10 +1,10 @@
import { describe, expect, it, vi } from 'vitest' import { describe, expect, it, vi } from 'vitest'
import { import {
type Clock,
createManualClock, createManualClock,
createRealClock, createRealClock,
realClock, realClock,
type Clock,
} from './clock.ts' } from './clock.ts'
describe('createRealClock', () => { describe('createRealClock', () => {

View File

@ -12,9 +12,7 @@ export interface Clock {
} }
/** A {@link Clock} backed by the browser's monotonic performance timer. */ /** A {@link Clock} backed by the browser's monotonic performance timer. */
export const createRealClock = (): Clock => ({ export const createRealClock = (): Clock => ({ now: (): number => performance.now() })
now: (): number => performance.now(),
})
/** /**
* The one real clock instance UI code should pass around. * The one real clock instance UI code should pass around.
@ -39,10 +37,10 @@ export interface ManualClock extends Clock {
export const createManualClock = (startMs = 0): ManualClock => { export const createManualClock = (startMs = 0): ManualClock => {
let current = startMs let current = startMs
return { return {
now: (): number => current,
advance: (deltaMs: number): void => { advance: (deltaMs: number): void => {
current += deltaMs current += deltaMs
}, },
now: (): number => current,
set: (timeMs: number): void => { set: (timeMs: number): void => {
current = timeMs current = timeMs
}, },

View File

@ -7,11 +7,11 @@
* which is how these ended up duplicated four ways to begin with. * which is how these ended up duplicated four ways to begin with.
*/ */
export { export {
type Clock,
createManualClock, createManualClock,
createRealClock, createRealClock,
realClock,
type Clock,
type ManualClock, type ManualClock,
realClock,
} from './clock.ts' } from './clock.ts'
export { export {
createRng, createRng,

View File

@ -20,8 +20,8 @@ import {
* would defeat the entire point of the test. * would defeat the entire point of the test.
*/ */
const GOLDEN_12345 = [ const GOLDEN_12345 = [
0.979_728_267_760_947_3, 0.306_752_264_499_664_3, 0.484_205_421_525_985, 0.9797282677609473, 0.3067522644996643, 0.484205421525985,
0.817_934_412_509_203, 0.509_428_369_347_006_1, 0.817934412509203, 0.5094283693470061,
] ]
describe('mulberry32 core', () => { describe('mulberry32 core', () => {
@ -51,7 +51,7 @@ describe('mulberry32 core', () => {
it('stays within [0, 1) across many draws', () => { it('stays within [0, 1) across many draws', () => {
const rng = createRng(42) const rng = createRng(42)
for (let i = 0; i < 1000; i += 1) { for (let index = 0; index < 1000; index += 1) {
const value = nextFloat(rng) const value = nextFloat(rng)
expect(value).toBeGreaterThanOrEqual(0) expect(value).toBeGreaterThanOrEqual(0)
expect(value).toBeLessThan(1) expect(value).toBeLessThan(1)
@ -68,10 +68,10 @@ describe('nextInt', () => {
it('is inclusive of both bounds', () => { it('is inclusive of both bounds', () => {
const rng = createRng(3) const rng = createRng(3)
const seen = new Set<number>() const seen = new Set<number>()
for (let i = 0; i < 500; i += 1) { for (let index = 0; index < 500; index += 1) {
seen.add(nextInt(rng, 1, 3)) seen.add(nextInt(rng, 1, 3))
} }
expect([...seen].sort()).toEqual([1, 2, 3]) expect([...seen].toSorted((a, b) => a - b)).toEqual([1, 2, 3])
}) })
it('returns the only value when min equals max', () => { it('returns the only value when min equals max', () => {
@ -97,7 +97,7 @@ describe('nextChance', () => {
it('lands near the requested probability over many draws', () => { it('lands near the requested probability over many draws', () => {
const rng = createRng(2024) const rng = createRng(2024)
let hits = 0 let hits = 0
for (let i = 0; i < 4000; i += 1) { for (let index = 0; index < 4000; index += 1) {
if (nextChance(rng, 0.25)) { if (nextChance(rng, 0.25)) {
hits += 1 hits += 1
} }
@ -115,10 +115,10 @@ describe('pick', () => {
it('eventually returns every element', () => { it('eventually returns every element', () => {
const rng = createRng(4) const rng = createRng(4)
const seen = new Set<string>() const seen = new Set<string>()
for (let i = 0; i < 500; i += 1) { for (let index = 0; index < 500; index += 1) {
seen.add(pick(rng, ['a', 'b', 'c'])) seen.add(pick(rng, ['a', 'b', 'c']))
} }
expect([...seen].sort()).toEqual(['a', 'b', 'c']) expect([...seen].toSorted((a, b) => a.localeCompare(b))).toEqual(['a', 'b', 'c'])
}) })
it('is deterministic for a given seed', () => { it('is deterministic for a given seed', () => {

View File

@ -40,7 +40,7 @@ export const createRng = (seed: number): Rng => ({ s: seed >>> 0 })
* of it, so they all consume exactly one step per call. * of it, so they all consume exactly one step per call.
*/ */
export const nextFloat = (rng: Rng): number => { export const nextFloat = (rng: Rng): number => {
rng.s = (rng.s + 0x6d_2b_79_f5) >>> 0 rng.s = (rng.s + 0x6D_2B_79_F5) >>> 0
let t = rng.s let t = rng.s
t = Math.imul(t ^ (t >>> 15), t | 1) t = Math.imul(t ^ (t >>> 15), t | 1)
t ^= t + Math.imul(t ^ (t >>> 7), t | 61) t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
@ -84,14 +84,14 @@ export const pick = <T>(rng: Rng, items: readonly [T, ...T[]]): T => {
* around rather than threading mutable state through free functions. * around rather than threading mutable state through free functions.
*/ */
export interface SeededRng { export interface SeededRng {
/** True with the given probability in [0, 1]. */
chance: (probability: number) => boolean
/** Uniform float in [0, 1). */ /** Uniform float in [0, 1). */
float: () => number float: () => number
/** Uniform integer in [minInclusive, maxInclusive]. */ /** Uniform integer in [minInclusive, maxInclusive]. */
int: (minInclusive: number, maxInclusive: number) => number int: (minInclusive: number, maxInclusive: number) => number
/** Uniform element of a non-empty array. */ /** Uniform element of a non-empty array. */
pick: <T>(items: readonly T[]) => T pick: <T>(items: readonly T[]) => T
/** True with the given probability in [0, 1]. */
chance: (probability: number) => boolean
} }
/** /**
@ -105,6 +105,7 @@ export const createSeededRng = (seed: number): SeededRng => {
const state = createRng(seed) const state = createRng(seed)
const float = (): number => nextFloat(state) const float = (): number => nextFloat(state)
return { return {
chance: (probability: number): boolean => float() < probability,
float, float,
int: (minInclusive: number, maxInclusive: number): number => int: (minInclusive: number, maxInclusive: number): number =>
nextInt(state, minInclusive, maxInclusive), nextInt(state, minInclusive, maxInclusive),
@ -115,6 +116,5 @@ export const createSeededRng = (seed: number): SeededRng => {
} }
return chosen return chosen
}, },
chance: (probability: number): boolean => float() < probability,
} }
} }

View File

@ -1,4 +1,6 @@
{ {
// Strictness comes from the shared base; only output/lib options live here.
"extends": "@kuhyx/ts-config/tsconfig",
"compilerOptions": { "compilerOptions": {
"target": "ES2023", "target": "ES2023",
// DOM is here only for `performance.now()` in clock.ts -- the package // DOM is here only for `performance.now()` in clock.ts -- the package
@ -9,21 +11,6 @@
"module": "ESNext", "module": "ESNext",
"moduleResolution": "bundler", "moduleResolution": "bundler",
/* Strictness: everything on, matching the 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,
/* Ships a prebuilt dist/ (ESM + .d.ts) because consumers use Vite, which /* Ships a prebuilt dist/ (ESM + .d.ts) because consumers use Vite, which
does not transpile dependencies. */ does not transpile dependencies. */
"rewriteRelativeImportExtensions": true, "rewriteRelativeImportExtensions": true,
@ -32,10 +19,7 @@
"declarationMap": true, "declarationMap": true,
"sourceMap": true, "sourceMap": true,
"outDir": "dist", "outDir": "dist",
"rootDir": "src", "rootDir": "src"
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}, },
"include": ["src"], "include": ["src"],
"exclude": ["src/**/*.test.ts"] "exclude": ["src/**/*.test.ts"]

View File

@ -5,6 +5,7 @@
* `import "@kuhyx/web-ui/tokens.css";` * `import "@kuhyx/web-ui/tokens.css";`
*/ */
export { clamp01, fractionFromPointer, nth, quantileValue, valueQuantile } from "./quantile.ts"; export { clamp01, fractionFromPointer, nth, quantileValue, valueQuantile } from "./quantile.ts";
export { RangeSlider, steppedValue, type RangeSliderProps } from "./range-slider.tsx"; export { RangeSlider, type RangeSliderProps } from "./range-slider.tsx";
export { keyTarget, steppedValue } from "./stepped-value.ts";
export { fuzzyMatch } from "./text.ts"; export { fuzzyMatch } from "./text.ts";
//# sourceMappingURL=index.d.ts.map //# sourceMappingURL=index.d.ts.map

View File

@ -1 +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"} {"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,KAAK,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACxE,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAC7D,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC"}

View File

@ -5,6 +5,7 @@
* `import "@kuhyx/web-ui/tokens.css";` * `import "@kuhyx/web-ui/tokens.css";`
*/ */
export { clamp01, fractionFromPointer, nth, quantileValue, valueQuantile } from "./quantile.js"; export { clamp01, fractionFromPointer, nth, quantileValue, valueQuantile } from "./quantile.js";
export { RangeSlider, steppedValue } from "./range-slider.js"; export { RangeSlider } from "./range-slider.js";
export { keyTarget, steppedValue } from "./stepped-value.js";
export { fuzzyMatch } from "./text.js"; export { fuzzyMatch } from "./text.js";
//# sourceMappingURL=index.js.map //# sourceMappingURL=index.js.map

View File

@ -1 +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"} {"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,EAAyB,MAAM,mBAAoB,CAAC;AACxE,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAC7D,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC"}

View File

@ -1 +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"} {"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,CAY9E;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAE1E"}

View File

@ -52,9 +52,7 @@ export function quantileValue(sorted, f) {
*/ */
export function valueQuantile(sorted, value) { export function valueQuantile(sorted, value) {
const n = sorted.length; const n = sorted.length;
if (n <= 1) if ((n <= 1) || (value <= nth(sorted, 0)))
return 0;
if (value <= nth(sorted, 0))
return 0; return 0;
if (value >= nth(sorted, n - 1)) if (value >= nth(sorted, n - 1))
return 1; return 1;

View File

@ -1 +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,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;IAC9D,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"} {"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,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;IAC9D,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,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,KAAK,IAAI,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;QAAE,OAAO,CAAC,CAAC;IACpD,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"}

View File

@ -16,16 +16,5 @@ export interface RangeSliderProps {
/** The full ascending distribution — not just its endpoints. */ /** The full ascending distribution — not just its endpoints. */
readonly values: readonly number[]; readonly values: readonly number[];
} }
/**
* Steps one thumb by `delta` positions through the distribution. Pure, so the
* keyboard contract is testable without layout.
*
* Steps to the next *distinct* value rather than the next index. Real
* distributions are lumpy -- awesome-mcp-explorer's star counts hold ~1500
* duplicate zeros -- and an index step inside a run of equal values changes the
* index while leaving the value (and therefore the thumb, and the filter)
* exactly where it was. That reads as a dead arrow key.
*/
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; export declare function RangeSlider({ format, hi, label, lo, onChange, values, }: RangeSliderProps): null | React.JSX.Element;
//# sourceMappingURL=range-slider.d.ts.map //# sourceMappingURL=range-slider.d.ts.map

View File

@ -1 +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;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,SAAS,MAAM,EAAE,EACzB,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,GACZ,MAAM,CAkBR;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"} {"version":3,"file":"range-slider.d.ts","sourceRoot":"","sources":["../src/range-slider.tsx"],"names":[],"mappings":"AA0BA,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,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,CAkG7C"}

View File

@ -22,36 +22,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
*/ */
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { fractionFromPointer, nth, quantileValue, valueQuantile } from "./quantile.js"; import { fractionFromPointer, nth, quantileValue, valueQuantile } from "./quantile.js";
/** import { keyTarget } from "./stepped-value.js";
* Steps one thumb by `delta` positions through the distribution. Pure, so the
* keyboard contract is testable without layout.
*
* Steps to the next *distinct* value rather than the next index. Real
* distributions are lumpy -- awesome-mcp-explorer's star counts hold ~1500
* duplicate zeros -- and an index step inside a run of equal values changes the
* index while leaving the value (and therefore the thumb, and the filter)
* exactly where it was. That reads as a dead arrow key.
*/
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 rather than stalling on a fractional step.
const index = Math.round(valueQuantile(values, current) * last);
const direction = Math.sign(delta);
if (direction === 0)
return nth(values, index);
let cursor = index;
for (let taken = 0; taken < Math.abs(delta); taken++) {
let next = cursor + direction;
while (next >= 0 && next <= last && nth(values, next) === nth(values, cursor)) {
next += direction;
}
if (next < 0 || next > last)
break;
cursor = next;
}
return nth(values, cursor);
}
export function RangeSlider({ format, hi, label, lo, onChange, values, }) { export function RangeSlider({ format, hi, label, lo, onChange, values, }) {
const [drag, setDrag] = useState(null); const [drag, setDrag] = useState(null);
const apply = useCallback((thumb, value) => { const apply = useCallback((thumb, value) => {
@ -63,29 +34,17 @@ export function RangeSlider({ format, hi, label, lo, onChange, values, }) {
// Reads the rect off the event's own target rather than a ref: the pointer // 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 // 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. // 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 valueAt = useCallback((track, clientX) => {
const fraction = fractionFromPointer(track.getBoundingClientRect(), clientX);
return quantileValue(values, fraction);
}, [values]);
const onKeyDown = useCallback((thumb, event) => { const onKeyDown = useCallback((thumb, event) => {
const current = thumb === "lo" ? lo : hi; const current = thumb === "lo" ? lo : hi;
const step = (delta) => { const target = keyTarget(event.key, values, current);
event.preventDefault(); if (target === undefined)
apply(thumb, steppedValue(values, current, delta)); return;
}; event.preventDefault();
if (event.key === "ArrowLeft" || event.key === "ArrowDown") apply(thumb, target);
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]); }, [apply, hi, lo, values]);
// Fewer than two values means there is nothing to range over. // Fewer than two values means there is nothing to range over.
if (values.length < 2) if (values.length < 2)
@ -95,19 +54,21 @@ export function RangeSlider({ format, hi, label, lo, onChange, values, }) {
const loFraction = valueQuantile(values, lo) * 100; const loFraction = valueQuantile(values, lo) * 100;
const hiFraction = valueQuantile(values, hi) * 100; const hiFraction = valueQuantile(values, hi) * 100;
const name = label ?? "Range"; const name = label ?? "Range";
const thumbProps = (thumb) => ({ const thumbProps = (thumb) => {
"aria-valuemax": highest, return {
"aria-valuemin": lowest, "aria-valuemax": highest,
"aria-valuenow": thumb === "lo" ? lo : hi, "aria-valuemin": lowest,
className: `slider-thumb slider-${thumb}`, "aria-valuenow": thumb === "lo" ? lo : hi,
onKeyDown: (event) => { className: `slider-thumb slider-${thumb}`,
onKeyDown(thumb, event); onKeyDown: (event) => {
}, onKeyDown(thumb, event);
role: "slider", },
style: { left: `${String(thumb === "lo" ? loFraction : hiFraction)}%` }, role: "slider",
tabIndex: 0, style: { left: `${String(thumb === "lo" ? loFraction : hiFraction)}%` },
type: "button", 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) => { 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); const value = valueAt(event.currentTarget, event.clientX);
// Grab whichever thumb is nearer, so a press anywhere on the track // Grab whichever thumb is nearer, so a press anywhere on the track

File diff suppressed because one or more lines are too long

19
web_ui/dist/stepped-value.d.ts vendored Normal file
View File

@ -0,0 +1,19 @@
/**
* Steps one thumb by `delta` positions through the distribution. Pure, so the
* keyboard contract is testable without layout.
*
* Steps to the next *distinct* value rather than the next index. Real
* distributions are lumpy -- awesome-mcp-explorer's star counts hold ~1500
* duplicate zeros -- and an index step inside a run of equal values changes the
* index while leaving the value (and therefore the thumb, and the filter)
* exactly where it was. That reads as a dead arrow key.
*/
export declare function steppedValue(values: readonly number[], current: number, delta: number): number;
/**
* The value a thumb at `current` moves to for a key, or `undefined` when the
* key is not one the slider handles (so the event is left to the browser).
* Arrow keys step one distinct value, Page keys ten, Home/End jump to the
* bounds.
*/
export declare function keyTarget(key: string, values: readonly number[], current: number): number | undefined;
//# sourceMappingURL=stepped-value.d.ts.map

1
web_ui/dist/stepped-value.d.ts.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"file":"stepped-value.d.ts","sourceRoot":"","sources":["../src/stepped-value.ts"],"names":[],"mappings":"AAOA;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,SAAS,MAAM,EAAE,EACzB,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,GACZ,MAAM,CAkBR;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CACvB,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,SAAS,MAAM,EAAE,EACzB,OAAO,EAAE,MAAM,GACd,MAAM,GAAG,SAAS,CA0BpB"}

70
web_ui/dist/stepped-value.js vendored Normal file
View File

@ -0,0 +1,70 @@
/**
* The keyboard contract of the range slider, kept pure and in its own module
* so range-slider.tsx exports only the component (react-refresh needs that
* for HMR to keep working) and the contract is testable without layout.
*/
import { nth, valueQuantile } from "./quantile.js";
/**
* Steps one thumb by `delta` positions through the distribution. Pure, so the
* keyboard contract is testable without layout.
*
* Steps to the next *distinct* value rather than the next index. Real
* distributions are lumpy -- awesome-mcp-explorer's star counts hold ~1500
* duplicate zeros -- and an index step inside a run of equal values changes the
* index while leaving the value (and therefore the thumb, and the filter)
* exactly where it was. That reads as a dead arrow key.
*/
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 rather than stalling on a fractional step.
const index = Math.round(valueQuantile(values, current) * last);
const direction = Math.sign(delta);
if (direction === 0)
return nth(values, index);
let cursor = index;
for (let taken = 0; taken < Math.abs(delta); taken++) {
let next = cursor + direction;
while (next >= 0 && next <= last && nth(values, next) === nth(values, cursor)) {
next += direction;
}
if (next < 0 || next > last)
break;
cursor = next;
}
return nth(values, cursor);
}
/**
* The value a thumb at `current` moves to for a key, or `undefined` when the
* key is not one the slider handles (so the event is left to the browser).
* Arrow keys step one distinct value, Page keys ten, Home/End jump to the
* bounds.
*/
export function keyTarget(key, values, current) {
switch (key) {
case "ArrowDown":
case "ArrowLeft": {
return steppedValue(values, current, -1);
}
case "ArrowRight":
case "ArrowUp": {
return steppedValue(values, current, 1);
}
case "End": {
return nth(values, values.length - 1);
}
case "Home": {
return nth(values, 0);
}
case "PageDown": {
return steppedValue(values, current, -10);
}
case "PageUp": {
return steppedValue(values, current, 10);
}
default: {
return undefined;
}
}
}
//# sourceMappingURL=stepped-value.js.map

1
web_ui/dist/stepped-value.js.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"file":"stepped-value.js","sourceRoot":"","sources":["../src/stepped-value.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,GAAG,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnD;;;;;;;;;GASG;AACH,MAAM,UAAU,YAAY,CAC1B,MAAyB,EACzB,OAAe,EACf,KAAa;IAEb,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC;IAC/B,4EAA4E;IAC5E,sDAAsD;IACtD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC;IAChE,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACnC,IAAI,SAAS,KAAK,CAAC;QAAE,OAAO,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAE/C,IAAI,MAAM,GAAG,KAAK,CAAC;IACnB,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC;QACrD,IAAI,IAAI,GAAG,MAAM,GAAG,SAAS,CAAC;QAC9B,OAAO,IAAI,IAAI,CAAC,IAAI,IAAI,IAAI,IAAI,IAAI,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC;YAC9E,IAAI,IAAI,SAAS,CAAC;QACpB,CAAC;QACD,IAAI,IAAI,GAAG,CAAC,IAAI,IAAI,GAAG,IAAI;YAAE,MAAM;QACnC,MAAM,GAAG,IAAI,CAAC;IAChB,CAAC;IACD,OAAO,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAC7B,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,SAAS,CACvB,GAAW,EACX,MAAyB,EACzB,OAAe;IAEf,QAAQ,GAAG,EAAE,CAAC;QACZ,KAAK,WAAW,CAAC;QACjB,KAAK,WAAW,CAAC,CAAC,CAAC;YACjB,OAAO,YAAY,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC;QAC3C,CAAC;QACD,KAAK,YAAY,CAAC;QAClB,KAAK,SAAS,CAAC,CAAC,CAAC;YACf,OAAO,YAAY,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC;QAC1C,CAAC;QACD,KAAK,KAAK,CAAC,CAAC,CAAC;YACX,OAAO,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QACxC,CAAC;QACD,KAAK,MAAM,CAAC,CAAC,CAAC;YACZ,OAAO,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;QACxB,CAAC;QACD,KAAK,UAAU,CAAC,CAAC,CAAC;YAChB,OAAO,YAAY,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;QAC5C,CAAC;QACD,KAAK,QAAQ,CAAC,CAAC,CAAC;YACd,OAAO,YAAY,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE,CAAC,CAAC;QAC3C,CAAC;QACD,OAAO,CAAC,CAAC,CAAC;YACR,OAAO,SAAS,CAAC;QACnB,CAAC;IACH,CAAC;AACH,CAAC"}

View File

@ -1,42 +1,25 @@
// Type-aware flat config for the shared component layer. // The shared preset with the React layer, plus one frozen API name. This
// // package is one of the repos the bar is defined for.
// A trimmed version of awesome-mcp-explorer's: typescript-eslint's import { defineConfig } from "@kuhyx/ts-config";
// *type-checked* strict + stylistic presets, so anything they flag is an error.
// The consumers layer unicorn/sonarjs/perfectionist on top; this package keeps
// to the presets its own devDependencies actually declare, because a config
// referencing plugins that are not installed is a lint script that cannot run.
import js from "@eslint/js";
import tseslint from "typescript-eslint";
export default tseslint.config( export default defineConfig({
// eslint.config.js itself is not in tsconfig.lint.json, so type-aware // Flat files under src/, no layers to fence and no alias configured.
// rules cannot run on it; linting the linter config buys nothing anyway. aliasOnly: false,
{ ignores: ["dist", "coverage", "eslint.config.js"] }, overrides: [
{ linterOptions: { reportUnusedDisableDirectives: "error" } }, {
files: ["src/text.ts"],
js.configs.recommended, rules: {
...tseslint.configs.strictTypeChecked, // `fuzzyMatch` is public API at four call sites in dufs-cloud and
...tseslint.configs.stylisticTypeChecked, // awesome-mcp-explorer; the preset's prefix list would rename it to
// `matchesFuzzy`, a breaking change for no safety gain. Revisit at
{ // the next major.
files: ["**/*.{ts,tsx}"], "unicorn/consistent-boolean-name": "off",
languageOptions: {
parserOptions: {
project: ["./tsconfig.lint.json"],
tsconfigRootDir: import.meta.dirname,
}, },
}, },
rules: { ],
// Every exported symbol here is consumed across a package boundary, so // tsconfig.json excludes the tests so they never land in dist/; the lint
// an explicit return type is documentation, not ceremony. // project widens the include for type-aware rules (see tsconfig.lint.json).
"@typescript-eslint/explicit-function-return-type": "error", project: ["./tsconfig.lint.json"],
}, react: true,
}, tsconfigRootDir: import.meta.dirname,
});
{
// Tests reach for non-null assertions on queries that cannot return null
// in a passing test; asserting them again would only add noise.
files: ["**/*.test.{ts,tsx}"],
rules: { "@typescript-eslint/no-non-null-assertion": "off" },
},
);

View File

@ -1,6 +1,6 @@
{ {
"name": "@kuhyx/web-ui", "name": "@kuhyx/web-ui",
"version": "0.3.2", "version": "0.4.0",
"private": false, "private": false,
"type": "module", "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.", "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.",
@ -9,7 +9,7 @@
"node": ">=24", "node": ">=24",
"pnpm": ">=10" "pnpm": ">=10"
}, },
"packageManager": "pnpm@12.4.1+sha512.2e81e399d73fe8390dab25e06aa788ab7a5908248d2f5a370f82b481147a6a7a367bf8048f9a6fdb6460f21a66f0542dedb8b94ca2c8723596741920b1656d4c", "packageManager": "pnpm@12.4.2",
"files": [ "files": [
"dist" "dist"
], ],
@ -34,19 +34,20 @@
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "10.0.1", "@eslint/js": "10.0.1",
"@kuhyx/ts-config": "link:../ts_config",
"@testing-library/jest-dom": "7.0.1", "@testing-library/jest-dom": "7.0.1",
"@testing-library/react": "16.3.3", "@testing-library/react": "16.3.3",
"@testing-library/user-event": "14.6.7", "@testing-library/user-event": "14.6.7",
"@types/node": "26.5.1",
"@types/react": "19.3.0", "@types/react": "19.3.0",
"@types/react-dom": "19.3.0", "@types/react-dom": "19.3.0",
"@vitest/coverage-v8": "5.0.0", "@vitest/coverage-v8": "5.0.1",
"eslint": "10.10.0", "eslint": "10.10.0",
"jsdom": "30.0.1", "jsdom": "30.0.1",
"react": "19.3.0", "react": "19.3.0",
"react-dom": "19.3.0", "react-dom": "19.3.0",
"typescript": "6.0.3", "typescript": "6.0.3",
"typescript-eslint": "8.70.0", "typescript-eslint": "8.70.0",
"vitest": "5.0.0", "vitest": "5.0.1"
"@types/node": "26.5.1"
} }
} }

View File

@ -7,153 +7,153 @@ importers:
configDependencies: {} configDependencies: {}
packageManagerDependencies: packageManagerDependencies:
pnpm: pnpm:
specifier: 12.4.1 specifier: 12.4.2
version: 12.4.1 version: 12.4.2
packages: packages:
'@pnpm/exe.android-arm64@12.4.1': '@pnpm/exe.android-arm64@12.4.2':
resolution: {integrity: sha512-/HwsqXMSmlOfgtV9+O0ratzjV6Vd/8n1hh4rGHpCGmDURvt52MwZxdbhyxP4K03ZfvgSDvotFPr8O+RzE5Eu8A==} resolution: {integrity: sha512-E255MbcQ0V1577M2BV0ajWuP7KmTPYA0jqZnk3rK6Lq3kTvZWulMMb7iqRmnLsQbyWTkMEB2CfyM70loVDA8xg==}
cpu: [arm64] cpu: [arm64]
os: [android] os: [android]
'@pnpm/exe.android-x64@12.4.1': '@pnpm/exe.android-x64@12.4.2':
resolution: {integrity: sha512-+l74Qb4c2YjOzNKHXJLg+1wr8xHM1ckkUhnU5KRUK9TJqiiczt6yeTZqqzHIlJ8i6pAoj5V0OJevDRwnQKLgrQ==} resolution: {integrity: sha512-J1pSeCUwuKxMG70ZzpWn8JzElxiEa8NN/3N0SlZRtU2xbztChaJCKF3HaBNIj9yPfeofWzJo/lwNvDDjraQuZw==}
cpu: [x64] cpu: [x64]
os: [android] os: [android]
'@pnpm/exe.darwin-arm64@12.4.1': '@pnpm/exe.darwin-arm64@12.4.2':
resolution: {integrity: sha512-6rkZkT3iGfaxknUdGHraqSWFvTa6N0ajAHluv9Ax0GRWs0sIcGNiFhDopv6xSZCsJZmG483aNS/b6UEDy3blfw==} resolution: {integrity: sha512-A0WDo8iErfZBXgrLseQxw8i8Y9ctUpOEl/Uu+cubnTzpD8tT9ykIB548L8YTM2WD4OS+ZOHSxy8aGZcvKq8PaQ==}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@pnpm/exe.darwin-x64@12.4.1': '@pnpm/exe.darwin-x64@12.4.2':
resolution: {integrity: sha512-Vb1CHlR88HghC1qUxjxjs82zQSXnXacPD+btG2CmG8Q/hBU7Q0/b2YWJKSQqZXicunV5khFtfTlAwJddV9RkYA==} resolution: {integrity: sha512-MSgJdovBWHcb5DEOvfPH9yNi/T5O1Xa4ess+E1ESGo/5yuty7S4JoiIjami+fsNXfnoQMlwsMUqYU4zAvBcNCw==}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@pnpm/exe.freebsd-x64@12.4.1': '@pnpm/exe.freebsd-x64@12.4.2':
resolution: {integrity: sha512-iT3iHz3Nl0Sxxj7UPOtZ/aQ81AFsQhjoeWMAlPkSRO04gsgDGAXv3UYxOFaesMWsfNxaGn0A+CITbuCHFF66FA==} resolution: {integrity: sha512-h2YumlQSNvgbRPv+RXwABohX65f9bOBZn+jMIt7bFDISZPCzQ+Nvpt6Awbp4ip5PwQgYxbu5iREJ1fHE39Fm8A==}
cpu: [x64] cpu: [x64]
os: [freebsd] os: [freebsd]
'@pnpm/exe.linux-arm64-musl@12.4.1': '@pnpm/exe.linux-arm64-musl@12.4.2':
resolution: {integrity: sha512-aBooZfNXM5f+OGUgCAMFWpE/kAhsWfvmqIyMtHy6zl3aNxyInWrcY/Saln/UElzo7lZWMC0Yktroxd/2J26lNQ==} resolution: {integrity: sha512-LwSEtSEDTv6S51YLs3YvSkPyun/QmfMic1UGICUkPWFu6ByP43RdMlkKvmVkfGhAYCpnxO057vrmyJqtfZrPCA==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl] libc: [musl]
'@pnpm/exe.linux-arm64@12.4.1': '@pnpm/exe.linux-arm64@12.4.2':
resolution: {integrity: sha512-TlOdacTTP09BgcMvwWBFRsu8VAjfwqwnslBj+XSq1JFM3ck4f3k+1O/747EuctxZxs3/o785b6Q3s7Pd92Ptsg==} resolution: {integrity: sha512-2dSiDXyhx+RTHsewxex8f/jVjqQXWJ2oow4kCVHEWdZKeBpgMxvZ6fHkTAUBJXgqhbKIDHvuNlZBP7gJfUWL5A==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@pnpm/exe.linux-ppc64@12.4.1': '@pnpm/exe.linux-ppc64@12.4.2':
resolution: {integrity: sha512-r/ab/MlIBo75oizUP5ITiziCCrnXz4SJwfErLQ+603AshB+Yq7xTMCoMtzTSCAMu6aaymIKkAFtZvd2J75Wq0w==} resolution: {integrity: sha512-8Itc+jQk+MTz04LS9D1RcH+VctAWmzM4l1cJQ+Sx7pAJNo7EKHdRMbC0Tok1jyQ7eEHNJZD5EleYneZqWfZM+g==}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@pnpm/exe.linux-riscv64@12.4.1': '@pnpm/exe.linux-riscv64@12.4.2':
resolution: {integrity: sha512-C/D1QWdKMiB8+wv/spl1rGITFUuqC+aI/fb6h8NB5sqxsOaGXeYc/g891oCuzggHn6SODk9p+I09hI76x/cMNw==} resolution: {integrity: sha512-hleOeqhTVpH+z9RVMGnxvU4ZnrkBUClWjCbHD9u6kwyqhGSpevoU1wTGish+CBRhmIgMAy9pAfFpqhbAKOKNfw==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@pnpm/exe.linux-s390x@12.4.1': '@pnpm/exe.linux-s390x@12.4.2':
resolution: {integrity: sha512-nxz5zD4yXt94uzbStDk0QTPKW+aE92hH1b5tFXK9ctB1HE9Xcq1vjTiA2LsZMFC6p4gdu5OwQeFqYNAVGa6QlA==} resolution: {integrity: sha512-LAsQRRdP9aToENR6dtcIJ9l+e1zMYOX0tQcLGpRyLPVBQcYRLlvAPcmDshsiIHQjp05SDa9FI0czX1ZQ1a7a/A==}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@pnpm/exe.linux-x64-musl@12.4.1': '@pnpm/exe.linux-x64-musl@12.4.2':
resolution: {integrity: sha512-5AwgFdGhVUg2kIweYGfxzSLEHiIG77PQhZAkXC3TwofQHsu1Wr+TrV5/rNX2PopFnHRzuE581zoB8F6Wle32yg==} resolution: {integrity: sha512-kzfzH2/0BWdTABK14Yj5a1xsdkTEQUp2eXEPNakaD9jKL025lq3hyaKHIz/gIZaPDMe/1bFFK/En4ztFlbBJxw==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl] libc: [musl]
'@pnpm/exe.linux-x64@12.4.1': '@pnpm/exe.linux-x64@12.4.2':
resolution: {integrity: sha512-FJOZuuuQMhp0oLzBtcKkLXknBI92hfkmSnlKc47vfin4HrmfID5khY2lGekL9tCzk1cpR+HShEw/meFl+nHtzQ==} resolution: {integrity: sha512-/pbt0UVTa8NMDhzOWLQRfZ6G9ROKXlJPZtx845BqyWfc7hCrWXhTLBd70yO2y8+E+IWN3oHM1s/JsIQNGk1yvg==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@pnpm/exe.win32-arm64@12.4.1': '@pnpm/exe.win32-arm64@12.4.2':
resolution: {integrity: sha512-OO7eKBL9S+xk5hRy+JUUZSNJknGuGe3GEUffsrlC2LbqKF02g+VX1anGIzSbJDvkkdnpJhSlxF5AUz2JzJu2Jw==} resolution: {integrity: sha512-PsW19e4dAUNpZ0cS9flaxFuAmpt2dKlH/Vvi8TZ4qyJjjQzue/CEsWm+6wKVP4CJ7IT8RdL4qh+soOPWIgF2Zw==}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
'@pnpm/exe.win32-x64@12.4.1': '@pnpm/exe.win32-x64@12.4.2':
resolution: {integrity: sha512-x7gJHZgHo6hp354xCYA2NvoFzYJkHwovt2kVsUBK5EmXULLcP51JGMq09CX+5FRFJUZFKoXjLu3mZWmfP7o6PQ==} resolution: {integrity: sha512-+xGoeE0g55ztWvl8i5QqdmNfW3nIrTVcoQrNshEOwxthm9Ag48oXton3uxOA3/SANyz5AXexEjj2KO20NnOrEw==}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
pnpm@12.4.1: pnpm@12.4.2:
resolution: {integrity: sha512-LoHjmdc/6DkNqyXgaqeIq3pZCCSNL1o3D4K0gRR6ano2e/gEj5pv22Rg8hpm8FQt7bi5TKLIcjWWdBkgsWVtTA==} resolution: {integrity: sha512-CK3GYTGAJ1x8ntraOdzwjJxhrU5+rzMKTzRh8QKw+QdCNFTRF/mOctR/7wYWBwZE17/8lzpqV/UJCm18NosHyQ==}
engines: {node: '>=18.*'} engines: {node: '>=18.*'}
hasBin: true hasBin: true
snapshots: snapshots:
'@pnpm/exe.android-arm64@12.4.1': '@pnpm/exe.android-arm64@12.4.2':
optional: true optional: true
'@pnpm/exe.android-x64@12.4.1': '@pnpm/exe.android-x64@12.4.2':
optional: true optional: true
'@pnpm/exe.darwin-arm64@12.4.1': '@pnpm/exe.darwin-arm64@12.4.2':
optional: true optional: true
'@pnpm/exe.darwin-x64@12.4.1': '@pnpm/exe.darwin-x64@12.4.2':
optional: true optional: true
'@pnpm/exe.freebsd-x64@12.4.1': '@pnpm/exe.freebsd-x64@12.4.2':
optional: true optional: true
'@pnpm/exe.linux-arm64-musl@12.4.1': '@pnpm/exe.linux-arm64-musl@12.4.2':
optional: true optional: true
'@pnpm/exe.linux-arm64@12.4.1': '@pnpm/exe.linux-arm64@12.4.2':
optional: true optional: true
'@pnpm/exe.linux-ppc64@12.4.1': '@pnpm/exe.linux-ppc64@12.4.2':
optional: true optional: true
'@pnpm/exe.linux-riscv64@12.4.1': '@pnpm/exe.linux-riscv64@12.4.2':
optional: true optional: true
'@pnpm/exe.linux-s390x@12.4.1': '@pnpm/exe.linux-s390x@12.4.2':
optional: true optional: true
'@pnpm/exe.linux-x64-musl@12.4.1': '@pnpm/exe.linux-x64-musl@12.4.2':
optional: true optional: true
'@pnpm/exe.linux-x64@12.4.1': '@pnpm/exe.linux-x64@12.4.2':
optional: true optional: true
'@pnpm/exe.win32-arm64@12.4.1': '@pnpm/exe.win32-arm64@12.4.2':
optional: true optional: true
'@pnpm/exe.win32-x64@12.4.1': '@pnpm/exe.win32-x64@12.4.2':
optional: true optional: true
pnpm@12.4.1: pnpm@12.4.2:
optionalDependencies: optionalDependencies:
'@pnpm/exe.android-arm64': 12.4.1 '@pnpm/exe.android-arm64': 12.4.2
'@pnpm/exe.android-x64': 12.4.1 '@pnpm/exe.android-x64': 12.4.2
'@pnpm/exe.darwin-arm64': 12.4.1 '@pnpm/exe.darwin-arm64': 12.4.2
'@pnpm/exe.darwin-x64': 12.4.1 '@pnpm/exe.darwin-x64': 12.4.2
'@pnpm/exe.freebsd-x64': 12.4.1 '@pnpm/exe.freebsd-x64': 12.4.2
'@pnpm/exe.linux-arm64': 12.4.1 '@pnpm/exe.linux-arm64': 12.4.2
'@pnpm/exe.linux-arm64-musl': 12.4.1 '@pnpm/exe.linux-arm64-musl': 12.4.2
'@pnpm/exe.linux-ppc64': 12.4.1 '@pnpm/exe.linux-ppc64': 12.4.2
'@pnpm/exe.linux-riscv64': 12.4.1 '@pnpm/exe.linux-riscv64': 12.4.2
'@pnpm/exe.linux-s390x': 12.4.1 '@pnpm/exe.linux-s390x': 12.4.2
'@pnpm/exe.linux-x64': 12.4.1 '@pnpm/exe.linux-x64': 12.4.2
'@pnpm/exe.linux-x64-musl': 12.4.1 '@pnpm/exe.linux-x64-musl': 12.4.2
'@pnpm/exe.win32-arm64': 12.4.1 '@pnpm/exe.win32-arm64': 12.4.2
'@pnpm/exe.win32-x64': 12.4.1 '@pnpm/exe.win32-x64': 12.4.2
--- ---
lockfileVersion: '9.0' lockfileVersion: '9.0'
@ -169,9 +169,12 @@ importers:
'@eslint/js': '@eslint/js':
specifier: 10.0.1 specifier: 10.0.1
version: 10.0.1(eslint@10.10.0(supports-color@7.2.0)) version: 10.0.1(eslint@10.10.0(supports-color@7.2.0))
'@kuhyx/ts-config':
specifier: link:../ts_config
version: link:../ts_config
'@testing-library/jest-dom': '@testing-library/jest-dom':
specifier: 7.0.1 specifier: 7.0.1
version: 7.0.1(@testing-library/dom@10.4.1)(vitest@5.0.0) version: 7.0.1(@testing-library/dom@10.4.1)(vitest@5.0.1)
'@testing-library/react': '@testing-library/react':
specifier: 16.3.3 specifier: 16.3.3
version: 16.3.3(@testing-library/dom@10.4.1)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) version: 16.3.3(@testing-library/dom@10.4.1)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
@ -188,8 +191,8 @@ importers:
specifier: 19.3.0 specifier: 19.3.0
version: 19.3.0(@types/react@19.3.0) version: 19.3.0(@types/react@19.3.0)
'@vitest/coverage-v8': '@vitest/coverage-v8':
specifier: 5.0.0 specifier: 5.0.1
version: 5.0.0(vitest@5.0.0) version: 5.0.1(vitest@5.0.1)
eslint: eslint:
specifier: 10.10.0 specifier: 10.10.0
version: 10.10.0(supports-color@7.2.0) version: 10.10.0(supports-color@7.2.0)
@ -209,8 +212,8 @@ importers:
specifier: 8.70.0 specifier: 8.70.0
version: 8.70.0(eslint@10.10.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) version: 8.70.0(eslint@10.10.0(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
vitest: vitest:
specifier: 5.0.0 specifier: 5.0.1
version: 5.0.0(@types/node@26.5.1)(@vitest/coverage-v8@5.0.0)(jsdom@30.0.1)(vite@8.2.2(@types/node@26.5.1)) version: 5.0.1(@types/node@26.5.1)(@vitest/coverage-v8@5.0.1)(jsdom@30.0.1)(vite@8.2.2(@types/node@26.5.1))
packages: packages:
@ -372,9 +375,6 @@ packages:
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
'@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
'@jridgewell/sourcemap-codec@1.6.0': '@jridgewell/sourcemap-codec@1.6.0':
resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==}
@ -615,11 +615,11 @@ packages:
resolution: {integrity: sha512-BoC8PiO4Hkdo0TVJh9Ntxr5MxPDI7/oFsrygN5ADelFSeXG/qgNuucIGA+L5Z6JpPTE/uRfcTWtscjbUaufepQ==} resolution: {integrity: sha512-BoC8PiO4Hkdo0TVJh9Ntxr5MxPDI7/oFsrygN5ADelFSeXG/qgNuucIGA+L5Z6JpPTE/uRfcTWtscjbUaufepQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@vitest/coverage-v8@5.0.0': '@vitest/coverage-v8@5.0.1':
resolution: {integrity: sha512-toMg6PZGCIa/lQNCDoASrfb1ly4hsUKXFtFYC9kD4t78o5Y6LyNJU7AENt8eHPr3quYdxaxK7hj2mnbFfUk9NA==} resolution: {integrity: sha512-FRC8ACiudC3dI6MTplzRSYWHDRnIv2IPfbzs4FdoJNsMal/35sWV8hwIfV8ZcqzSPy+uXHeMVONt9CEqtOU17w==}
peerDependencies: peerDependencies:
'@vitest/browser': 5.0.0 '@vitest/browser': 5.0.1
vitest: 5.0.0 vitest: 5.0.1
peerDependenciesMeta: peerDependenciesMeta:
'@vitest/browser': '@vitest/browser':
optional: true optional: true
@ -632,8 +632,8 @@ packages:
resolution: {integrity: sha512-1EOLRfsTMnyAr3+kEAsP4o9dhaDlGPpD7H5iLBBeq//YpNB1VIahkPhB+eRp9N2Dkfw8oySROjE3yf9XDeaIkQ==} resolution: {integrity: sha512-1EOLRfsTMnyAr3+kEAsP4o9dhaDlGPpD7H5iLBBeq//YpNB1VIahkPhB+eRp9N2Dkfw8oySROjE3yf9XDeaIkQ==}
engines: {node: '>=22'} engines: {node: '>=22'}
'@vitest/mocker@5.0.0': '@vitest/mocker@5.0.1':
resolution: {integrity: sha512-66PGTMIiVJP3t4a5yxU9qPtf7MdTBs8jmToMvy+HVflB3Yy13WJZTtPePdvU+wjRV02SKK5doLbSA6o9pwOmiA==} resolution: {integrity: sha512-6K1DoBNAPGvuOcSsGA4D6x+5zEEff/KmOOP3uetT2TrGpVfI+HRHRnJJfKi5ib/g1vx8IYHQD8s0pbJz8WQI7Q==}
peerDependencies: peerDependencies:
msw: ^2.4.9 msw: ^2.4.9
vite: ^6.0.0 || ^7.0.0 || ^8.0.0 vite: ^6.0.0 || ^7.0.0 || ^8.0.0
@ -643,8 +643,8 @@ packages:
vite: vite:
optional: true optional: true
'@vitest/spy@5.0.0': '@vitest/spy@5.0.1':
resolution: {integrity: sha512-uy+luWBAPw9XfthoHi5AkfHUnuPYEESjl0p/r+meoBnU8bxg5GDQ3Ey8MjcJ6sqahkL4PFyrvfMJJBw7LbU06g==} resolution: {integrity: sha512-rbto/mF/SGERxEgYOek7Xm6B9b+y+mVoo+f4b2LymYO8zM1b7uB5nHuhVMTP2hxdzgxvGiZYGxGIaMvL5y180Q==}
acorn-jsx@5.3.2: acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
@ -1265,20 +1265,20 @@ packages:
yaml: yaml:
optional: true optional: true
vitest@5.0.0: vitest@5.0.1:
resolution: {integrity: sha512-gpsMNoRhMjMktVxPtstOH4/PJuPyovVaMDr4oDilXaGH1EcqM2OE96SoHT2VIQ6fTGtTjqmHDrEu2X9RQiXf8Q==} resolution: {integrity: sha512-iA95lQbKEkvrtTkdAgnWbXfbipWiiWe/hDl2P5tMi6WFwD76G0NxXAGp/M9EOcYupeGJRr6wppMc7CoA41TQjg==}
engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0} engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
'@edge-runtime/vm': '*' '@edge-runtime/vm': '*'
'@opentelemetry/api': ^1.9.0 '@opentelemetry/api': ^1.9.0
'@types/node': ^22.0.0 || >=24.0.0 '@types/node': ^22.0.0 || >=24.0.0
'@vitest/browser-playwright': 5.0.0 '@vitest/browser-playwright': 5.0.1
'@vitest/browser-preview': 5.0.0 '@vitest/browser-preview': 5.0.1
'@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0 '@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0
'@vitest/coverage-istanbul': 5.0.0 '@vitest/coverage-istanbul': 5.0.1
'@vitest/coverage-v8': 5.0.0 '@vitest/coverage-v8': 5.0.1
'@vitest/ui': 5.0.0 '@vitest/ui': 5.0.1
happy-dom: '*' happy-dom: '*'
jsdom: '*' jsdom: '*'
vite: ^6.4.0 || ^7.0.0 || ^8.0.0 vite: ^6.4.0 || ^7.0.0 || ^8.0.0
@ -1487,14 +1487,12 @@ snapshots:
'@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/resolve-uri@3.1.2': {}
'@jridgewell/sourcemap-codec@1.5.5': {}
'@jridgewell/sourcemap-codec@1.6.0': {} '@jridgewell/sourcemap-codec@1.6.0': {}
'@jridgewell/trace-mapping@0.3.31': '@jridgewell/trace-mapping@0.3.31':
dependencies: dependencies:
'@jridgewell/resolve-uri': 3.1.2 '@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/sourcemap-codec': 1.6.0
'@keyv/bigmap@1.3.1(keyv@5.6.0)': '@keyv/bigmap@1.3.1(keyv@5.6.0)':
dependencies: dependencies:
@ -1564,7 +1562,7 @@ snapshots:
picocolors: 1.1.1 picocolors: 1.1.1
pretty-format: 27.5.1 pretty-format: 27.5.1
'@testing-library/jest-dom@7.0.1(@testing-library/dom@10.4.1)(vitest@5.0.0)': '@testing-library/jest-dom@7.0.1(@testing-library/dom@10.4.1)(vitest@5.0.1)':
dependencies: dependencies:
'@adobe/css-tools': 4.5.0 '@adobe/css-tools': 4.5.0
'@testing-library/dom': 10.4.1 '@testing-library/dom': 10.4.1
@ -1574,7 +1572,7 @@ snapshots:
picocolors: 1.1.1 picocolors: 1.1.1
redent: 3.0.0 redent: 3.0.0
optionalDependencies: optionalDependencies:
vitest: 5.0.0(@types/node@26.5.1)(@vitest/coverage-v8@5.0.0)(jsdom@30.0.1)(vite@8.2.2(@types/node@26.5.1)) vitest: 5.0.1(@types/node@26.5.1)(@vitest/coverage-v8@5.0.1)(jsdom@30.0.1)(vite@8.2.2(@types/node@26.5.1))
'@testing-library/react@16.3.3(@testing-library/dom@10.4.1)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': '@testing-library/react@16.3.3(@testing-library/dom@10.4.1)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies: dependencies:
@ -1708,7 +1706,7 @@ snapshots:
'@typescript-eslint/types': 8.70.0 '@typescript-eslint/types': 8.70.0
eslint-visitor-keys: 5.0.1 eslint-visitor-keys: 5.0.1
'@vitest/coverage-v8@5.0.0(vitest@5.0.0)': '@vitest/coverage-v8@5.0.1(vitest@5.0.1)':
dependencies: dependencies:
'@bcoe/v8-coverage': 1.0.2 '@bcoe/v8-coverage': 1.0.2
'@vitest/istanbul-lib-coverage': 1.0.1 '@vitest/istanbul-lib-coverage': 1.0.1
@ -1718,7 +1716,7 @@ snapshots:
obug: 2.1.4 obug: 2.1.4
std-env: 4.2.0 std-env: 4.2.0
tinyrainbow: 3.1.1 tinyrainbow: 3.1.1
vitest: 5.0.0(@types/node@26.5.1)(@vitest/coverage-v8@5.0.0)(jsdom@30.0.1)(vite@8.2.2(@types/node@26.5.1)) vitest: 5.0.1(@types/node@26.5.1)(@vitest/coverage-v8@5.0.1)(jsdom@30.0.1)(vite@8.2.2(@types/node@26.5.1))
'@vitest/istanbul-lib-coverage@1.0.1': {} '@vitest/istanbul-lib-coverage@1.0.1': {}
@ -1726,16 +1724,16 @@ snapshots:
dependencies: dependencies:
'@vitest/istanbul-lib-coverage': 1.0.1 '@vitest/istanbul-lib-coverage': 1.0.1
'@vitest/mocker@5.0.0(vite@8.2.2(@types/node@26.5.1))': '@vitest/mocker@5.0.1(vite@8.2.2(@types/node@26.5.1))':
dependencies: dependencies:
'@jridgewell/trace-mapping': 0.3.31 '@jridgewell/trace-mapping': 0.3.31
'@vitest/spy': 5.0.0 '@vitest/spy': 5.0.1
estree-walker: 3.0.3 estree-walker: 3.0.3
magic-string: 1.3.1 magic-string: 1.3.1
optionalDependencies: optionalDependencies:
vite: 8.2.2(@types/node@26.5.1) vite: 8.2.2(@types/node@26.5.1)
'@vitest/spy@5.0.0': {} '@vitest/spy@5.0.1': {}
acorn-jsx@5.3.2(acorn@8.18.0): acorn-jsx@5.3.2(acorn@8.18.0):
dependencies: dependencies:
@ -2283,10 +2281,10 @@ snapshots:
'@types/node': 26.5.1 '@types/node': 26.5.1
fsevents: 2.3.3 fsevents: 2.3.3
vitest@5.0.0(@types/node@26.5.1)(@vitest/coverage-v8@5.0.0)(jsdom@30.0.1)(vite@8.2.2(@types/node@26.5.1)): vitest@5.0.1(@types/node@26.5.1)(@vitest/coverage-v8@5.0.1)(jsdom@30.0.1)(vite@8.2.2(@types/node@26.5.1)):
dependencies: dependencies:
'@types/chai': 5.2.3 '@types/chai': 5.2.3
'@vitest/mocker': 5.0.0(vite@8.2.2(@types/node@26.5.1)) '@vitest/mocker': 5.0.1(vite@8.2.2(@types/node@26.5.1))
chai: 6.2.2 chai: 6.2.2
es-module-lexer: 2.3.2 es-module-lexer: 2.3.2
expect-type: 1.4.0 expect-type: 1.4.0
@ -2301,7 +2299,7 @@ snapshots:
why-is-node-running: 2.3.0 why-is-node-running: 2.3.0
optionalDependencies: optionalDependencies:
'@types/node': 26.5.1 '@types/node': 26.5.1
'@vitest/coverage-v8': 5.0.0(vitest@5.0.0) '@vitest/coverage-v8': 5.0.1(vitest@5.0.1)
jsdom: 30.0.1 jsdom: 30.0.1
transitivePeerDependencies: transitivePeerDependencies:
- msw - msw

View File

@ -5,5 +5,6 @@
* `import "@kuhyx/web-ui/tokens.css";` * `import "@kuhyx/web-ui/tokens.css";`
*/ */
export { clamp01, fractionFromPointer, nth, quantileValue, valueQuantile } from "./quantile.ts"; export { clamp01, fractionFromPointer, nth, quantileValue, valueQuantile } from "./quantile.ts";
export { RangeSlider, steppedValue, type RangeSliderProps } from "./range-slider.tsx"; export { RangeSlider, type RangeSliderProps } from "./range-slider.tsx";
export { keyTarget, steppedValue } from "./stepped-value.ts";
export { fuzzyMatch } from "./text.ts"; export { fuzzyMatch } from "./text.ts";

View File

@ -55,8 +55,7 @@ export function quantileValue(sorted: readonly number[], f: number): number {
*/ */
export function valueQuantile(sorted: readonly number[], value: number): number { export function valueQuantile(sorted: readonly number[], value: number): number {
const n = sorted.length; const n = sorted.length;
if (n <= 1) return 0; if ((n <= 1) || (value <= nth(sorted, 0))) return 0;
if (value <= nth(sorted, 0)) return 0;
if (value >= nth(sorted, n - 1)) return 1; if (value >= nth(sorted, n - 1)) return 1;
// value strictly inside the range, so there is an index hi >= 1 with // value strictly inside the range, so there is an index hi >= 1 with
// sorted[hi] >= value and sorted[hi - 1] < value. // sorted[hi] >= value and sorted[hi - 1] < value.

View File

@ -1,7 +1,7 @@
import { fireEvent, render, screen } from "@testing-library/react"; import { fireEvent, render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest"; import { describe, expect, it, vi } from "vitest";
import { RangeSlider, steppedValue } from "./range-slider.tsx"; import { RangeSlider } from "./range-slider.tsx";
/** Ten evenly spaced values, so a quantile fraction maps to a predictable value. */ /** 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]; const VALUES = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90];
@ -29,63 +29,6 @@ function thumbs(): HTMLElement[] {
return screen.getAllByRole("slider"); 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);
});
it("returns the current sample for a zero delta", () => {
expect(steppedValue(VALUES, 30, 0)).toBe(30);
});
describe("on a lumpy distribution", () => {
// The shape that actually broke in the browser: awesome-mcp-explorer's
// star counts hold ~1500 duplicate zeros, so an index step inside the run
// moved the index but not the value, and the arrow key looked dead.
const LUMPY = [...Array.from({ length: 50 }, () => 0), 5, 5, 5, 100, 5000];
it("escapes a run of duplicates instead of stalling", () => {
expect(steppedValue(LUMPY, 0, 1)).toBe(5);
});
it("keeps stepping past the second run", () => {
expect(steppedValue(LUMPY, 5, 1)).toBe(100);
});
it("steps back out of a run", () => {
expect(steppedValue(LUMPY, 5, -1)).toBe(0);
});
it("counts distinct values for a multi-step jump", () => {
expect(steppedValue(LUMPY, 0, 2)).toBe(100);
});
it("clamps at the top rather than running off the end", () => {
expect(steppedValue(LUMPY, 5000, 1)).toBe(5000);
});
it("clamps at the bottom", () => {
expect(steppedValue(LUMPY, 0, -1)).toBe(0);
});
});
});
describe("RangeSlider", () => { describe("RangeSlider", () => {
it("renders nothing when there is nothing to range over", () => { it("renders nothing when there is nothing to range over", () => {
const { container } = render(<RangeSlider hi={1} lo={1} onChange={vi.fn()} values={[1]} />); const { container } = render(<RangeSlider hi={1} lo={1} onChange={vi.fn()} values={[1]} />);

View File

@ -22,6 +22,7 @@
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { fractionFromPointer, nth, quantileValue, valueQuantile } from "./quantile.ts"; import { fractionFromPointer, nth, quantileValue, valueQuantile } from "./quantile.ts";
import { keyTarget } from "./stepped-value.ts";
export interface RangeSliderProps { export interface RangeSliderProps {
/** /**
@ -44,40 +45,6 @@ export interface RangeSliderProps {
type Thumb = "hi" | "lo"; type Thumb = "hi" | "lo";
/**
* Steps one thumb by `delta` positions through the distribution. Pure, so the
* keyboard contract is testable without layout.
*
* Steps to the next *distinct* value rather than the next index. Real
* distributions are lumpy -- awesome-mcp-explorer's star counts hold ~1500
* duplicate zeros -- and an index step inside a run of equal values changes the
* index while leaving the value (and therefore the thumb, and the filter)
* exactly where it was. That reads as a dead arrow key.
*/
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 rather than stalling on a fractional step.
const index = Math.round(valueQuantile(values, current) * last);
const direction = Math.sign(delta);
if (direction === 0) return nth(values, index);
let cursor = index;
for (let taken = 0; taken < Math.abs(delta); taken++) {
let next = cursor + direction;
while (next >= 0 && next <= last && nth(values, next) === nth(values, cursor)) {
next += direction;
}
if (next < 0 || next > last) break;
cursor = next;
}
return nth(values, cursor);
}
export function RangeSlider({ export function RangeSlider({
format, format,
hi, hi,
@ -86,7 +53,7 @@ export function RangeSlider({
onChange, onChange,
values, values,
}: RangeSliderProps): null | React.JSX.Element { }: RangeSliderProps): null | React.JSX.Element {
const [drag, setDrag] = useState<Thumb | null>(null); const [drag, setDrag] = useState<null | Thumb>(null);
const apply = useCallback( const apply = useCallback(
(thumb: Thumb, value: number): void => { (thumb: Thumb, value: number): void => {
@ -100,29 +67,20 @@ export function RangeSlider({
// handlers are bound to the track, so currentTarget *is* the track. A ref // 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. // would add a null branch that cannot happen once the element is mounted.
const valueAt = useCallback( const valueAt = useCallback(
(track: HTMLElement, clientX: number): number => (track: HTMLElement, clientX: number): number => {
quantileValue(values, fractionFromPointer(track.getBoundingClientRect(), clientX)), const fraction = fractionFromPointer(track.getBoundingClientRect(), clientX);
return quantileValue(values, fraction);
},
[values], [values],
); );
const onKeyDown = useCallback( const onKeyDown = useCallback(
(thumb: Thumb, event: React.KeyboardEvent): void => { (thumb: Thumb, event: React.KeyboardEvent): void => {
const current = thumb === "lo" ? lo : hi; const current = thumb === "lo" ? lo : hi;
const step = (delta: number): void => { const target = keyTarget(event.key, values, current);
event.preventDefault(); if (target === undefined) return;
apply(thumb, steppedValue(values, current, delta)); event.preventDefault();
}; apply(thumb, target);
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], [apply, hi, lo, values],
); );
@ -136,19 +94,21 @@ export function RangeSlider({
const hiFraction = valueQuantile(values, hi) * 100; const hiFraction = valueQuantile(values, hi) * 100;
const name = label ?? "Range"; const name = label ?? "Range";
const thumbProps = (thumb: Thumb): React.ComponentProps<"button"> => ({ const thumbProps = (thumb: Thumb): React.ComponentProps<"button"> => {
"aria-valuemax": highest, return {
"aria-valuemin": lowest, "aria-valuemax": highest,
"aria-valuenow": thumb === "lo" ? lo : hi, "aria-valuemin": lowest,
className: `slider-thumb slider-${thumb}`, "aria-valuenow": thumb === "lo" ? lo : hi,
onKeyDown: (event): void => { className: `slider-thumb slider-${thumb}`,
onKeyDown(thumb, event); onKeyDown: (event): void => {
}, onKeyDown(thumb, event);
role: "slider", },
style: { left: `${String(thumb === "lo" ? loFraction : hiFraction)}%` }, role: "slider",
tabIndex: 0, style: { left: `${String(thumb === "lo" ? loFraction : hiFraction)}%` },
type: "button", tabIndex: 0,
}); type: "button",
};
};
return ( return (
<div className="slider"> <div className="slider">

View File

@ -0,0 +1,79 @@
import { describe, expect, it } from "vitest";
import { keyTarget, steppedValue } from "./stepped-value.ts";
/** 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];
describe("keyTarget", () => {
it("leaves keys the slider does not own to the browser", () => {
expect(keyTarget("Tab", VALUES, 30)).toBeUndefined();
expect(keyTarget("a", VALUES, 30)).toBeUndefined();
});
it("maps every owned key onto the distribution", () => {
expect(keyTarget("ArrowLeft", VALUES, 30)).toBe(20);
expect(keyTarget("ArrowDown", VALUES, 30)).toBe(20);
expect(keyTarget("ArrowRight", VALUES, 30)).toBe(40);
expect(keyTarget("ArrowUp", VALUES, 30)).toBe(40);
expect(keyTarget("Home", VALUES, 30)).toBe(0);
expect(keyTarget("End", VALUES, 30)).toBe(90);
});
});
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);
});
it("returns the current sample for a zero delta", () => {
expect(steppedValue(VALUES, 30, 0)).toBe(30);
});
describe("on a lumpy distribution", () => {
// The shape that actually broke in the browser: awesome-mcp-explorer's
// star counts hold ~1500 duplicate zeros, so an index step inside the run
// moved the index but not the value, and the arrow key looked dead.
const LUMPY = [...Array.from({ length: 50 }, () => 0), 5, 5, 5, 100, 5000];
it("escapes a run of duplicates instead of stalling", () => {
expect(steppedValue(LUMPY, 0, 1)).toBe(5);
});
it("keeps stepping past the second run", () => {
expect(steppedValue(LUMPY, 5, 1)).toBe(100);
});
it("steps back out of a run", () => {
expect(steppedValue(LUMPY, 5, -1)).toBe(0);
});
it("counts distinct values for a multi-step jump", () => {
expect(steppedValue(LUMPY, 0, 2)).toBe(100);
});
it("clamps at the top rather than running off the end", () => {
expect(steppedValue(LUMPY, 5000, 1)).toBe(5000);
});
it("clamps at the bottom", () => {
expect(steppedValue(LUMPY, 0, -1)).toBe(0);
});
});
});

View File

@ -0,0 +1,78 @@
/**
* The keyboard contract of the range slider, kept pure and in its own module
* so range-slider.tsx exports only the component (react-refresh needs that
* for HMR to keep working) and the contract is testable without layout.
*/
import { nth, valueQuantile } from "./quantile.ts";
/**
* Steps one thumb by `delta` positions through the distribution. Pure, so the
* keyboard contract is testable without layout.
*
* Steps to the next *distinct* value rather than the next index. Real
* distributions are lumpy -- awesome-mcp-explorer's star counts hold ~1500
* duplicate zeros -- and an index step inside a run of equal values changes the
* index while leaving the value (and therefore the thumb, and the filter)
* exactly where it was. That reads as a dead arrow key.
*/
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 rather than stalling on a fractional step.
const index = Math.round(valueQuantile(values, current) * last);
const direction = Math.sign(delta);
if (direction === 0) return nth(values, index);
let cursor = index;
for (let taken = 0; taken < Math.abs(delta); taken++) {
let next = cursor + direction;
while (next >= 0 && next <= last && nth(values, next) === nth(values, cursor)) {
next += direction;
}
if (next < 0 || next > last) break;
cursor = next;
}
return nth(values, cursor);
}
/**
* The value a thumb at `current` moves to for a key, or `undefined` when the
* key is not one the slider handles (so the event is left to the browser).
* Arrow keys step one distinct value, Page keys ten, Home/End jump to the
* bounds.
*/
export function keyTarget(
key: string,
values: readonly number[],
current: number,
): number | undefined {
switch (key) {
case "ArrowDown":
case "ArrowLeft": {
return steppedValue(values, current, -1);
}
case "ArrowRight":
case "ArrowUp": {
return steppedValue(values, current, 1);
}
case "End": {
return nth(values, values.length - 1);
}
case "Home": {
return nth(values, 0);
}
case "PageDown": {
return steppedValue(values, current, -10);
}
case "PageUp": {
return steppedValue(values, current, 10);
}
default: {
return undefined;
}
}
}

View File

@ -13,7 +13,7 @@ const TRACK_WIDTH = 1000;
if (hasDom) { if (hasDom) {
Object.defineProperty(Element.prototype, "getBoundingClientRect", { Object.defineProperty(Element.prototype, "getBoundingClientRect", {
configurable: true, configurable: true,
value(): DOMRect { value: (): DOMRect => {
return { return {
bottom: 20, bottom: 20,
height: 20, height: 20,

View File

@ -1,6 +1,5 @@
// @vitest-environment node // @vitest-environment node
import { readFileSync } from "node:fs"; import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
const css = readFileSync(new URL("tokens.css", import.meta.url), "utf8"); const css = readFileSync(new URL("tokens.css", import.meta.url), "utf8");
@ -13,9 +12,10 @@ const css = readFileSync(new URL("tokens.css", import.meta.url), "utf8");
*/ */
function declared(name: string): string[] { function declared(name: string): string[] {
return [...css.matchAll(new RegExp(`--${name}:\\s*([^;]+);`, "g"))].map((m) => return css
(m[1] ?? "").trim(), .matchAll(new RegExp(String.raw`--${name}:\s*([^;]+);`, "g"))
); .map((m) => (m[1] ?? "").trim())
.toArray();
} }
describe("tokens.css", () => { describe("tokens.css", () => {
@ -124,7 +124,7 @@ describe("tokens.css", () => {
const block = /@media \(prefers-reduced-motion: reduce\)\s*\{([\s\S]*?)\n\}/.exec(css)?.[1]; const block = /@media \(prefers-reduced-motion: reduce\)\s*\{([\s\S]*?)\n\}/.exec(css)?.[1];
expect(block).toBeDefined(); expect(block).toBeDefined();
for (const step of ["fast", "base", "slow"]) { for (const step of ["fast", "base", "slow"]) {
expect(block).toMatch(new RegExp(`--duration-${step}:\\s*0ms;`)); expect(block).toMatch(new RegExp(String.raw`--duration-${step}:\s*0ms;`));
} }
}); });

View File

@ -1,4 +1,6 @@
{ {
// Strictness comes from the shared base; only output/lib options live here.
"extends": "@kuhyx/ts-config/tsconfig",
"compilerOptions": { "compilerOptions": {
"target": "ES2023", "target": "ES2023",
"lib": ["ESNext", "DOM", "DOM.Iterable"], "lib": ["ESNext", "DOM", "DOM.Iterable"],
@ -7,20 +9,6 @@
"jsx": "react-jsx", "jsx": "react-jsx",
"types": ["vitest/globals"], "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 /* This package ships a prebuilt dist/ (ESM + .d.ts) because consumers use
Vite, which does not transpile dependencies. So unlike the consumers, Vite, which does not transpile dependencies. So unlike the consumers,
@ -31,10 +19,7 @@
"declarationMap": true, "declarationMap": true,
"sourceMap": true, "sourceMap": true,
"outDir": "dist", "outDir": "dist",
"rootDir": "src", "rootDir": "src"
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}, },
"include": ["src"], "include": ["src"],
"exclude": ["src/**/*.test.ts", "src/**/*.test.tsx", "src/test"] "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx", "src/test"]