@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,306 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { useState } from 'react'
3
+
4
+ import { Badge } from '../badge'
5
+ import {
6
+ SortableHeaderCell,
7
+ type SortDirection,
8
+ Table,
9
+ TableBody,
10
+ TableCell,
11
+ TableHeader,
12
+ TableHeaderCell,
13
+ TableHeaderSpacer,
14
+ TableRow,
15
+ } from './table'
16
+
17
+ /* =========================================================================
18
+ Stories for the Table chrome — pure presentational primitives.
19
+ These render short, non-virtualized tables. The virtualized + infinite
20
+ variant lives in DataTable's own stories.
21
+ ========================================================================= */
22
+
23
+ const meta: Meta<typeof Table> = {
24
+ title: 'Components/Table',
25
+ component: Table,
26
+ tags: ['autodocs'],
27
+ argTypes: {
28
+ variant: {
29
+ control: { type: 'inline-radio' },
30
+ options: ['panel', 'bare'],
31
+ },
32
+ },
33
+ decorators: [
34
+ (Story) => (
35
+ <div className="w-[1080px] h-[420px]">
36
+ <Story />
37
+ </div>
38
+ ),
39
+ ],
40
+ }
41
+ export default meta
42
+
43
+ type Story = StoryObj<typeof Table>
44
+
45
+ /* ---------- Sample data -------------------------------------------------- */
46
+
47
+ type ShipmentRow = {
48
+ id: string
49
+ lane: string
50
+ carrier: string
51
+ status: string
52
+ variant: 'active' | 'success' | 'warning' | 'danger' | 'neutral'
53
+ rate: string
54
+ }
55
+
56
+ const ROWS: ShipmentRow[] = [
57
+ {
58
+ id: 'SH-10482',
59
+ lane: 'Dallas, TX → Atlanta, GA',
60
+ carrier: 'Blue Ridge',
61
+ status: 'in transit',
62
+ variant: 'active',
63
+ rate: '$3,420',
64
+ },
65
+ {
66
+ id: 'SH-10483',
67
+ lane: 'Chicago, IL → Minneapolis, MN',
68
+ carrier: 'North Star',
69
+ status: 'dispatching',
70
+ variant: 'warning',
71
+ rate: '$1,840',
72
+ },
73
+ {
74
+ id: 'SH-10484',
75
+ lane: 'Long Beach, CA → Phoenix, AZ',
76
+ carrier: 'Sunline',
77
+ status: 'picked up',
78
+ variant: 'active',
79
+ rate: '$2,140',
80
+ },
81
+ {
82
+ id: 'SH-10485',
83
+ lane: 'Seattle, WA → Boise, ID',
84
+ carrier: 'Cascade',
85
+ status: 'quoted',
86
+ variant: 'neutral',
87
+ rate: '$1,260',
88
+ },
89
+ {
90
+ id: 'SH-10486',
91
+ lane: 'Miami, FL → Newark, NJ',
92
+ carrier: 'Atlantic',
93
+ status: 'past due',
94
+ variant: 'danger',
95
+ rate: '$4,180',
96
+ },
97
+ {
98
+ id: 'SH-10487',
99
+ lane: 'Houston, TX → Denver, CO',
100
+ carrier: 'Frontier Haul',
101
+ status: 'in transit',
102
+ variant: 'active',
103
+ rate: '$2,720',
104
+ },
105
+ {
106
+ id: 'SH-10488',
107
+ lane: 'Boston, MA → Philadelphia, PA',
108
+ carrier: 'Atlantic',
109
+ status: 'delivered',
110
+ variant: 'success',
111
+ rate: '$960',
112
+ },
113
+ ]
114
+
115
+ const COLUMN_WIDTHS = { id: 110, lane: 320, carrier: 180, status: 140, rate: 120 } as const
116
+
117
+ /* ---------- Default ------------------------------------------------------ */
118
+
119
+ export const Default: Story = {
120
+ render: (args) => (
121
+ <Table {...args} aria-label="Shipments">
122
+ <TableHeader sticky>
123
+ <TableHeaderCell width={COLUMN_WIDTHS.id}>Shipment</TableHeaderCell>
124
+ <TableHeaderCell width={COLUMN_WIDTHS.lane}>Lane</TableHeaderCell>
125
+ <TableHeaderCell width={COLUMN_WIDTHS.carrier}>Carrier</TableHeaderCell>
126
+ <TableHeaderCell width={COLUMN_WIDTHS.status}>Status</TableHeaderCell>
127
+ <TableHeaderCell width={COLUMN_WIDTHS.rate} align="right">
128
+ Rate
129
+ </TableHeaderCell>
130
+ <TableHeaderSpacer />
131
+ </TableHeader>
132
+ <TableBody>
133
+ {ROWS.map((row, i) => (
134
+ <TableRow key={row.id} alt={i % 2 === 1} style={{ height: 36 }}>
135
+ <TableCell width={COLUMN_WIDTHS.id} mono ink>
136
+ {row.id}
137
+ </TableCell>
138
+ <TableCell width={COLUMN_WIDTHS.lane}>{row.lane}</TableCell>
139
+ <TableCell width={COLUMN_WIDTHS.carrier}>{row.carrier}</TableCell>
140
+ <TableCell width={COLUMN_WIDTHS.status}>
141
+ <Badge variant={row.variant}>{row.status}</Badge>
142
+ </TableCell>
143
+ <TableCell width={COLUMN_WIDTHS.rate} align="right" mono>
144
+ {row.rate}
145
+ </TableCell>
146
+ </TableRow>
147
+ ))}
148
+ </TableBody>
149
+ </Table>
150
+ ),
151
+ }
152
+
153
+ /* ---------- Sortable header --------------------------------------------- */
154
+
155
+ export const Sortable: Story = {
156
+ parameters: { controls: { hideNoControlsWarning: true } },
157
+ render: () => {
158
+ const [sortKey, setSortKey] = useState<keyof ShipmentRow>('id')
159
+ const [sortDir, setSortDir] = useState<SortDirection>('asc')
160
+
161
+ const dirFor = (key: keyof ShipmentRow): SortDirection => (key === sortKey ? sortDir : null)
162
+
163
+ const onSort = (key: keyof ShipmentRow) => {
164
+ if (sortKey === key) {
165
+ setSortDir((d) => (d === 'asc' ? 'desc' : 'asc'))
166
+ } else {
167
+ setSortKey(key)
168
+ setSortDir('asc')
169
+ }
170
+ }
171
+
172
+ const sorted = [...ROWS].sort((a, b) => {
173
+ const av = a[sortKey]
174
+ const bv = b[sortKey]
175
+ const cmp = String(av).localeCompare(String(bv))
176
+ return sortDir === 'desc' ? -cmp : cmp
177
+ })
178
+
179
+ return (
180
+ <Table aria-label="Shipments — sortable">
181
+ <TableHeader sticky>
182
+ <SortableHeaderCell
183
+ width={COLUMN_WIDTHS.id}
184
+ sortDirection={dirFor('id')}
185
+ onPress={() => onSort('id')}
186
+ >
187
+ Shipment
188
+ </SortableHeaderCell>
189
+ <SortableHeaderCell
190
+ width={COLUMN_WIDTHS.lane}
191
+ sortDirection={dirFor('lane')}
192
+ onPress={() => onSort('lane')}
193
+ >
194
+ Lane
195
+ </SortableHeaderCell>
196
+ <SortableHeaderCell
197
+ width={COLUMN_WIDTHS.carrier}
198
+ sortDirection={dirFor('carrier')}
199
+ onPress={() => onSort('carrier')}
200
+ >
201
+ Carrier
202
+ </SortableHeaderCell>
203
+ <TableHeaderCell width={COLUMN_WIDTHS.status}>Status</TableHeaderCell>
204
+ <SortableHeaderCell
205
+ width={COLUMN_WIDTHS.rate}
206
+ align="right"
207
+ sortDirection={dirFor('rate')}
208
+ onPress={() => onSort('rate')}
209
+ >
210
+ Rate
211
+ </SortableHeaderCell>
212
+ <TableHeaderSpacer />
213
+ </TableHeader>
214
+ <TableBody>
215
+ {sorted.map((row, i) => (
216
+ <TableRow key={row.id} alt={i % 2 === 1} style={{ height: 36 }}>
217
+ <TableCell width={COLUMN_WIDTHS.id} mono ink>
218
+ {row.id}
219
+ </TableCell>
220
+ <TableCell width={COLUMN_WIDTHS.lane}>{row.lane}</TableCell>
221
+ <TableCell width={COLUMN_WIDTHS.carrier}>{row.carrier}</TableCell>
222
+ <TableCell width={COLUMN_WIDTHS.status}>
223
+ <Badge variant={row.variant}>{row.status}</Badge>
224
+ </TableCell>
225
+ <TableCell width={COLUMN_WIDTHS.rate} align="right" mono>
226
+ {row.rate}
227
+ </TableCell>
228
+ </TableRow>
229
+ ))}
230
+ </TableBody>
231
+ </Table>
232
+ )
233
+ },
234
+ }
235
+
236
+ /* ---------- Interactive rows -------------------------------------------- */
237
+
238
+ export const InteractiveRows: Story = {
239
+ parameters: { controls: { hideNoControlsWarning: true } },
240
+ render: () => {
241
+ const [selected, setSelected] = useState<string | null>(null)
242
+ return (
243
+ <Table aria-label="Shipments — interactive">
244
+ <TableHeader sticky>
245
+ <TableHeaderCell width={COLUMN_WIDTHS.id}>Shipment</TableHeaderCell>
246
+ <TableHeaderCell width={COLUMN_WIDTHS.lane}>Lane</TableHeaderCell>
247
+ <TableHeaderCell width={COLUMN_WIDTHS.carrier}>Carrier</TableHeaderCell>
248
+ <TableHeaderCell width={COLUMN_WIDTHS.status}>Status</TableHeaderCell>
249
+ <TableHeaderSpacer />
250
+ </TableHeader>
251
+ <TableBody>
252
+ {ROWS.map((row, i) => (
253
+ <TableRow
254
+ key={row.id}
255
+ alt={i % 2 === 1}
256
+ interactive
257
+ onRowClick={() => setSelected(row.id)}
258
+ tabIndex={0}
259
+ style={{ height: 36 }}
260
+ aria-selected={selected === row.id}
261
+ className={selected === row.id ? '!bg-surface-2' : undefined}
262
+ >
263
+ <TableCell width={COLUMN_WIDTHS.id} mono ink>
264
+ {row.id}
265
+ </TableCell>
266
+ <TableCell width={COLUMN_WIDTHS.lane}>{row.lane}</TableCell>
267
+ <TableCell width={COLUMN_WIDTHS.carrier}>{row.carrier}</TableCell>
268
+ <TableCell width={COLUMN_WIDTHS.status}>
269
+ <Badge variant={row.variant}>{row.status}</Badge>
270
+ </TableCell>
271
+ </TableRow>
272
+ ))}
273
+ </TableBody>
274
+ </Table>
275
+ )
276
+ },
277
+ }
278
+
279
+ /* ---------- Bare variant (embedded) ------------------------------------- */
280
+
281
+ export const BareEmbedded: Story = {
282
+ args: { variant: 'bare' },
283
+ render: (args) => (
284
+ <Table {...args} aria-label="Shipments — embedded">
285
+ <TableHeader>
286
+ <TableHeaderCell width={COLUMN_WIDTHS.id}>Shipment</TableHeaderCell>
287
+ <TableHeaderCell width={COLUMN_WIDTHS.lane}>Lane</TableHeaderCell>
288
+ <TableHeaderCell width={COLUMN_WIDTHS.status}>Status</TableHeaderCell>
289
+ <TableHeaderSpacer />
290
+ </TableHeader>
291
+ <TableBody>
292
+ {ROWS.slice(0, 4).map((row, i) => (
293
+ <TableRow key={row.id} alt={i % 2 === 1} style={{ height: 36 }}>
294
+ <TableCell width={COLUMN_WIDTHS.id} mono ink>
295
+ {row.id}
296
+ </TableCell>
297
+ <TableCell width={COLUMN_WIDTHS.lane}>{row.lane}</TableCell>
298
+ <TableCell width={COLUMN_WIDTHS.status}>
299
+ <Badge variant={row.variant}>{row.status}</Badge>
300
+ </TableCell>
301
+ </TableRow>
302
+ ))}
303
+ </TableBody>
304
+ </Table>
305
+ ),
306
+ }
@@ -0,0 +1,115 @@
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
+ Table,
7
+ TableBody,
8
+ TableCell,
9
+ TableHeader,
10
+ TableHeaderCell,
11
+ TableRow,
12
+ } from './table'
13
+
14
+ describe('Table chrome', () => {
15
+ it('renders semantic ARIA grid roles', () => {
16
+ render(
17
+ <Table aria-label="Shipments">
18
+ <TableHeader>
19
+ <TableHeaderCell>Lane</TableHeaderCell>
20
+ </TableHeader>
21
+ <TableBody>
22
+ <TableRow>
23
+ <TableCell>SH-10042</TableCell>
24
+ </TableRow>
25
+ </TableBody>
26
+ </Table>,
27
+ )
28
+ expect(screen.getByRole('grid', { name: 'Shipments' })).toBeInTheDocument()
29
+ expect(screen.getByRole('columnheader', { name: 'Lane' })).toBeInTheDocument()
30
+ expect(screen.getByRole('gridcell')).toHaveTextContent('SH-10042')
31
+ })
32
+
33
+ it('TableHeaderCell with sortDirection exposes aria-sort and toggles on click', async () => {
34
+ const onSortToggle = vi.fn()
35
+ render(
36
+ <Table>
37
+ <TableHeader>
38
+ <TableHeaderCell sortDirection="asc" onSortToggle={onSortToggle}>
39
+ ETA
40
+ </TableHeaderCell>
41
+ </TableHeader>
42
+ </Table>,
43
+ )
44
+ const header = screen.getByRole('columnheader', { name: /ETA/ })
45
+ expect(header).toHaveAttribute('aria-sort', 'ascending')
46
+ await userEvent.click(header)
47
+ expect(onSortToggle).toHaveBeenCalledTimes(1)
48
+ })
49
+
50
+ it('TableHeaderCell without onSortToggle does not expose aria-sort', () => {
51
+ render(
52
+ <Table>
53
+ <TableHeader>
54
+ <TableHeaderCell>ETA</TableHeaderCell>
55
+ </TableHeader>
56
+ </Table>,
57
+ )
58
+ expect(screen.getByRole('columnheader')).not.toHaveAttribute('aria-sort')
59
+ })
60
+
61
+ it('TableHeaderCell with onSortToggle and sortDirection=null is still sortable (aria-sort=none)', async () => {
62
+ const onSortToggle = vi.fn()
63
+ render(
64
+ <Table>
65
+ <TableHeader>
66
+ <TableHeaderCell sortDirection={null} onSortToggle={onSortToggle}>
67
+ ETA
68
+ </TableHeaderCell>
69
+ </TableHeader>
70
+ </Table>,
71
+ )
72
+ const header = screen.getByRole('columnheader', { name: /ETA/ })
73
+ expect(header).toHaveAttribute('aria-sort', 'none')
74
+ await userEvent.click(header)
75
+ expect(onSortToggle).toHaveBeenCalledTimes(1)
76
+ })
77
+
78
+ it('TableHeaderCell renders × button when onRemove is provided and fires it without toggling sort', async () => {
79
+ const onRemove = vi.fn()
80
+ const onSortToggle = vi.fn()
81
+ render(
82
+ <Table>
83
+ <TableHeader>
84
+ <TableHeaderCell
85
+ sortDirection={null}
86
+ onSortToggle={onSortToggle}
87
+ onRemove={onRemove}
88
+ ariaLabel="ETA"
89
+ >
90
+ ETA
91
+ </TableHeaderCell>
92
+ </TableHeader>
93
+ </Table>,
94
+ )
95
+ const removeButton = screen.getByRole('button', { name: /Remove column ETA/ })
96
+ await userEvent.click(removeButton)
97
+ expect(onRemove).toHaveBeenCalledTimes(1)
98
+ expect(onSortToggle).not.toHaveBeenCalled()
99
+ })
100
+
101
+ it('TableRow fires onRowClick on click', async () => {
102
+ const onClick = vi.fn()
103
+ render(
104
+ <Table>
105
+ <TableBody>
106
+ <TableRow interactive onRowClick={onClick}>
107
+ <TableCell>row content</TableCell>
108
+ </TableRow>
109
+ </TableBody>
110
+ </Table>,
111
+ )
112
+ await userEvent.click(screen.getByText('row content'))
113
+ expect(onClick).toHaveBeenCalledTimes(1)
114
+ })
115
+ })