@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,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One intent → class map, for the whole fleet.
|
|
3
|
+
*
|
|
4
|
+
* This file is what M5 deletes from rip-deck. `TONE_CLASS` is
|
|
5
|
+
* declared *identically* in `VerdictBadge.tsx` and
|
|
6
|
+
* `TowerAlerts.tsx` today — hardcoded hexes and `slate-400`s, no
|
|
7
|
+
* light mode, no relationship to mux-magic's `statusClassMap` which
|
|
8
|
+
* spells the same idea a fourth way. Six intents x four
|
|
9
|
+
* appearances, named once, is the generalisation.
|
|
10
|
+
*
|
|
11
|
+
* **Every class name is written out in full, and that is not
|
|
12
|
+
* negotiable.** Tailwind v4 scans source text for *complete* class
|
|
13
|
+
* strings, so `` `bg-intent-${intent}-solid` `` generates nothing
|
|
14
|
+
* at all — the element renders unstyled with no error, no warning,
|
|
15
|
+
* and no failing test unless something checks. Something does:
|
|
16
|
+
* `tailwindCandidates.test.ts` compiles every literal in this
|
|
17
|
+
* package through the real Tailwind and fails on any candidate
|
|
18
|
+
* Tailwind cannot generate. That test is the reason this file is
|
|
19
|
+
* allowed to be 24 lines of near-duplication instead of a loop.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { IntentName } from "@charcuterie/tokens"
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Four treatments, and the split is semantic rather than cosmetic:
|
|
26
|
+
*
|
|
27
|
+
* - `solid` — the saturated fill. One per view, usually; it is the
|
|
28
|
+
* thing you are meant to press.
|
|
29
|
+
* - `soft` — the tinted treatment the fleet already reaches for on
|
|
30
|
+
* status pills (`bg-blue-950 text-blue-300` in mux-magic,
|
|
31
|
+
* `bg-amber-950/40 text-amber-200` in rip-deck).
|
|
32
|
+
* - `outline` — border and content only. Reads as secondary
|
|
33
|
+
* without claiming a surface.
|
|
34
|
+
* - `ghost` — nothing until hovered. Toolbars and icon rows.
|
|
35
|
+
*/
|
|
36
|
+
export type IntentAppearance =
|
|
37
|
+
| "ghost"
|
|
38
|
+
| "outline"
|
|
39
|
+
| "soft"
|
|
40
|
+
| "solid"
|
|
41
|
+
|
|
42
|
+
export const INTENT_APPEARANCE_CLASS: Record<
|
|
43
|
+
IntentName,
|
|
44
|
+
Record<IntentAppearance, string>
|
|
45
|
+
> = {
|
|
46
|
+
neutral: {
|
|
47
|
+
ghost:
|
|
48
|
+
"border-transparent bg-transparent text-content-primary",
|
|
49
|
+
outline:
|
|
50
|
+
"border-intent-neutral-border bg-transparent text-intent-neutral-content",
|
|
51
|
+
soft: "border-intent-neutral-border bg-intent-neutral-surface text-intent-neutral-content",
|
|
52
|
+
solid:
|
|
53
|
+
"border-transparent bg-intent-neutral-solid text-intent-neutral-on-solid",
|
|
54
|
+
},
|
|
55
|
+
accent: {
|
|
56
|
+
ghost:
|
|
57
|
+
"border-transparent bg-transparent text-intent-accent-content",
|
|
58
|
+
outline:
|
|
59
|
+
"border-intent-accent-border bg-transparent text-intent-accent-content",
|
|
60
|
+
soft: "border-intent-accent-border bg-intent-accent-surface text-intent-accent-content",
|
|
61
|
+
solid:
|
|
62
|
+
"border-transparent bg-intent-accent-solid text-intent-accent-on-solid",
|
|
63
|
+
},
|
|
64
|
+
success: {
|
|
65
|
+
ghost:
|
|
66
|
+
"border-transparent bg-transparent text-intent-success-content",
|
|
67
|
+
outline:
|
|
68
|
+
"border-intent-success-border bg-transparent text-intent-success-content",
|
|
69
|
+
soft: "border-intent-success-border bg-intent-success-surface text-intent-success-content",
|
|
70
|
+
solid:
|
|
71
|
+
"border-transparent bg-intent-success-solid text-intent-success-on-solid",
|
|
72
|
+
},
|
|
73
|
+
warning: {
|
|
74
|
+
ghost:
|
|
75
|
+
"border-transparent bg-transparent text-intent-warning-content",
|
|
76
|
+
outline:
|
|
77
|
+
"border-intent-warning-border bg-transparent text-intent-warning-content",
|
|
78
|
+
soft: "border-intent-warning-border bg-intent-warning-surface text-intent-warning-content",
|
|
79
|
+
solid:
|
|
80
|
+
"border-transparent bg-intent-warning-solid text-intent-warning-on-solid",
|
|
81
|
+
},
|
|
82
|
+
danger: {
|
|
83
|
+
ghost:
|
|
84
|
+
"border-transparent bg-transparent text-intent-danger-content",
|
|
85
|
+
outline:
|
|
86
|
+
"border-intent-danger-border bg-transparent text-intent-danger-content",
|
|
87
|
+
soft: "border-intent-danger-border bg-intent-danger-surface text-intent-danger-content",
|
|
88
|
+
solid:
|
|
89
|
+
"border-transparent bg-intent-danger-solid text-intent-danger-on-solid",
|
|
90
|
+
},
|
|
91
|
+
info: {
|
|
92
|
+
ghost:
|
|
93
|
+
"border-transparent bg-transparent text-intent-info-content",
|
|
94
|
+
outline:
|
|
95
|
+
"border-intent-info-border bg-transparent text-intent-info-content",
|
|
96
|
+
soft: "border-intent-info-border bg-intent-info-surface text-intent-info-content",
|
|
97
|
+
solid:
|
|
98
|
+
"border-transparent bg-intent-info-solid text-intent-info-on-solid",
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Hover, split out rather than folded into the appearance above —
|
|
104
|
+
* because most things wearing an intent are **not** interactive.
|
|
105
|
+
*
|
|
106
|
+
* A `Badge` that lightens under the cursor is telling the user it
|
|
107
|
+
* can be clicked, and rip-deck's verdict pills (which cannot) would
|
|
108
|
+
* inherit exactly that if the two maps were one. So an interactive
|
|
109
|
+
* component opts in, and a static one cannot accidentally opt in.
|
|
110
|
+
*
|
|
111
|
+
* `solid` hovers toward `solidHover`; the other three land on the
|
|
112
|
+
* intent's tinted surface, which is what gives a ghost toolbar
|
|
113
|
+
* button something to become.
|
|
114
|
+
*/
|
|
115
|
+
export const INTENT_HOVER_CLASS: Record<
|
|
116
|
+
IntentName,
|
|
117
|
+
Record<IntentAppearance, string>
|
|
118
|
+
> = {
|
|
119
|
+
neutral: {
|
|
120
|
+
ghost: "hover:bg-intent-neutral-surface",
|
|
121
|
+
outline: "hover:bg-intent-neutral-surface",
|
|
122
|
+
soft: "hover:bg-intent-neutral-surface-hover",
|
|
123
|
+
solid: "hover:bg-intent-neutral-solid-hover",
|
|
124
|
+
},
|
|
125
|
+
accent: {
|
|
126
|
+
ghost: "hover:bg-intent-accent-surface",
|
|
127
|
+
outline: "hover:bg-intent-accent-surface",
|
|
128
|
+
soft: "hover:bg-intent-accent-surface-hover",
|
|
129
|
+
solid: "hover:bg-intent-accent-solid-hover",
|
|
130
|
+
},
|
|
131
|
+
success: {
|
|
132
|
+
ghost: "hover:bg-intent-success-surface",
|
|
133
|
+
outline: "hover:bg-intent-success-surface",
|
|
134
|
+
soft: "hover:bg-intent-success-surface-hover",
|
|
135
|
+
solid: "hover:bg-intent-success-solid-hover",
|
|
136
|
+
},
|
|
137
|
+
warning: {
|
|
138
|
+
ghost: "hover:bg-intent-warning-surface",
|
|
139
|
+
outline: "hover:bg-intent-warning-surface",
|
|
140
|
+
soft: "hover:bg-intent-warning-surface-hover",
|
|
141
|
+
solid: "hover:bg-intent-warning-solid-hover",
|
|
142
|
+
},
|
|
143
|
+
danger: {
|
|
144
|
+
ghost: "hover:bg-intent-danger-surface",
|
|
145
|
+
outline: "hover:bg-intent-danger-surface",
|
|
146
|
+
soft: "hover:bg-intent-danger-surface-hover",
|
|
147
|
+
solid: "hover:bg-intent-danger-solid-hover",
|
|
148
|
+
},
|
|
149
|
+
info: {
|
|
150
|
+
ghost: "hover:bg-intent-info-surface",
|
|
151
|
+
outline: "hover:bg-intent-info-surface",
|
|
152
|
+
soft: "hover:bg-intent-info-surface-hover",
|
|
153
|
+
solid: "hover:bg-intent-info-solid-hover",
|
|
154
|
+
},
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* The fill colour on its own, for the parts of a component that are
|
|
159
|
+
* a bar or a dot rather than a box — a `ProgressBar`'s fill, a
|
|
160
|
+
* `LiveStatusIndicator`'s dot. Same values, no border and no hover,
|
|
161
|
+
* because neither of those is a surface anyone can point at.
|
|
162
|
+
*/
|
|
163
|
+
export const INTENT_SOLID_FILL_CLASS: Record<
|
|
164
|
+
IntentName,
|
|
165
|
+
string
|
|
166
|
+
> = {
|
|
167
|
+
neutral: "bg-intent-neutral-solid",
|
|
168
|
+
accent: "bg-intent-accent-solid",
|
|
169
|
+
success: "bg-intent-success-solid",
|
|
170
|
+
warning: "bg-intent-warning-solid",
|
|
171
|
+
danger: "bg-intent-danger-solid",
|
|
172
|
+
info: "bg-intent-info-solid",
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** The same six as text, for a glyph or a dot's ring. */
|
|
176
|
+
export const INTENT_CONTENT_CLASS: Record<
|
|
177
|
+
IntentName,
|
|
178
|
+
string
|
|
179
|
+
> = {
|
|
180
|
+
neutral: "text-intent-neutral-content",
|
|
181
|
+
accent: "text-intent-accent-content",
|
|
182
|
+
success: "text-intent-success-content",
|
|
183
|
+
warning: "text-intent-warning-content",
|
|
184
|
+
danger: "text-intent-danger-content",
|
|
185
|
+
info: "text-intent-info-content",
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The focus ring, in one place, reading the variant's own width and
|
|
190
|
+
* offset.
|
|
191
|
+
*
|
|
192
|
+
* `focus-visible` rather than `focus`: a mouse click on a button
|
|
193
|
+
* should not leave a ring behind, and every hand-rolled button in
|
|
194
|
+
* the inventory either rings on click or — more often — has
|
|
195
|
+
* `outline-none` with nothing put back.
|
|
196
|
+
*/
|
|
197
|
+
export const FOCUS_RING_CLASS =
|
|
198
|
+
"focus-visible:outline-solid focus-visible:outline-(length:--focus-ring-width) focus-visible:outline-offset-(--focus-ring-offset) focus-visible:outline-focus-ring"
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* WCAG 1.4.3 exempts *inactive* controls from contrast, which is
|
|
202
|
+
* the only reason `content.disabled` is allowed to be this quiet —
|
|
203
|
+
* and the exemption is honest only on something actually disabled.
|
|
204
|
+
* Keeping it in one constant is what stops it drifting onto prose.
|
|
205
|
+
*/
|
|
206
|
+
export const DISABLED_CLASS =
|
|
207
|
+
"disabled:cursor-not-allowed disabled:border-border-subtle disabled:bg-surface-sunken disabled:text-content-disabled disabled:shadow-none"
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every `<Canvas of={XStories.Y} />` in an `.mdx` names a story that
|
|
3
|
+
* still exists.
|
|
4
|
+
*
|
|
5
|
+
* A missing one does not fail the build, does not fail typecheck,
|
|
6
|
+
* and does not fail `vitest` — MDX resolves `of` at *runtime*, so a
|
|
7
|
+
* renamed story turns into `of={undefined}` and the docs page
|
|
8
|
+
* renders Storybook's "component failed to render" panel instead of
|
|
9
|
+
* the page. `yarn smoke:storybook` catches it, which is good, but
|
|
10
|
+
* only after a full Storybook build; this catches it in under a
|
|
11
|
+
* second, which is the difference between noticing during the edit
|
|
12
|
+
* and noticing in CI.
|
|
13
|
+
*
|
|
14
|
+
* Found the expensive way: the M4 story/test split renamed
|
|
15
|
+
* `Tabs.Interactive` to `Tabs.Manual` and deleted three
|
|
16
|
+
* assertion-only stories, and every gate stayed green while
|
|
17
|
+
* `Components/Tabs › Docs` was broken.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { readdir, readFile } from "node:fs/promises"
|
|
21
|
+
import { join, resolve } from "node:path"
|
|
22
|
+
|
|
23
|
+
import { expect, test } from "vitest"
|
|
24
|
+
|
|
25
|
+
const sourceDirectory = resolve(import.meta.dirname)
|
|
26
|
+
|
|
27
|
+
const componentNames = (
|
|
28
|
+
await readdir(sourceDirectory, { withFileTypes: true })
|
|
29
|
+
)
|
|
30
|
+
.filter(
|
|
31
|
+
(entry) =>
|
|
32
|
+
entry.isDirectory() && /^[A-Z]/.test(entry.name),
|
|
33
|
+
)
|
|
34
|
+
.map((entry) => entry.name)
|
|
35
|
+
.sort()
|
|
36
|
+
|
|
37
|
+
const readIfPresent = async (path: string) => {
|
|
38
|
+
try {
|
|
39
|
+
return await readFile(path, "utf8")
|
|
40
|
+
} catch {
|
|
41
|
+
return undefined
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const documented = (
|
|
46
|
+
await Promise.all(
|
|
47
|
+
componentNames.map(async (name) => {
|
|
48
|
+
const [mdx, story] = await Promise.all([
|
|
49
|
+
readIfPresent(
|
|
50
|
+
join(sourceDirectory, name, `${name}.mdx`),
|
|
51
|
+
),
|
|
52
|
+
readIfPresent(
|
|
53
|
+
join(
|
|
54
|
+
sourceDirectory,
|
|
55
|
+
name,
|
|
56
|
+
`${name}.stories.tsx`,
|
|
57
|
+
),
|
|
58
|
+
),
|
|
59
|
+
])
|
|
60
|
+
|
|
61
|
+
return mdx && story ? [{ mdx, name, story }] : []
|
|
62
|
+
}),
|
|
63
|
+
)
|
|
64
|
+
).flat()
|
|
65
|
+
|
|
66
|
+
test("every component with stories has a docs page", () => {
|
|
67
|
+
// A canary on the derivation — an empty list would make the rule
|
|
68
|
+
// below pass forever.
|
|
69
|
+
expect(documented.length).toBeGreaterThanOrEqual(14)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test("no docs page references a story that no longer exists", () => {
|
|
73
|
+
const offenders = documented.flatMap(
|
|
74
|
+
({ mdx, name, story }) => {
|
|
75
|
+
const exported = new Set(
|
|
76
|
+
Array.from(
|
|
77
|
+
story.matchAll(/^export const (\w+): Story/gm),
|
|
78
|
+
(match) => match[1],
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return Array.from(
|
|
83
|
+
mdx.matchAll(/of=\{\w+Stories\.(\w+)\}/g),
|
|
84
|
+
(match) => match[1],
|
|
85
|
+
)
|
|
86
|
+
.filter((reference) => !exported.has(reference))
|
|
87
|
+
.map(
|
|
88
|
+
(reference) =>
|
|
89
|
+
`${name}.mdx references ${name}Stories.${reference}, which is not exported`,
|
|
90
|
+
)
|
|
91
|
+
},
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
expect(offenders).toEqual([])
|
|
95
|
+
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { within } from "storybook/test"
|
|
2
|
+
|
|
3
|
+
type Canvas = ReturnType<typeof within>
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Structural on purpose — the one thing this helper needs off a
|
|
7
|
+
* composed story. `ComposedStoryFn` is invariant in its `Args`, so
|
|
8
|
+
* naming it here would force every caller to restate the props of
|
|
9
|
+
* the component it is testing.
|
|
10
|
+
*/
|
|
11
|
+
type MountableStory = {
|
|
12
|
+
run: (context: {
|
|
13
|
+
canvasElement: HTMLElement
|
|
14
|
+
}) => Promise<void>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Mount a composed story into a fresh canvas and hand back the same
|
|
19
|
+
* two things a `play` function used to receive.
|
|
20
|
+
*
|
|
21
|
+
* `run()` — rather than rendering the component directly — is what
|
|
22
|
+
* keeps these tests honest after the story/test split: the subject
|
|
23
|
+
* is the story a reader sees, with its args, its decorators, and its
|
|
24
|
+
* loaders, not a second assembly of the same component that can
|
|
25
|
+
* drift from it. It is also what triggers the a11y addon's
|
|
26
|
+
* `afterEach`, so every mount here is an axe run.
|
|
27
|
+
*/
|
|
28
|
+
export const mountStory = async (
|
|
29
|
+
story: MountableStory,
|
|
30
|
+
): Promise<{
|
|
31
|
+
canvas: Canvas
|
|
32
|
+
canvasElement: HTMLElement
|
|
33
|
+
}> => {
|
|
34
|
+
const canvasElement = document.createElement("div")
|
|
35
|
+
|
|
36
|
+
document.body.append(canvasElement)
|
|
37
|
+
|
|
38
|
+
await story.run({ canvasElement })
|
|
39
|
+
|
|
40
|
+
return { canvas: within(canvasElement), canvasElement }
|
|
41
|
+
}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The three rules about this package's source that no compiler and no
|
|
3
|
+
* axe run can see.
|
|
4
|
+
*
|
|
5
|
+
* All three come straight from the plan's verification list, and each
|
|
6
|
+
* one is a thing the fleet does today that the library exists to
|
|
7
|
+
* stop.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readdir, readFile } from "node:fs/promises"
|
|
11
|
+
import { join, relative, resolve } from "node:path"
|
|
12
|
+
|
|
13
|
+
import { expect, test } from "vitest"
|
|
14
|
+
|
|
15
|
+
const sourceDirectory = resolve(import.meta.dirname)
|
|
16
|
+
|
|
17
|
+
const packageDirectory = resolve(sourceDirectory, "..")
|
|
18
|
+
|
|
19
|
+
const getSourceFiles = async (
|
|
20
|
+
directory: string,
|
|
21
|
+
): Promise<{ contents: string; file: string }[]> => {
|
|
22
|
+
const entries = await readdir(directory, {
|
|
23
|
+
withFileTypes: true,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const files = await Promise.all(
|
|
27
|
+
entries.map(async (entry) => {
|
|
28
|
+
const path = join(directory, entry.name)
|
|
29
|
+
|
|
30
|
+
if (entry.isDirectory()) {
|
|
31
|
+
return getSourceFiles(path)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!/\.(css|tsx?)$/.test(entry.name)) {
|
|
35
|
+
return []
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return [
|
|
39
|
+
{
|
|
40
|
+
contents: await readFile(path, "utf8"),
|
|
41
|
+
file: relative(sourceDirectory, path),
|
|
42
|
+
},
|
|
43
|
+
]
|
|
44
|
+
}),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
return files.flat()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const sourceFiles = await getSourceFiles(sourceDirectory)
|
|
51
|
+
|
|
52
|
+
const componentFiles = sourceFiles.filter(
|
|
53
|
+
(one) =>
|
|
54
|
+
!one.file.endsWith(".test.ts") &&
|
|
55
|
+
!one.file.endsWith(".test.tsx"),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Comments are prose about the fleet, not code. `intentStyles.ts`
|
|
60
|
+
* quotes mux-magic's `bg-blue-950 text-blue-300` as the evidence for
|
|
61
|
+
* the intent scale existing at all, and a rule that cannot tell a
|
|
62
|
+
* citation from a usage is a rule that gets switched off.
|
|
63
|
+
*/
|
|
64
|
+
const stripComments = (contents: string) =>
|
|
65
|
+
contents
|
|
66
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
67
|
+
.replace(/^\s*\/\/.*$/gm, "")
|
|
68
|
+
|
|
69
|
+
test("`data-testid` appears nowhere in @charcuterie/ui", () => {
|
|
70
|
+
// A rule, not a preference. A testid is a name only the test suite
|
|
71
|
+
// can see, so reaching for one is how a component ends up
|
|
72
|
+
// unnameable by a screen reader *and* undrivable by an agent — the
|
|
73
|
+
// exact gap that makes the fleet's current UIs unautomatable. If a
|
|
74
|
+
// control is hard to query, the fix is an accessible name.
|
|
75
|
+
// `data-testid=` — the attribute in *use*. `testing/` mentions the
|
|
76
|
+
// name in a selector and an error message, which is the opposite
|
|
77
|
+
// of the offence; and this file has to spell it to search for it,
|
|
78
|
+
// which is why the scan is over shipped source and stories rather
|
|
79
|
+
// than over the tests.
|
|
80
|
+
const offenders = componentFiles
|
|
81
|
+
.filter((one) => /data-testid=/.test(one.contents))
|
|
82
|
+
.map((one) => one.file)
|
|
83
|
+
|
|
84
|
+
expect(offenders).toEqual([])
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test("no component reaches past tier 2 for a colour", () => {
|
|
88
|
+
// The M6 problem, prevented here rather than repeated: mux-magic
|
|
89
|
+
// has **993 hardcoded `*-slate-*` utilities across 134 files**, and
|
|
90
|
+
// every one of them is why that app cannot have a light mode. A
|
|
91
|
+
// component may name a semantic role — `bg-surface-raised`,
|
|
92
|
+
// `text-intent-danger-content` — and nothing else.
|
|
93
|
+
//
|
|
94
|
+
// Tier 1 ramps (`colour.slate.500`) exist so a *variant author*
|
|
95
|
+
// has something to build tier 2 out of. A component naming one
|
|
96
|
+
// pins itself to a palette.
|
|
97
|
+
const palettePattern =
|
|
98
|
+
/\b(?:bg|text|border|from|to|via|outline|ring|fill|stroke|shadow|decoration|accent|caret|divide)-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-\d{2,3}\b/
|
|
99
|
+
|
|
100
|
+
const hexPattern = /#[0-9a-fA-F]{3,8}\b/
|
|
101
|
+
|
|
102
|
+
const offenders = componentFiles
|
|
103
|
+
.filter(
|
|
104
|
+
(one) =>
|
|
105
|
+
// The story fixtures are allowed literal hexes: the SVG
|
|
106
|
+
// poster data-URI in `MediaTile.stories.tsx` is a stand-in
|
|
107
|
+
// for an app's own image, not component styling.
|
|
108
|
+
!one.file.endsWith(".stories.tsx"),
|
|
109
|
+
)
|
|
110
|
+
.filter((one) => {
|
|
111
|
+
const code = stripComments(one.contents)
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
palettePattern.test(code) || hexPattern.test(code)
|
|
115
|
+
)
|
|
116
|
+
})
|
|
117
|
+
.map((one) => one.file)
|
|
118
|
+
|
|
119
|
+
expect(offenders).toEqual([])
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test("nothing queries a container it declares itself", () => {
|
|
123
|
+
// A container query matches **descendants of the container**, never
|
|
124
|
+
// the container element. So `className="@container cq-md:px-8"`
|
|
125
|
+
// compiles, generates real CSS, passes `tailwindCandidates.test.ts`
|
|
126
|
+
// — and never fires. Nothing errors; the padding is simply always
|
|
127
|
+
// the small one, forever.
|
|
128
|
+
//
|
|
129
|
+
// Caught here because it is invisible everywhere else: axe does not
|
|
130
|
+
// care, the class exists, and a reviewer sees a plausible-looking
|
|
131
|
+
// className. It cost this milestone one round of screenshots.
|
|
132
|
+
const offenders = componentFiles
|
|
133
|
+
.filter((one) => /\.tsx$/.test(one.file))
|
|
134
|
+
.flatMap((one) => {
|
|
135
|
+
const classStrings =
|
|
136
|
+
stripComments(one.contents).match(/"[^"\n]*"/g) ??
|
|
137
|
+
[]
|
|
138
|
+
|
|
139
|
+
return classStrings
|
|
140
|
+
.filter(
|
|
141
|
+
(candidate) =>
|
|
142
|
+
candidate.includes("@container") &&
|
|
143
|
+
/\bcq-(?:xs|sm|md|lg|xl):/.test(candidate),
|
|
144
|
+
)
|
|
145
|
+
.map((candidate) => `${one.file}: ${candidate}`)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
expect(offenders).toEqual([])
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
test("a container-query component is never storied in a shrink-to-fit cell", () => {
|
|
152
|
+
// The twin of the rule above, and the one the M3 screenshots
|
|
153
|
+
// actually caught: the component is right, the *story* is wrong.
|
|
154
|
+
//
|
|
155
|
+
// `container-type: inline-size` implies `contain: inline-size`,
|
|
156
|
+
// which forbids the element from being sized by its own contents.
|
|
157
|
+
// In a default `StoryCell` — `items-start`, so shrink-to-fit — the
|
|
158
|
+
// card has no width to shrink *to*, collapses to min-content, and
|
|
159
|
+
// every line of text wraps after one word. Valid CSS, no error, no
|
|
160
|
+
// axe violation, and it only shows up if somebody looks at a
|
|
161
|
+
// screenshot. `align="stretch"` hands it a definite inline size
|
|
162
|
+
// from the grid track.
|
|
163
|
+
//
|
|
164
|
+
// The container list is derived rather than hardcoded, so the
|
|
165
|
+
// component M4 adds with an `@container` on it joins this rule the
|
|
166
|
+
// moment it lands.
|
|
167
|
+
const containerComponents = componentFiles
|
|
168
|
+
.flatMap((one) => {
|
|
169
|
+
const match = /^([A-Z]\w+)\/\1\.tsx$/.exec(one.file)
|
|
170
|
+
|
|
171
|
+
return match?.[1] &&
|
|
172
|
+
stripComments(one.contents).includes("@container")
|
|
173
|
+
? [match[1]]
|
|
174
|
+
: []
|
|
175
|
+
})
|
|
176
|
+
.sort()
|
|
177
|
+
|
|
178
|
+
// A canary on the derivation itself: if this ever comes back
|
|
179
|
+
// empty the rule silently passes forever.
|
|
180
|
+
expect(containerComponents).toEqual([
|
|
181
|
+
"Alert",
|
|
182
|
+
"Card",
|
|
183
|
+
"EmptyState",
|
|
184
|
+
"MediaTile",
|
|
185
|
+
])
|
|
186
|
+
|
|
187
|
+
// `StoryCell`s are never nested, so the lazy match really does
|
|
188
|
+
// pair each opening tag with its own closing one.
|
|
189
|
+
const cellPattern =
|
|
190
|
+
/<StoryCell\b([^>]*)>([\s\S]*?)<\/StoryCell>/g
|
|
191
|
+
|
|
192
|
+
const offenders = componentFiles
|
|
193
|
+
.filter((one) => one.file.endsWith(".stories.tsx"))
|
|
194
|
+
.flatMap((one) =>
|
|
195
|
+
Array.from(
|
|
196
|
+
stripComments(one.contents).matchAll(cellPattern),
|
|
197
|
+
)
|
|
198
|
+
.filter(
|
|
199
|
+
([, attributes, body]) =>
|
|
200
|
+
!attributes.includes('align="stretch"') &&
|
|
201
|
+
containerComponents.some((name) =>
|
|
202
|
+
body.includes(`<${name}`),
|
|
203
|
+
),
|
|
204
|
+
)
|
|
205
|
+
.map(
|
|
206
|
+
([, attributes]) =>
|
|
207
|
+
`${one.file}: <StoryCell${attributes}>`,
|
|
208
|
+
),
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
expect(offenders).toEqual([])
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
test("the barrel is the only place components are re-exported", async () => {
|
|
215
|
+
// One sanctioned barrel per package. A component importing a
|
|
216
|
+
// sibling *through* `index.ts` turns every component into a
|
|
217
|
+
// dependency of every other one, which is how a 3 KB `Spinner`
|
|
218
|
+
// starts pulling in `MediaTile`.
|
|
219
|
+
const offenders = componentFiles
|
|
220
|
+
.filter((one) => one.file !== "index.ts")
|
|
221
|
+
.filter(
|
|
222
|
+
(one) =>
|
|
223
|
+
/from "\.\.?\/index\.ts"/.test(one.contents) ||
|
|
224
|
+
/from "@charcuterie\/ui"/.test(one.contents),
|
|
225
|
+
)
|
|
226
|
+
.map((one) => one.file)
|
|
227
|
+
|
|
228
|
+
expect(offenders).toEqual([])
|
|
229
|
+
|
|
230
|
+
// And the barrel really does export every component, so a new one
|
|
231
|
+
// cannot land unexported — which would make it invisible to every
|
|
232
|
+
// consumer while all its own tests pass.
|
|
233
|
+
const barrel = await readFile(
|
|
234
|
+
join(sourceDirectory, "index.ts"),
|
|
235
|
+
"utf8",
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
// `Button/Button.tsx` — the component files, as distinct from
|
|
239
|
+
// their stories and helpers.
|
|
240
|
+
const componentNames = componentFiles
|
|
241
|
+
.map((one) => /^([A-Z]\w+)\/\1\.tsx$/.exec(one.file))
|
|
242
|
+
.flatMap((match) => (match?.[1] ? [match[1]] : []))
|
|
243
|
+
|
|
244
|
+
// M3's ten P0 components, plus `VisuallyHidden` — a Layer-0
|
|
245
|
+
// primitive `Spinner`, `ProgressBar`, and `LiveStatusIndicator` all
|
|
246
|
+
// need, so it shipped here rather than being stubbed three times.
|
|
247
|
+
// M4 adds the three overlays: `Modal`, `Popover`, `Tabs`.
|
|
248
|
+
//
|
|
249
|
+
// M5 adds the two the first consumer proved were shared rather
|
|
250
|
+
// than app-specific: `Alert` (rip-deck spells it four times, twice
|
|
251
|
+
// with a byte-identical `TONE_CLASS`) and `SegmentedControl`.
|
|
252
|
+
//
|
|
253
|
+
// `TabTrigger` and `SegmentedOption` are deliberately not in this
|
|
254
|
+
// count and not exported. Each is its own component's member —
|
|
255
|
+
// its own file only because both registrations are effects and an
|
|
256
|
+
// effect cannot run in a loop — and the `<Name>/<Name>.tsx`
|
|
257
|
+
// pattern this regex matches is what keeps them out.
|
|
258
|
+
expect(componentNames.length).toBe(16)
|
|
259
|
+
|
|
260
|
+
for (const name of componentNames) {
|
|
261
|
+
expect(barrel).toContain(`export { ${name} }`)
|
|
262
|
+
}
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
test("dependency direction is tokens ← logic ← ui", async () => {
|
|
266
|
+
// CI-enforced by the plan, and this is the enforcement. `ui` may
|
|
267
|
+
// depend on both; `logic` may depend on `tokens`; `tokens` depends
|
|
268
|
+
// on nothing, which is what lets `castkit/packages/views` read a
|
|
269
|
+
// colour without a React tree.
|
|
270
|
+
const readPackage = async (name: string) =>
|
|
271
|
+
JSON.parse(
|
|
272
|
+
await readFile(
|
|
273
|
+
join(packageDirectory, "..", name, "package.json"),
|
|
274
|
+
"utf8",
|
|
275
|
+
),
|
|
276
|
+
) as {
|
|
277
|
+
dependencies?: Record<string, string>
|
|
278
|
+
peerDependencies?: Record<string, string>
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const getCharcuterieDependencies = (manifest: {
|
|
282
|
+
dependencies?: Record<string, string>
|
|
283
|
+
peerDependencies?: Record<string, string>
|
|
284
|
+
}) =>
|
|
285
|
+
Object.keys({
|
|
286
|
+
...manifest.dependencies,
|
|
287
|
+
...manifest.peerDependencies,
|
|
288
|
+
}).filter((name) => name.startsWith("@charcuterie/"))
|
|
289
|
+
|
|
290
|
+
expect(
|
|
291
|
+
getCharcuterieDependencies(await readPackage("tokens")),
|
|
292
|
+
).toEqual([])
|
|
293
|
+
|
|
294
|
+
expect(
|
|
295
|
+
getCharcuterieDependencies(await readPackage("logic")),
|
|
296
|
+
).toEqual([])
|
|
297
|
+
|
|
298
|
+
expect(
|
|
299
|
+
getCharcuterieDependencies(
|
|
300
|
+
await readPackage("ui"),
|
|
301
|
+
).sort(),
|
|
302
|
+
).toEqual(["@charcuterie/logic", "@charcuterie/tokens"])
|
|
303
|
+
})
|