@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.
- package/README.md +10 -0
- package/dist/Badge/Badge.d.ts +2 -37
- package/dist/Badge/Badge.d.ts.map +1 -1
- package/dist/Badge/Badge.js +15 -30
- package/dist/Badge/Badge.js.map +1 -1
- package/dist/Badge/useBadgeShape.d.ts +57 -0
- package/dist/Badge/useBadgeShape.d.ts.map +1 -0
- package/dist/Badge/useBadgeShape.js +35 -0
- package/dist/Badge/useBadgeShape.js.map +1 -0
- package/dist/BadgeButton/BadgeButton.d.ts +59 -0
- package/dist/BadgeButton/BadgeButton.d.ts.map +1 -0
- package/dist/BadgeButton/BadgeButton.js +64 -0
- package/dist/BadgeButton/BadgeButton.js.map +1 -0
- package/dist/Slider/Slider.d.ts +85 -0
- package/dist/Slider/Slider.d.ts.map +1 -0
- package/dist/Slider/Slider.js +193 -0
- package/dist/Slider/Slider.js.map +1 -0
- package/dist/Slider/sliderValue.d.ts +48 -0
- package/dist/Slider/sliderValue.d.ts.map +1 -0
- package/dist/Slider/sliderValue.js +98 -0
- package/dist/Slider/sliderValue.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/Badge/Badge.tsx +30 -113
- package/src/Badge/useBadgeShape.tsx +142 -0
- package/src/BadgeButton/BadgeButton.mdx +67 -0
- package/src/BadgeButton/BadgeButton.stories.tsx +199 -0
- package/src/BadgeButton/BadgeButton.test.tsx +164 -0
- package/src/BadgeButton/BadgeButton.tsx +129 -0
- package/src/Slider/Slider.mdx +78 -0
- package/src/Slider/Slider.stories.tsx +256 -0
- package/src/Slider/Slider.test.tsx +241 -0
- package/src/Slider/Slider.tsx +412 -0
- package/src/Slider/sliderValue.test.ts +156 -0
- package/src/Slider/sliderValue.ts +149 -0
- package/src/index.ts +8 -0
- 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,
|
package/dist/Badge/Badge.d.ts
CHANGED
|
@@ -1,41 +1,6 @@
|
|
|
1
|
-
import type { CategoricalIndex, IntentName } from "@charcuterie/tokens";
|
|
2
1
|
import type { ComponentPropsWithRef, ReactNode } from "react";
|
|
3
|
-
import type {
|
|
4
|
-
|
|
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,
|
|
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"}
|
package/dist/Badge/Badge.js
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
import { jsx as _jsx
|
|
2
|
-
import {
|
|
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
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
package/dist/Badge/Badge.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Badge.js","sourceRoot":"","sources":["../../src/Badge/Badge.tsx"],"names":[],"mappings":";
|
|
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"}
|