@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,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"
|