@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,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"}
|
package/dist/tokens.d.ts
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.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} />
|