@charcuterie/ui 3.8.1 → 3.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +10 -0
  2. package/dist/Badge/Badge.d.ts +2 -37
  3. package/dist/Badge/Badge.d.ts.map +1 -1
  4. package/dist/Badge/Badge.js +15 -30
  5. package/dist/Badge/Badge.js.map +1 -1
  6. package/dist/Badge/useBadgeShape.d.ts +57 -0
  7. package/dist/Badge/useBadgeShape.d.ts.map +1 -0
  8. package/dist/Badge/useBadgeShape.js +35 -0
  9. package/dist/Badge/useBadgeShape.js.map +1 -0
  10. package/dist/BadgeButton/BadgeButton.d.ts +59 -0
  11. package/dist/BadgeButton/BadgeButton.d.ts.map +1 -0
  12. package/dist/BadgeButton/BadgeButton.js +64 -0
  13. package/dist/BadgeButton/BadgeButton.js.map +1 -0
  14. package/dist/Slider/Slider.d.ts +85 -0
  15. package/dist/Slider/Slider.d.ts.map +1 -0
  16. package/dist/Slider/Slider.js +193 -0
  17. package/dist/Slider/Slider.js.map +1 -0
  18. package/dist/Slider/sliderValue.d.ts +48 -0
  19. package/dist/Slider/sliderValue.d.ts.map +1 -0
  20. package/dist/Slider/sliderValue.js +98 -0
  21. package/dist/Slider/sliderValue.js.map +1 -0
  22. package/dist/index.d.ts +5 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +2 -0
  25. package/dist/index.js.map +1 -1
  26. package/package.json +1 -1
  27. package/src/Badge/Badge.tsx +30 -113
  28. package/src/Badge/useBadgeShape.tsx +142 -0
  29. package/src/BadgeButton/BadgeButton.mdx +67 -0
  30. package/src/BadgeButton/BadgeButton.stories.tsx +199 -0
  31. package/src/BadgeButton/BadgeButton.test.tsx +164 -0
  32. package/src/BadgeButton/BadgeButton.tsx +129 -0
  33. package/src/Slider/Slider.mdx +78 -0
  34. package/src/Slider/Slider.stories.tsx +256 -0
  35. package/src/Slider/Slider.test.tsx +241 -0
  36. package/src/Slider/Slider.tsx +412 -0
  37. package/src/Slider/sliderValue.test.ts +156 -0
  38. package/src/Slider/sliderValue.ts +149 -0
  39. package/src/index.ts +8 -0
  40. package/src/sourceRules.test.ts +14 -1
package/README.md CHANGED
@@ -48,6 +48,16 @@ Moving a card takes **no drag-and-drop dependency**: one handle per card, a `Men
48
48
  other lanes as the primary path, Pointer-Event dragging at 1.4 KB gzip beside it
49
49
  ([decision](../../docs/decisions/2026-08-19-the-board-owns-the-move-and-takes-no-drag-and-drop-dependency.md)).
50
50
 
51
+ **`BadgeButton` is a `Badge` you can press**, and it came from a consumer rather than from a
52
+ plan: QueuePilot has six pill-shaped controls — per-entry setting tags, an Edit chip, two
53
+ start-point chips, a group chip, a pool's Exclude chip — that were hand-rolled
54
+ `<button className="badge …">` because `Badge` is a `<span>`. A sibling component, exactly
55
+ as `ButtonLink` is to `Button`: one paint through `useBadgeShape`, compared as **computed**
56
+ styles in the test, and a real `<button>` for focus, Enter, Space, `:disabled` and a role a
57
+ screen reader announces. `onClick` is required, so a pill nobody can press is a `Badge` by
58
+ construction
59
+ ([decision](../../docs/decisions/2026-08-22-a-pressable-badge-is-a-sibling-component-not-a-prop.md)).
60
+
51
61
  **The unified app shell adds four**: `Shell`, `Header`, `Rail`, `Main` — the fleet's largest
52
62
  single duplication, where ten of twelve UI repos hand-roll the page chrome and three of them
53
63
  have a file named `AppShell.tsx` (two of those headers are a *byte-identical* class string,
@@ -1,41 +1,6 @@
1
- import type { CategoricalIndex, IntentName } from "@charcuterie/tokens";
2
1
  import type { ComponentPropsWithRef, ReactNode } from "react";
3
- import type { BadgeSize } from "../controlStyles.ts";
4
- import type { IntentAppearance } from "../intentStyles.ts";
5
- export type BadgeProps = ComponentPropsWithRef<"span"> & {
6
- appearance?: Exclude<IntentAppearance, "ghost">;
7
- /**
8
- * A numbered, **non-semantic** colour: 1–10, no meaning attached.
9
- *
10
- * For a badge whose colour the user picked — a Docket label, a
11
- * project, a chart series — where `intent` would be a lie. A
12
- * "Homelab" label is not a `danger`, and the palette is curated
13
- * and contrast-gated in both schemes precisely so the user cannot
14
- * pick one that is unreadable. `getCategoricalIndex(key)` from
15
- * `@charcuterie/tokens` is the stable fallback for a row nobody
16
- * has chosen a colour for yet.
17
- *
18
- * Mutually exclusive with `intent` **in the type**: a badge is one
19
- * colour, and passing both is a question with no answer rather
20
- * than a precedence rule to remember.
21
- */
22
- categorical?: CategoricalIndex;
23
- children: ReactNode;
24
- icon?: ReactNode;
25
- intent?: IntentName;
26
- /**
27
- * What happens when the label is wider than the space it is given.
28
- *
29
- * - `truncate` — one line, capped at the container, ellipsis. The
30
- * default, because a status pill that changes its row's height
31
- * when a message gets longer breaks the table it sits in.
32
- * - `wrap` — the pill grows taller and shows everything. For the
33
- * kiosk and any touch context, where the hover readout that
34
- * `truncate` relies on does not exist.
35
- */
36
- overflow?: "truncate" | "wrap";
37
- size?: BadgeSize;
38
- } & ({
2
+ import type { BadgeShapeProps } from "./useBadgeShape.tsx";
3
+ export type BadgeProps = ComponentPropsWithRef<"span"> & BadgeShapeProps & {} & ({
39
4
  categorical?: never;
40
5
  } | {
41
6
  intent?: never;
@@ -1 +1 @@
1
- {"version":3,"file":"Badge.d.ts","sourceRoot":"","sources":["../../src/Badge/Badge.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,UAAU,EACX,MAAM,qBAAqB,CAAA;AAC5B,OAAO,KAAK,EACV,qBAAqB,EACrB,SAAS,EACV,MAAM,OAAO,CAAA;AAGd,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAEpD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAK1D,MAAM,MAAM,UAAU,GAAG,qBAAqB,CAAC,MAAM,CAAC,GAAG;IACvD,UAAU,CAAC,EAAE,OAAO,CAAC,gBAAgB,EAAE,OAAO,CAAC,CAAA;IAC/C;;;;;;;;;;;;;;OAcG;IACH,WAAW,CAAC,EAAE,gBAAgB,CAAA;IAC9B,QAAQ,EAAE,SAAS,CAAA;IACnB,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB,MAAM,CAAC,EAAE,UAAU,CAAA;IACnB;;;;;;;;;OASG;IACH,QAAQ,CAAC,EAAE,UAAU,GAAG,MAAM,CAAA;IAC9B,IAAI,CAAC,EAAE,SAAS,CAAA;CAQjB,GAAG,CAAC;IAAE,WAAW,CAAC,EAAE,KAAK,CAAA;CAAE,GAAG;IAAE,MAAM,CAAC,EAAE,KAAK,CAAA;CAAE,CAAC,CAAA;AAElD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,eAAO,MAAM,KAAK,GAAI,qGAWnB,UAAU,KAAG,SAiEf,CAAA"}
1
+ {"version":3,"file":"Badge.d.ts","sourceRoot":"","sources":["../../src/Badge/Badge.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,qBAAqB,EACrB,SAAS,EACV,MAAM,OAAO,CAAA;AAEd,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AAG1D,MAAM,MAAM,UAAU,GAAG,qBAAqB,CAAC,MAAM,CAAC,GACpD,eAAe,GAAG,EAQjB,GAAG,CAAC;IAAE,WAAW,CAAC,EAAE,KAAK,CAAA;CAAE,GAAG;IAAE,MAAM,CAAC,EAAE,KAAK,CAAA;CAAE,CAAC,CAAA;AAEpD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,eAAO,MAAM,KAAK,GAAI,qGAWnB,UAAU,KAAG,SAsBf,CAAA"}
@@ -1,9 +1,5 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { CATEGORICAL_APPEARANCE_CLASS } from "../categoricalStyles.js";
3
- import { BADGE_SIZE_CLASS } from "../controlStyles.js";
4
- import { INTENT_APPEARANCE_CLASS } from "../intentStyles.js";
5
- import { toClassName } from "../toClassName.js";
6
- import { useIsTextClipped } from "./useIsTextClipped.js";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useBadgeShape } from "./useBadgeShape.js";
7
3
  /**
8
4
  * Four repos, and rip-deck declares the **identical** `TONE_CLASS`
9
5
  * map twice — `VerdictBadge.tsx` and `TowerAlerts.tsx`, same five
@@ -44,29 +40,18 @@ import { useIsTextClipped } from "./useIsTextClipped.js";
44
40
  * across the next cell. `max-inline-size: 100%` is what stops it, and
45
41
  * `overflow` is what happens next.
46
42
  */
47
- export const Badge = ({ appearance = "soft", categorical, children, className, icon, intent = "neutral", overflow = "truncate", size = "md", title, ...spanProps }) => {
48
- const [labelRef, clippedText] = useIsTextClipped();
49
- return (_jsxs("span", { ...spanProps, className: toClassName("inline-flex shrink-0 items-center rounded-full border font-medium",
50
- // The cap. Without it every other rule here is decoration.
51
- "max-w-full", overflow === "wrap"
52
- ? // A stadium end-cap on a three-line box reads as a
53
- // rendering fault rather than a badge, so the wrapping
54
- // pill relaxes to a rounded rectangle.
55
- "rounded-2xl"
56
- : "rounded-full", BADGE_SIZE_CLASS[size],
57
- // A ternary rather than a merged map: the two are indexed
58
- // by different keys, and `undefined` is the only honest
59
- // discriminant once the type has already refused the case
60
- // where both arrive.
61
- categorical === undefined
62
- ? INTENT_APPEARANCE_CLASS[intent][appearance]
63
- : CATEGORICAL_APPEARANCE_CLASS[categorical][appearance], className),
64
- // Only when something is actually hidden. A tooltip on every
65
- // short pill in a bay list is noise, and a `title` that
66
- // duplicates fully-visible text is a screen-reader duplicate
67
- // for no gain.
68
- title: title ?? clippedText, children: [icon ? (_jsx("span", { "aria-hidden": "true", className: "contents shrink-0", children: icon })) : null, _jsx("span", { className: overflow === "wrap"
69
- ? "min-w-0"
70
- : "min-w-0 overflow-hidden text-ellipsis whitespace-nowrap", ref: labelRef, children: children })] }));
43
+ export const Badge = ({ appearance, categorical, children, className, icon, intent, overflow, size, title, ...spanProps }) => {
44
+ const shape = useBadgeShape({
45
+ appearance,
46
+ categorical,
47
+ children,
48
+ className,
49
+ icon,
50
+ intent,
51
+ overflow,
52
+ size,
53
+ title,
54
+ });
55
+ return (_jsx("span", { ...spanProps, className: shape.className, title: shape.title, children: shape.content }));
71
56
  };
72
57
  //# sourceMappingURL=Badge.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Badge.js","sourceRoot":"","sources":["../../src/Badge/Badge.tsx"],"names":[],"mappings":";AASA,OAAO,EAAE,4BAA4B,EAAE,MAAM,yBAAyB,CAAA;AAEtE,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AAEtD,OAAO,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAA;AAC5D,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AA4CxD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,CAAC,EACpB,UAAU,GAAG,MAAM,EACnB,WAAW,EACX,QAAQ,EACR,SAAS,EACT,IAAI,EACJ,MAAM,GAAG,SAAS,EAClB,QAAQ,GAAG,UAAU,EACrB,IAAI,GAAG,IAAI,EACX,KAAK,EACL,GAAG,SAAS,EACD,EAAa,EAAE;IAC1B,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,gBAAgB,EAAE,CAAA;IAElD,OAAO,CACL,mBACM,SAAS,EACb,SAAS,EAAE,WAAW,CACpB,mEAAmE;QACnE,2DAA2D;QAC3D,YAAY,EACZ,QAAQ,KAAK,MAAM;YACjB,CAAC,CAAC,mDAAmD;gBACnD,uDAAuD;gBACvD,uCAAuC;gBACvC,aAAa;YACf,CAAC,CAAC,cAAc,EAClB,gBAAgB,CAAC,IAAI,CAAC;QACtB,0DAA0D;QAC1D,wDAAwD;QACxD,0DAA0D;QAC1D,qBAAqB;QACrB,WAAW,KAAK,SAAS;YACvB,CAAC,CAAC,uBAAuB,CAAC,MAAM,CAAC,CAAC,UAAU,CAAC;YAC7C,CAAC,CAAC,4BAA4B,CAAC,WAAW,CAAC,CACvC,UAAU,CACX,EACL,SAAS,CACV;QACD,6DAA6D;QAC7D,wDAAwD;QACxD,6DAA6D;QAC7D,eAAe;QACf,KAAK,EAAE,KAAK,IAAI,WAAW,aAE1B,IAAI,CAAC,CAAC,CAAC,CACN,8BACc,MAAM,EAClB,SAAS,EAAC,mBAAmB,YAE5B,IAAI,GACA,CACR,CAAC,CAAC,CAAC,IAAI,EAYR,eACE,SAAS,EACP,QAAQ,KAAK,MAAM;oBACjB,CAAC,CAAC,SAAS;oBACX,CAAC,CAAC,yDAAyD,EAE/D,GAAG,EAAE,QAAQ,YAEZ,QAAQ,GACJ,IACF,CACR,CAAA;AACH,CAAC,CAAA"}
1
+ {"version":3,"file":"Badge.js","sourceRoot":"","sources":["../../src/Badge/Badge.tsx"],"names":[],"mappings":";AAMA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAqB,CAAA;AAanD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,CAAC,EACpB,UAAU,EACV,WAAW,EACX,QAAQ,EACR,SAAS,EACT,IAAI,EACJ,MAAM,EACN,QAAQ,EACR,IAAI,EACJ,KAAK,EACL,GAAG,SAAS,EACD,EAAa,EAAE;IAC1B,MAAM,KAAK,GAAG,aAAa,CAAC;QAC1B,UAAU;QACV,WAAW;QACX,QAAQ;QACR,SAAS;QACT,IAAI;QACJ,MAAM;QACN,QAAQ;QACR,IAAI;QACJ,KAAK;KACN,CAAC,CAAA;IAEF,OAAO,CACL,kBACM,SAAS,EACb,SAAS,EAAE,KAAK,CAAC,SAAS,EAC1B,KAAK,EAAE,KAAK,CAAC,KAAK,YAEjB,KAAK,CAAC,OAAO,GACT,CACR,CAAA;AACH,CAAC,CAAA"}
@@ -0,0 +1,57 @@
1
+ import type { CategoricalIndex, IntentName } from "@charcuterie/tokens";
2
+ import type { ReactNode } from "react";
3
+ import type { BadgeSize } from "../controlStyles.ts";
4
+ import type { IntentAppearance } from "../intentStyles.ts";
5
+ /**
6
+ * The visual half of a badge, shared by the two elements that wear
7
+ * it.
8
+ *
9
+ * `Badge` is a `<span>` and `BadgeButton` is a `<button>`, and the
10
+ * pill is supposed to be indistinguishable between them — so the
11
+ * class list, the icon slot, the truncation and the
12
+ * only-when-clipped `title` are computed **here**, once, exactly as
13
+ * `Button` and `ButtonLink` both call `getControlClassName`. A
14
+ * second copy of this arithmetic is how the two drift: the wrapping
15
+ * pill's `rounded-2xl` and the label's `min-w-0` are each one line,
16
+ * and each is load-bearing.
17
+ */
18
+ export type BadgeShapeProps = {
19
+ appearance?: Exclude<IntentAppearance, "ghost">;
20
+ /**
21
+ * A numbered, **non-semantic** colour: 1–10, no meaning attached.
22
+ *
23
+ * For a badge whose colour the user picked — a Docket label, a
24
+ * project, a chart series — where `intent` would be a lie.
25
+ * `getCategoricalIndex(key)` from `@charcuterie/tokens` is the
26
+ * stable fallback for a row nobody has chosen a colour for yet.
27
+ *
28
+ * Mutually exclusive with `intent` **in the type**: a badge is one
29
+ * colour, and passing both is a question with no answer rather
30
+ * than a precedence rule to remember.
31
+ */
32
+ categorical?: CategoricalIndex;
33
+ children: ReactNode;
34
+ icon?: ReactNode;
35
+ intent?: IntentName;
36
+ /**
37
+ * What happens when the label is wider than the space it is given.
38
+ *
39
+ * - `truncate` — one line, capped at the container, ellipsis. The
40
+ * default, because a status pill that changes its row's height
41
+ * when a message gets longer breaks the table it sits in.
42
+ * - `wrap` — the pill grows taller and shows everything. For the
43
+ * kiosk and any touch context, where the hover readout that
44
+ * `truncate` relies on does not exist.
45
+ */
46
+ overflow?: "truncate" | "wrap";
47
+ size?: BadgeSize;
48
+ };
49
+ export declare const useBadgeShape: ({ appearance, categorical, children, className, icon, intent, overflow, size, title, }: BadgeShapeProps & {
50
+ className?: string;
51
+ title?: string;
52
+ }) => {
53
+ className: string;
54
+ content: ReactNode;
55
+ title?: string;
56
+ };
57
+ //# sourceMappingURL=useBadgeShape.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useBadgeShape.d.ts","sourceRoot":"","sources":["../../src/Badge/useBadgeShape.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,UAAU,EACX,MAAM,qBAAqB,CAAA;AAC5B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAGtC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAEpD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAK1D;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,UAAU,CAAC,EAAE,OAAO,CAAC,gBAAgB,EAAE,OAAO,CAAC,CAAA;IAC/C;;;;;;;;;;;OAWG;IACH,WAAW,CAAC,EAAE,gBAAgB,CAAA;IAC9B,QAAQ,EAAE,SAAS,CAAA;IACnB,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB,MAAM,CAAC,EAAE,UAAU,CAAA;IACnB;;;;;;;;;OASG;IACH,QAAQ,CAAC,EAAE,UAAU,GAAG,MAAM,CAAA;IAC9B,IAAI,CAAC,EAAE,SAAS,CAAA;CACjB,CAAA;AAED,eAAO,MAAM,aAAa,GAAI,wFAU3B,eAAe,GAAG;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,KAAG;IACF,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,SAAS,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;CAkEf,CAAA"}
@@ -0,0 +1,35 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { CATEGORICAL_APPEARANCE_CLASS } from "../categoricalStyles.js";
3
+ import { BADGE_SIZE_CLASS } from "../controlStyles.js";
4
+ import { INTENT_APPEARANCE_CLASS } from "../intentStyles.js";
5
+ import { toClassName } from "../toClassName.js";
6
+ import { useIsTextClipped } from "./useIsTextClipped.js";
7
+ export const useBadgeShape = ({ appearance = "soft", categorical, children, className, icon, intent = "neutral", overflow = "truncate", size = "md", title, }) => {
8
+ const [labelRef, clippedText] = useIsTextClipped();
9
+ return {
10
+ className: toClassName("inline-flex shrink-0 items-center border font-medium",
11
+ // The cap. Without it every other rule here is decoration.
12
+ "max-w-full", overflow === "wrap"
13
+ ? // A stadium end-cap on a three-line box reads as a
14
+ // rendering fault rather than a badge, so the wrapping
15
+ // pill relaxes to a rounded rectangle.
16
+ "rounded-2xl"
17
+ : "rounded-full", BADGE_SIZE_CLASS[size],
18
+ // A ternary rather than a merged map: the two are indexed
19
+ // by different keys, and `undefined` is the only honest
20
+ // discriminant once the type has already refused the case
21
+ // where both arrive.
22
+ categorical === undefined
23
+ ? INTENT_APPEARANCE_CLASS[intent][appearance]
24
+ : CATEGORICAL_APPEARANCE_CLASS[categorical][appearance], className),
25
+ content: (_jsxs(_Fragment, { children: [icon ? (_jsx("span", { "aria-hidden": "true", className: "contents shrink-0", children: icon })) : null, _jsx("span", { className: overflow === "wrap"
26
+ ? "min-w-0"
27
+ : "min-w-0 overflow-hidden text-ellipsis whitespace-nowrap", ref: labelRef, children: children })] })),
28
+ // Only when something is actually hidden. A tooltip on every
29
+ // short pill in a bay list is noise, and a `title` that
30
+ // duplicates fully-visible text is a screen-reader duplicate
31
+ // for no gain.
32
+ title: title ?? clippedText,
33
+ };
34
+ };
35
+ //# sourceMappingURL=useBadgeShape.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useBadgeShape.js","sourceRoot":"","sources":["../../src/Badge/useBadgeShape.tsx"],"names":[],"mappings":";AAMA,OAAO,EAAE,4BAA4B,EAAE,MAAM,yBAAyB,CAAA;AAEtE,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AAEtD,OAAO,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAA;AAC5D,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AA+CxD,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,EAC5B,UAAU,GAAG,MAAM,EACnB,WAAW,EACX,QAAQ,EACR,SAAS,EACT,IAAI,EACJ,MAAM,GAAG,SAAS,EAClB,QAAQ,GAAG,UAAU,EACrB,IAAI,GAAG,IAAI,EACX,KAAK,GAIN,EAIC,EAAE;IACF,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,gBAAgB,EAAE,CAAA;IAElD,OAAO;QACL,SAAS,EAAE,WAAW,CACpB,sDAAsD;QACtD,2DAA2D;QAC3D,YAAY,EACZ,QAAQ,KAAK,MAAM;YACjB,CAAC,CAAC,mDAAmD;gBACnD,uDAAuD;gBACvD,uCAAuC;gBACvC,aAAa;YACf,CAAC,CAAC,cAAc,EAClB,gBAAgB,CAAC,IAAI,CAAC;QACtB,0DAA0D;QAC1D,wDAAwD;QACxD,0DAA0D;QAC1D,qBAAqB;QACrB,WAAW,KAAK,SAAS;YACvB,CAAC,CAAC,uBAAuB,CAAC,MAAM,CAAC,CAAC,UAAU,CAAC;YAC7C,CAAC,CAAC,4BAA4B,CAAC,WAAW,CAAC,CACvC,UAAU,CACX,EACL,SAAS,CACV;QACD,OAAO,EAAE,CACP,8BACG,IAAI,CAAC,CAAC,CAAC,CACN,8BACc,MAAM,EAClB,SAAS,EAAC,mBAAmB,YAE5B,IAAI,GACA,CACR,CAAC,CAAC,CAAC,IAAI,EAYR,eACE,SAAS,EACP,QAAQ,KAAK,MAAM;wBACjB,CAAC,CAAC,SAAS;wBACX,CAAC,CAAC,yDAAyD,EAE/D,GAAG,EAAE,QAAQ,YAEZ,QAAQ,GACJ,IACN,CACJ;QACD,6DAA6D;QAC7D,wDAAwD;QACxD,6DAA6D;QAC7D,eAAe;QACf,KAAK,EAAE,KAAK,IAAI,WAAW;KAC5B,CAAA;AACH,CAAC,CAAA"}
@@ -0,0 +1,59 @@
1
+ import type { ComponentPropsWithRef, ReactNode } from "react";
2
+ import type { BadgeShapeProps } from "../Badge/useBadgeShape.tsx";
3
+ export type BadgeButtonProps = Omit<ComponentPropsWithRef<"button">, "disabled"> & BadgeShapeProps & {
4
+ /**
5
+ * Required, and that is the component's argument in one line: a
6
+ * pill nobody can press is a `Badge`, and a pill that looks
7
+ * pressable and is not is the defect this exists to stop.
8
+ */
9
+ onClick: NonNullable<ComponentPropsWithRef<"button">["onClick"]>;
10
+ /**
11
+ * `isDisabled`, not `disabled` — the same prop `Button` keeps
12
+ * instead of the native one, so a control's disabled state is
13
+ * spelled one way across the library.
14
+ */
15
+ isDisabled?: boolean;
16
+ } & ({
17
+ categorical?: never;
18
+ } | {
19
+ intent?: never;
20
+ });
21
+ /**
22
+ * A badge you can press.
23
+ *
24
+ * A sibling component rather than a prop, exactly as `ButtonLink` is
25
+ * to `Button`: the paint is shared (both call `useBadgeShape`, so
26
+ * there is one class list and `BadgeButton.test.tsx` compares the
27
+ * two elements' *computed* styles rather than trusting that), and
28
+ * what differs is the element and everything the platform hangs off
29
+ * it.
30
+ *
31
+ * **Why not `asChild`, and why not `Badge` growing an `onClick`.**
32
+ * This library has no polymorphism pattern and has not needed one;
33
+ * adding it here would mean every future component answering "can it
34
+ * be something else?" instead of "what is it?". And an element type
35
+ * that changes as a *side effect* of passing a handler is a footgun
36
+ * with no error: forget the handler and a control silently ships as
37
+ * a `<span>` — not focusable, not in the tab order, invisible to
38
+ * `getByRole("button")` and to every agent driving the app.
39
+ *
40
+ * **What the element buys, and none of it is paint.** Focus, the tab
41
+ * order, Enter and Space, `:disabled`, form participation, and a
42
+ * `button` role a screen reader announces as pressable. QueuePilot
43
+ * had six pills doing all of this with a hand-rolled
44
+ * `<button className="badge …">`, which is where this came from —
45
+ * six chips that open an editor, change a start point, or drop a
46
+ * title from a pool.
47
+ *
48
+ * **`type="button"` by default.** These chips sit inside forms —
49
+ * QueuePilot's entry sheet is one — and the platform's default of
50
+ * `submit` would make a "change the start point" chip save the whole
51
+ * dialog. Overridable, because a badge-shaped submit is a real thing
52
+ * and the default is a safe floor rather than a ban.
53
+ *
54
+ * **`ghost` is still excluded**, inherited from `Badge`: a pill that
55
+ * paints nothing until hovered has no pill left, and a control that
56
+ * only advertises itself on hover cannot be found by touch.
57
+ */
58
+ export declare const BadgeButton: ({ appearance, categorical, children, className, icon, intent, isDisabled, overflow, size, title, type, ...buttonProps }: BadgeButtonProps) => ReactNode;
59
+ //# sourceMappingURL=BadgeButton.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BadgeButton.d.ts","sourceRoot":"","sources":["../../src/BadgeButton/BadgeButton.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,qBAAqB,EACrB,SAAS,EACV,MAAM,OAAO,CAAA;AAEd,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAA;AAUjE,MAAM,MAAM,gBAAgB,GAAG,IAAI,CACjC,qBAAqB,CAAC,QAAQ,CAAC,EAC/B,UAAU,CACX,GACC,eAAe,GAAG;IAChB;;;;OAIG;IACH,OAAO,EAAE,WAAW,CAClB,qBAAqB,CAAC,QAAQ,CAAC,CAAC,SAAS,CAAC,CAC3C,CAAA;IACD;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;CAKrB,GAAG,CAAC;IAAE,WAAW,CAAC,EAAE,KAAK,CAAA;CAAE,GAAG;IAAE,MAAM,CAAC,EAAE,KAAK,CAAA;CAAE,CAAC,CAAA;AAEpD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,eAAO,MAAM,WAAW,GAAI,yHAazB,gBAAgB,KAAG,SAsCrB,CAAA"}
@@ -0,0 +1,64 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useBadgeShape } from "../Badge/useBadgeShape.js";
3
+ import { CATEGORICAL_HOVER_CLASS } from "../categoricalStyles.js";
4
+ import { DISABLED_CLASS, FOCUS_RING_CLASS, INTENT_HOVER_CLASS, } from "../intentStyles.js";
5
+ import { toClassName } from "../toClassName.js";
6
+ /**
7
+ * A badge you can press.
8
+ *
9
+ * A sibling component rather than a prop, exactly as `ButtonLink` is
10
+ * to `Button`: the paint is shared (both call `useBadgeShape`, so
11
+ * there is one class list and `BadgeButton.test.tsx` compares the
12
+ * two elements' *computed* styles rather than trusting that), and
13
+ * what differs is the element and everything the platform hangs off
14
+ * it.
15
+ *
16
+ * **Why not `asChild`, and why not `Badge` growing an `onClick`.**
17
+ * This library has no polymorphism pattern and has not needed one;
18
+ * adding it here would mean every future component answering "can it
19
+ * be something else?" instead of "what is it?". And an element type
20
+ * that changes as a *side effect* of passing a handler is a footgun
21
+ * with no error: forget the handler and a control silently ships as
22
+ * a `<span>` — not focusable, not in the tab order, invisible to
23
+ * `getByRole("button")` and to every agent driving the app.
24
+ *
25
+ * **What the element buys, and none of it is paint.** Focus, the tab
26
+ * order, Enter and Space, `:disabled`, form participation, and a
27
+ * `button` role a screen reader announces as pressable. QueuePilot
28
+ * had six pills doing all of this with a hand-rolled
29
+ * `<button className="badge …">`, which is where this came from —
30
+ * six chips that open an editor, change a start point, or drop a
31
+ * title from a pool.
32
+ *
33
+ * **`type="button"` by default.** These chips sit inside forms —
34
+ * QueuePilot's entry sheet is one — and the platform's default of
35
+ * `submit` would make a "change the start point" chip save the whole
36
+ * dialog. Overridable, because a badge-shaped submit is a real thing
37
+ * and the default is a safe floor rather than a ban.
38
+ *
39
+ * **`ghost` is still excluded**, inherited from `Badge`: a pill that
40
+ * paints nothing until hovered has no pill left, and a control that
41
+ * only advertises itself on hover cannot be found by touch.
42
+ */
43
+ export const BadgeButton = ({ appearance = "soft", categorical, children, className, icon, intent = "neutral", isDisabled = false, overflow, size, title, type = "button", ...buttonProps }) => {
44
+ const shape = useBadgeShape({
45
+ appearance,
46
+ categorical,
47
+ children,
48
+ icon,
49
+ intent,
50
+ overflow,
51
+ size,
52
+ title,
53
+ });
54
+ return (_jsx("button", { ...buttonProps, className: toClassName(shape.className, "cursor-pointer",
55
+ // The three states the `<span>` has no business carrying,
56
+ // keyed the same way the appearance above was.
57
+ categorical === undefined
58
+ ? INTENT_HOVER_CLASS[intent][appearance]
59
+ : CATEGORICAL_HOVER_CLASS[categorical][appearance], FOCUS_RING_CLASS, DISABLED_CLASS,
60
+ // Last, so a caller's layout beats the defaults above — the
61
+ // same order `getControlClassName` uses.
62
+ className), disabled: isDisabled, title: shape.title, type: type, children: shape.content }));
63
+ };
64
+ //# sourceMappingURL=BadgeButton.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BadgeButton.js","sourceRoot":"","sources":["../../src/BadgeButton/BadgeButton.tsx"],"names":[],"mappings":";AAMA,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA4B,CAAA;AAC1D,OAAO,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAA;AACjE,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,kBAAkB,GACnB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AA2B/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,EAC1B,UAAU,GAAG,MAAM,EACnB,WAAW,EACX,QAAQ,EACR,SAAS,EACT,IAAI,EACJ,MAAM,GAAG,SAAS,EAClB,UAAU,GAAG,KAAK,EAClB,QAAQ,EACR,IAAI,EACJ,KAAK,EACL,IAAI,GAAG,QAAQ,EACf,GAAG,WAAW,EACG,EAAa,EAAE;IAChC,MAAM,KAAK,GAAG,aAAa,CAAC;QAC1B,UAAU;QACV,WAAW;QACX,QAAQ;QACR,IAAI;QACJ,MAAM;QACN,QAAQ;QACR,IAAI;QACJ,KAAK;KACN,CAAC,CAAA;IAEF,OAAO,CACL,oBACM,WAAW,EACf,SAAS,EAAE,WAAW,CACpB,KAAK,CAAC,SAAS,EACf,gBAAgB;QAChB,0DAA0D;QAC1D,+CAA+C;QAC/C,WAAW,KAAK,SAAS;YACvB,CAAC,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAC,UAAU,CAAC;YACxC,CAAC,CAAC,uBAAuB,CAAC,WAAW,CAAC,CAClC,UAAU,CACX,EACL,gBAAgB,EAChB,cAAc;QACd,4DAA4D;QAC5D,yCAAyC;QACzC,SAAS,CACV,EACD,QAAQ,EAAE,UAAU,EACpB,KAAK,EAAE,KAAK,CAAC,KAAK,EAClB,IAAI,EAAE,IAAI,YAET,KAAK,CAAC,OAAO,GACP,CACV,CAAA;AACH,CAAC,CAAA"}
@@ -0,0 +1,85 @@
1
+ import type { IntentName } from "@charcuterie/tokens";
2
+ import type { ComponentPropsWithRef, ReactNode } from "react";
3
+ export type SliderSize = "lg" | "md" | "sm";
4
+ export type SliderProps = Omit<ComponentPropsWithRef<"div">, "children" | "defaultValue" | "onChange"> & {
5
+ intent?: IntentName;
6
+ isDisabled?: boolean;
7
+ isLabelVisible?: boolean;
8
+ /**
9
+ * Read-only in the `aria-readonly` sense: focusable, full contrast,
10
+ * announced as unchangeable. A media scrubber for a stream with no
11
+ * seekable range is this, not `isDisabled` — the position is still
12
+ * worth reading.
13
+ */
14
+ isReadOnly?: boolean;
15
+ isValueShown?: boolean;
16
+ /** Required. A slider with no name is an unlabelled number. */
17
+ label: string;
18
+ /**
19
+ * PageUp / PageDown. Defaults to ten steps, which is the APG's
20
+ * suggestion and the only sensible answer that does not need the
21
+ * caller to know the range.
22
+ */
23
+ largeStep?: number;
24
+ max?: number;
25
+ min?: number;
26
+ /**
27
+ * Fires on every movement — each arrow key, and each pointer sample
28
+ * while dragging. Use it to paint. See `onChangeEnd` for committing.
29
+ */
30
+ onChange?: (value: number) => void;
31
+ /**
32
+ * Fires once, when the interaction ends: pointer release, or key up.
33
+ *
34
+ * This exists because the expensive consumer of a slider is almost
35
+ * never the paint. A seek scrubber that issued its network call from
36
+ * `onChange` would send one request per pointer sample and arrive at
37
+ * the right offset last, after a queue of stale ones.
38
+ */
39
+ onChangeEnd?: (value: number) => void;
40
+ size?: SliderSize;
41
+ step?: number;
42
+ value?: number;
43
+ /**
44
+ * How the value reads aloud and, when `isValueShown`, on screen.
45
+ * Without it a scrubber announces "1274", which is a number of
46
+ * seconds nobody has.
47
+ */
48
+ valueFormat?: (value: number) => string;
49
+ };
50
+ /**
51
+ * A single-value range control: the thing `<input type="range">` is,
52
+ * without the OS painting it.
53
+ *
54
+ * The fleet had no way to ask for a value along a range, so QueuePilot's
55
+ * Now-playing bar could not have a scrubber
56
+ * ([decision](../../../../docs/decisions/2026-08-22-the-slider-is-a-div-with-role-slider-not-an-input-range.md)).
57
+ * `ProgressBar` looks like the answer and is not: it is output, it takes
58
+ * no input, and `role="progressbar"` tells assistive technology exactly
59
+ * that.
60
+ *
61
+ * ## Why a `div` and not `<input type="range">`
62
+ *
63
+ * The same reason `Select` is deprecated. A range input's thumb and
64
+ * track are UA pseudo-elements — `::-webkit-slider-thumb`,
65
+ * `::-moz-range-track` — and they are not one surface: the two engines
66
+ * disagree about which of them the track's fill even belongs to, so a
67
+ * token-styled range is two stylesheets that drift. Windows paints its
68
+ * own. `role="slider"` on a focusable `div` is the APG's own pattern,
69
+ * gets the same AT support, and every pixel of it is ours.
70
+ *
71
+ * ## Drag does not commit
72
+ *
73
+ * `onChange` fires per movement, `onChangeEnd` once on release. Both are
74
+ * optional and the component is fully controlled through `value`, so a
75
+ * caller that wants the simple thing passes `onChange` alone and a
76
+ * caller with a network call behind it splits them.
77
+ *
78
+ * While dragging, the component holds the value locally and paints from
79
+ * that rather than from the `value` prop. Without it a controlled
80
+ * scrubber whose owner only commits `onChangeEnd` would render the thumb
81
+ * pinned under the finger at the old position — the drag would look
82
+ * broken while behaving correctly.
83
+ */
84
+ export declare const Slider: ({ className, intent, isDisabled, isLabelVisible, isReadOnly, isValueShown, label, largeStep, max, min, onChange, onChangeEnd, size, step, value, valueFormat, ...divProps }: SliderProps) => ReactNode;
85
+ //# sourceMappingURL=Slider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Slider.d.ts","sourceRoot":"","sources":["../../src/Slider/Slider.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AACrD,OAAO,KAAK,EACV,qBAAqB,EAGrB,SAAS,EACV,MAAM,OAAO,CAAA;AAgBd,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAA;AAE3C,MAAM,MAAM,WAAW,GAAG,IAAI,CAC5B,qBAAqB,CAAC,KAAK,CAAC,EAC5B,UAAU,GAAG,cAAc,GAAG,UAAU,CACzC,GAAG;IACF,MAAM,CAAC,EAAE,UAAU,CAAA;IACnB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,+DAA+D;IAC/D,KAAK,EAAE,MAAM,CAAA;IACb;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAClC;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACrC,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAA;CACxC,CAAA;AAqBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,eAAO,MAAM,MAAM,GAAI,6KAkBpB,WAAW,KAAG,SAyQhB,CAAA"}