@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,53 @@
1
+ /**
2
+ * Agent-drivability as a red/green property, not a hope.
3
+ *
4
+ * A stated goal of this library is that **Playwright MCP and AI
5
+ * agents can drive the fleet's apps**, and that reduces to one
6
+ * mechanical requirement: `getByRole(role, { name })` finds exactly
7
+ * one thing. The fleet fails it comprehensively today —
8
+ * plex-channels' icon buttons are bare glyphs (`↶`, `↷`, `▶`),
9
+ * mux-magic's status pills are `<div>`s with no role, xander has
10
+ * zero `aria-*` attributes — and every one of those failures is
11
+ * invisible in a browser and invisible to axe.
12
+ *
13
+ * So every component's story calls this, and the assertion is the
14
+ * same one Playwright will make.
15
+ *
16
+ * Deliberately dependency-free — no `expect`, no jest-dom, no
17
+ * testing-library import. This ships in `@charcuterie/ui/testing`
18
+ * for consumers to use on *their* components, and a published
19
+ * package should not drag a test framework into an app's dependency
20
+ * graph to do it. Plain `Error`s with actionable messages work in a
21
+ * story `play`, in Vitest, in Playwright, and in a scratch script.
22
+ */
23
+ /**
24
+ * The subset of a testing-library query object this needs —
25
+ * structural, so Storybook's `canvas`, a `within(element)` result,
26
+ * and `screen` all satisfy it without an import.
27
+ */
28
+ export type AgentQueries = {
29
+ queryAllByRole: (role: string, options?: {
30
+ name?: RegExp | string;
31
+ }) => HTMLElement[];
32
+ };
33
+ export type AgentTarget = {
34
+ name?: RegExp | string;
35
+ role: string;
36
+ };
37
+ /**
38
+ * Asserts the control an agent would reach for is reachable, unique,
39
+ * and keyboard-operable. Returns it, so a caller can go on to assert
40
+ * something specific about it.
41
+ */
42
+ export declare const expectAgentDrivable: (canvas: AgentQueries, target: AgentTarget) => HTMLElement;
43
+ /**
44
+ * The other half of the contract, for decoration.
45
+ *
46
+ * A `Skeleton` that assistive technology can see is a screen reader
47
+ * reading three empty bars, and an agent finding a "loading" element
48
+ * it cannot act on. Asserting the *absence* of a role is how that
49
+ * stays true — axe will not tell you, because there is nothing
50
+ * invalid about it.
51
+ */
52
+ export declare const expectHiddenFromAgents: (element: Element) => void;
53
+ //# sourceMappingURL=expectAgentDrivable.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"expectAgentDrivable.d.ts","sourceRoot":"","sources":["../../src/testing/expectAgentDrivable.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,cAAc,EAAE,CACd,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,KACjC,WAAW,EAAE,CAAA;CACnB,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACtB,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AA6FD;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,GAC9B,QAAQ,YAAY,EACpB,QAAQ,WAAW,KAClB,WAkHF,CAAA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB,GACjC,SAAS,OAAO,KACf,IAMF,CAAA"}
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Agent-drivability as a red/green property, not a hope.
3
+ *
4
+ * A stated goal of this library is that **Playwright MCP and AI
5
+ * agents can drive the fleet's apps**, and that reduces to one
6
+ * mechanical requirement: `getByRole(role, { name })` finds exactly
7
+ * one thing. The fleet fails it comprehensively today —
8
+ * plex-channels' icon buttons are bare glyphs (`↶`, `↷`, `▶`),
9
+ * mux-magic's status pills are `<div>`s with no role, xander has
10
+ * zero `aria-*` attributes — and every one of those failures is
11
+ * invisible in a browser and invisible to axe.
12
+ *
13
+ * So every component's story calls this, and the assertion is the
14
+ * same one Playwright will make.
15
+ *
16
+ * Deliberately dependency-free — no `expect`, no jest-dom, no
17
+ * testing-library import. This ships in `@charcuterie/ui/testing`
18
+ * for consumers to use on *their* components, and a published
19
+ * package should not drag a test framework into an app's dependency
20
+ * graph to do it. Plain `Error`s with actionable messages work in a
21
+ * story `play`, in Vitest, in Playwright, and in a scratch script.
22
+ */
23
+ const describeTarget = ({ name, role }) => name === undefined
24
+ ? `role "${role}"`
25
+ : `role "${role}" named ${typeof name === "string"
26
+ ? `"${name}"`
27
+ : String(name)}`;
28
+ /**
29
+ * The composite widgets whose members are *supposed* to carry
30
+ * `tabindex="-1"`.
31
+ *
32
+ * Found by M4. `Tabs` is the first component here to use the
33
+ * roving-tabindex pattern, and this helper rejected it outright:
34
+ * "has a negative tabindex, so it can be clicked but never reached
35
+ * with Tab". That rule is right for a standalone button and wrong
36
+ * for a tab — a roving group's whole design is that exactly one
37
+ * member is in the tab order and the arrow keys reach the rest, so
38
+ * a tab list where every tab had `tabindex="0"` would be the bug.
39
+ *
40
+ * So the check is not skipped for these; it is **replaced by the
41
+ * roving rule itself**, which is a stronger assertion than the one
42
+ * it stands in for.
43
+ */
44
+ const COMPOSITE_ROLES = new Set([
45
+ "grid",
46
+ "listbox",
47
+ "menu",
48
+ "menubar",
49
+ "radiogroup",
50
+ "tablist",
51
+ "toolbar",
52
+ "tree",
53
+ "treegrid",
54
+ ]);
55
+ const COMPOSITE_MEMBER_ROLES = new Set([
56
+ "gridcell",
57
+ "menuitem",
58
+ "menuitemcheckbox",
59
+ "menuitemradio",
60
+ "option",
61
+ "radio",
62
+ "tab",
63
+ "treeitem",
64
+ ]);
65
+ const getCompositeGroup = (element) => {
66
+ if (!COMPOSITE_MEMBER_ROLES.has(element.getAttribute("role") ?? "")) {
67
+ return null;
68
+ }
69
+ let current = element.parentElement;
70
+ while (current) {
71
+ if (COMPOSITE_ROLES.has(current.getAttribute("role") ?? "")) {
72
+ return current;
73
+ }
74
+ current = current.parentElement;
75
+ }
76
+ return null;
77
+ };
78
+ const getIsHiddenFromAgents = (element) => {
79
+ let current = element;
80
+ while (current) {
81
+ if (current.getAttribute("aria-hidden") === "true" ||
82
+ current.hasAttribute("inert")) {
83
+ return true;
84
+ }
85
+ current = current.parentElement;
86
+ }
87
+ return false;
88
+ };
89
+ /**
90
+ * Asserts the control an agent would reach for is reachable, unique,
91
+ * and keyboard-operable. Returns it, so a caller can go on to assert
92
+ * something specific about it.
93
+ */
94
+ export const expectAgentDrivable = (canvas, target) => {
95
+ const matches = canvas.queryAllByRole(target.role, target.name === undefined
96
+ ? undefined
97
+ : { name: target.name });
98
+ if (matches.length === 0) {
99
+ throw new Error(`Not agent-drivable: nothing matches ${describeTarget(target)}. An agent cannot click what it cannot name — give the control an accessible name (visible text, \`aria-label\`, or a \`VisuallyHidden\` label) rather than a \`data-testid\`.`);
100
+ }
101
+ // Ambiguity is a failure, not a warning. Two "Start" buttons on a
102
+ // page mean an agent picks one at random, which is worse than not
103
+ // finding either — and the fix is a `Card` heading turning each
104
+ // one into its own `region`.
105
+ if (matches.length > 1) {
106
+ throw new Error(`Not agent-drivable: ${matches.length} elements match ${describeTarget(target)}. Scope them — a labelled region per group — or make the names distinct.`);
107
+ }
108
+ const [element] = matches;
109
+ if (!element) {
110
+ throw new Error(`Not agent-drivable: ${describeTarget(target)} matched an empty element.`);
111
+ }
112
+ if (getIsHiddenFromAgents(element)) {
113
+ throw new Error(`Not agent-drivable: ${describeTarget(target)} is inside an \`aria-hidden\` or \`inert\` subtree, so a real screen reader and a real agent both skip it.`);
114
+ }
115
+ if (element.querySelector("[data-testid]")) {
116
+ throw new Error(`\`data-testid\` found inside ${describeTarget(target)}. This library has none by rule: a testid is a name only the test suite can see, which is exactly the gap that leaves an app untestable by an agent.`);
117
+ }
118
+ const isNativelyInteractive = [
119
+ "a",
120
+ "button",
121
+ "input",
122
+ "select",
123
+ "textarea",
124
+ ].includes(element.tagName.toLowerCase());
125
+ const isDisabled = element.hasAttribute("disabled") ||
126
+ element.getAttribute("aria-disabled") === "true";
127
+ if (isNativelyInteractive &&
128
+ !isDisabled &&
129
+ element.tabIndex < 0) {
130
+ const group = getCompositeGroup(element);
131
+ if (!group) {
132
+ throw new Error(`Not agent-drivable: ${describeTarget(target)} has a negative tabindex, so it can be clicked but never reached with Tab.`);
133
+ }
134
+ // A roving group, so the question changes: not "is this
135
+ // element in the tab order" — it must not be — but "is the
136
+ // *group* reachable, exactly once". Zero tab stops strands the
137
+ // whole widget; several put every member back in the tab
138
+ // order, which is the pattern not being implemented at all.
139
+ const tabStops = Array.from(group.querySelectorAll("[role]")).filter((member) => COMPOSITE_MEMBER_ROLES.has(member.getAttribute("role") ?? "") &&
140
+ !member.hasAttribute("disabled") &&
141
+ member.getAttribute("aria-disabled") !== "true" &&
142
+ member.tabIndex >= 0);
143
+ if (tabStops.length !== 1) {
144
+ throw new Error(`Not agent-drivable: ${describeTarget(target)} is inside a \`${group.getAttribute("role")}\` with ${tabStops.length} tab stops. A roving-tabindex group needs exactly one — Tab enters and leaves it once, and the arrow keys move within it.`);
145
+ }
146
+ }
147
+ return element;
148
+ };
149
+ /**
150
+ * The other half of the contract, for decoration.
151
+ *
152
+ * A `Skeleton` that assistive technology can see is a screen reader
153
+ * reading three empty bars, and an agent finding a "loading" element
154
+ * it cannot act on. Asserting the *absence* of a role is how that
155
+ * stays true — axe will not tell you, because there is nothing
156
+ * invalid about it.
157
+ */
158
+ export const expectHiddenFromAgents = (element) => {
159
+ if (!getIsHiddenFromAgents(element)) {
160
+ throw new Error('Expected this element to be hidden from assistive technology (`aria-hidden="true"` or `inert`). Decoration that is announced is noise: the load has to be announced by the region that owns it, not by its placeholder.');
161
+ }
162
+ };
163
+ //# sourceMappingURL=expectAgentDrivable.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"expectAgentDrivable.js","sourceRoot":"","sources":["../../src/testing/expectAgentDrivable.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * `@charcuterie/ui/testing` — the drivability gates, shipped so
3
+ * consumers can hold their own components to them.
4
+ *
5
+ * A separate entry point rather than part of the barrel: nothing
6
+ * here belongs in an app's production bundle, and a deep import is
7
+ * what keeps it out without a build-time trick.
8
+ */
9
+ export type { AgentQueries, AgentTarget, } from "./expectAgentDrivable.ts";
10
+ export { expectAgentDrivable, expectHiddenFromAgents, } from "./expectAgentDrivable.ts";
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,YAAY,EACV,YAAY,EACZ,WAAW,GACZ,MAAM,0BAA0B,CAAA;AACjC,OAAO,EACL,mBAAmB,EACnB,sBAAsB,GACvB,MAAM,0BAA0B,CAAA"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `@charcuterie/ui/testing` — the drivability gates, shipped so
3
+ * consumers can hold their own components to them.
4
+ *
5
+ * A separate entry point rather than part of the barrel: nothing
6
+ * here belongs in an app's production bundle, and a deep import is
7
+ * what keeps it out without a build-time trick.
8
+ */
9
+ export { expectAgentDrivable, expectHiddenFromAgents, } from "./expectAgentDrivable.js";
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,OAAO,EACL,mBAAmB,EACnB,sBAAsB,GACvB,MAAM,0BAA0B,CAAA"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Filter-and-join. Deliberately not `clsx`.
3
+ *
4
+ * Every call site in this package passes a flat list of strings and
5
+ * conditionals, which is four lines of work — and a published
6
+ * component library's dependency list is a thing consumers inherit,
7
+ * so it earns its entries. `clsx` also accepts objects and nested
8
+ * arrays, both of which make a className harder for the Tailwind
9
+ * scanner and for `tailwindCandidates.test.ts` to read statically.
10
+ */
11
+ export declare const toClassName: (...parts: (false | string | null | undefined)[]) => string;
12
+ //# sourceMappingURL=toClassName.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toClassName.d.ts","sourceRoot":"","sources":["../src/toClassName.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,eAAO,MAAM,WAAW,GACtB,GAAG,OAAO,CAAC,KAAK,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,WACb,CAAA"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Filter-and-join. Deliberately not `clsx`.
3
+ *
4
+ * Every call site in this package passes a flat list of strings and
5
+ * conditionals, which is four lines of work — and a published
6
+ * component library's dependency list is a thing consumers inherit,
7
+ * so it earns its entries. `clsx` also accepts objects and nested
8
+ * arrays, both of which make a className harder for the Tailwind
9
+ * scanner and for `tailwindCandidates.test.ts` to read statically.
10
+ */
11
+ export const toClassName = (...parts) => parts.filter(Boolean).join(" ");
12
+ //# sourceMappingURL=toClassName.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toClassName.js","sourceRoot":"","sources":["../src/toClassName.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CACzB,GAAG,KAA4C,EAC/C,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * `@charcuterie/ui/tokens` — the tokens, under the name a React
3
+ * consumer already has installed.
4
+ *
5
+ * The build-graph split between `tokens` and `ui` exists for two
6
+ * consumers that cannot take a React tree: `castkit/packages/views`
7
+ * renders through Satori and needs resolved literals, and
8
+ * `slatecast` has 60 KB gz to spend. Neither should have to install
9
+ * `@charcuterie/ui` to read a colour.
10
+ *
11
+ * That is a build-graph split, **not an API split** — so a React app
12
+ * importing `@charcuterie/ui` gets the tokens from the same package
13
+ * name and never has to know the boundary exists.
14
+ */
15
+ export * from "@charcuterie/tokens";
16
+ //# sourceMappingURL=tokens.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,cAAc,qBAAqB,CAAA"}
package/dist/tokens.js ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * `@charcuterie/ui/tokens` — the tokens, under the name a React
3
+ * consumer already has installed.
4
+ *
5
+ * The build-graph split between `tokens` and `ui` exists for two
6
+ * consumers that cannot take a React tree: `castkit/packages/views`
7
+ * renders through Satori and needs resolved literals, and
8
+ * `slatecast` has 60 KB gz to spend. Neither should have to install
9
+ * `@charcuterie/ui` to read a colour.
10
+ *
11
+ * That is a build-graph split, **not an API split** — so a React app
12
+ * importing `@charcuterie/ui` gets the tokens from the same package
13
+ * name and never has to know the boundary exists.
14
+ */
15
+ export * from "@charcuterie/tokens";
16
+ //# sourceMappingURL=tokens.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.js","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,cAAc,qBAAqB,CAAA"}
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@charcuterie/ui",
3
+ "version": "0.1.0",
4
+ "description": "The Charcuterie components. Tokens for look, @charcuterie/logic for state, ARIA that agents can actually drive.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": [
8
+ "**/*.css"
9
+ ],
10
+ "main": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "publishConfig": {
13
+ "access": "public",
14
+ "provenance": true
15
+ },
16
+ "exports": {
17
+ ".": {
18
+ "source": "./src/index.ts",
19
+ "types": "./dist/index.d.ts",
20
+ "default": "./dist/index.js"
21
+ },
22
+ "./styles.css": "./dist/styles.css",
23
+ "./testing": {
24
+ "source": "./src/testing/index.ts",
25
+ "types": "./dist/testing/index.d.ts",
26
+ "default": "./dist/testing/index.js"
27
+ },
28
+ "./tokens": {
29
+ "source": "./src/tokens.ts",
30
+ "types": "./dist/tokens.d.ts",
31
+ "default": "./dist/tokens.js"
32
+ },
33
+ "./src/*": "./src/*"
34
+ },
35
+ "files": [
36
+ "dist",
37
+ "src"
38
+ ],
39
+ "scripts": {
40
+ "build": "tsc -p tsconfig.build.json && node scripts/copyStyles.ts",
41
+ "prepack": "yarn build",
42
+ "test": "vitest",
43
+ "typecheck": "tsc --noEmit"
44
+ },
45
+ "dependencies": {
46
+ "@charcuterie/logic": "0.1.0",
47
+ "@charcuterie/tokens": "0.1.0",
48
+ "@floating-ui/react": "^0.27.20"
49
+ },
50
+ "peerDependencies": {
51
+ "react": ">=19",
52
+ "react-dom": ">=19"
53
+ },
54
+ "devDependencies": {
55
+ "@storybook/react": "^10.3.6",
56
+ "@types/react": "^19.2.14",
57
+ "@types/react-dom": "^19.2.3",
58
+ "react": "^19.2.8",
59
+ "react-dom": "^19.2.8",
60
+ "storybook": "^10.3.6",
61
+ "tailwindcss": "^4.3.0",
62
+ "typescript": "^6.0.3",
63
+ "vitest": "^4.1.8"
64
+ },
65
+ "repository": {
66
+ "type": "git",
67
+ "url": "https://github.com/Sawtaytoes/charcuterie.git",
68
+ "directory": "packages/ui"
69
+ }
70
+ }
@@ -0,0 +1,70 @@
1
+ import { Canvas, Controls, Meta } from "@storybook/addon-docs/blocks";
2
+
3
+ import * as AlertStories from "./Alert.stories";
4
+
5
+ <Meta of={AlertStories} />
6
+
7
+ # Alert
8
+
9
+ **The component M5 found, rather than one the plan predicted.** rip-deck spells this shape
10
+ four times — `TowerAlerts`, `UsbAlertBanner`, `LoadedDiscsBanner`, `VerdictBadge` — and two
11
+ of those carry a byte-identical `TONE_CLASS` map of hardcoded hexes with no light mode.
12
+ mux-magic spells the same idea a third way in `statusClassMap`.
13
+
14
+ <Canvas of={AlertStories.Default} />
15
+
16
+ ## Props
17
+
18
+ <Controls />
19
+
20
+ <Canvas of={AlertStories.AllVariants} />
21
+
22
+ ## It is a block by default, and a landmark only when named
23
+
24
+ `label` decides, and the evidence divides cleanly.
25
+
26
+ The **banners** are one alert at the top of a page and want to be addressable —
27
+ `getByRole("region", { name: "USB connection alert" })`. They pass a `label`.
28
+
29
+ The **verdict** renders inside every bay card, and one of its two forms is the single
30
+ sentence *"Part of the tower-wide problem above."* Nine landmarks sharing one name is axe's
31
+ `landmark-unique`, so it passes none.
32
+
33
+ A component that always took the landmark would fail its own a11y gate on a nine-bay tower.
34
+ One that never took it leaves three `<section aria-label>` wrappers hand-rolled in the
35
+ consumer — which is what rip-deck has today.
36
+
37
+ <Canvas of={AlertStories.AllStates} />
38
+
39
+ ## The heading is not a heading element
40
+
41
+ An alert's headline is a **sentence** — "Bay 7 is reading with errors; clean the disc and
42
+ try again" — not a node in the document outline. Nine bays' worth of those as `<h3>`s
43
+ builds a table of contents out of nine complaints, which is worse for a screen reader than
44
+ no outline entry at all.
45
+
46
+ `Card` and `EmptyState` take a real `headingLevel` precisely because their headings *are*
47
+ short outline nodes. This one is styled text, and `label` is how it becomes findable.
48
+
49
+ ## It does not announce, and that is a component boundary
50
+
51
+ No `role="alert"`, no `aria-live`. Two reasons, and the second is the load-bearing one:
52
+
53
+ 1. A live region takes **no accessible name from its content**
54
+ ([decision](../../../docs/decisions/2026-07-29-status-regions-carry-an-aria-label.md)),
55
+ so an announcing variant needs a name that duplicates the sentence it is announcing.
56
+ 2. `role="alert"` on a `<section>` **overrides the landmark**. It cannot be both a thing
57
+ you can scope queries to and a thing that interrupts you.
58
+
59
+ A thing that interrupts you is a `Toast` (M6). This is a thing that is *on the page*.
60
+
61
+ <Canvas of={AlertStories.Responsive} />
62
+
63
+ ## The evidence is a list
64
+
65
+ `details` renders a `<ul>`, so a screen reader announces "list, 3 items" instead of running
66
+ three measurements together into one sentence. The key is `${index}-${line}` rather than
67
+ the line itself, because **evidence repeats**: two drives both reporting "3 read errors" is
68
+ the normal case on a tower, not an edge one.
69
+
70
+ <Canvas of={AlertStories.Interactive} />