@charcuterie/ui 0.1.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/CHANGELOG.md +13 -0
- package/README.md +161 -0
- package/dist/Alert/Alert.d.ts +88 -0
- package/dist/Alert/Alert.d.ts.map +1 -0
- package/dist/Alert/Alert.js +67 -0
- package/dist/Alert/Alert.js.map +1 -0
- package/dist/Badge/Badge.d.ts +54 -0
- package/dist/Badge/Badge.d.ts.map +1 -0
- package/dist/Badge/Badge.js +54 -0
- package/dist/Badge/Badge.js.map +1 -0
- package/dist/Badge/useIsTextClipped.d.ts +22 -0
- package/dist/Badge/useIsTextClipped.d.ts.map +1 -0
- package/dist/Badge/useIsTextClipped.js +57 -0
- package/dist/Badge/useIsTextClipped.js.map +1 -0
- package/dist/Button/Button.d.ts +58 -0
- package/dist/Button/Button.d.ts.map +1 -0
- package/dist/Button/Button.js +37 -0
- package/dist/Button/Button.js.map +1 -0
- package/dist/Card/Card.d.ts +44 -0
- package/dist/Card/Card.d.ts.map +1 -0
- package/dist/Card/Card.js +59 -0
- package/dist/Card/Card.js.map +1 -0
- package/dist/EmptyState/EmptyState.d.ts +42 -0
- package/dist/EmptyState/EmptyState.d.ts.map +1 -0
- package/dist/EmptyState/EmptyState.js +37 -0
- package/dist/EmptyState/EmptyState.js.map +1 -0
- package/dist/IconButton/IconButton.d.ts +34 -0
- package/dist/IconButton/IconButton.d.ts.map +1 -0
- package/dist/IconButton/IconButton.js +23 -0
- package/dist/IconButton/IconButton.js.map +1 -0
- package/dist/LiveStatusIndicator/LiveStatusIndicator.d.ts +37 -0
- package/dist/LiveStatusIndicator/LiveStatusIndicator.d.ts.map +1 -0
- package/dist/LiveStatusIndicator/LiveStatusIndicator.js +39 -0
- package/dist/LiveStatusIndicator/LiveStatusIndicator.js.map +1 -0
- package/dist/MediaTile/MediaTile.d.ts +46 -0
- package/dist/MediaTile/MediaTile.d.ts.map +1 -0
- package/dist/MediaTile/MediaTile.js +79 -0
- package/dist/MediaTile/MediaTile.js.map +1 -0
- package/dist/MediaTile/mediaStatus.d.ts +22 -0
- package/dist/MediaTile/mediaStatus.d.ts.map +1 -0
- package/dist/MediaTile/mediaStatus.js +24 -0
- package/dist/MediaTile/mediaStatus.js.map +1 -0
- package/dist/Modal/Modal.d.ts +66 -0
- package/dist/Modal/Modal.d.ts.map +1 -0
- package/dist/Modal/Modal.js +111 -0
- package/dist/Modal/Modal.js.map +1 -0
- package/dist/Modal/lockScrollBehind.d.ts +22 -0
- package/dist/Modal/lockScrollBehind.d.ts.map +1 -0
- package/dist/Modal/lockScrollBehind.js +48 -0
- package/dist/Modal/lockScrollBehind.js.map +1 -0
- package/dist/Popover/Popover.d.ts +66 -0
- package/dist/Popover/Popover.d.ts.map +1 -0
- package/dist/Popover/Popover.js +88 -0
- package/dist/Popover/Popover.js.map +1 -0
- package/dist/ProgressBar/ProgressBar.d.ts +51 -0
- package/dist/ProgressBar/ProgressBar.d.ts.map +1 -0
- package/dist/ProgressBar/ProgressBar.js +50 -0
- package/dist/ProgressBar/ProgressBar.js.map +1 -0
- package/dist/ProgressBar/progressValue.d.ts +44 -0
- package/dist/ProgressBar/progressValue.d.ts.map +1 -0
- package/dist/ProgressBar/progressValue.js +55 -0
- package/dist/ProgressBar/progressValue.js.map +1 -0
- package/dist/SegmentedControl/SegmentedControl.d.ts +59 -0
- package/dist/SegmentedControl/SegmentedControl.d.ts.map +1 -0
- package/dist/SegmentedControl/SegmentedControl.js +129 -0
- package/dist/SegmentedControl/SegmentedControl.js.map +1 -0
- package/dist/SegmentedControl/SegmentedOption.d.ts +28 -0
- package/dist/SegmentedControl/SegmentedOption.d.ts.map +1 -0
- package/dist/SegmentedControl/SegmentedOption.js +40 -0
- package/dist/SegmentedControl/SegmentedOption.js.map +1 -0
- package/dist/Skeleton/Skeleton.d.ts +30 -0
- package/dist/Skeleton/Skeleton.d.ts.map +1 -0
- package/dist/Skeleton/Skeleton.js +43 -0
- package/dist/Skeleton/Skeleton.js.map +1 -0
- package/dist/Spinner/Spinner.d.ts +37 -0
- package/dist/Spinner/Spinner.d.ts.map +1 -0
- package/dist/Spinner/Spinner.js +33 -0
- package/dist/Spinner/Spinner.js.map +1 -0
- package/dist/Tabs/TabTrigger.d.ts +32 -0
- package/dist/Tabs/TabTrigger.d.ts.map +1 -0
- package/dist/Tabs/TabTrigger.js +62 -0
- package/dist/Tabs/TabTrigger.js.map +1 -0
- package/dist/Tabs/Tabs.d.ts +95 -0
- package/dist/Tabs/Tabs.d.ts.map +1 -0
- package/dist/Tabs/Tabs.js +195 -0
- package/dist/Tabs/Tabs.js.map +1 -0
- package/dist/VisuallyHidden/VisuallyHidden.d.ts +25 -0
- package/dist/VisuallyHidden/VisuallyHidden.d.ts.map +1 -0
- package/dist/VisuallyHidden/VisuallyHidden.js +23 -0
- package/dist/VisuallyHidden/VisuallyHidden.js.map +1 -0
- package/dist/controlStyles.d.ts +50 -0
- package/dist/controlStyles.d.ts.map +1 -0
- package/dist/controlStyles.js +60 -0
- package/dist/controlStyles.js.map +1 -0
- package/dist/expectNoAxeViolations.testHelpers.d.ts +19 -0
- package/dist/expectNoAxeViolations.testHelpers.d.ts.map +1 -0
- package/dist/expectNoAxeViolations.testHelpers.js +32 -0
- package/dist/expectNoAxeViolations.testHelpers.js.map +1 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +55 -0
- package/dist/index.js.map +1 -0
- package/dist/intentStyles.d.ts +76 -0
- package/dist/intentStyles.d.ts.map +1 -0
- package/dist/intentStyles.js +150 -0
- package/dist/intentStyles.js.map +1 -0
- package/dist/mountStory.testHelpers.d.ts +30 -0
- package/dist/mountStory.testHelpers.d.ts.map +1 -0
- package/dist/mountStory.testHelpers.js +19 -0
- package/dist/mountStory.testHelpers.js.map +1 -0
- package/dist/statusIntent.d.ts +33 -0
- package/dist/statusIntent.d.ts.map +1 -0
- package/dist/statusIntent.js +99 -0
- package/dist/statusIntent.js.map +1 -0
- package/dist/styles.css +130 -0
- package/dist/testing/expectAgentDrivable.d.ts +53 -0
- package/dist/testing/expectAgentDrivable.d.ts.map +1 -0
- package/dist/testing/expectAgentDrivable.js +163 -0
- package/dist/testing/expectAgentDrivable.js.map +1 -0
- package/dist/testing/index.d.ts +11 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +10 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/toClassName.d.ts +12 -0
- package/dist/toClassName.d.ts.map +1 -0
- package/dist/toClassName.js +12 -0
- package/dist/toClassName.js.map +1 -0
- package/dist/tokens.d.ts +16 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +16 -0
- package/dist/tokens.js.map +1 -0
- package/package.json +70 -0
- package/src/Alert/Alert.mdx +70 -0
- package/src/Alert/Alert.stories.tsx +218 -0
- package/src/Alert/Alert.test.tsx +112 -0
- package/src/Alert/Alert.tsx +166 -0
- package/src/Badge/Badge.mdx +43 -0
- package/src/Badge/Badge.stories.tsx +229 -0
- package/src/Badge/Badge.test.tsx +153 -0
- package/src/Badge/Badge.tsx +130 -0
- package/src/Badge/useIsTextClipped.ts +77 -0
- package/src/Button/Button.mdx +44 -0
- package/src/Button/Button.stories.tsx +175 -0
- package/src/Button/Button.test.tsx +114 -0
- package/src/Button/Button.tsx +125 -0
- package/src/Card/Card.mdx +53 -0
- package/src/Card/Card.stories.tsx +207 -0
- package/src/Card/Card.test.tsx +56 -0
- package/src/Card/Card.tsx +142 -0
- package/src/EmptyState/EmptyState.mdx +40 -0
- package/src/EmptyState/EmptyState.stories.tsx +183 -0
- package/src/EmptyState/EmptyState.test.tsx +41 -0
- package/src/EmptyState/EmptyState.tsx +114 -0
- package/src/IconButton/IconButton.mdx +34 -0
- package/src/IconButton/IconButton.stories.tsx +191 -0
- package/src/IconButton/IconButton.test.tsx +70 -0
- package/src/IconButton/IconButton.tsx +54 -0
- package/src/LiveStatusIndicator/LiveStatusIndicator.mdx +43 -0
- package/src/LiveStatusIndicator/LiveStatusIndicator.stories.tsx +199 -0
- package/src/LiveStatusIndicator/LiveStatusIndicator.test.tsx +63 -0
- package/src/LiveStatusIndicator/LiveStatusIndicator.tsx +116 -0
- package/src/MediaTile/MediaTile.mdx +51 -0
- package/src/MediaTile/MediaTile.stories.tsx +211 -0
- package/src/MediaTile/MediaTile.test.tsx +41 -0
- package/src/MediaTile/MediaTile.tsx +227 -0
- package/src/MediaTile/mediaStatus.ts +28 -0
- package/src/Modal/Modal.mdx +113 -0
- package/src/Modal/Modal.stories.tsx +272 -0
- package/src/Modal/Modal.test.tsx +248 -0
- package/src/Modal/Modal.tsx +228 -0
- package/src/Modal/lockScrollBehind.ts +59 -0
- package/src/Popover/Popover.mdx +102 -0
- package/src/Popover/Popover.stories.tsx +247 -0
- package/src/Popover/Popover.test.tsx +191 -0
- package/src/Popover/Popover.tsx +164 -0
- package/src/ProgressBar/ProgressBar.mdx +48 -0
- package/src/ProgressBar/ProgressBar.stories.tsx +232 -0
- package/src/ProgressBar/ProgressBar.test.tsx +74 -0
- package/src/ProgressBar/ProgressBar.tsx +172 -0
- package/src/ProgressBar/progressValue.test.ts +101 -0
- package/src/ProgressBar/progressValue.ts +87 -0
- package/src/SegmentedControl/SegmentedControl.mdx +71 -0
- package/src/SegmentedControl/SegmentedControl.stories.tsx +150 -0
- package/src/SegmentedControl/SegmentedControl.test.tsx +208 -0
- package/src/SegmentedControl/SegmentedControl.tsx +219 -0
- package/src/SegmentedControl/SegmentedOption.tsx +94 -0
- package/src/Skeleton/Skeleton.mdx +30 -0
- package/src/Skeleton/Skeleton.stories.tsx +130 -0
- package/src/Skeleton/Skeleton.test.tsx +47 -0
- package/src/Skeleton/Skeleton.tsx +106 -0
- package/src/Spinner/Spinner.mdx +36 -0
- package/src/Spinner/Spinner.stories.tsx +118 -0
- package/src/Spinner/Spinner.test.tsx +38 -0
- package/src/Spinner/Spinner.tsx +92 -0
- package/src/Tabs/TabTrigger.tsx +126 -0
- package/src/Tabs/Tabs.mdx +126 -0
- package/src/Tabs/Tabs.stories.tsx +187 -0
- package/src/Tabs/Tabs.test.tsx +283 -0
- package/src/Tabs/Tabs.tsx +333 -0
- package/src/VisuallyHidden/VisuallyHidden.mdx +29 -0
- package/src/VisuallyHidden/VisuallyHidden.stories.tsx +58 -0
- package/src/VisuallyHidden/VisuallyHidden.test.tsx +31 -0
- package/src/VisuallyHidden/VisuallyHidden.tsx +43 -0
- package/src/argTypes.storyHelpers.ts +115 -0
- package/src/board.storyHelpers.tsx +150 -0
- package/src/controlStyles.ts +79 -0
- package/src/expectNoAxeViolations.testHelpers.ts +41 -0
- package/src/icons.storyHelpers.tsx +116 -0
- package/src/index.ts +132 -0
- package/src/intentStyles.ts +207 -0
- package/src/mdxReferences.test.ts +95 -0
- package/src/mountStory.testHelpers.ts +41 -0
- package/src/sourceRules.test.ts +303 -0
- package/src/statusIntent.test.ts +136 -0
- package/src/statusIntent.ts +137 -0
- package/src/storyControls.test.ts +128 -0
- package/src/styles.css +130 -0
- package/src/tailwindCandidates.test.ts +377 -0
- package/src/testing/expectAgentDrivable.test.tsx +71 -0
- package/src/testing/expectAgentDrivable.ts +273 -0
- package/src/testing/index.ts +17 -0
- package/src/toClassName.ts +13 -0
- package/src/tokens.ts +16 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clamped, rounded, and NaN-proof, in a file a Node test can reach
|
|
3
|
+
* without a browser.
|
|
4
|
+
*
|
|
5
|
+
* All three cases are real. rip-deck's `fillPercent` comes from a
|
|
6
|
+
* ripper's stdout, and a progress value that arrives as `-1`,
|
|
7
|
+
* `101`, or `NaN` from a log parse currently renders as a bar that
|
|
8
|
+
* overflows its track or vanishes. `max: 0` is the one that looks
|
|
9
|
+
* hypothetical and is not: it is "0 of 0 files done", which every
|
|
10
|
+
* empty queue produces on its first render.
|
|
11
|
+
*/
|
|
12
|
+
export const toProgressPercent = (value, max) => {
|
|
13
|
+
if (!Number.isFinite(value) || !Number.isFinite(max)) {
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
if (max <= 0) {
|
|
17
|
+
return 0;
|
|
18
|
+
}
|
|
19
|
+
return Math.min(100, Math.max(0, Math.round((value / max) * 100)));
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* The same clamp in the value's own units, for `aria-valuenow`.
|
|
23
|
+
*
|
|
24
|
+
* Reporting the raw prop there would hand assistive technology a
|
|
25
|
+
* `valuenow` outside its own `valuemin`/`valuemax` — which some
|
|
26
|
+
* screen readers render as a nonsense percentage and others drop
|
|
27
|
+
* entirely — while the bar on screen showed the clamped one. The
|
|
28
|
+
* two must not disagree.
|
|
29
|
+
*/
|
|
30
|
+
export const toProgressValue = (value, max) => {
|
|
31
|
+
if (!Number.isFinite(value) || !Number.isFinite(max)) {
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
return Math.min(Math.max(0, max), Math.max(0, value));
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* The threshold colour, or the component's own intent when no
|
|
38
|
+
* threshold applies.
|
|
39
|
+
*
|
|
40
|
+
* This is the piece rip-deck hardcodes as `FILL_CLASS` keyed by a
|
|
41
|
+
* rip state, and mux-magic does not have at all. Generalising it to
|
|
42
|
+
* "percentage → intent" covers both the "green when done" case and
|
|
43
|
+
* the "amber past 80% of the disk" case with one mechanism.
|
|
44
|
+
*/
|
|
45
|
+
export const getProgressIntent = (percent, intent, thresholds) => {
|
|
46
|
+
if (!thresholds || thresholds.length === 0) {
|
|
47
|
+
return intent;
|
|
48
|
+
}
|
|
49
|
+
const matched = thresholds
|
|
50
|
+
.filter((threshold) => percent >= threshold.from)
|
|
51
|
+
.sort((first, second) => second.from - first.from)
|
|
52
|
+
.at(0);
|
|
53
|
+
return matched?.intent ?? intent;
|
|
54
|
+
};
|
|
55
|
+
//# sourceMappingURL=progressValue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"progressValue.js","sourceRoot":"","sources":["../../src/ProgressBar/progressValue.ts"],"names":[],"mappings":"AAaA;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAC/B,KAAa,EACb,GAAW,EACX,EAAE;IACF,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACrD,OAAO,CAAC,CAAA;IACV,CAAC;IAED,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC;QACb,OAAO,CAAC,CAAA;IACV,CAAC;IAED,OAAO,IAAI,CAAC,GAAG,CACb,GAAG,EACH,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,CAC7C,CAAA;AACH,CAAC,CAAA;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAC7B,KAAa,EACb,GAAW,EACX,EAAE;IACF,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACrD,OAAO,CAAC,CAAA;IACV,CAAC;IAED,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAA;AACvD,CAAC,CAAA;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAC/B,OAAe,EACf,MAAkB,EAClB,UAAyC,EAC7B,EAAE;IACd,IAAI,CAAC,UAAU,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3C,OAAO,MAAM,CAAA;IACf,CAAC;IAED,MAAM,OAAO,GAAG,UAAU;SACvB,MAAM,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,OAAO,IAAI,SAAS,CAAC,IAAI,CAAC;SAChD,IAAI,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC;SACjD,EAAE,CAAC,CAAC,CAAC,CAAA;IAER,OAAO,OAAO,EAAE,MAAM,IAAI,MAAM,CAAA;AAClC,CAAC,CAAA"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { ControlSize } from "@charcuterie/tokens";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
export type SegmentedItem = {
|
|
4
|
+
/**
|
|
5
|
+
* A disabled option is simply **not registered** with the roving
|
|
6
|
+
* group, so the arrow keys skip it without any command in
|
|
7
|
+
* `RovingFocus` having to know what "disabled" means. It stays
|
|
8
|
+
* registered with the picker: it is still one of the options.
|
|
9
|
+
*/
|
|
10
|
+
isDisabled?: boolean;
|
|
11
|
+
label: ReactNode;
|
|
12
|
+
value: string;
|
|
13
|
+
};
|
|
14
|
+
export type SegmentedControlProps = {
|
|
15
|
+
className?: string;
|
|
16
|
+
items: readonly SegmentedItem[];
|
|
17
|
+
/** The group's accessible name. Required. */
|
|
18
|
+
label: string;
|
|
19
|
+
onChange?: (selectedValue: string | null) => void;
|
|
20
|
+
/** **Initial** only. Charcuterie owns it from then on. */
|
|
21
|
+
selectedValue?: string;
|
|
22
|
+
size?: ControlSize;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* One choice out of a handful, all of them on screen at once.
|
|
26
|
+
*
|
|
27
|
+
* This is `Tabs`' composition — `SinglePicker` + `RovingFocus` —
|
|
28
|
+
* with the panels taken away, and it exists because taking the
|
|
29
|
+
* panels away used to be impossible. Under M4's model a tab bar's
|
|
30
|
+
* selection *was* a group of visibilities, so a control that chooses
|
|
31
|
+
* something without revealing anything had nothing to reuse and
|
|
32
|
+
* hand-rolled its own state. rip-deck's `ColumnPicker` is that
|
|
33
|
+
* hand-roll
|
|
34
|
+
* ([decision](../../../docs/decisions/2026-07-30-tab-selection-is-a-single-picker.md)).
|
|
35
|
+
*
|
|
36
|
+
* ### `radiogroup`, not a row of pressed buttons
|
|
37
|
+
*
|
|
38
|
+
* rip-deck's version is five `<button aria-pressed>` in a
|
|
39
|
+
* `<fieldset>`, which is the **toolbar of independent toggles**
|
|
40
|
+
* pattern: nothing in it says the five are mutually exclusive, so a
|
|
41
|
+
* screen reader announces "auto, pressed" with no "1 of 5" and no
|
|
42
|
+
* clue that pressing another un-presses this one. A single choice
|
|
43
|
+
* from a named set is a radio group, and ARIA has a pattern for it.
|
|
44
|
+
*
|
|
45
|
+
* ### Selection follows focus, and here that is correct
|
|
46
|
+
*
|
|
47
|
+
* `Tabs` has two activation modes because a tab panel can cost a
|
|
48
|
+
* network request. A radio group has one: APG says the arrow keys
|
|
49
|
+
* both move focus and check, and there is nothing behind an option
|
|
50
|
+
* here to be expensive. So the mode is `automatic` and there is no
|
|
51
|
+
* prop — a `manual` segmented control would be a control the arrow
|
|
52
|
+
* keys appear to do nothing in.
|
|
53
|
+
*
|
|
54
|
+
* The two kinds are still separate underneath, which is what makes
|
|
55
|
+
* Space's behaviour trivial rather than a special case: it checks
|
|
56
|
+
* whatever focus is on, whether or not focus moved there by arrow.
|
|
57
|
+
*/
|
|
58
|
+
export declare const SegmentedControl: ({ className, items, label, onChange, selectedValue, size, }: SegmentedControlProps) => ReactNode;
|
|
59
|
+
//# sourceMappingURL=SegmentedControl.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SegmentedControl.d.ts","sourceRoot":"","sources":["../../src/SegmentedControl/SegmentedControl.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AACtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAMtC,MAAM,MAAM,aAAa,GAAG;IAC1B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,KAAK,EAAE,SAAS,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,SAAS,aAAa,EAAE,CAAA;IAC/B,6CAA6C;IAC7C,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IACjD,0DAA0D;IAC1D,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,IAAI,CAAC,EAAE,WAAW,CAAA;CACnB,CAAA;AAQD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,eAAO,MAAM,gBAAgB,GAAI,6DAO9B,qBAAqB,KAAG,SAwI1B,CAAA"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { selectTabIndex, useRovingFocus, useSinglePicker, } from "@charcuterie/logic";
|
|
3
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
4
|
+
import { toClassName } from "../toClassName.js";
|
|
5
|
+
import { SegmentedOption } from "./SegmentedOption.js";
|
|
6
|
+
const GAP_CLASS = {
|
|
7
|
+
sm: "gap-0.5",
|
|
8
|
+
md: "gap-1",
|
|
9
|
+
lg: "gap-1",
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* One choice out of a handful, all of them on screen at once.
|
|
13
|
+
*
|
|
14
|
+
* This is `Tabs`' composition — `SinglePicker` + `RovingFocus` —
|
|
15
|
+
* with the panels taken away, and it exists because taking the
|
|
16
|
+
* panels away used to be impossible. Under M4's model a tab bar's
|
|
17
|
+
* selection *was* a group of visibilities, so a control that chooses
|
|
18
|
+
* something without revealing anything had nothing to reuse and
|
|
19
|
+
* hand-rolled its own state. rip-deck's `ColumnPicker` is that
|
|
20
|
+
* hand-roll
|
|
21
|
+
* ([decision](../../../docs/decisions/2026-07-30-tab-selection-is-a-single-picker.md)).
|
|
22
|
+
*
|
|
23
|
+
* ### `radiogroup`, not a row of pressed buttons
|
|
24
|
+
*
|
|
25
|
+
* rip-deck's version is five `<button aria-pressed>` in a
|
|
26
|
+
* `<fieldset>`, which is the **toolbar of independent toggles**
|
|
27
|
+
* pattern: nothing in it says the five are mutually exclusive, so a
|
|
28
|
+
* screen reader announces "auto, pressed" with no "1 of 5" and no
|
|
29
|
+
* clue that pressing another un-presses this one. A single choice
|
|
30
|
+
* from a named set is a radio group, and ARIA has a pattern for it.
|
|
31
|
+
*
|
|
32
|
+
* ### Selection follows focus, and here that is correct
|
|
33
|
+
*
|
|
34
|
+
* `Tabs` has two activation modes because a tab panel can cost a
|
|
35
|
+
* network request. A radio group has one: APG says the arrow keys
|
|
36
|
+
* both move focus and check, and there is nothing behind an option
|
|
37
|
+
* here to be expensive. So the mode is `automatic` and there is no
|
|
38
|
+
* prop — a `manual` segmented control would be a control the arrow
|
|
39
|
+
* keys appear to do nothing in.
|
|
40
|
+
*
|
|
41
|
+
* The two kinds are still separate underneath, which is what makes
|
|
42
|
+
* Space's behaviour trivial rather than a special case: it checks
|
|
43
|
+
* whatever focus is on, whether or not focus moved there by arrow.
|
|
44
|
+
*/
|
|
45
|
+
export const SegmentedControl = ({ className, items, label, onChange, selectedValue, size = "md", }) => {
|
|
46
|
+
const groupRef = useRef(null);
|
|
47
|
+
const optionElements = useRef(new Map());
|
|
48
|
+
const initialValue = selectedValue ??
|
|
49
|
+
items.find((one) => !one.isDisabled)?.value ??
|
|
50
|
+
null;
|
|
51
|
+
const selection = useSinglePicker({
|
|
52
|
+
onChange,
|
|
53
|
+
selectedValue: initialValue,
|
|
54
|
+
});
|
|
55
|
+
const focus = useRovingFocus({
|
|
56
|
+
activeValue: initialValue,
|
|
57
|
+
});
|
|
58
|
+
const { select } = selection;
|
|
59
|
+
const { activeValue, setActiveValue } = focus;
|
|
60
|
+
/**
|
|
61
|
+
* Selected **or** pending. Members register from an effect, so on
|
|
62
|
+
* the first paint `selectedValue` is still null and the intent
|
|
63
|
+
* lives in `pendingValue` — reading only the former renders a
|
|
64
|
+
* group with nothing checked, then corrects itself a frame later.
|
|
65
|
+
* A flash on a desktop; a visible blank on a kiosk Pi.
|
|
66
|
+
*/
|
|
67
|
+
const checkedValue = selection.selectedValue ?? selection.pendingValue;
|
|
68
|
+
const chooseOption = useCallback((value) => {
|
|
69
|
+
setActiveValue(value);
|
|
70
|
+
select(value);
|
|
71
|
+
}, [select, setActiveValue]);
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
// Selection follows focus — the radio-group rule, and the one
|
|
74
|
+
// line that would be absent in a `manual` mode this component
|
|
75
|
+
// deliberately does not have.
|
|
76
|
+
if (activeValue !== null) {
|
|
77
|
+
select(activeValue);
|
|
78
|
+
}
|
|
79
|
+
}, [activeValue, select]);
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
const group = groupRef.current;
|
|
82
|
+
if (!group ||
|
|
83
|
+
activeValue === null ||
|
|
84
|
+
!group.contains(document.activeElement)) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
optionElements.current.get(activeValue)?.focus();
|
|
88
|
+
}, [activeValue]);
|
|
89
|
+
return (_jsx("div", { "aria-label": label, className: toClassName("inline-flex items-center rounded-md border border-border-subtle bg-surface-sunken p-0.5", GAP_CLASS[size], className), onKeyDown: (keyEvent) => {
|
|
90
|
+
const commands = {
|
|
91
|
+
ArrowDown: focus.next,
|
|
92
|
+
ArrowLeft: focus.previous,
|
|
93
|
+
ArrowRight: focus.next,
|
|
94
|
+
ArrowUp: focus.previous,
|
|
95
|
+
End: focus.last,
|
|
96
|
+
Home: focus.first,
|
|
97
|
+
};
|
|
98
|
+
const command = commands[keyEvent.key];
|
|
99
|
+
if (command) {
|
|
100
|
+
// Arrow keys scroll the page by default, and a control
|
|
101
|
+
// that scrolls the window while it moves focus is the
|
|
102
|
+
// hand-rolled version of this component.
|
|
103
|
+
keyEvent.preventDefault();
|
|
104
|
+
command();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
// Space checks the focused option. In a group where
|
|
108
|
+
// selection already follows focus this is almost always a
|
|
109
|
+
// no-op — which is the point: it is one command, not a
|
|
110
|
+
// second code path, because `select` is idempotent.
|
|
111
|
+
if (keyEvent.key === " " && activeValue !== null) {
|
|
112
|
+
keyEvent.preventDefault();
|
|
113
|
+
select(activeValue);
|
|
114
|
+
}
|
|
115
|
+
}, ref: groupRef, role: "radiogroup", children: items.map((item) => (_jsx(SegmentedOption, { isChecked: checkedValue === item.value, item: item, onChoose: chooseOption, registerFocus: focus.register, registerSelection: selection.register, size: size,
|
|
116
|
+
// The roving-tabindex rule read from the core rather than
|
|
117
|
+
// restated here: exactly one option is in the tab order,
|
|
118
|
+
// so Tab enters and leaves the group once while the arrow
|
|
119
|
+
// keys move inside it.
|
|
120
|
+
tabIndex: selectTabIndex(focus, item.value), trackElement: (value, element) => {
|
|
121
|
+
if (element) {
|
|
122
|
+
optionElements.current.set(value, element);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
optionElements.current.delete(value);
|
|
126
|
+
}
|
|
127
|
+
} }, item.value))) }));
|
|
128
|
+
};
|
|
129
|
+
//# sourceMappingURL=SegmentedControl.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SegmentedControl.js","sourceRoot":"","sources":["../../src/SegmentedControl/SegmentedControl.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ControlSize } from "@charcuterie/tokens";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import type { SegmentedItem } from "./SegmentedControl.tsx";
|
|
4
|
+
export type SegmentedOptionProps = {
|
|
5
|
+
isChecked: boolean;
|
|
6
|
+
item: SegmentedItem;
|
|
7
|
+
onChoose: (value: string) => void;
|
|
8
|
+
/** `RovingFocus.register` — membership of the arrow-key group. */
|
|
9
|
+
registerFocus: (value: string) => () => void;
|
|
10
|
+
/** `SinglePicker.register` — membership of the choice. */
|
|
11
|
+
registerSelection: (value: string) => () => void;
|
|
12
|
+
size: ControlSize;
|
|
13
|
+
tabIndex: number;
|
|
14
|
+
trackElement: (value: string, element: HTMLButtonElement | null) => void;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Its own file for the same reason `TabTrigger` is: **both
|
|
18
|
+
* registrations are effects**, and an effect cannot run in a loop.
|
|
19
|
+
* Any member of a registering group ends up shaped like this.
|
|
20
|
+
*
|
|
21
|
+
* The checked option is filled rather than merely tinted. A
|
|
22
|
+
* segmented control's whole job is to be readable at a glance from
|
|
23
|
+
* across a room — rip-deck's lives above a nine-bay grid on a wall
|
|
24
|
+
* display — and a one-shade difference does not survive that or a
|
|
25
|
+
* `daylight` theme in sunlight.
|
|
26
|
+
*/
|
|
27
|
+
export declare const SegmentedOption: ({ isChecked, item, onChoose, registerFocus, registerSelection, size, tabIndex, trackElement, }: SegmentedOptionProps) => ReactNode;
|
|
28
|
+
//# sourceMappingURL=SegmentedOption.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SegmentedOption.d.ts","sourceRoot":"","sources":["../../src/SegmentedControl/SegmentedOption.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AACtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAMtC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAE3D,MAAM,MAAM,oBAAoB,GAAG;IACjC,SAAS,EAAE,OAAO,CAAA;IAClB,IAAI,EAAE,aAAa,CAAA;IACnB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACjC,kEAAkE;IAClE,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,IAAI,CAAA;IAC5C,0DAA0D;IAC1D,iBAAiB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,IAAI,CAAA;IAChD,IAAI,EAAE,WAAW,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,CACZ,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,iBAAiB,GAAG,IAAI,KAC9B,IAAI,CAAA;CACV,CAAA;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,eAAe,GAAI,gGAS7B,oBAAoB,KAAG,SAgDzB,CAAA"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
import { CONTROL_SIZE_CLASS } from "../controlStyles.js";
|
|
4
|
+
import { FOCUS_RING_CLASS } from "../intentStyles.js";
|
|
5
|
+
import { toClassName } from "../toClassName.js";
|
|
6
|
+
/**
|
|
7
|
+
* Its own file for the same reason `TabTrigger` is: **both
|
|
8
|
+
* registrations are effects**, and an effect cannot run in a loop.
|
|
9
|
+
* Any member of a registering group ends up shaped like this.
|
|
10
|
+
*
|
|
11
|
+
* The checked option is filled rather than merely tinted. A
|
|
12
|
+
* segmented control's whole job is to be readable at a glance from
|
|
13
|
+
* across a room — rip-deck's lives above a nine-bay grid on a wall
|
|
14
|
+
* display — and a one-shade difference does not survive that or a
|
|
15
|
+
* `daylight` theme in sunlight.
|
|
16
|
+
*/
|
|
17
|
+
export const SegmentedOption = ({ isChecked, item, onChoose, registerFocus, registerSelection, size, tabIndex, trackElement, }) => {
|
|
18
|
+
const { isDisabled = false, label, value } = item;
|
|
19
|
+
useEffect(() => registerSelection(value), [registerSelection, value]);
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (isDisabled) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
return registerFocus(value);
|
|
25
|
+
}, [isDisabled, registerFocus, value]);
|
|
26
|
+
return (_jsx("button", { "aria-checked": isChecked, className: toClassName("cursor-pointer rounded-sm border border-transparent font-medium tabular-nums transition-colors duration-(--duration-fast) ease-standard", CONTROL_SIZE_CLASS[size],
|
|
27
|
+
// Two entries rather than one interpolated string:
|
|
28
|
+
// `tailwindCandidates.test.ts` rejects a template literal in
|
|
29
|
+
// a className outright, because Tailwind's scanner cannot
|
|
30
|
+
// see a class that only exists at runtime.
|
|
31
|
+
isChecked
|
|
32
|
+
? "bg-intent-accent-solid text-intent-accent-on-solid"
|
|
33
|
+
: "text-content-secondary hover:bg-intent-neutral-surface hover:text-content-primary", isDisabled &&
|
|
34
|
+
"cursor-not-allowed text-content-disabled hover:bg-transparent hover:text-content-disabled", FOCUS_RING_CLASS), disabled: isDisabled, onClick: () => {
|
|
35
|
+
onChoose(value);
|
|
36
|
+
}, ref: (element) => {
|
|
37
|
+
trackElement(value, element);
|
|
38
|
+
}, role: "radio", tabIndex: tabIndex, type: "button", children: label }));
|
|
39
|
+
};
|
|
40
|
+
//# sourceMappingURL=SegmentedOption.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SegmentedOption.js","sourceRoot":"","sources":["../../src/SegmentedControl/SegmentedOption.tsx"],"names":[],"mappings":";AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEjC,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAA;AACxD,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAmB/C;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,EAC9B,SAAS,EACT,IAAI,EACJ,QAAQ,EACR,aAAa,EACb,iBAAiB,EACjB,IAAI,EACJ,QAAQ,EACR,YAAY,GACS,EAAa,EAAE;IACpC,MAAM,EAAE,UAAU,GAAG,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,IAAI,CAAA;IAEjD,SAAS,CACP,GAAG,EAAE,CAAC,iBAAiB,CAAC,KAAK,CAAC,EAC9B,CAAC,iBAAiB,EAAE,KAAK,CAAC,CAC3B,CAAA;IAED,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,UAAU,EAAE,CAAC;YACf,OAAM;QACR,CAAC;QAED,OAAO,aAAa,CAAC,KAAK,CAAC,CAAA;IAC7B,CAAC,EAAE,CAAC,UAAU,EAAE,aAAa,EAAE,KAAK,CAAC,CAAC,CAAA;IAEtC,OAAO,CAEL,iCACgB,SAAS,EACvB,SAAS,EAAE,WAAW,CACpB,yIAAyI,EACzI,kBAAkB,CAAC,IAAI,CAAC;QACxB,mDAAmD;QACnD,6DAA6D;QAC7D,0DAA0D;QAC1D,2CAA2C;QAC3C,SAAS;YACP,CAAC,CAAC,oDAAoD;YACtD,CAAC,CAAC,mFAAmF,EACvF,UAAU;YACR,2FAA2F,EAC7F,gBAAgB,CACjB,EACD,QAAQ,EAAE,UAAU,EACpB,OAAO,EAAE,GAAG,EAAE;YACZ,QAAQ,CAAC,KAAK,CAAC,CAAA;QACjB,CAAC,EACD,GAAG,EAAE,CAAC,OAAO,EAAE,EAAE;YACf,YAAY,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;QAC9B,CAAC,EACD,IAAI,EAAC,OAAO,EACZ,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAC,QAAQ,YAEZ,KAAK,GACC,CACV,CAAA;AACH,CAAC,CAAA"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ComponentPropsWithRef, ReactNode } from "react";
|
|
2
|
+
export type SkeletonShape = "block" | "circle" | "text";
|
|
3
|
+
export type SkeletonProps = Omit<ComponentPropsWithRef<"div">, "children"> & {
|
|
4
|
+
blockSize?: string;
|
|
5
|
+
inlineSize?: string;
|
|
6
|
+
/**
|
|
7
|
+
* Text skeletons render this many bars, the last one short —
|
|
8
|
+
* because a paragraph placeholder whose every line is full width
|
|
9
|
+
* reads as a table, not as prose.
|
|
10
|
+
*/
|
|
11
|
+
lineCount?: number;
|
|
12
|
+
shape?: SkeletonShape;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* **Also zero of these fleet-wide.** The counterpart to `Spinner`
|
|
16
|
+
* and the better answer wherever the shape of the incoming content
|
|
17
|
+
* is already known: a poster grid that skeletons does not reflow
|
|
18
|
+
* when the images land, and layout shift is the thing users
|
|
19
|
+
* actually notice.
|
|
20
|
+
*
|
|
21
|
+
* `aria-hidden` is not an optimisation, it is the contract. A
|
|
22
|
+
* skeleton is decoration standing in for content that does not
|
|
23
|
+
* exist yet; announcing it means a screen reader reading three
|
|
24
|
+
* empty bars. **The load has to be announced by the region that
|
|
25
|
+
* owns it** — a `Spinner`, or an `aria-busy` on the container — and
|
|
26
|
+
* a skeleton that AT can see is a bug, which is why the story
|
|
27
|
+
* asserts it cannot be found by role.
|
|
28
|
+
*/
|
|
29
|
+
export declare const Skeleton: ({ blockSize, className, inlineSize, lineCount, shape, style, ...divProps }: SkeletonProps) => ReactNode;
|
|
30
|
+
//# sourceMappingURL=Skeleton.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Skeleton.d.ts","sourceRoot":"","sources":["../../src/Skeleton/Skeleton.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,qBAAqB,EACrB,SAAS,EACV,MAAM,OAAO,CAAA;AAId,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAA;AAEvD,MAAM,MAAM,aAAa,GAAG,IAAI,CAC9B,qBAAqB,CAAC,KAAK,CAAC,EAC5B,UAAU,CACX,GAAG;IACF,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,aAAa,CAAA;CACtB,CAAA;AAuBD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,QAAQ,GAAI,4EAQtB,aAAa,KAAG,SAqClB,CAAA"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { toClassName } from "../toClassName.js";
|
|
3
|
+
const SHAPE_CLASS = {
|
|
4
|
+
block: "rounded-md",
|
|
5
|
+
circle: "rounded-full",
|
|
6
|
+
text: "rounded-sm",
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* The bars of a multi-line text skeleton, described rather than
|
|
10
|
+
* indexed.
|
|
11
|
+
*
|
|
12
|
+
* The last line is **60%**, fixed. Not random: a placeholder that
|
|
13
|
+
* differs between two renders of the same list makes every
|
|
14
|
+
* screenshot diff useless, and visual regression over P0 x scheme x
|
|
15
|
+
* density is a planned addition once tokens stop moving.
|
|
16
|
+
*/
|
|
17
|
+
const buildLines = (lineCount) => Array.from({ length: lineCount }, (_, index) => ({
|
|
18
|
+
id: `charcuterie-skeleton-line-${index + 1}`,
|
|
19
|
+
inlineSize: index === lineCount - 1 ? "60%" : undefined,
|
|
20
|
+
}));
|
|
21
|
+
/**
|
|
22
|
+
* **Also zero of these fleet-wide.** The counterpart to `Spinner`
|
|
23
|
+
* and the better answer wherever the shape of the incoming content
|
|
24
|
+
* is already known: a poster grid that skeletons does not reflow
|
|
25
|
+
* when the images land, and layout shift is the thing users
|
|
26
|
+
* actually notice.
|
|
27
|
+
*
|
|
28
|
+
* `aria-hidden` is not an optimisation, it is the contract. A
|
|
29
|
+
* skeleton is decoration standing in for content that does not
|
|
30
|
+
* exist yet; announcing it means a screen reader reading three
|
|
31
|
+
* empty bars. **The load has to be announced by the region that
|
|
32
|
+
* owns it** — a `Spinner`, or an `aria-busy` on the container — and
|
|
33
|
+
* a skeleton that AT can see is a bug, which is why the story
|
|
34
|
+
* asserts it cannot be found by role.
|
|
35
|
+
*/
|
|
36
|
+
export const Skeleton = ({ blockSize, className, inlineSize, lineCount = 1, shape = "block", style, ...divProps }) => {
|
|
37
|
+
const barClassName = toClassName("charcuterie-shimmer block bg-surface-sunken", SHAPE_CLASS[shape], shape === "text" && "h-[1em]");
|
|
38
|
+
if (shape === "text" && lineCount > 1) {
|
|
39
|
+
return (_jsx("div", { ...divProps, "aria-hidden": "true", className: toClassName("flex flex-col gap-2", className), style: { inlineSize, ...style }, children: buildLines(lineCount).map((line) => (_jsx("span", { className: barClassName, style: { inlineSize: line.inlineSize } }, line.id))) }));
|
|
40
|
+
}
|
|
41
|
+
return (_jsx("div", { ...divProps, "aria-hidden": "true", className: toClassName(barClassName, className), style: { blockSize, inlineSize, ...style } }));
|
|
42
|
+
};
|
|
43
|
+
//# sourceMappingURL=Skeleton.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Skeleton.js","sourceRoot":"","sources":["../../src/Skeleton/Skeleton.tsx"],"names":[],"mappings":";AAKA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAmB/C,MAAM,WAAW,GAAkC;IACjD,KAAK,EAAE,YAAY;IACnB,MAAM,EAAE,cAAc;IACtB,IAAI,EAAE,YAAY;CACnB,CAAA;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,GAAG,CAAC,SAAiB,EAAE,EAAE,CACvC,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC;IAC/C,EAAE,EAAE,6BAA6B,KAAK,GAAG,CAAC,EAAE;IAC5C,UAAU,EAAE,KAAK,KAAK,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS;CACxD,CAAC,CAAC,CAAA;AAEL;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,EACvB,SAAS,EACT,SAAS,EACT,UAAU,EACV,SAAS,GAAG,CAAC,EACb,KAAK,GAAG,OAAO,EACf,KAAK,EACL,GAAG,QAAQ,EACG,EAAa,EAAE;IAC7B,MAAM,YAAY,GAAG,WAAW,CAC9B,6CAA6C,EAC7C,WAAW,CAAC,KAAK,CAAC,EAClB,KAAK,KAAK,MAAM,IAAI,SAAS,CAC9B,CAAA;IAED,IAAI,KAAK,KAAK,MAAM,IAAI,SAAS,GAAG,CAAC,EAAE,CAAC;QACtC,OAAO,CACL,iBACM,QAAQ,iBACA,MAAM,EAClB,SAAS,EAAE,WAAW,CACpB,qBAAqB,EACrB,SAAS,CACV,EACD,KAAK,EAAE,EAAE,UAAU,EAAE,GAAG,KAAK,EAAE,YAE9B,UAAU,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CACnC,eACE,SAAS,EAAE,YAAY,EAEvB,KAAK,EAAE,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE,IADjC,IAAI,CAAC,EAAE,CAEZ,CACH,CAAC,GACE,CACP,CAAA;IACH,CAAC;IAED,OAAO,CACL,iBACM,QAAQ,iBACA,MAAM,EAClB,SAAS,EAAE,WAAW,CAAC,YAAY,EAAE,SAAS,CAAC,EAC/C,KAAK,EAAE,EAAE,SAAS,EAAE,UAAU,EAAE,GAAG,KAAK,EAAE,GAC1C,CACH,CAAA;AACH,CAAC,CAAA"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ControlSize } from "@charcuterie/tokens";
|
|
2
|
+
import type { ComponentPropsWithRef, ReactNode } from "react";
|
|
3
|
+
export type SpinnerProps = Omit<ComponentPropsWithRef<"span">, "children"> & {
|
|
4
|
+
/**
|
|
5
|
+
* Required by omission of a default that could be wrong.
|
|
6
|
+
* "Loading…" is right for a fetch and wrong for "Ripping disc 3",
|
|
7
|
+
* and the label is the *only* thing a screen-reader user gets —
|
|
8
|
+
* the animation says nothing.
|
|
9
|
+
*/
|
|
10
|
+
label?: string;
|
|
11
|
+
isLabelVisible?: boolean;
|
|
12
|
+
size?: ControlSize;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* **Zero of these exist anywhere in the fleet.** Not one repo has a
|
|
16
|
+
* spinner; every app renders bare `"loading…"` text, which is why
|
|
17
|
+
* this is the best effort-to-payoff component in the M3 list.
|
|
18
|
+
*
|
|
19
|
+
* Three things it does that a hand-rolled `animate-spin` does not:
|
|
20
|
+
*
|
|
21
|
+
* - **It is a live region.** `role="status"` with the label inside
|
|
22
|
+
* means a screen reader announces the load starting. A bare
|
|
23
|
+
* rotating `<div>` announces nothing, which is the state of the
|
|
24
|
+
* art in every repo that renders `"loading…"` into a `<span>`.
|
|
25
|
+
* - **It stops.** The animation is `--duration-loop-fast`, which
|
|
26
|
+
* `prefers-reduced-motion` zeroes and
|
|
27
|
+
* `@charcuterie/ui/styles.css` then switches off outright —
|
|
28
|
+
* vestibular-triggering motion is the single most common a11y
|
|
29
|
+
* complaint about spinners, and Tailwind's `animate-spin`
|
|
30
|
+
* hardcodes 1s past the media query.
|
|
31
|
+
* - **It leaves a static fallback that still reads.** With motion
|
|
32
|
+
* off, the ring stays a ring and the label stays a label, so the
|
|
33
|
+
* component degrades to "there is a thing here, and it says
|
|
34
|
+
* loading" rather than to nothing.
|
|
35
|
+
*/
|
|
36
|
+
export declare const Spinner: ({ className, isLabelVisible, label, size, ...spanProps }: SpinnerProps) => ReactNode;
|
|
37
|
+
//# sourceMappingURL=Spinner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Spinner.d.ts","sourceRoot":"","sources":["../../src/Spinner/Spinner.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AACtD,OAAO,KAAK,EACV,qBAAqB,EACrB,SAAS,EACV,MAAM,OAAO,CAAA;AAMd,MAAM,MAAM,YAAY,GAAG,IAAI,CAC7B,qBAAqB,CAAC,MAAM,CAAC,EAC7B,UAAU,CACX,GAAG;IACF;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,IAAI,CAAC,EAAE,WAAW,CAAA;CACnB,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,OAAO,GAAI,0DAMrB,YAAY,KAAG,SAsCjB,CAAA"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { SPINNER_SIZE_CLASS } from "../controlStyles.js";
|
|
3
|
+
import { toClassName } from "../toClassName.js";
|
|
4
|
+
import { VisuallyHidden } from "../VisuallyHidden/VisuallyHidden.js";
|
|
5
|
+
/**
|
|
6
|
+
* **Zero of these exist anywhere in the fleet.** Not one repo has a
|
|
7
|
+
* spinner; every app renders bare `"loading…"` text, which is why
|
|
8
|
+
* this is the best effort-to-payoff component in the M3 list.
|
|
9
|
+
*
|
|
10
|
+
* Three things it does that a hand-rolled `animate-spin` does not:
|
|
11
|
+
*
|
|
12
|
+
* - **It is a live region.** `role="status"` with the label inside
|
|
13
|
+
* means a screen reader announces the load starting. A bare
|
|
14
|
+
* rotating `<div>` announces nothing, which is the state of the
|
|
15
|
+
* art in every repo that renders `"loading…"` into a `<span>`.
|
|
16
|
+
* - **It stops.** The animation is `--duration-loop-fast`, which
|
|
17
|
+
* `prefers-reduced-motion` zeroes and
|
|
18
|
+
* `@charcuterie/ui/styles.css` then switches off outright —
|
|
19
|
+
* vestibular-triggering motion is the single most common a11y
|
|
20
|
+
* complaint about spinners, and Tailwind's `animate-spin`
|
|
21
|
+
* hardcodes 1s past the media query.
|
|
22
|
+
* - **It leaves a static fallback that still reads.** With motion
|
|
23
|
+
* off, the ring stays a ring and the label stays a label, so the
|
|
24
|
+
* component degrades to "there is a thing here, and it says
|
|
25
|
+
* loading" rather than to nothing.
|
|
26
|
+
*/
|
|
27
|
+
export const Spinner = ({ className, isLabelVisible = false, label = "Loading…", size = "md", ...spanProps }) => (_jsxs("span", { ...spanProps, "aria-label": label, className: toClassName("inline-flex items-center gap-2 align-middle text-current", className), role: "status", children: [_jsx("span", { "aria-hidden": "true", className: toClassName(
|
|
28
|
+
// `border-current` on three sides and a transparent fourth
|
|
29
|
+
// is the whole trick: it inherits the parent's text colour,
|
|
30
|
+
// so a spinner inside a solid button is legible without the
|
|
31
|
+
// button telling it anything.
|
|
32
|
+
"charcuterie-spin inline-block shrink-0 rounded-full border-current border-t-transparent", SPINNER_SIZE_CLASS[size]) }), isLabelVisible ? (_jsx("span", { className: "text-content-secondary", children: label })) : (_jsx(VisuallyHidden, { children: label }))] }));
|
|
33
|
+
//# sourceMappingURL=Spinner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Spinner.js","sourceRoot":"","sources":["../../src/Spinner/Spinner.tsx"],"names":[],"mappings":";AAMA,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAA;AACxD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,EAAE,cAAc,EAAE,MAAM,qCAAsC,CAAA;AAiBrE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,EACtB,SAAS,EACT,cAAc,GAAG,KAAK,EACtB,KAAK,GAAG,UAAU,EAClB,IAAI,GAAG,IAAI,EACX,GAAG,SAAS,EACC,EAAa,EAAE,CAAC,CAC7B,mBACM,SAAS,gBASD,KAAK,EACjB,SAAS,EAAE,WAAW,CACpB,0DAA0D,EAC1D,SAAS,CACV,EACD,IAAI,EAAC,QAAQ,aAEb,8BACc,MAAM,EAClB,SAAS,EAAE,WAAW;YACpB,2DAA2D;YAC3D,4DAA4D;YAC5D,4DAA4D;YAC5D,8BAA8B;YAC9B,yFAAyF,EACzF,kBAAkB,CAAC,IAAI,CAAC,CACzB,GACD,EAED,cAAc,CAAC,CAAC,CAAC,CAChB,eAAM,SAAS,EAAC,wBAAwB,YACrC,KAAK,GACD,CACR,CAAC,CAAC,CAAC,CACF,KAAC,cAAc,cAAE,KAAK,GAAkB,CACzC,IACI,CACR,CAAA"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { TabItem, TabsOrientation } from "./Tabs.tsx";
|
|
3
|
+
export type TabTriggerProps = {
|
|
4
|
+
id: string;
|
|
5
|
+
isSelected: boolean;
|
|
6
|
+
onSelect: (key: string) => void;
|
|
7
|
+
orientation: TabsOrientation;
|
|
8
|
+
panelId: string;
|
|
9
|
+
/** `RovingFocus.register` — membership of the arrow-key group. */
|
|
10
|
+
registerFocus: (value: string) => () => void;
|
|
11
|
+
/** `SinglePicker.register` — membership of the choice. */
|
|
12
|
+
registerSelection: (value: string) => () => void;
|
|
13
|
+
tab: TabItem;
|
|
14
|
+
tabIndex: number;
|
|
15
|
+
trackElement: (key: string, element: HTMLButtonElement | null) => void;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Its own file for a reason that is not style: **both
|
|
19
|
+
* registrations are effects**, and an effect cannot run in a loop.
|
|
20
|
+
* Any member of a registering group ends up shaped like this, so
|
|
21
|
+
* it is worth seeing on its own.
|
|
22
|
+
*
|
|
23
|
+
* The two registrations are separate on purpose. A disabled tab
|
|
24
|
+
* joins the *choice* — it is still one of the options, still owns
|
|
25
|
+
* a panel and an id, and can still be the one selected when a
|
|
26
|
+
* consumer says so — and stays out of the *focus* group, so the
|
|
27
|
+
* arrow keys skip it without any command in `RovingFocus` having
|
|
28
|
+
* to know what "disabled" means. Registration is membership; that
|
|
29
|
+
* is the whole mechanism.
|
|
30
|
+
*/
|
|
31
|
+
export declare const TabTrigger: ({ id, isSelected, onSelect, orientation, panelId, registerFocus, registerSelection, tab, tabIndex, trackElement, }: TabTriggerProps) => ReactNode;
|
|
32
|
+
//# sourceMappingURL=TabTrigger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TabTrigger.d.ts","sourceRoot":"","sources":["../../src/Tabs/TabTrigger.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAKtC,OAAO,KAAK,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAyB1D,MAAM,MAAM,eAAe,GAAG;IAC5B,EAAE,EAAE,MAAM,CAAA;IACV,UAAU,EAAE,OAAO,CAAA;IACnB,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;IAC/B,WAAW,EAAE,eAAe,CAAA;IAC5B,OAAO,EAAE,MAAM,CAAA;IACf,kEAAkE;IAClE,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,IAAI,CAAA;IAC5C,0DAA0D;IAC1D,iBAAiB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,IAAI,CAAA;IAChD,GAAG,EAAE,OAAO,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,CACZ,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,iBAAiB,GAAG,IAAI,KAC9B,IAAI,CAAA;CACV,CAAA;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,UAAU,GAAI,oHAWxB,eAAe,KAAG,SAoDpB,CAAA"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
import { FOCUS_RING_CLASS } from "../intentStyles.js";
|
|
4
|
+
import { toClassName } from "../toClassName.js";
|
|
5
|
+
/**
|
|
6
|
+
* The selected marker runs along the edge the tab list itself
|
|
7
|
+
* sits on — under a horizontal bar, down the inline-end of a
|
|
8
|
+
* vertical rail — and the negative margin pulls it over the
|
|
9
|
+
* list's own border so the two read as one line rather than two.
|
|
10
|
+
*
|
|
11
|
+
* Logical properties throughout, which is the house rule and also
|
|
12
|
+
* the only way a vertical rail lands on the correct side in RTL.
|
|
13
|
+
*/
|
|
14
|
+
const ORIENTATION_EDGE_CLASS = {
|
|
15
|
+
horizontal: {
|
|
16
|
+
base: "-mb-px border-b-2",
|
|
17
|
+
selected: "border-b-intent-accent-solid",
|
|
18
|
+
},
|
|
19
|
+
vertical: {
|
|
20
|
+
base: "-me-px border-e-2 text-start",
|
|
21
|
+
selected: "border-e-intent-accent-solid",
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Its own file for a reason that is not style: **both
|
|
26
|
+
* registrations are effects**, and an effect cannot run in a loop.
|
|
27
|
+
* Any member of a registering group ends up shaped like this, so
|
|
28
|
+
* it is worth seeing on its own.
|
|
29
|
+
*
|
|
30
|
+
* The two registrations are separate on purpose. A disabled tab
|
|
31
|
+
* joins the *choice* — it is still one of the options, still owns
|
|
32
|
+
* a panel and an id, and can still be the one selected when a
|
|
33
|
+
* consumer says so — and stays out of the *focus* group, so the
|
|
34
|
+
* arrow keys skip it without any command in `RovingFocus` having
|
|
35
|
+
* to know what "disabled" means. Registration is membership; that
|
|
36
|
+
* is the whole mechanism.
|
|
37
|
+
*/
|
|
38
|
+
export const TabTrigger = ({ id, isSelected, onSelect, orientation, panelId, registerFocus, registerSelection, tab, tabIndex, trackElement, }) => {
|
|
39
|
+
const { isDisabled = false, key, label } = tab;
|
|
40
|
+
const edge = ORIENTATION_EDGE_CLASS[orientation];
|
|
41
|
+
useEffect(() => registerSelection(key), [key, registerSelection]);
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (isDisabled) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
return registerFocus(key);
|
|
47
|
+
}, [isDisabled, key, registerFocus]);
|
|
48
|
+
return (_jsx("button", { "aria-controls": panelId, "aria-selected": isSelected, className: toClassName("cursor-pointer border-transparent px-3 py-2 font-medium text-sm whitespace-nowrap transition-colors duration-(--duration-fast) ease-standard", edge.base,
|
|
49
|
+
// Two entries rather than one interpolated string:
|
|
50
|
+
// `tailwindCandidates.test.ts` rejects a template literal
|
|
51
|
+
// in a className outright, because Tailwind's scanner
|
|
52
|
+
// cannot see a class that only exists at runtime.
|
|
53
|
+
isSelected && edge.selected, isSelected
|
|
54
|
+
? "text-content-primary"
|
|
55
|
+
: "text-content-secondary hover:text-content-primary", isDisabled &&
|
|
56
|
+
"cursor-not-allowed text-content-disabled hover:text-content-disabled", FOCUS_RING_CLASS), disabled: isDisabled, id: id, onClick: () => {
|
|
57
|
+
onSelect(key);
|
|
58
|
+
}, ref: (element) => {
|
|
59
|
+
trackElement(key, element);
|
|
60
|
+
}, role: "tab", tabIndex: tabIndex, type: "button", children: label }));
|
|
61
|
+
};
|
|
62
|
+
//# sourceMappingURL=TabTrigger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TabTrigger.js","sourceRoot":"","sources":["../../src/Tabs/TabTrigger.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEjC,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAG/C;;;;;;;;GAQG;AACH,MAAM,sBAAsB,GAGxB;IACF,UAAU,EAAE;QACV,IAAI,EAAE,mBAAmB;QACzB,QAAQ,EAAE,8BAA8B;KACzC;IACD,QAAQ,EAAE;QACR,IAAI,EAAE,8BAA8B;QACpC,QAAQ,EAAE,8BAA8B;KACzC;CACF,CAAA;AAoBD;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,EACzB,EAAE,EACF,UAAU,EACV,QAAQ,EACR,WAAW,EACX,OAAO,EACP,aAAa,EACb,iBAAiB,EACjB,GAAG,EACH,QAAQ,EACR,YAAY,GACI,EAAa,EAAE;IAC/B,MAAM,EAAE,UAAU,GAAG,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,GAAG,CAAA;IAE9C,MAAM,IAAI,GAAG,sBAAsB,CAAC,WAAW,CAAC,CAAA;IAEhD,SAAS,CACP,GAAG,EAAE,CAAC,iBAAiB,CAAC,GAAG,CAAC,EAC5B,CAAC,GAAG,EAAE,iBAAiB,CAAC,CACzB,CAAA;IAED,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,UAAU,EAAE,CAAC;YACf,OAAM;QACR,CAAC;QAED,OAAO,aAAa,CAAC,GAAG,CAAC,CAAA;IAC3B,CAAC,EAAE,CAAC,UAAU,EAAE,GAAG,EAAE,aAAa,CAAC,CAAC,CAAA;IAEpC,OAAO,CACL,kCACiB,OAAO,mBACP,UAAU,EACzB,SAAS,EAAE,WAAW,CACpB,8IAA8I,EAC9I,IAAI,CAAC,IAAI;QACT,mDAAmD;QACnD,0DAA0D;QAC1D,sDAAsD;QACtD,kDAAkD;QAClD,UAAU,IAAI,IAAI,CAAC,QAAQ,EAC3B,UAAU;YACR,CAAC,CAAC,sBAAsB;YACxB,CAAC,CAAC,mDAAmD,EACvD,UAAU;YACR,sEAAsE,EACxE,gBAAgB,CACjB,EACD,QAAQ,EAAE,UAAU,EACpB,EAAE,EAAE,EAAE,EACN,OAAO,EAAE,GAAG,EAAE;YACZ,QAAQ,CAAC,GAAG,CAAC,CAAA;QACf,CAAC,EACD,GAAG,EAAE,CAAC,OAAO,EAAE,EAAE;YACf,YAAY,CAAC,GAAG,EAAE,OAAO,CAAC,CAAA;QAC5B,CAAC,EACD,IAAI,EAAC,KAAK,EACV,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAC,QAAQ,YAEZ,KAAK,GACC,CACV,CAAA;AACH,CAAC,CAAA"}
|