@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,150 @@
1
+ import type { ReactNode } from "react"
2
+
3
+ /**
4
+ * The furniture every story set uses: a labelled grid for
5
+ * `AllVariants`, a labelled row for `AllStates`, and the three
6
+ * container widths `Responsive` renders into.
7
+ *
8
+ * `.storyHelpers.tsx` rather than `.tsx` because `react/no-multi-comp`
9
+ * is switched off for that suffix — a file whose entire job is
10
+ * several small layout components is the case that rule exists to
11
+ * exempt, and it is excluded from the package build so none of this
12
+ * ships.
13
+ */
14
+
15
+ export const StoryGrid = ({
16
+ children,
17
+ columns = 4,
18
+ }: {
19
+ children: ReactNode
20
+ columns?: number
21
+ }): ReactNode => (
22
+ <div
23
+ className="grid items-start gap-4"
24
+ style={{
25
+ gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`,
26
+ }}
27
+ >
28
+ {children}
29
+ </div>
30
+ )
31
+
32
+ export const StoryCell = ({
33
+ align = "start",
34
+ children,
35
+ label,
36
+ }: {
37
+ /**
38
+ * `stretch` for anything that establishes a **container query** —
39
+ * `Card`, `MediaTile`, `EmptyState`.
40
+ *
41
+ * Not a cosmetic choice. `container-type: inline-size` implies
42
+ * `contain: inline-size`, which means the element's inline size may
43
+ * not depend on its own contents — so in a shrink-to-fit context
44
+ * (`items-start`, an inline-flex parent) it collapses to
45
+ * min-content and every line of text wraps after one word. It
46
+ * needs a definite inline size from its parent, which a grid track
47
+ * gives it for free.
48
+ *
49
+ * The default stays `start`, because a `Button` in a stretched cell
50
+ * would silently become full-width and misrepresent itself.
51
+ */
52
+ align?: "start" | "stretch"
53
+ children: ReactNode
54
+ label: string
55
+ }): ReactNode => (
56
+ <div
57
+ className={
58
+ align === "stretch"
59
+ ? "flex flex-col items-stretch gap-2"
60
+ : "flex flex-col items-start gap-2"
61
+ }
62
+ >
63
+ <span className="font-mono text-content-muted text-xs">
64
+ {label}
65
+ </span>
66
+
67
+ {children}
68
+ </div>
69
+ )
70
+
71
+ export const StoryRow = ({
72
+ children,
73
+ }: {
74
+ children: ReactNode
75
+ }): ReactNode => (
76
+ <div className="flex flex-wrap items-center gap-4">
77
+ {children}
78
+ </div>
79
+ )
80
+
81
+ export const StorySection = ({
82
+ children,
83
+ title,
84
+ }: {
85
+ children: ReactNode
86
+ title: string
87
+ }): ReactNode => (
88
+ <section className="flex flex-col gap-3">
89
+ <h3 className="font-semibold text-content-secondary text-sm">
90
+ {title}
91
+ </h3>
92
+
93
+ {children}
94
+ </section>
95
+ )
96
+
97
+ /**
98
+ * The three container widths, side by side.
99
+ *
100
+ * The **only honest way to story a container query**: resizing the
101
+ * viewport does not exercise one, because the component's container
102
+ * is what changed, not the window. Each panel is a fixed inline size
103
+ * matching a `--cq-*` step, so a `cq-sm:` utility flips between the
104
+ * first and second panel while the browser window never moves.
105
+ *
106
+ * The labels are the token names on purpose — a reviewer should be
107
+ * able to read "this is what the card does below `cq-sm`" straight
108
+ * off the board.
109
+ */
110
+ export const ContainerBoard = ({
111
+ children,
112
+ }: {
113
+ /**
114
+ * A function form as well as a node, because three copies of the
115
+ * same subtree can be an a11y violation rather than a convenience:
116
+ * two `<section>`s with the same accessible name is axe's
117
+ * `landmark-unique`, and a board comparing widths must not fail for
118
+ * a reason the component would never hit in an app. Callers whose
119
+ * component becomes a landmark pass a function and vary the name.
120
+ */
121
+ children: ReactNode | ((width: string) => ReactNode)
122
+ }): ReactNode => (
123
+ <div className="flex flex-wrap items-start gap-6">
124
+ {[
125
+ {
126
+ inlineSize: "15rem",
127
+ label: "narrower than --cq-xs (15rem)",
128
+ },
129
+ { inlineSize: "24rem", label: "--cq-sm (24rem)" },
130
+ {
131
+ inlineSize: "34rem",
132
+ label: "wider than --cq-md (34rem)",
133
+ },
134
+ ].map((panel) => (
135
+ <div
136
+ className="flex flex-col gap-2"
137
+ key={panel.label}
138
+ style={{ inlineSize: panel.inlineSize }}
139
+ >
140
+ <span className="font-mono text-content-muted text-xs">
141
+ {panel.label}
142
+ </span>
143
+
144
+ {typeof children === "function"
145
+ ? children(panel.inlineSize)
146
+ : children}
147
+ </div>
148
+ ))}
149
+ </div>
150
+ )
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Control sizing, read from the density axis rather than chosen per
3
+ * component.
4
+ *
5
+ * The values are never in this file — `h-(--control-height-md)`
6
+ * points at a variable that `[data-density]` rewrites, so the same
7
+ * `<Button size="md">` is 2.25rem on a desktop, 1.875rem in a
8
+ * compact bay list, and 3.25rem on the kiosk with no prop change
9
+ * and no re-render. That is the entire argument for a density axis
10
+ * over a `size="kiosk"` prop, and it only works because nothing
11
+ * here hardcodes a length.
12
+ *
13
+ * `text-sm`/`text-md`/`text-lg` are ours too, not Tailwind's:
14
+ * `theme.css` bridges `--text-*` onto `--font-size-*`, which the
15
+ * density axis also scales. See
16
+ * `packages/tokens/src/tailwindCollisions.test.ts`.
17
+ */
18
+
19
+ import type { ControlSize } from "@charcuterie/tokens"
20
+
21
+ export const CONTROL_SIZE_CLASS: Record<
22
+ ControlSize,
23
+ string
24
+ > = {
25
+ sm: "h-(--control-height-sm) gap-(--control-gap-sm) px-(--control-padding-inline-sm) text-sm",
26
+ md: "h-(--control-height-md) gap-(--control-gap-md) px-(--control-padding-inline-md) text-md",
27
+ lg: "h-(--control-height-lg) gap-(--control-gap-lg) px-(--control-padding-inline-lg) text-lg",
28
+ }
29
+
30
+ /**
31
+ * Square, and sized by the *height* token on both axes so an icon
32
+ * button lines up with the text button beside it in a toolbar. No
33
+ * inline padding at all — a glyph centres itself.
34
+ */
35
+ export const ICON_CONTROL_SIZE_CLASS: Record<
36
+ ControlSize,
37
+ string
38
+ > = {
39
+ sm: "size-(--control-height-sm) px-0 text-sm",
40
+ md: "size-(--control-height-md) px-0 text-md",
41
+ lg: "size-(--control-height-lg) px-0 text-lg",
42
+ }
43
+
44
+ /**
45
+ * The kiosk floor. `--control-min-touch-target` is 44px in every
46
+ * density — a finger does not get smaller because the list is
47
+ * dense — so this is opt-in per control rather than baked into the
48
+ * size classes, which would make `compact` a lie.
49
+ */
50
+ export const MIN_TOUCH_TARGET_CLASS =
51
+ "min-h-(--control-min-touch-target) min-w-(--control-min-touch-target)"
52
+
53
+ /**
54
+ * Non-control sizing: badges, indicator dots, spinners. These are
55
+ * type-relative on purpose — a badge beside `text-sm` prose that
56
+ * stays 20px tall while the kiosk scales the prose to 1.19x reads
57
+ * as a rendering bug.
58
+ */
59
+ export const BADGE_SIZE_CLASS = {
60
+ sm: "gap-1 px-2 py-0.5 text-xs",
61
+ md: "gap-1.5 px-2.5 py-1 text-sm",
62
+ } as const
63
+
64
+ export type BadgeSize = keyof typeof BADGE_SIZE_CLASS
65
+
66
+ export const DOT_SIZE_CLASS = {
67
+ sm: "size-1.5",
68
+ md: "size-2",
69
+ lg: "size-2.5",
70
+ } as const
71
+
72
+ export const SPINNER_SIZE_CLASS: Record<
73
+ ControlSize,
74
+ string
75
+ > = {
76
+ sm: "size-3.5 border-2",
77
+ md: "size-4 border-2",
78
+ lg: "size-5 border-2",
79
+ }
@@ -0,0 +1,41 @@
1
+ import axe from "axe-core"
2
+
3
+ /**
4
+ * Run axe against a subtree, now.
5
+ *
6
+ * The a11y addon audits a story **once**, in an `afterEach` that
7
+ * fires when `run()` resolves — i.e. immediately after the first
8
+ * paint. That was enough while the assertions lived in `play`,
9
+ * because the addon's hook ran after the play had finished driving.
10
+ * With the tests split out, `run()` returns before a test has
11
+ * clicked anything, so the states that most need auditing — an open
12
+ * dialog, a shown popover, a selected tab panel — would never be
13
+ * seen by axe at all.
14
+ *
15
+ * So a driven state audits itself explicitly. The rule set is left
16
+ * at axe's default, matching the addon, except that `region` is
17
+ * disabled for the same reason the addon disables it: a component
18
+ * mounted on its own is not a page and owes nobody a landmark.
19
+ */
20
+ export const expectNoAxeViolations = async (
21
+ element: Element,
22
+ ): Promise<void> => {
23
+ const results = await axe.run(element, {
24
+ rules: { region: { enabled: false } },
25
+ })
26
+
27
+ if (results.violations.length === 0) {
28
+ return
29
+ }
30
+
31
+ throw new Error(
32
+ `Expected no accessibility violations, found ${results.violations.length}:\n\n${results.violations
33
+ .map(
34
+ (violation) =>
35
+ ` ${violation.id} (${violation.impact}) — ${violation.help}\n${violation.nodes
36
+ .map((node) => ` ${node.html}`)
37
+ .join("\n")}`,
38
+ )
39
+ .join("\n\n")}`,
40
+ )
41
+ }
@@ -0,0 +1,116 @@
1
+ import type { ReactNode } from "react"
2
+
3
+ /**
4
+ * Story-only icons. **Nothing here ships.**
5
+ *
6
+ * The library deliberately owns no icon set — `IconButton`,
7
+ * `EmptyState`, `Badge`, and `MediaTile` all take a `ReactNode`, and
8
+ * lucide (ISC) is the fleet recommendation — but the boards still
9
+ * have to show *something*, and what they show should be what an app
10
+ * will actually pass: an inline SVG inheriting `currentColor`.
11
+ *
12
+ * They are hand-drawn rather than copied, and they exist for a second
13
+ * reason worth writing down: **this sandbox's headless Chromium has
14
+ * no font covering `⚙`, `↶`, `▨`, or `⚠`** — every one of those
15
+ * measures blank, so a glyph-only board screenshots as an empty box
16
+ * and a reviewer cannot tell a missing font from a broken component.
17
+ * `IconButton`'s `RawGlyph` story keeps one glyph on purpose, because
18
+ * a raw glyph is what plex-channels renders today and the point of
19
+ * that story is that it gets a *name* regardless.
20
+ */
21
+
22
+ /**
23
+ * `aria-hidden` is repeated on every `<svg>` below rather than living
24
+ * here, because Biome's `noSvgWithoutTitle` cannot see through a
25
+ * spread — and a rule that fires on correct code gets switched off,
26
+ * which would then miss the icon that really is unlabelled. Every
27
+ * icon in this file is decoration beside a real name.
28
+ */
29
+ const iconProps = {
30
+ className: "size-[1.15em] shrink-0",
31
+ fill: "none",
32
+ focusable: false,
33
+ stroke: "currentColor",
34
+ strokeLinecap: "round",
35
+ strokeLinejoin: "round",
36
+ strokeWidth: 1.75,
37
+ viewBox: "0 0 24 24",
38
+ xmlns: "http://www.w3.org/2000/svg",
39
+ } as const
40
+
41
+ export const SettingsIcon = (): ReactNode => (
42
+ <svg {...iconProps} aria-hidden="true">
43
+ <path d="M4 7h10M18 7h2M4 17h4M12 17h8" />
44
+
45
+ <circle cx="16" cy="7" r="2.2" />
46
+
47
+ <circle cx="10" cy="17" r="2.2" />
48
+ </svg>
49
+ )
50
+
51
+ export const UndoIcon = (): ReactNode => (
52
+ <svg {...iconProps} aria-hidden="true">
53
+ <path d="M9 14 4 9l5-5" />
54
+
55
+ <path d="M4 9h9a6 6 0 0 1 0 12h-3" />
56
+ </svg>
57
+ )
58
+
59
+ export const RedoIcon = (): ReactNode => (
60
+ <svg {...iconProps} aria-hidden="true">
61
+ <path d="m15 14 5-5-5-5" />
62
+
63
+ <path d="M20 9h-9a6 6 0 0 0 0 12h3" />
64
+ </svg>
65
+ )
66
+
67
+ export const PlayIcon = (): ReactNode => (
68
+ <svg {...iconProps} aria-hidden="true">
69
+ <path d="M8 5.5v13l11-6.5z" />
70
+ </svg>
71
+ )
72
+
73
+ export const InboxIcon = (): ReactNode => (
74
+ <svg {...iconProps} aria-hidden="true">
75
+ <path d="M3 13h4l2 3h6l2-3h4" />
76
+
77
+ <path d="M5 5h14l2 8v6H3v-6z" />
78
+ </svg>
79
+ )
80
+
81
+ export const SearchIcon = (): ReactNode => (
82
+ <svg {...iconProps} aria-hidden="true">
83
+ <circle cx="10.5" cy="10.5" r="6.5" />
84
+
85
+ <path d="m15.5 15.5 4.5 4.5" />
86
+ </svg>
87
+ )
88
+
89
+ export const AlertIcon = (): ReactNode => (
90
+ <svg {...iconProps} aria-hidden="true">
91
+ <path d="M12 4 2.5 20h19z" />
92
+
93
+ <path d="M12 10v4.5M12 17.5h.01" />
94
+ </svg>
95
+ )
96
+
97
+ export const ImageOffIcon = (): ReactNode => (
98
+ <svg {...iconProps} aria-hidden="true">
99
+ <path d="M4 4h16v16H4z" />
100
+
101
+ <path d="m4 20 6-7 4 4 3-3 3 3.5" />
102
+
103
+ <path d="m3 3 18 18" />
104
+ </svg>
105
+ )
106
+
107
+ export const DotIcon = (): ReactNode => (
108
+ <svg
109
+ {...iconProps}
110
+ aria-hidden="true"
111
+ fill="currentColor"
112
+ stroke="none"
113
+ >
114
+ <circle cx="12" cy="12" r="4" />
115
+ </svg>
116
+ )
package/src/index.ts ADDED
@@ -0,0 +1,132 @@
1
+ /**
2
+ * `@charcuterie/ui` — the one sanctioned barrel.
3
+ *
4
+ * Components never import each other through this file; they import
5
+ * the sibling module directly (`../Spinner/Spinner.tsx`). A barrel
6
+ * that a package's own internals go through turns every component
7
+ * into a dependency of every other one, which is how a 3 KB
8
+ * `Spinner` starts pulling in `MediaTile`.
9
+ *
10
+ * `"./src/*"` is also in `exports`, for the budget-sensitive
11
+ * consumers — `castkit/packages/slatecast` has 60 KB gz to spend and
12
+ * should be able to reach one component without the barrel. Same
13
+ * arrangement as `mux-magic/packages/tools`.
14
+ *
15
+ * The token **types and values** are re-exported at
16
+ * `@charcuterie/ui/tokens` rather than from here, so a React
17
+ * consumer's TypeScript never names a second package while Satori
18
+ * still gets `@charcuterie/tokens` with no React in sight.
19
+ *
20
+ * The token **stylesheet** is not, and M5 is why. There was a
21
+ * `"./tokens.css": "./node_modules/@charcuterie/tokens/dist/theme.css"`
22
+ * export here, and it resolved to nothing in the first real
23
+ * consumer: a hoisting linker puts `@charcuterie/tokens` at the
24
+ * *project* root, not inside this package, so that path exists only
25
+ * under a nesting one. A CSS `@import` fails silently in Tailwind —
26
+ * no error, no utilities, an unstyled app — which is exactly the
27
+ * class of failure `portal:` was chosen to catch
28
+ * ([decision](../../../docs/decisions/2026-07-29-consumers-link-tokens-by-portal-until-publish.md)).
29
+ * So a consumer installs both package names and imports
30
+ * `@charcuterie/tokens/theme.css` by its own name, which is what
31
+ * this package's README has always told it to do.
32
+ */
33
+
34
+ export type {
35
+ AlertProps,
36
+ AlertSize,
37
+ } from "./Alert/Alert.tsx"
38
+ export { Alert } from "./Alert/Alert.tsx"
39
+ export type { BadgeProps } from "./Badge/Badge.tsx"
40
+ export { Badge } from "./Badge/Badge.tsx"
41
+ export type { ButtonProps } from "./Button/Button.tsx"
42
+ export { Button } from "./Button/Button.tsx"
43
+ export type {
44
+ CardPadding,
45
+ CardProps,
46
+ CardSurface,
47
+ } from "./Card/Card.tsx"
48
+ export { Card } from "./Card/Card.tsx"
49
+ export type { BadgeSize } from "./controlStyles.ts"
50
+ export {
51
+ BADGE_SIZE_CLASS,
52
+ CONTROL_SIZE_CLASS,
53
+ DOT_SIZE_CLASS,
54
+ ICON_CONTROL_SIZE_CLASS,
55
+ MIN_TOUCH_TARGET_CLASS,
56
+ SPINNER_SIZE_CLASS,
57
+ } from "./controlStyles.ts"
58
+ export type {
59
+ EmptyStateProps,
60
+ EmptyStateSize,
61
+ } from "./EmptyState/EmptyState.tsx"
62
+ export { EmptyState } from "./EmptyState/EmptyState.tsx"
63
+ export type { IconButtonProps } from "./IconButton/IconButton.tsx"
64
+ export { IconButton } from "./IconButton/IconButton.tsx"
65
+ export type { IntentAppearance } from "./intentStyles.ts"
66
+ export {
67
+ DISABLED_CLASS,
68
+ FOCUS_RING_CLASS,
69
+ INTENT_APPEARANCE_CLASS,
70
+ INTENT_CONTENT_CLASS,
71
+ INTENT_HOVER_CLASS,
72
+ INTENT_SOLID_FILL_CLASS,
73
+ } from "./intentStyles.ts"
74
+ export type {
75
+ LiveStatusIndicatorProps,
76
+ LiveStatusIndicatorSize,
77
+ } from "./LiveStatusIndicator/LiveStatusIndicator.tsx"
78
+ export { LiveStatusIndicator } from "./LiveStatusIndicator/LiveStatusIndicator.tsx"
79
+ export type {
80
+ MediaTileProps,
81
+ MediaTileRatio,
82
+ } from "./MediaTile/MediaTile.tsx"
83
+ export { MediaTile } from "./MediaTile/MediaTile.tsx"
84
+ export type { MediaStatus } from "./MediaTile/mediaStatus.ts"
85
+ export { mediaTransitions } from "./MediaTile/mediaStatus.ts"
86
+ export type {
87
+ ModalProps,
88
+ ModalSize,
89
+ } from "./Modal/Modal.tsx"
90
+ export { Modal } from "./Modal/Modal.tsx"
91
+ export type { PopoverProps } from "./Popover/Popover.tsx"
92
+ export { Popover } from "./Popover/Popover.tsx"
93
+ export type {
94
+ ProgressBarProps,
95
+ ProgressBarSize,
96
+ } from "./ProgressBar/ProgressBar.tsx"
97
+ export { ProgressBar } from "./ProgressBar/ProgressBar.tsx"
98
+ export type { ProgressThreshold } from "./ProgressBar/progressValue.ts"
99
+ export {
100
+ getProgressIntent,
101
+ toProgressPercent,
102
+ toProgressValue,
103
+ } from "./ProgressBar/progressValue.ts"
104
+ export type {
105
+ SegmentedControlProps,
106
+ SegmentedItem,
107
+ } from "./SegmentedControl/SegmentedControl.tsx"
108
+ export { SegmentedControl } from "./SegmentedControl/SegmentedControl.tsx"
109
+ export type {
110
+ SkeletonProps,
111
+ SkeletonShape,
112
+ } from "./Skeleton/Skeleton.tsx"
113
+ export { Skeleton } from "./Skeleton/Skeleton.tsx"
114
+ export type { SpinnerProps } from "./Spinner/Spinner.tsx"
115
+ export { Spinner } from "./Spinner/Spinner.tsx"
116
+ export {
117
+ getAsyncIntent,
118
+ getAsyncLabel,
119
+ getConnectionIntent,
120
+ getConnectionLabel,
121
+ getIsConnectionBusy,
122
+ } from "./statusIntent.ts"
123
+ export type {
124
+ TabItem,
125
+ TabsActivation,
126
+ TabsOrientation,
127
+ TabsProps,
128
+ } from "./Tabs/Tabs.tsx"
129
+ export { Tabs } from "./Tabs/Tabs.tsx"
130
+ export { toClassName } from "./toClassName.ts"
131
+ export type { VisuallyHiddenProps } from "./VisuallyHidden/VisuallyHidden.tsx"
132
+ export { VisuallyHidden } from "./VisuallyHidden/VisuallyHidden.tsx"