@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.
Files changed (223) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +161 -0
  3. package/dist/Alert/Alert.d.ts +88 -0
  4. package/dist/Alert/Alert.d.ts.map +1 -0
  5. package/dist/Alert/Alert.js +67 -0
  6. package/dist/Alert/Alert.js.map +1 -0
  7. package/dist/Badge/Badge.d.ts +54 -0
  8. package/dist/Badge/Badge.d.ts.map +1 -0
  9. package/dist/Badge/Badge.js +54 -0
  10. package/dist/Badge/Badge.js.map +1 -0
  11. package/dist/Badge/useIsTextClipped.d.ts +22 -0
  12. package/dist/Badge/useIsTextClipped.d.ts.map +1 -0
  13. package/dist/Badge/useIsTextClipped.js +57 -0
  14. package/dist/Badge/useIsTextClipped.js.map +1 -0
  15. package/dist/Button/Button.d.ts +58 -0
  16. package/dist/Button/Button.d.ts.map +1 -0
  17. package/dist/Button/Button.js +37 -0
  18. package/dist/Button/Button.js.map +1 -0
  19. package/dist/Card/Card.d.ts +44 -0
  20. package/dist/Card/Card.d.ts.map +1 -0
  21. package/dist/Card/Card.js +59 -0
  22. package/dist/Card/Card.js.map +1 -0
  23. package/dist/EmptyState/EmptyState.d.ts +42 -0
  24. package/dist/EmptyState/EmptyState.d.ts.map +1 -0
  25. package/dist/EmptyState/EmptyState.js +37 -0
  26. package/dist/EmptyState/EmptyState.js.map +1 -0
  27. package/dist/IconButton/IconButton.d.ts +34 -0
  28. package/dist/IconButton/IconButton.d.ts.map +1 -0
  29. package/dist/IconButton/IconButton.js +23 -0
  30. package/dist/IconButton/IconButton.js.map +1 -0
  31. package/dist/LiveStatusIndicator/LiveStatusIndicator.d.ts +37 -0
  32. package/dist/LiveStatusIndicator/LiveStatusIndicator.d.ts.map +1 -0
  33. package/dist/LiveStatusIndicator/LiveStatusIndicator.js +39 -0
  34. package/dist/LiveStatusIndicator/LiveStatusIndicator.js.map +1 -0
  35. package/dist/MediaTile/MediaTile.d.ts +46 -0
  36. package/dist/MediaTile/MediaTile.d.ts.map +1 -0
  37. package/dist/MediaTile/MediaTile.js +79 -0
  38. package/dist/MediaTile/MediaTile.js.map +1 -0
  39. package/dist/MediaTile/mediaStatus.d.ts +22 -0
  40. package/dist/MediaTile/mediaStatus.d.ts.map +1 -0
  41. package/dist/MediaTile/mediaStatus.js +24 -0
  42. package/dist/MediaTile/mediaStatus.js.map +1 -0
  43. package/dist/Modal/Modal.d.ts +66 -0
  44. package/dist/Modal/Modal.d.ts.map +1 -0
  45. package/dist/Modal/Modal.js +111 -0
  46. package/dist/Modal/Modal.js.map +1 -0
  47. package/dist/Modal/lockScrollBehind.d.ts +22 -0
  48. package/dist/Modal/lockScrollBehind.d.ts.map +1 -0
  49. package/dist/Modal/lockScrollBehind.js +48 -0
  50. package/dist/Modal/lockScrollBehind.js.map +1 -0
  51. package/dist/Popover/Popover.d.ts +66 -0
  52. package/dist/Popover/Popover.d.ts.map +1 -0
  53. package/dist/Popover/Popover.js +88 -0
  54. package/dist/Popover/Popover.js.map +1 -0
  55. package/dist/ProgressBar/ProgressBar.d.ts +51 -0
  56. package/dist/ProgressBar/ProgressBar.d.ts.map +1 -0
  57. package/dist/ProgressBar/ProgressBar.js +50 -0
  58. package/dist/ProgressBar/ProgressBar.js.map +1 -0
  59. package/dist/ProgressBar/progressValue.d.ts +44 -0
  60. package/dist/ProgressBar/progressValue.d.ts.map +1 -0
  61. package/dist/ProgressBar/progressValue.js +55 -0
  62. package/dist/ProgressBar/progressValue.js.map +1 -0
  63. package/dist/SegmentedControl/SegmentedControl.d.ts +59 -0
  64. package/dist/SegmentedControl/SegmentedControl.d.ts.map +1 -0
  65. package/dist/SegmentedControl/SegmentedControl.js +129 -0
  66. package/dist/SegmentedControl/SegmentedControl.js.map +1 -0
  67. package/dist/SegmentedControl/SegmentedOption.d.ts +28 -0
  68. package/dist/SegmentedControl/SegmentedOption.d.ts.map +1 -0
  69. package/dist/SegmentedControl/SegmentedOption.js +40 -0
  70. package/dist/SegmentedControl/SegmentedOption.js.map +1 -0
  71. package/dist/Skeleton/Skeleton.d.ts +30 -0
  72. package/dist/Skeleton/Skeleton.d.ts.map +1 -0
  73. package/dist/Skeleton/Skeleton.js +43 -0
  74. package/dist/Skeleton/Skeleton.js.map +1 -0
  75. package/dist/Spinner/Spinner.d.ts +37 -0
  76. package/dist/Spinner/Spinner.d.ts.map +1 -0
  77. package/dist/Spinner/Spinner.js +33 -0
  78. package/dist/Spinner/Spinner.js.map +1 -0
  79. package/dist/Tabs/TabTrigger.d.ts +32 -0
  80. package/dist/Tabs/TabTrigger.d.ts.map +1 -0
  81. package/dist/Tabs/TabTrigger.js +62 -0
  82. package/dist/Tabs/TabTrigger.js.map +1 -0
  83. package/dist/Tabs/Tabs.d.ts +95 -0
  84. package/dist/Tabs/Tabs.d.ts.map +1 -0
  85. package/dist/Tabs/Tabs.js +195 -0
  86. package/dist/Tabs/Tabs.js.map +1 -0
  87. package/dist/VisuallyHidden/VisuallyHidden.d.ts +25 -0
  88. package/dist/VisuallyHidden/VisuallyHidden.d.ts.map +1 -0
  89. package/dist/VisuallyHidden/VisuallyHidden.js +23 -0
  90. package/dist/VisuallyHidden/VisuallyHidden.js.map +1 -0
  91. package/dist/controlStyles.d.ts +50 -0
  92. package/dist/controlStyles.d.ts.map +1 -0
  93. package/dist/controlStyles.js +60 -0
  94. package/dist/controlStyles.js.map +1 -0
  95. package/dist/expectNoAxeViolations.testHelpers.d.ts +19 -0
  96. package/dist/expectNoAxeViolations.testHelpers.d.ts.map +1 -0
  97. package/dist/expectNoAxeViolations.testHelpers.js +32 -0
  98. package/dist/expectNoAxeViolations.testHelpers.js.map +1 -0
  99. package/dist/index.d.ts +75 -0
  100. package/dist/index.d.ts.map +1 -0
  101. package/dist/index.js +55 -0
  102. package/dist/index.js.map +1 -0
  103. package/dist/intentStyles.d.ts +76 -0
  104. package/dist/intentStyles.d.ts.map +1 -0
  105. package/dist/intentStyles.js +150 -0
  106. package/dist/intentStyles.js.map +1 -0
  107. package/dist/mountStory.testHelpers.d.ts +30 -0
  108. package/dist/mountStory.testHelpers.d.ts.map +1 -0
  109. package/dist/mountStory.testHelpers.js +19 -0
  110. package/dist/mountStory.testHelpers.js.map +1 -0
  111. package/dist/statusIntent.d.ts +33 -0
  112. package/dist/statusIntent.d.ts.map +1 -0
  113. package/dist/statusIntent.js +99 -0
  114. package/dist/statusIntent.js.map +1 -0
  115. package/dist/styles.css +130 -0
  116. package/dist/testing/expectAgentDrivable.d.ts +53 -0
  117. package/dist/testing/expectAgentDrivable.d.ts.map +1 -0
  118. package/dist/testing/expectAgentDrivable.js +163 -0
  119. package/dist/testing/expectAgentDrivable.js.map +1 -0
  120. package/dist/testing/index.d.ts +11 -0
  121. package/dist/testing/index.d.ts.map +1 -0
  122. package/dist/testing/index.js +10 -0
  123. package/dist/testing/index.js.map +1 -0
  124. package/dist/toClassName.d.ts +12 -0
  125. package/dist/toClassName.d.ts.map +1 -0
  126. package/dist/toClassName.js +12 -0
  127. package/dist/toClassName.js.map +1 -0
  128. package/dist/tokens.d.ts +16 -0
  129. package/dist/tokens.d.ts.map +1 -0
  130. package/dist/tokens.js +16 -0
  131. package/dist/tokens.js.map +1 -0
  132. package/package.json +70 -0
  133. package/src/Alert/Alert.mdx +70 -0
  134. package/src/Alert/Alert.stories.tsx +218 -0
  135. package/src/Alert/Alert.test.tsx +112 -0
  136. package/src/Alert/Alert.tsx +166 -0
  137. package/src/Badge/Badge.mdx +43 -0
  138. package/src/Badge/Badge.stories.tsx +229 -0
  139. package/src/Badge/Badge.test.tsx +153 -0
  140. package/src/Badge/Badge.tsx +130 -0
  141. package/src/Badge/useIsTextClipped.ts +77 -0
  142. package/src/Button/Button.mdx +44 -0
  143. package/src/Button/Button.stories.tsx +175 -0
  144. package/src/Button/Button.test.tsx +114 -0
  145. package/src/Button/Button.tsx +125 -0
  146. package/src/Card/Card.mdx +53 -0
  147. package/src/Card/Card.stories.tsx +207 -0
  148. package/src/Card/Card.test.tsx +56 -0
  149. package/src/Card/Card.tsx +142 -0
  150. package/src/EmptyState/EmptyState.mdx +40 -0
  151. package/src/EmptyState/EmptyState.stories.tsx +183 -0
  152. package/src/EmptyState/EmptyState.test.tsx +41 -0
  153. package/src/EmptyState/EmptyState.tsx +114 -0
  154. package/src/IconButton/IconButton.mdx +34 -0
  155. package/src/IconButton/IconButton.stories.tsx +191 -0
  156. package/src/IconButton/IconButton.test.tsx +70 -0
  157. package/src/IconButton/IconButton.tsx +54 -0
  158. package/src/LiveStatusIndicator/LiveStatusIndicator.mdx +43 -0
  159. package/src/LiveStatusIndicator/LiveStatusIndicator.stories.tsx +199 -0
  160. package/src/LiveStatusIndicator/LiveStatusIndicator.test.tsx +63 -0
  161. package/src/LiveStatusIndicator/LiveStatusIndicator.tsx +116 -0
  162. package/src/MediaTile/MediaTile.mdx +51 -0
  163. package/src/MediaTile/MediaTile.stories.tsx +211 -0
  164. package/src/MediaTile/MediaTile.test.tsx +41 -0
  165. package/src/MediaTile/MediaTile.tsx +227 -0
  166. package/src/MediaTile/mediaStatus.ts +28 -0
  167. package/src/Modal/Modal.mdx +113 -0
  168. package/src/Modal/Modal.stories.tsx +272 -0
  169. package/src/Modal/Modal.test.tsx +248 -0
  170. package/src/Modal/Modal.tsx +228 -0
  171. package/src/Modal/lockScrollBehind.ts +59 -0
  172. package/src/Popover/Popover.mdx +102 -0
  173. package/src/Popover/Popover.stories.tsx +247 -0
  174. package/src/Popover/Popover.test.tsx +191 -0
  175. package/src/Popover/Popover.tsx +164 -0
  176. package/src/ProgressBar/ProgressBar.mdx +48 -0
  177. package/src/ProgressBar/ProgressBar.stories.tsx +232 -0
  178. package/src/ProgressBar/ProgressBar.test.tsx +74 -0
  179. package/src/ProgressBar/ProgressBar.tsx +172 -0
  180. package/src/ProgressBar/progressValue.test.ts +101 -0
  181. package/src/ProgressBar/progressValue.ts +87 -0
  182. package/src/SegmentedControl/SegmentedControl.mdx +71 -0
  183. package/src/SegmentedControl/SegmentedControl.stories.tsx +150 -0
  184. package/src/SegmentedControl/SegmentedControl.test.tsx +208 -0
  185. package/src/SegmentedControl/SegmentedControl.tsx +219 -0
  186. package/src/SegmentedControl/SegmentedOption.tsx +94 -0
  187. package/src/Skeleton/Skeleton.mdx +30 -0
  188. package/src/Skeleton/Skeleton.stories.tsx +130 -0
  189. package/src/Skeleton/Skeleton.test.tsx +47 -0
  190. package/src/Skeleton/Skeleton.tsx +106 -0
  191. package/src/Spinner/Spinner.mdx +36 -0
  192. package/src/Spinner/Spinner.stories.tsx +118 -0
  193. package/src/Spinner/Spinner.test.tsx +38 -0
  194. package/src/Spinner/Spinner.tsx +92 -0
  195. package/src/Tabs/TabTrigger.tsx +126 -0
  196. package/src/Tabs/Tabs.mdx +126 -0
  197. package/src/Tabs/Tabs.stories.tsx +187 -0
  198. package/src/Tabs/Tabs.test.tsx +283 -0
  199. package/src/Tabs/Tabs.tsx +333 -0
  200. package/src/VisuallyHidden/VisuallyHidden.mdx +29 -0
  201. package/src/VisuallyHidden/VisuallyHidden.stories.tsx +58 -0
  202. package/src/VisuallyHidden/VisuallyHidden.test.tsx +31 -0
  203. package/src/VisuallyHidden/VisuallyHidden.tsx +43 -0
  204. package/src/argTypes.storyHelpers.ts +115 -0
  205. package/src/board.storyHelpers.tsx +150 -0
  206. package/src/controlStyles.ts +79 -0
  207. package/src/expectNoAxeViolations.testHelpers.ts +41 -0
  208. package/src/icons.storyHelpers.tsx +116 -0
  209. package/src/index.ts +132 -0
  210. package/src/intentStyles.ts +207 -0
  211. package/src/mdxReferences.test.ts +95 -0
  212. package/src/mountStory.testHelpers.ts +41 -0
  213. package/src/sourceRules.test.ts +303 -0
  214. package/src/statusIntent.test.ts +136 -0
  215. package/src/statusIntent.ts +137 -0
  216. package/src/storyControls.test.ts +128 -0
  217. package/src/styles.css +130 -0
  218. package/src/tailwindCandidates.test.ts +377 -0
  219. package/src/testing/expectAgentDrivable.test.tsx +71 -0
  220. package/src/testing/expectAgentDrivable.ts +273 -0
  221. package/src/testing/index.ts +17 -0
  222. package/src/toClassName.ts +13 -0
  223. 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"}