@proteos/ui 0.31.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 (226) hide show
  1. package/LICENSE +105 -0
  2. package/dist/chunk-IKAX76MB.js +113 -0
  3. package/dist/chunk-IKAX76MB.js.map +1 -0
  4. package/dist/chunk-MWAXK66H.cjs +119 -0
  5. package/dist/chunk-MWAXK66H.cjs.map +1 -0
  6. package/dist/index.cjs +6246 -0
  7. package/dist/index.cjs.map +1 -0
  8. package/dist/index.d.cts +2126 -0
  9. package/dist/index.d.ts +2126 -0
  10. package/dist/index.js +6069 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/pdf-view-3ZMMS6ML.cjs +95 -0
  13. package/dist/pdf-view-3ZMMS6ML.cjs.map +1 -0
  14. package/dist/pdf-view-BV5ZD6OC.js +93 -0
  15. package/dist/pdf-view-BV5ZD6OC.js.map +1 -0
  16. package/package.json +96 -0
  17. package/src/components/attachment-chip/attachment-chip.tsx +129 -0
  18. package/src/components/attachment-chip/index.ts +2 -0
  19. package/src/components/attribute-type-icon/attribute-type-icon.stories.tsx +70 -0
  20. package/src/components/attribute-type-icon/attribute-type-icon.tsx +125 -0
  21. package/src/components/attribute-type-icon/index.ts +8 -0
  22. package/src/components/avatar/avatar.stories.tsx +42 -0
  23. package/src/components/avatar/avatar.tsx +48 -0
  24. package/src/components/avatar/index.ts +2 -0
  25. package/src/components/avatar-group/avatar-group.test.tsx +38 -0
  26. package/src/components/avatar-group/avatar-group.tsx +60 -0
  27. package/src/components/avatar-group/index.ts +2 -0
  28. package/src/components/badge/badge.stories.tsx +62 -0
  29. package/src/components/badge/badge.test.tsx +36 -0
  30. package/src/components/badge/badge.tsx +54 -0
  31. package/src/components/badge/index.ts +2 -0
  32. package/src/components/badge-multi-select/badge-multi-select.stories.tsx +177 -0
  33. package/src/components/badge-multi-select/badge-multi-select.test.tsx +155 -0
  34. package/src/components/badge-multi-select/badge-multi-select.tsx +272 -0
  35. package/src/components/badge-multi-select/index.ts +2 -0
  36. package/src/components/breadcrumbs/breadcrumbs.stories.tsx +43 -0
  37. package/src/components/breadcrumbs/breadcrumbs.tsx +86 -0
  38. package/src/components/breadcrumbs/index.ts +2 -0
  39. package/src/components/button/button.stories.tsx +290 -0
  40. package/src/components/button/button.test.tsx +59 -0
  41. package/src/components/button/button.tsx +86 -0
  42. package/src/components/button/index.ts +2 -0
  43. package/src/components/card/card.stories.tsx +351 -0
  44. package/src/components/card/card.test.tsx +141 -0
  45. package/src/components/card/card.tsx +173 -0
  46. package/src/components/card/index.ts +13 -0
  47. package/src/components/checkbox/checkbox-group.tsx +52 -0
  48. package/src/components/checkbox/checkbox.stories.tsx +48 -0
  49. package/src/components/checkbox/checkbox.test.tsx +50 -0
  50. package/src/components/checkbox/checkbox.tsx +71 -0
  51. package/src/components/checkbox/index.ts +4 -0
  52. package/src/components/combobox/combobox.stories.tsx +97 -0
  53. package/src/components/combobox/combobox.test.tsx +123 -0
  54. package/src/components/combobox/combobox.tsx +239 -0
  55. package/src/components/combobox/index.ts +2 -0
  56. package/src/components/command/command-hint.tsx +46 -0
  57. package/src/components/command/command.stories.tsx +88 -0
  58. package/src/components/command/command.test.tsx +102 -0
  59. package/src/components/command/command.tsx +76 -0
  60. package/src/components/command/index.ts +16 -0
  61. package/src/components/currency-input/currency-input.tsx +192 -0
  62. package/src/components/currency-input/index.ts +6 -0
  63. package/src/components/data-table/cells.stories.tsx +120 -0
  64. package/src/components/data-table/cells.tsx +165 -0
  65. package/src/components/data-table/data-table.stories.tsx +371 -0
  66. package/src/components/data-table/data-table.tsx +556 -0
  67. package/src/components/data-table/index.ts +22 -0
  68. package/src/components/data-table/make-actions-column.ts +30 -0
  69. package/src/components/data-table/states/data-table-empty.tsx +40 -0
  70. package/src/components/data-table/states/data-table-error.tsx +30 -0
  71. package/src/components/data-table/states/data-table-skeleton.tsx +42 -0
  72. package/src/components/data-table/use-infinite-scroll-sentinel.ts +51 -0
  73. package/src/components/date-picker/calendar-internals.tsx +138 -0
  74. package/src/components/date-picker/date-picker.stories.tsx +78 -0
  75. package/src/components/date-picker/date-picker.test.tsx +71 -0
  76. package/src/components/date-picker/date-picker.tsx +140 -0
  77. package/src/components/date-picker/index.ts +2 -0
  78. package/src/components/date-time-picker/date-time-picker.stories.tsx +96 -0
  79. package/src/components/date-time-picker/date-time-picker.test.tsx +87 -0
  80. package/src/components/date-time-picker/date-time-picker.tsx +203 -0
  81. package/src/components/date-time-picker/index.ts +2 -0
  82. package/src/components/empty-state/empty-state.tsx +63 -0
  83. package/src/components/empty-state/index.ts +2 -0
  84. package/src/components/error-animation/error-animation.stories.tsx +50 -0
  85. package/src/components/error-animation/error-animation.test.tsx +22 -0
  86. package/src/components/error-animation/error-animation.tsx +26 -0
  87. package/src/components/error-animation/index.ts +2 -0
  88. package/src/components/field/field.tsx +142 -0
  89. package/src/components/field/index.ts +8 -0
  90. package/src/components/file-field/file-field.tsx +285 -0
  91. package/src/components/file-field/index.ts +2 -0
  92. package/src/components/file-viewer/fallback-card.tsx +68 -0
  93. package/src/components/file-viewer/file-viewer.stories.tsx +67 -0
  94. package/src/components/file-viewer/file-viewer.test.tsx +81 -0
  95. package/src/components/file-viewer/file-viewer.tsx +116 -0
  96. package/src/components/file-viewer/image-view.tsx +38 -0
  97. package/src/components/file-viewer/index.ts +7 -0
  98. package/src/components/file-viewer/pdf-view.tsx +122 -0
  99. package/src/components/file-viewer/video-view.tsx +16 -0
  100. package/src/components/image-attachment/image-attachment.stories.tsx +39 -0
  101. package/src/components/image-attachment/image-attachment.test.tsx +54 -0
  102. package/src/components/image-attachment/image-attachment.tsx +77 -0
  103. package/src/components/image-attachment/index.ts +3 -0
  104. package/src/components/image-attachment/is-previewable-image-name.ts +14 -0
  105. package/src/components/image-lightbox/image-lightbox.stories.tsx +38 -0
  106. package/src/components/image-lightbox/image-lightbox.test.tsx +72 -0
  107. package/src/components/image-lightbox/image-lightbox.tsx +92 -0
  108. package/src/components/image-lightbox/index.ts +2 -0
  109. package/src/components/input/index.ts +2 -0
  110. package/src/components/input/input.stories.tsx +71 -0
  111. package/src/components/input/input.test.tsx +36 -0
  112. package/src/components/input/input.tsx +76 -0
  113. package/src/components/kbd/index.ts +2 -0
  114. package/src/components/kbd/kbd.stories.tsx +34 -0
  115. package/src/components/kbd/kbd.tsx +27 -0
  116. package/src/components/menu/index.ts +2 -0
  117. package/src/components/menu/menu.tsx +121 -0
  118. package/src/components/modal/index.ts +10 -0
  119. package/src/components/modal/modal.stories.tsx +153 -0
  120. package/src/components/modal/modal.test.tsx +93 -0
  121. package/src/components/modal/modal.tsx +149 -0
  122. package/src/components/multi-select/index.ts +2 -0
  123. package/src/components/multi-select/multi-select.stories.tsx +211 -0
  124. package/src/components/multi-select/multi-select.test.tsx +151 -0
  125. package/src/components/multi-select/multi-select.tsx +339 -0
  126. package/src/components/nav/index.ts +7 -0
  127. package/src/components/nav/nav.stories.tsx +139 -0
  128. package/src/components/nav/nav.tsx +246 -0
  129. package/src/components/popover/index.ts +8 -0
  130. package/src/components/popover/popover.stories.tsx +92 -0
  131. package/src/components/popover/popover.tsx +135 -0
  132. package/src/components/progress-bar/index.ts +2 -0
  133. package/src/components/progress-bar/progress-bar.stories.tsx +75 -0
  134. package/src/components/progress-bar/progress-bar.test.tsx +29 -0
  135. package/src/components/progress-bar/progress-bar.tsx +57 -0
  136. package/src/components/radio-group/index.ts +2 -0
  137. package/src/components/radio-group/radio-group.stories.tsx +113 -0
  138. package/src/components/radio-group/radio-group.test.tsx +87 -0
  139. package/src/components/radio-group/radio-group.tsx +104 -0
  140. package/src/components/record-filter/attribute-picker.tsx +74 -0
  141. package/src/components/record-filter/chip-value.tsx +171 -0
  142. package/src/components/record-filter/filter-row.tsx +262 -0
  143. package/src/components/record-filter/filter-value-input.tsx +351 -0
  144. package/src/components/record-filter/group-add-controls.tsx +77 -0
  145. package/src/components/record-filter/group-body.tsx +173 -0
  146. package/src/components/record-filter/group-mutations.test.ts +104 -0
  147. package/src/components/record-filter/group-mutations.ts +116 -0
  148. package/src/components/record-filter/index.ts +58 -0
  149. package/src/components/record-filter/nested-group.tsx +138 -0
  150. package/src/components/record-filter/operator-picker.tsx +62 -0
  151. package/src/components/record-filter/operators.test.ts +105 -0
  152. package/src/components/record-filter/operators.ts +97 -0
  153. package/src/components/record-filter/record-filter-builder-panel.tsx +236 -0
  154. package/src/components/record-filter/record-filter-button.tsx +126 -0
  155. package/src/components/record-filter/record-filter-chips.tsx +527 -0
  156. package/src/components/record-filter/record-filter-editor.tsx +91 -0
  157. package/src/components/record-filter/record-filter.stories.tsx +196 -0
  158. package/src/components/record-filter/record-filter.test.tsx +437 -0
  159. package/src/components/record-filter/show-details-store.ts +72 -0
  160. package/src/components/record-filter/types.ts +150 -0
  161. package/src/components/segmented-control/index.ts +6 -0
  162. package/src/components/segmented-control/segmented-control.stories.tsx +67 -0
  163. package/src/components/segmented-control/segmented-control.test.tsx +47 -0
  164. package/src/components/segmented-control/segmented-control.tsx +105 -0
  165. package/src/components/select/index.ts +2 -0
  166. package/src/components/select/select.stories.tsx +76 -0
  167. package/src/components/select/select.test.tsx +56 -0
  168. package/src/components/select/select.tsx +159 -0
  169. package/src/components/separator/index.ts +2 -0
  170. package/src/components/separator/separator.stories.tsx +73 -0
  171. package/src/components/separator/separator.test.tsx +29 -0
  172. package/src/components/separator/separator.tsx +51 -0
  173. package/src/components/skeleton/index.ts +2 -0
  174. package/src/components/skeleton/skeleton.stories.tsx +68 -0
  175. package/src/components/skeleton/skeleton.test.tsx +38 -0
  176. package/src/components/skeleton/skeleton.tsx +43 -0
  177. package/src/components/success-animation/index.ts +2 -0
  178. package/src/components/success-animation/success-animation.stories.tsx +50 -0
  179. package/src/components/success-animation/success-animation.test.tsx +22 -0
  180. package/src/components/success-animation/success-animation.tsx +26 -0
  181. package/src/components/switch/index.ts +2 -0
  182. package/src/components/switch/switch.stories.tsx +60 -0
  183. package/src/components/switch/switch.test.tsx +50 -0
  184. package/src/components/switch/switch.tsx +49 -0
  185. package/src/components/table/index.ts +25 -0
  186. package/src/components/table/table.stories.tsx +306 -0
  187. package/src/components/table/table.test.tsx +115 -0
  188. package/src/components/table/table.tsx +543 -0
  189. package/src/components/tabs/index.ts +8 -0
  190. package/src/components/tabs/tabs.tsx +141 -0
  191. package/src/components/tag-input/index.ts +2 -0
  192. package/src/components/tag-input/tag-input.stories.tsx +73 -0
  193. package/src/components/tag-input/tag-input.test.tsx +80 -0
  194. package/src/components/tag-input/tag-input.tsx +193 -0
  195. package/src/components/textarea/index.ts +2 -0
  196. package/src/components/textarea/textarea.stories.tsx +75 -0
  197. package/src/components/textarea/textarea.test.tsx +41 -0
  198. package/src/components/textarea/textarea.tsx +86 -0
  199. package/src/components/time-field/index.ts +2 -0
  200. package/src/components/time-field/time-field.stories.tsx +76 -0
  201. package/src/components/time-field/time-field.test.tsx +51 -0
  202. package/src/components/time-field/time-field.tsx +98 -0
  203. package/src/components/tooltip/index.ts +2 -0
  204. package/src/components/tooltip/tooltip.stories.tsx +176 -0
  205. package/src/components/tooltip/tooltip.test.tsx +52 -0
  206. package/src/components/tooltip/tooltip.tsx +56 -0
  207. package/src/index.ts +47 -0
  208. package/src/lib/cn.ts +6 -0
  209. package/src/lib/commands/discovery-store.test.tsx +76 -0
  210. package/src/lib/commands/discovery-store.ts +110 -0
  211. package/src/lib/commands/index.ts +9 -0
  212. package/src/lib/commands/match-event.test.ts +99 -0
  213. package/src/lib/commands/match-event.ts +31 -0
  214. package/src/lib/commands/parse-combo.test.ts +65 -0
  215. package/src/lib/commands/parse-combo.ts +48 -0
  216. package/src/lib/commands/platform.ts +27 -0
  217. package/src/lib/commands/protected-combos.test.ts +18 -0
  218. package/src/lib/commands/protected-combos.ts +38 -0
  219. package/src/lib/commands/registry.test.ts +97 -0
  220. package/src/lib/commands/registry.ts +59 -0
  221. package/src/lib/commands/use-command.test.tsx +91 -0
  222. package/src/lib/commands/use-command.ts +32 -0
  223. package/src/lib/commands/use-escape-key.test.tsx +97 -0
  224. package/src/lib/commands/use-escape-key.ts +62 -0
  225. package/src/styles.css +321 -0
  226. package/src/test/setup.ts +9 -0
@@ -0,0 +1,351 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+
3
+ import { Separator } from '@/components/separator/separator'
4
+
5
+ import {
6
+ Card,
7
+ CardBody,
8
+ CardContent,
9
+ CardEyebrow,
10
+ CardFeatureTitle,
11
+ CardFooter,
12
+ CardHeader,
13
+ CardTitle,
14
+ PressableCard,
15
+ } from './card'
16
+
17
+ const meta: Meta<typeof Card> = {
18
+ title: 'Components/Card',
19
+ component: Card,
20
+ tags: ['autodocs'],
21
+ argTypes: {
22
+ variant: {
23
+ control: { type: 'inline-radio' },
24
+ options: ['default', 'accent', 'outline', 'raised'],
25
+ },
26
+ },
27
+ decorators: [
28
+ (Story) => (
29
+ <div className="w-[360px]">
30
+ <Story />
31
+ </div>
32
+ ),
33
+ ],
34
+ }
35
+
36
+ export default meta
37
+ type Story = StoryObj<typeof Card>
38
+
39
+ /* =========================================================================
40
+ Playground — flip the `variant` control to recolor the chrome. Content
41
+ stays the same; slots own their own padding so no variant can break it.
42
+ ========================================================================= */
43
+
44
+ export const Playground: Story = {
45
+ render: (args) => (
46
+ <Card {...args}>
47
+ <CardContent>
48
+ <CardEyebrow>
49
+ <span>Standard</span>
50
+ <span>card</span>
51
+ </CardEyebrow>
52
+ <CardTitle>Open shipments</CardTitle>
53
+ <CardBody>
54
+ The system employees open every morning. Records, tables, dashboards, approvals.
55
+ </CardBody>
56
+ </CardContent>
57
+ <Separator />
58
+ <CardFooter>
59
+ <span>1,284 open</span>
60
+ <span>→</span>
61
+ </CardFooter>
62
+ </Card>
63
+ ),
64
+ }
65
+
66
+ export const Accent: Story = {
67
+ args: { variant: 'accent' },
68
+ render: (args) => (
69
+ <Card {...args}>
70
+ <CardContent>
71
+ <CardEyebrow>
72
+ <span>Accent</span>
73
+ <span>card · accent</span>
74
+ </CardEyebrow>
75
+ <CardTitle>Architect AI</CardTitle>
76
+ <CardBody>
77
+ Describe a change in plain language. The architect edits schema, workflows and views —
78
+ never leaves the platform.
79
+ </CardBody>
80
+ </CardContent>
81
+ <Separator />
82
+ <CardFooter>
83
+ <span>always on</span>
84
+ <span>→</span>
85
+ </CardFooter>
86
+ </Card>
87
+ ),
88
+ }
89
+
90
+ export const Outline: Story = {
91
+ args: { variant: 'outline' },
92
+ render: (args) => (
93
+ <Card {...args}>
94
+ <CardContent>
95
+ <CardEyebrow>
96
+ <span>Outline</span>
97
+ <span>card · outline</span>
98
+ </CardEyebrow>
99
+ <CardTitle>Reshape workflow</CardTitle>
100
+ <CardBody>
101
+ "Group shipments by lane, show weekly margin." The system accepts the instruction and
102
+ edits itself.
103
+ </CardBody>
104
+ </CardContent>
105
+ <Separator />
106
+ <CardFooter>
107
+ <span>dry-run available</span>
108
+ <span>→</span>
109
+ </CardFooter>
110
+ </Card>
111
+ ),
112
+ }
113
+
114
+ /* =========================================================================
115
+ WithHeader — demonstrates the flush full-bleed bar pattern. Compose
116
+ <CardHeader/> + <CardContent/> to get what used to be the `flush` variant.
117
+ Works with any chrome variant.
118
+ ========================================================================= */
119
+
120
+ export const WithHeader: Story = {
121
+ render: (args) => (
122
+ <Card {...args}>
123
+ <CardHeader>
124
+ <span>Shipment</span>
125
+ <span>SH-10482</span>
126
+ </CardHeader>
127
+ <CardContent>
128
+ <CardTitle>DAL → ATL</CardTitle>
129
+ <CardBody>Blue Ridge · Apr 24 · in transit</CardBody>
130
+ </CardContent>
131
+ </Card>
132
+ ),
133
+ }
134
+
135
+ export const Raised: Story = {
136
+ args: { variant: 'raised' },
137
+ decorators: [
138
+ (Story) => (
139
+ <div className="w-[640px]">
140
+ <Story />
141
+ </div>
142
+ ),
143
+ ],
144
+ render: (args) => (
145
+ <Card {...args}>
146
+ <CardContent>
147
+ <CardEyebrow>
148
+ <span>Feature</span>
149
+ <span>card · raised</span>
150
+ </CardEyebrow>
151
+ <CardFeatureTitle>"The system reshapes when the business does."</CardFeatureTitle>
152
+ <CardBody>
153
+ A feature card is reserved for editorial moments — a pull quote, a manifesto line, the
154
+ answer to "why now." One per page, never two.
155
+ </CardBody>
156
+ </CardContent>
157
+ <Separator />
158
+ <CardFooter>
159
+ <span>— Proteos manifesto</span>
160
+ <span>2026</span>
161
+ </CardFooter>
162
+ </Card>
163
+ ),
164
+ }
165
+
166
+ /* =========================================================================
167
+ AllVariants — every chrome variant side by side, plus a CardHeader example
168
+ to show the flush-bar composition.
169
+ ========================================================================= */
170
+
171
+ export const AllVariants: Story = {
172
+ parameters: { controls: { hideNoControlsWarning: true } },
173
+ decorators: [
174
+ (Story) => (
175
+ <div className="w-[1080px] grid grid-cols-3 gap-5">
176
+ <Story />
177
+ </div>
178
+ ),
179
+ ],
180
+ render: () => (
181
+ <>
182
+ <Card>
183
+ <CardContent>
184
+ <CardEyebrow>
185
+ <span>Standard</span>
186
+ <span>card</span>
187
+ </CardEyebrow>
188
+ <CardTitle>Open shipments</CardTitle>
189
+ <CardBody>The system employees open every morning.</CardBody>
190
+ </CardContent>
191
+ <Separator />
192
+ <CardFooter>
193
+ <span>1,284 open</span>
194
+ <span>→</span>
195
+ </CardFooter>
196
+ </Card>
197
+
198
+ <Card variant="accent">
199
+ <CardContent>
200
+ <CardEyebrow>
201
+ <span>Accent</span>
202
+ <span>card · accent</span>
203
+ </CardEyebrow>
204
+ <CardTitle>Architect AI</CardTitle>
205
+ <CardBody>Describe a change in plain language. The architect edits schema.</CardBody>
206
+ </CardContent>
207
+ <Separator />
208
+ <CardFooter>
209
+ <span>always on</span>
210
+ <span>→</span>
211
+ </CardFooter>
212
+ </Card>
213
+
214
+ <Card variant="outline">
215
+ <CardContent>
216
+ <CardEyebrow>
217
+ <span>Outline</span>
218
+ <span>card · outline</span>
219
+ </CardEyebrow>
220
+ <CardTitle>Reshape workflow</CardTitle>
221
+ <CardBody>
222
+ "Group shipments by lane, show weekly margin." The system edits itself.
223
+ </CardBody>
224
+ </CardContent>
225
+ <Separator />
226
+ <CardFooter>
227
+ <span>dry-run available</span>
228
+ <span>→</span>
229
+ </CardFooter>
230
+ </Card>
231
+
232
+ <Card>
233
+ <CardHeader>
234
+ <span>Shipment</span>
235
+ <span>SH-10482</span>
236
+ </CardHeader>
237
+ <CardContent>
238
+ <CardTitle>DAL → ATL</CardTitle>
239
+ <CardBody>Blue Ridge · Apr 24 · in transit</CardBody>
240
+ </CardContent>
241
+ </Card>
242
+
243
+ <Card variant="raised" className="col-span-2">
244
+ <CardContent>
245
+ <CardEyebrow>
246
+ <span>Feature</span>
247
+ <span>card · raised</span>
248
+ </CardEyebrow>
249
+ <CardFeatureTitle>"The system reshapes when the business does."</CardFeatureTitle>
250
+ <CardBody>
251
+ A feature card is reserved for editorial moments — one per page, never two.
252
+ </CardBody>
253
+ </CardContent>
254
+ <Separator />
255
+ <CardFooter>
256
+ <span>— Proteos manifesto</span>
257
+ <span>2026</span>
258
+ </CardFooter>
259
+ </Card>
260
+ </>
261
+ ),
262
+ }
263
+
264
+ /* =========================================================================
265
+ Pressable — chrome-identical, wired as a RAC Button. Same slot API.
266
+ Hover lifts the shadow, press sinks 1px, focus shows indigo ring.
267
+ ========================================================================= */
268
+
269
+ export const Pressable: Story = {
270
+ parameters: { controls: { hideNoControlsWarning: true } },
271
+ render: () => (
272
+ <PressableCard
273
+ aria-label="Open shipment SH-10482"
274
+ onPress={() => {
275
+ /* in real use: navigate, open drawer, etc. */
276
+ }}
277
+ >
278
+ <CardHeader>
279
+ <span>Shipment</span>
280
+ <span>SH-10482</span>
281
+ </CardHeader>
282
+ <CardContent>
283
+ <CardTitle>DAL → ATL</CardTitle>
284
+ <CardBody>Blue Ridge · Apr 24 · in transit</CardBody>
285
+ </CardContent>
286
+ <Separator />
287
+ <CardFooter>
288
+ <span>Tap anywhere to open</span>
289
+ <span>→</span>
290
+ </CardFooter>
291
+ </PressableCard>
292
+ ),
293
+ }
294
+
295
+ export const PressableVariants: Story = {
296
+ parameters: { controls: { hideNoControlsWarning: true } },
297
+ decorators: [
298
+ (Story) => (
299
+ <div className="w-[720px] grid grid-cols-2 gap-5">
300
+ <Story />
301
+ </div>
302
+ ),
303
+ ],
304
+ render: () => (
305
+ <>
306
+ <PressableCard aria-label="Open default card">
307
+ <CardContent>
308
+ <CardEyebrow>
309
+ <span>Default</span>
310
+ <span>pressable</span>
311
+ </CardEyebrow>
312
+ <CardTitle>Standard + pressable</CardTitle>
313
+ <CardBody>Hover to lift, focus to ring, press to sink.</CardBody>
314
+ </CardContent>
315
+ </PressableCard>
316
+
317
+ <PressableCard aria-label="Open accent card" variant="accent">
318
+ <CardContent>
319
+ <CardEyebrow>
320
+ <span>Accent</span>
321
+ <span>pressable</span>
322
+ </CardEyebrow>
323
+ <CardTitle>Accent + pressable</CardTitle>
324
+ <CardBody>Indigo-tinted surface, still click/keyboard-reachable.</CardBody>
325
+ </CardContent>
326
+ </PressableCard>
327
+
328
+ <PressableCard aria-label="Open outline card" variant="outline">
329
+ <CardContent>
330
+ <CardEyebrow>
331
+ <span>Outline</span>
332
+ <span>pressable</span>
333
+ </CardEyebrow>
334
+ <CardTitle>Outline + pressable</CardTitle>
335
+ <CardBody>No rest shadow, but the hover lift still fires on interaction.</CardBody>
336
+ </CardContent>
337
+ </PressableCard>
338
+
339
+ <PressableCard aria-label="Disabled card" isDisabled>
340
+ <CardContent>
341
+ <CardEyebrow>
342
+ <span>Disabled</span>
343
+ <span>pressable</span>
344
+ </CardEyebrow>
345
+ <CardTitle>Disabled</CardTitle>
346
+ <CardBody>Receives no pointer events, fades to 45% opacity.</CardBody>
347
+ </CardContent>
348
+ </PressableCard>
349
+ </>
350
+ ),
351
+ }
@@ -0,0 +1,141 @@
1
+ import { render, screen } from '@testing-library/react'
2
+ import userEvent from '@testing-library/user-event'
3
+ import { describe, expect, it, vi } from 'vitest'
4
+
5
+ import {
6
+ Card,
7
+ CardBody,
8
+ CardContent,
9
+ CardEyebrow,
10
+ CardFooter,
11
+ CardHeader,
12
+ CardTitle,
13
+ PressableCard,
14
+ } from './card'
15
+
16
+ describe('Card', () => {
17
+ it('renders children', () => {
18
+ render(
19
+ <Card>
20
+ <CardContent>Open shipments</CardContent>
21
+ </Card>,
22
+ )
23
+ expect(screen.getByText('Open shipments')).toBeInTheDocument()
24
+ })
25
+
26
+ it('applies chrome variant classes', () => {
27
+ const { container } = render(
28
+ <Card variant="outline">
29
+ <CardContent>x</CardContent>
30
+ </Card>,
31
+ )
32
+ expect(container.firstElementChild?.className).toContain('shadow-none')
33
+ })
34
+
35
+ it('composes slots end to end (eyebrow + title + body + footer)', () => {
36
+ render(
37
+ <Card>
38
+ <CardContent>
39
+ <CardEyebrow>
40
+ <span>Standard</span>
41
+ <span>card</span>
42
+ </CardEyebrow>
43
+ <CardTitle>Open shipments</CardTitle>
44
+ <CardBody>The system employees open every morning.</CardBody>
45
+ </CardContent>
46
+ <CardFooter>
47
+ <span>1,284 open</span>
48
+ <span>→</span>
49
+ </CardFooter>
50
+ </Card>,
51
+ )
52
+
53
+ expect(screen.getByRole('heading', { name: 'Open shipments' })).toBeInTheDocument()
54
+ expect(screen.getByText('The system employees open every morning.')).toBeInTheDocument()
55
+ expect(screen.getByText('1,284 open')).toBeInTheDocument()
56
+ })
57
+
58
+ it('composes a CardHeader flush bar with padded CardContent', () => {
59
+ render(
60
+ <Card>
61
+ <CardHeader>
62
+ <span>Shipment</span>
63
+ <span>SH-10482</span>
64
+ </CardHeader>
65
+ <CardContent>
66
+ <CardTitle>DAL → ATL</CardTitle>
67
+ </CardContent>
68
+ </Card>,
69
+ )
70
+
71
+ expect(screen.getByText('Shipment')).toBeInTheDocument()
72
+ expect(screen.getByText('SH-10482')).toBeInTheDocument()
73
+ expect(screen.getByRole('heading', { name: 'DAL → ATL' })).toBeInTheDocument()
74
+ })
75
+ })
76
+
77
+ describe('PressableCard', () => {
78
+ it('renders as a button with an accessible name', () => {
79
+ render(
80
+ <PressableCard aria-label="Open shipment SH-10482">
81
+ <CardContent>
82
+ <CardTitle>DAL → ATL</CardTitle>
83
+ </CardContent>
84
+ </PressableCard>,
85
+ )
86
+ expect(screen.getByRole('button', { name: 'Open shipment SH-10482' })).toBeInTheDocument()
87
+ })
88
+
89
+ it('fires onPress when clicked', async () => {
90
+ const onPress = vi.fn()
91
+ const user = userEvent.setup()
92
+
93
+ render(
94
+ <PressableCard aria-label="Open shipment" onPress={onPress}>
95
+ <CardContent>DAL → ATL</CardContent>
96
+ </PressableCard>,
97
+ )
98
+
99
+ await user.click(screen.getByRole('button', { name: 'Open shipment' }))
100
+ expect(onPress).toHaveBeenCalledOnce()
101
+ })
102
+
103
+ it('fires onPress on keyboard Enter', async () => {
104
+ const onPress = vi.fn()
105
+ const user = userEvent.setup()
106
+
107
+ render(
108
+ <PressableCard aria-label="Open shipment" onPress={onPress}>
109
+ <CardContent>content</CardContent>
110
+ </PressableCard>,
111
+ )
112
+
113
+ await user.tab()
114
+ expect(screen.getByRole('button')).toHaveFocus()
115
+ await user.keyboard('{Enter}')
116
+ expect(onPress).toHaveBeenCalledOnce()
117
+ })
118
+
119
+ it('does not fire onPress when disabled', async () => {
120
+ const onPress = vi.fn()
121
+ const user = userEvent.setup()
122
+
123
+ render(
124
+ <PressableCard aria-label="Disabled card" onPress={onPress} isDisabled>
125
+ <CardContent>content</CardContent>
126
+ </PressableCard>,
127
+ )
128
+
129
+ await user.click(screen.getByRole('button', { name: 'Disabled card' }))
130
+ expect(onPress).not.toHaveBeenCalled()
131
+ })
132
+
133
+ it('applies the requested variant styles', () => {
134
+ render(
135
+ <PressableCard aria-label="Accent card" variant="accent">
136
+ <CardContent>content</CardContent>
137
+ </PressableCard>,
138
+ )
139
+ expect(screen.getByRole('button').className).toContain('text-accent-ink')
140
+ })
141
+ })
@@ -0,0 +1,173 @@
1
+ import type { HTMLAttributes } from 'react'
2
+ import { Button as AriaButton, type ButtonProps as AriaButtonProps } from 'react-aria-components'
3
+ import { tv, type VariantProps } from 'tailwind-variants'
4
+
5
+ import { cn } from '../../lib/cn'
6
+
7
+ /* =========================================================================
8
+ Card — chrome-only shell.
9
+ Padding lives on the slot components (<CardHeader/>, <CardContent/>,
10
+ <CardFooter/>), never on Card itself. Variants control cosmetics only —
11
+ border tint, shadow depth — so slot composition and variant are orthogonal.
12
+ ========================================================================= */
13
+
14
+ export const cardStyles = tv({
15
+ base: [
16
+ 'flex flex-col overflow-hidden',
17
+ 'border border-rule bg-surface rounded-lg',
18
+ 'shadow-[var(--shadow-rest)]',
19
+ 'text-[14px] text-ink-2',
20
+ ],
21
+ variants: {
22
+ variant: {
23
+ default: '',
24
+ accent: [
25
+ 'text-accent-ink',
26
+ 'border-[color-mix(in_oklab,var(--color-accent)_35%,var(--color-rule))]',
27
+ 'bg-[color-mix(in_oklab,var(--color-accent)_6%,var(--color-surface))]',
28
+ ],
29
+ outline: 'shadow-none',
30
+ raised: 'shadow-[var(--shadow-raised)]',
31
+ },
32
+ },
33
+ defaultVariants: {
34
+ variant: 'default',
35
+ },
36
+ })
37
+
38
+ export type CardVariants = VariantProps<typeof cardStyles>
39
+ export type CardProps = HTMLAttributes<HTMLDivElement> & CardVariants
40
+
41
+ export function Card({ variant, className, ...props }: CardProps) {
42
+ return <div {...props} className={cn(cardStyles({ variant }), className)} />
43
+ }
44
+
45
+ /* =========================================================================
46
+ PressableCard — same chrome + variants, rendered as a keyboard-focusable
47
+ RAC Button. Hover lifts the shadow, press sinks 1px, focus shows indigo ring.
48
+ ========================================================================= */
49
+
50
+ const pressableCardStyles = tv({
51
+ extend: cardStyles,
52
+ base: [
53
+ 'text-left w-full cursor-pointer outline-none',
54
+ 'transition-[box-shadow,transform,border-color] duration-150',
55
+ 'hover:shadow-[var(--shadow-raised)] hover:border-ink-4',
56
+ 'pressed:translate-y-px',
57
+ 'focus-visible:shadow-[var(--shadow-focus)]',
58
+ 'disabled:opacity-45 disabled:cursor-not-allowed disabled:pointer-events-none',
59
+ ],
60
+ })
61
+
62
+ export type PressableCardProps = AriaButtonProps & CardVariants
63
+
64
+ export function PressableCard({ variant, className, ...props }: PressableCardProps) {
65
+ return (
66
+ <AriaButton
67
+ {...props}
68
+ className={(renderProps) =>
69
+ cn(
70
+ pressableCardStyles({ variant }),
71
+ typeof className === 'function' ? className(renderProps) : className,
72
+ )
73
+ }
74
+ />
75
+ )
76
+ }
77
+
78
+ /* =========================================================================
79
+ Structural slots — each owns its padding. Drop-in composable.
80
+ ========================================================================= */
81
+
82
+ /**
83
+ * Flush, full-bleed strip at the top of a Card.
84
+ * Sand-tinted surface, uppercase mono, anchors a record identifier / status.
85
+ * The strip bleeds to the card edges because the Card has no padding.
86
+ */
87
+ export function CardHeader({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
88
+ return (
89
+ <div
90
+ {...props}
91
+ className={cn(
92
+ 'flex justify-between items-center',
93
+ 'px-5 py-5 border-b border-rule',
94
+ 'bg-[color-mix(in_oklab,var(--color-surface-2)_60%,var(--color-surface))]',
95
+ 'font-mono text-[11.5px] uppercase tracking-[0.06em] text-ink-4',
96
+ className,
97
+ )}
98
+ />
99
+ )
100
+ }
101
+
102
+ /**
103
+ * Padded content block. This is where inline slots (CardEyebrow, CardTitle,
104
+ * CardBody) must live — they do not carry their own padding. Repeatable for
105
+ * multi-section layouts; separate sections with a `<hr class="border-rule"/>`.
106
+ */
107
+ export function CardContent({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
108
+ return <div {...props} className={cn('flex flex-col gap-3 p-5', className)} />
109
+ }
110
+
111
+ /**
112
+ * Footer anchored to the bottom of the card via `mt-auto`. Place a
113
+ * `<Separator />` above it when you want the dashed divider.
114
+ * Mono meta-sized text. Typically holds counts + a trailing arrow / action.
115
+ */
116
+ export function CardFooter({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
117
+ return (
118
+ <div
119
+ {...props}
120
+ className={cn(
121
+ 'mt-auto flex justify-between items-center',
122
+ 'px-5 py-4',
123
+ 'font-mono text-[11.5px] text-ink-4',
124
+ className,
125
+ )}
126
+ />
127
+ )
128
+ }
129
+
130
+ /* =========================================================================
131
+ Inline slots — no self-padding; must live inside CardContent / CardHeader.
132
+ ========================================================================= */
133
+
134
+ export function CardEyebrow({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
135
+ return (
136
+ <div
137
+ {...props}
138
+ className={cn(
139
+ 'flex justify-between items-center',
140
+ 'font-mono text-[11px] uppercase tracking-[0.08em] text-ink-4',
141
+ className,
142
+ )}
143
+ />
144
+ )
145
+ }
146
+
147
+ export function CardTitle({ className, ...props }: HTMLAttributes<HTMLHeadingElement>) {
148
+ return (
149
+ <h3
150
+ {...props}
151
+ className={cn(
152
+ 'font-sans font-medium text-[20px] leading-[1.25] tracking-[-0.015em] text-ink',
153
+ className,
154
+ )}
155
+ />
156
+ )
157
+ }
158
+
159
+ export function CardFeatureTitle({ className, ...props }: HTMLAttributes<HTMLHeadingElement>) {
160
+ return (
161
+ <h3
162
+ {...props}
163
+ className={cn(
164
+ 'font-sans font-medium text-[26px] leading-[1.2] tracking-[-0.02em] text-ink',
165
+ className,
166
+ )}
167
+ />
168
+ )
169
+ }
170
+
171
+ export function CardBody({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
172
+ return <div {...props} className={cn('text-[14px] leading-[1.55] text-ink-3', className)} />
173
+ }
@@ -0,0 +1,13 @@
1
+ export type { CardProps, CardVariants, PressableCardProps } from './card'
2
+ export {
3
+ Card,
4
+ CardBody,
5
+ CardContent,
6
+ CardEyebrow,
7
+ CardFeatureTitle,
8
+ CardFooter,
9
+ CardHeader,
10
+ CardTitle,
11
+ cardStyles,
12
+ PressableCard,
13
+ } from './card'