diff --git a/web_ui/README.md b/web_ui/README.md index aed7e66..03b31f3 100644 --- a/web_ui/README.md +++ b/web_ui/README.md @@ -13,7 +13,7 @@ this package is its executable form for the web. ```jsonc // package.json — pnpm only, see "Why the &path: suffix" below. "dependencies": { - "@kuhyx/web-ui": "github:kuhyx/utils#web_ui-v0.2.0&path:/web_ui" + "@kuhyx/web-ui": "github:kuhyx/utils#web_ui-v0.3.0&path:/web_ui" } ``` diff --git a/web_ui/dist/range-slider.d.ts b/web_ui/dist/range-slider.d.ts index 2e633c6..0bf8b57 100644 --- a/web_ui/dist/range-slider.d.ts +++ b/web_ui/dist/range-slider.d.ts @@ -16,8 +16,16 @@ export interface RangeSliderProps { /** The full ascending distribution — not just its endpoints. */ readonly values: readonly number[]; } -/** Steps one thumb by `delta` positions through the distribution. Pure, so the - * keyboard contract is testable without layout. */ +/** + * 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; //# sourceMappingURL=range-slider.d.ts.map \ No newline at end of file diff --git a/web_ui/dist/range-slider.d.ts.map b/web_ui/dist/range-slider.d.ts.map index ac62093..af3fcb5 100644 --- a/web_ui/dist/range-slider.d.ts.map +++ b/web_ui/dist/range-slider.d.ts.map @@ -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;mDACmD;AACnD,wBAAgB,YAAY,CAC1B,MAAM,EAAE,SAAS,MAAM,EAAE,EACzB,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,GACZ,MAAM,CAMR;AAED,wBAAgB,WAAW,CAAC,EAC1B,MAAM,EACN,EAAE,EACF,KAAK,EACL,EAAE,EACF,QAAQ,EACR,MAAM,GACP,EAAE,gBAAgB,GAAG,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CAyG7C"} \ No newline at end of file +{"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"} \ No newline at end of file diff --git a/web_ui/dist/range-slider.js b/web_ui/dist/range-slider.js index 40a4240..9e238d7 100644 --- a/web_ui/dist/range-slider.js +++ b/web_ui/dist/range-slider.js @@ -22,14 +22,35 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; */ import { useCallback, useState } from "react"; import { fractionFromPointer, nth, quantileValue, valueQuantile } from "./quantile.js"; -/** Steps one thumb by `delta` positions through the distribution. Pure, so the - * keyboard contract is testable without layout. */ +/** + * 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 by exactly one item rather than stalling on a fractional step. + // advances rather than stalling on a fractional step. const index = Math.round(valueQuantile(values, current) * last); - return nth(values, Math.min(last, Math.max(0, index + delta))); + 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, }) { const [drag, setDrag] = useState(null); diff --git a/web_ui/dist/range-slider.js.map b/web_ui/dist/range-slider.js.map index d76b440..537c126 100644 --- a/web_ui/dist/range-slider.js.map +++ b/web_ui/dist/range-slider.js.map @@ -1 +1 @@ -{"version":3,"file":"range-slider.js","sourceRoot":"","sources":["../src/range-slider.tsx"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"range-slider.js","sourceRoot":"","sources":["../src/range-slider.tsx"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAE9C,OAAO,EAAE,mBAAmB,EAAE,GAAG,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAuBvF;;;;;;;;;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,MAAM,UAAU,WAAW,CAAC,EAC1B,MAAM,EACN,EAAE,EACF,KAAK,EACL,EAAE,EACF,QAAQ,EACR,MAAM,GACW;IACjB,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAe,IAAI,CAAC,CAAC;IAErD,MAAM,KAAK,GAAG,WAAW,CACvB,CAAC,KAAY,EAAE,KAAa,EAAQ,EAAE;QACpC,IAAI,KAAK,KAAK,IAAI;YAAE,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;;YACjD,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC;IACzC,CAAC,EACD,CAAC,EAAE,EAAE,EAAE,EAAE,QAAQ,CAAC,CACnB,CAAC;IAEF,2EAA2E;IAC3E,0EAA0E;IAC1E,0EAA0E;IAC1E,MAAM,OAAO,GAAG,WAAW,CACzB,CAAC,KAAkB,EAAE,OAAe,EAAU,EAAE,CAC9C,aAAa,CAAC,MAAM,EAAE,mBAAmB,CAAC,KAAK,CAAC,qBAAqB,EAAE,EAAE,OAAO,CAAC,CAAC,EACpF,CAAC,MAAM,CAAC,CACT,CAAC;IAEF,MAAM,SAAS,GAAG,WAAW,CAC3B,CAAC,KAAY,EAAE,KAA0B,EAAQ,EAAE;QACjD,MAAM,OAAO,GAAG,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACzC,MAAM,IAAI,GAAG,CAAC,KAAa,EAAQ,EAAE;YACnC,KAAK,CAAC,cAAc,EAAE,CAAC;YACvB,KAAK,CAAC,KAAK,EAAE,YAAY,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC;QACrD,CAAC,CAAC;QACF,IAAI,KAAK,CAAC,GAAG,KAAK,WAAW,IAAI,KAAK,CAAC,GAAG,KAAK,WAAW;YAAE,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;aAChE,IAAI,KAAK,CAAC,GAAG,KAAK,YAAY,IAAI,KAAK,CAAC,GAAG,KAAK,SAAS;YAAE,IAAI,CAAC,CAAC,CAAC,CAAC;aACnE,IAAI,KAAK,CAAC,GAAG,KAAK,UAAU;YAAE,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC;aACxC,IAAI,KAAK,CAAC,GAAG,KAAK,QAAQ;YAAE,IAAI,CAAC,EAAE,CAAC,CAAC;aACrC,IAAI,KAAK,CAAC,GAAG,KAAK,MAAM,EAAE,CAAC;YAC9B,KAAK,CAAC,cAAc,EAAE,CAAC;YACvB,KAAK,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC;QAC/B,CAAC;aAAM,IAAI,KAAK,CAAC,GAAG,KAAK,KAAK,EAAE,CAAC;YAC/B,KAAK,CAAC,cAAc,EAAE,CAAC;YACvB,KAAK,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC;QAC/C,CAAC;IACH,CAAC,EACD,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,CACxB,CAAC;IAEF,8DAA8D;IAC9D,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAEnC,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IAC9B,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC/C,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC;IACnD,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC;IACnD,MAAM,IAAI,GAAG,KAAK,IAAI,OAAO,CAAC;IAE9B,MAAM,UAAU,GAAG,CAAC,KAAY,EAAkC,EAAE,CAAC,CAAC;QACpE,eAAe,EAAE,OAAO;QACxB,eAAe,EAAE,MAAM;QACvB,eAAe,EAAE,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE;QACzC,SAAS,EAAE,uBAAuB,KAAK,EAAE;QACzC,SAAS,EAAE,CAAC,KAAK,EAAQ,EAAE;YACzB,SAAS,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QAC1B,CAAC;QACD,IAAI,EAAE,QAAQ;QACd,KAAK,EAAE,EAAE,IAAI,EAAE,GAAG,MAAM,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,EAAE;QACvE,QAAQ,EAAE,CAAC;QACX,IAAI,EAAE,QAAQ;KACf,CAAC,CAAC;IAEH,OAAO,CACL,eAAK,SAAS,EAAC,QAAQ,aACpB,KAAK,KAAK,SAAS,IAAI,MAAM,KAAK,SAAS,IAAI,CAC9C,eAAK,SAAS,EAAC,aAAa,aAC1B,yBAAO,KAAK,GAAQ,EACpB,gBAAM,SAAS,EAAC,cAAc,aAC3B,MAAM,CAAC,EAAE,CAAC,cAAK,MAAM,CAAC,EAAE,CAAC,IACrB,IACH,CACP,EACD,eACE,SAAS,EAAC,cAAc,EACxB,aAAa,EAAE,CAAC,KAAK,EAAQ,EAAE;oBAC7B,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,aAAa,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;oBAC1D,mEAAmE;oBACnE,6DAA6D;oBAC7D,MAAM,KAAK,GAAU,IAAI,CAAC,GAAG,CAAC,KAAK,GAAG,EAAE,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;oBAChF,OAAO,CAAC,KAAK,CAAC,CAAC;oBACf,KAAK,CAAC,aAAa,CAAC,iBAAiB,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;oBACvD,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;gBACtB,CAAC,EACD,aAAa,EAAE,CAAC,KAAK,EAAQ,EAAE;oBAC7B,IAAI,IAAI,KAAK,IAAI;wBAAE,OAAO;oBAC1B,KAAK,CAAC,IAAI,EAAE,OAAO,CAAC,KAAK,CAAC,aAAa,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;gBAC3D,CAAC,EACD,WAAW,EAAE,CAAC,KAAK,EAAQ,EAAE;oBAC3B,IAAI,IAAI,KAAK,IAAI;wBAAE,OAAO;oBAC1B,OAAO,CAAC,IAAI,CAAC,CAAC;oBACd,KAAK,CAAC,aAAa,CAAC,qBAAqB,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;gBAC7D,CAAC,aAED,cACE,SAAS,EAAC,aAAa,EACvB,KAAK,EAAE,EAAE,IAAI,EAAE,GAAG,MAAM,CAAC,UAAU,CAAC,GAAG,EAAE,KAAK,EAAE,GAAG,MAAM,CAAC,UAAU,GAAG,UAAU,CAAC,GAAG,EAAE,GACvF,EACF,+BAAoB,GAAG,IAAI,UAAU,KAAM,UAAU,CAAC,IAAI,CAAC,GAAI,EAC/D,+BAAoB,GAAG,IAAI,UAAU,KAAM,UAAU,CAAC,IAAI,CAAC,GAAI,IAC3D,IACF,CACP,CAAC;AACJ,CAAC"} \ No newline at end of file diff --git a/web_ui/package.json b/web_ui/package.json index 1317a7c..f57e80b 100644 --- a/web_ui/package.json +++ b/web_ui/package.json @@ -1,6 +1,6 @@ { "name": "@kuhyx/web-ui", - "version": "0.2.0", + "version": "0.3.0", "private": false, "type": "module", "description": "The shared token + component layer for kuhy's web apps: the frozen palette, spacing/radius/type scales as CSS custom properties, and the components that were each reimplemented in two or more repos.", diff --git a/web_ui/src/range-slider.test.tsx b/web_ui/src/range-slider.test.tsx index 4cc75b5..57a770e 100644 --- a/web_ui/src/range-slider.test.tsx +++ b/web_ui/src/range-slider.test.tsx @@ -49,6 +49,41 @@ describe("steppedValue", () => { 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", () => { diff --git a/web_ui/src/range-slider.tsx b/web_ui/src/range-slider.tsx index ce8604d..fc1b97d 100644 --- a/web_ui/src/range-slider.tsx +++ b/web_ui/src/range-slider.tsx @@ -44,8 +44,16 @@ export interface RangeSliderProps { type Thumb = "hi" | "lo"; -/** Steps one thumb by `delta` positions through the distribution. Pure, so the - * keyboard contract is testable without layout. */ +/** + * 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, @@ -53,9 +61,21 @@ export function steppedValue( ): number { const last = values.length - 1; // Round to a real sample index so a thumb sitting between two samples still - // advances by exactly one item rather than stalling on a fractional step. + // advances rather than stalling on a fractional step. const index = Math.round(valueQuantile(values, current) * last); - return nth(values, Math.min(last, Math.max(0, index + delta))); + 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({