@duro-app/ui 0.12.2 → 0.14.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 (134) hide show
  1. package/dist/components/PageShell/PageShell.d.ts +15 -0
  2. package/dist/components/PageShell/PageShell.d.ts.map +1 -0
  3. package/dist/components/PageShell/index.d.ts +3 -0
  4. package/dist/components/PageShell/index.d.ts.map +1 -0
  5. package/dist/components/PageShell/styles.css.d.ts +41 -0
  6. package/dist/components/PageShell/styles.css.d.ts.map +1 -0
  7. package/dist/components/ThemeProvider/ThemeProvider.d.ts.map +1 -1
  8. package/dist/index.css +1 -1
  9. package/dist/index.d.ts +1 -2
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +3283 -3434
  12. package/dist/index.js.map +1 -1
  13. package/package.json +4 -4
  14. package/src/components/Alert/Alert.stories.tsx +76 -0
  15. package/src/components/Alert/Alert.tsx +45 -0
  16. package/src/components/Alert/styles.css.ts +50 -0
  17. package/src/components/Badge/Badge.stories.tsx +94 -0
  18. package/src/components/Badge/Badge.tsx +21 -0
  19. package/src/components/Badge/styles.css.ts +51 -0
  20. package/src/components/Button/Button.stories.tsx +130 -0
  21. package/src/components/Button/Button.tsx +48 -0
  22. package/src/components/Button/styles.css.ts +107 -0
  23. package/src/components/Callout/Callout.stories.tsx +97 -0
  24. package/src/components/Callout/Callout.tsx +39 -0
  25. package/src/components/Callout/index.ts +1 -0
  26. package/src/components/Callout/styles.css.ts +45 -0
  27. package/src/components/Card/Card.stories.tsx +119 -0
  28. package/src/components/Card/Card.tsx +35 -0
  29. package/src/components/Card/styles.css.ts +67 -0
  30. package/src/components/Checkbox/Checkbox.stories.tsx +88 -0
  31. package/src/components/Checkbox/Checkbox.tsx +73 -0
  32. package/src/components/Checkbox/styles.css.ts +57 -0
  33. package/src/components/Cluster/Cluster.stories.tsx +92 -0
  34. package/src/components/Cluster/Cluster.tsx +43 -0
  35. package/src/components/Cluster/styles.css.ts +25 -0
  36. package/src/components/EmptyState/EmptyState.stories.tsx +54 -0
  37. package/src/components/EmptyState/EmptyState.tsx +19 -0
  38. package/src/components/EmptyState/styles.css.ts +25 -0
  39. package/src/components/Field/Field.stories.tsx +92 -0
  40. package/src/components/Field/Field.tsx +80 -0
  41. package/src/components/Field/FieldContext.ts +14 -0
  42. package/src/components/Field/styles.css.ts +25 -0
  43. package/src/components/Fieldset/Fieldset.stories.tsx +85 -0
  44. package/src/components/Fieldset/Fieldset.tsx +48 -0
  45. package/src/components/Fieldset/index.ts +1 -0
  46. package/src/components/Fieldset/styles.css.ts +33 -0
  47. package/src/components/Grid/Grid.stories.tsx +107 -0
  48. package/src/components/Grid/Grid.tsx +41 -0
  49. package/src/components/Grid/styles.css.ts +25 -0
  50. package/src/components/Heading/Heading.tsx +48 -0
  51. package/src/components/Heading/styles.css.ts +26 -0
  52. package/src/components/Icon/Icon.tsx +168 -0
  53. package/src/components/Icon/index.ts +2 -0
  54. package/src/components/Inline/Inline.stories.tsx +88 -0
  55. package/src/components/Inline/Inline.tsx +45 -0
  56. package/src/components/Inline/styles.css.ts +27 -0
  57. package/src/components/Input/Input.stories.tsx +89 -0
  58. package/src/components/Input/Input.tsx +77 -0
  59. package/src/components/Input/styles.css.ts +60 -0
  60. package/src/components/InputGroup/InputGroup.stories.tsx +119 -0
  61. package/src/components/InputGroup/InputGroup.tsx +60 -0
  62. package/src/components/InputGroup/InputGroupContext.ts +11 -0
  63. package/src/components/InputGroup/styles.css.ts +61 -0
  64. package/src/components/LinkButton/LinkButton.stories.tsx +91 -0
  65. package/src/components/LinkButton/LinkButton.tsx +42 -0
  66. package/src/components/LinkButton/styles.css.ts +56 -0
  67. package/src/components/Menu/Menu.stories.tsx +146 -0
  68. package/src/components/Menu/Menu.tsx +151 -0
  69. package/src/components/Menu/MenuContext.ts +20 -0
  70. package/src/components/Menu/styles.css.ts +89 -0
  71. package/src/components/Menu/useMenuRoot.ts +136 -0
  72. package/src/components/PageShell/PageShell.tsx +45 -0
  73. package/src/components/PageShell/index.ts +2 -0
  74. package/src/components/PageShell/styles.css.ts +26 -0
  75. package/src/components/ScrollArea/ScrollArea.stories.tsx +82 -0
  76. package/src/components/ScrollArea/ScrollArea.tsx +170 -0
  77. package/src/components/ScrollArea/ScrollAreaContext.ts +21 -0
  78. package/src/components/ScrollArea/styles.css.ts +81 -0
  79. package/src/components/ScrollArea/useScrollAreaRoot.ts +72 -0
  80. package/src/components/Select/Select.stories.tsx +144 -0
  81. package/src/components/Select/Select.tsx +183 -0
  82. package/src/components/Select/SelectContext.ts +24 -0
  83. package/src/components/Select/styles.css.ts +97 -0
  84. package/src/components/Select/useSelectRoot.ts +178 -0
  85. package/src/components/SideNav/SideNav.stories.tsx +77 -0
  86. package/src/components/SideNav/SideNav.tsx +172 -0
  87. package/src/components/SideNav/SideNavContext.ts +18 -0
  88. package/src/components/SideNav/styles.css.ts +95 -0
  89. package/src/components/Spinner/Spinner.stories.tsx +59 -0
  90. package/src/components/Spinner/Spinner.tsx +24 -0
  91. package/src/components/Spinner/styles.css.ts +47 -0
  92. package/src/components/Stack/Stack.stories.tsx +103 -0
  93. package/src/components/Stack/Stack.tsx +33 -0
  94. package/src/components/Stack/styles.css.ts +21 -0
  95. package/src/components/StatusIcon/StatusIcon.stories.tsx +81 -0
  96. package/src/components/StatusIcon/StatusIcon.tsx +24 -0
  97. package/src/components/StatusIcon/styles.css.ts +27 -0
  98. package/src/components/Switch/Switch.stories.tsx +88 -0
  99. package/src/components/Switch/Switch.tsx +78 -0
  100. package/src/components/Switch/styles.css.ts +71 -0
  101. package/src/components/Table/Table.stories.tsx +308 -0
  102. package/src/components/Table/Table.tsx +179 -0
  103. package/src/components/Table/styles.css.ts +97 -0
  104. package/src/components/Tabs/Tabs.stories.tsx +142 -0
  105. package/src/components/Tabs/Tabs.tsx +210 -0
  106. package/src/components/Tabs/TabsContext.ts +20 -0
  107. package/src/components/Tabs/styles.css.ts +98 -0
  108. package/src/components/Tabs/useTabsRoot.ts +42 -0
  109. package/src/components/Text/Text.tsx +52 -0
  110. package/src/components/Text/styles.css.ts +57 -0
  111. package/src/components/Textarea/Textarea.stories.tsx +80 -0
  112. package/src/components/Textarea/Textarea.tsx +50 -0
  113. package/src/components/Textarea/styles.css.ts +56 -0
  114. package/src/components/ThemeProvider/ThemeProvider.stories.tsx +163 -0
  115. package/src/components/ThemeProvider/ThemeProvider.tsx +33 -0
  116. package/src/components/Toggle/Toggle.stories.tsx +84 -0
  117. package/src/components/Toggle/Toggle.tsx +85 -0
  118. package/src/components/Toggle/styles.css.ts +66 -0
  119. package/src/components/ToggleGroup/ToggleGroup.stories.tsx +159 -0
  120. package/src/components/ToggleGroup/ToggleGroup.tsx +63 -0
  121. package/src/components/ToggleGroup/ToggleGroupContext.ts +18 -0
  122. package/src/components/ToggleGroup/styles.css.ts +17 -0
  123. package/src/components/Tooltip/Tooltip.stories.tsx +127 -0
  124. package/src/components/Tooltip/Tooltip.tsx +97 -0
  125. package/src/components/Tooltip/styles.css.ts +56 -0
  126. package/src/docs/Spacing.mdx +80 -0
  127. package/src/docs/Spacing.stories.tsx +202 -0
  128. package/src/docs/Typography.mdx +93 -0
  129. package/src/docs/Typography.stories.tsx +211 -0
  130. package/src/docs/helpers.tsx +135 -0
  131. package/src/hooks/useContainerQuery.ts +54 -0
  132. package/src/hooks/useControllableValue.ts +18 -0
  133. package/src/index.ts +56 -0
  134. package/src/stubs/assets-registry.ts +3 -0
@@ -0,0 +1,308 @@
1
+ import type {Meta, StoryObj} from '@storybook/react'
2
+ import {expect} from 'storybook/test'
3
+ import {css, html} from 'react-strict-dom'
4
+ import {Table} from './Table'
5
+ import {Badge} from '../Badge/Badge'
6
+ import {colors} from '@duro-app/tokens/tokens/colors.css'
7
+
8
+ const meta: Meta = {
9
+ title: 'Components/Table',
10
+ }
11
+
12
+ export default meta
13
+ type Story = StoryObj
14
+
15
+ const services = [
16
+ {name: 'Traefik', status: 'Running', port: '443', uptime: '14d 3h'},
17
+ {name: 'Pi-hole', status: 'Running', port: '80', uptime: '14d 3h'},
18
+ {name: 'Portainer', status: 'Stopped', port: '9000', uptime: '\u2014'},
19
+ {name: 'Grafana', status: 'Running', port: '3000', uptime: '7d 12h'},
20
+ {name: 'Prometheus', status: 'Warning', port: '9090', uptime: '7d 12h'},
21
+ ]
22
+
23
+ function statusVariant(status: string) {
24
+ switch (status) {
25
+ case 'Running':
26
+ return 'success' as const
27
+ case 'Stopped':
28
+ return 'error' as const
29
+ case 'Warning':
30
+ return 'warning' as const
31
+ default:
32
+ return 'default' as const
33
+ }
34
+ }
35
+
36
+ export const Default: Story = {
37
+ render: () => (
38
+ <Table.Root columns={4}>
39
+ <Table.Header>
40
+ <Table.Row>
41
+ <Table.HeaderCell>Service</Table.HeaderCell>
42
+ <Table.HeaderCell>Status</Table.HeaderCell>
43
+ <Table.HeaderCell>Port</Table.HeaderCell>
44
+ <Table.HeaderCell>Uptime</Table.HeaderCell>
45
+ </Table.Row>
46
+ </Table.Header>
47
+ <Table.Body>
48
+ {services.map((s) => (
49
+ <Table.Row key={s.name}>
50
+ <Table.Cell>{s.name}</Table.Cell>
51
+ <Table.Cell>
52
+ <Badge variant={statusVariant(s.status)} size="sm">
53
+ {s.status}
54
+ </Badge>
55
+ </Table.Cell>
56
+ <Table.Cell>{s.port}</Table.Cell>
57
+ <Table.Cell>{s.uptime}</Table.Cell>
58
+ </Table.Row>
59
+ ))}
60
+ </Table.Body>
61
+ </Table.Root>
62
+ ),
63
+ play: async ({canvas}) => {
64
+ // ARIA table structure
65
+ await expect(canvas.getByRole('table')).toBeInTheDocument()
66
+
67
+ const rowgroups = canvas.getAllByRole('rowgroup')
68
+ await expect(rowgroups.length).toBe(2) // header + body
69
+
70
+ // Header cells
71
+ const columnHeaders = canvas.getAllByRole('columnheader')
72
+ await expect(columnHeaders.length).toBe(4)
73
+ await expect(columnHeaders[0]).toHaveTextContent('Service')
74
+ await expect(columnHeaders[1]).toHaveTextContent('Status')
75
+ await expect(columnHeaders[2]).toHaveTextContent('Port')
76
+ await expect(columnHeaders[3]).toHaveTextContent('Uptime')
77
+
78
+ // Rows (1 header + 5 body)
79
+ const rows = canvas.getAllByRole('row')
80
+ await expect(rows.length).toBe(6)
81
+
82
+ // Body cells
83
+ const cells = canvas.getAllByRole('cell')
84
+ await expect(cells.length).toBe(20) // 5 rows * 4 columns
85
+ await expect(cells[0]).toHaveTextContent('Traefik')
86
+ },
87
+ }
88
+
89
+ export const Striped: Story = {
90
+ render: () => (
91
+ <Table.Root variant="striped" columns={4}>
92
+ <Table.Header>
93
+ <Table.Row>
94
+ <Table.HeaderCell>Service</Table.HeaderCell>
95
+ <Table.HeaderCell>Status</Table.HeaderCell>
96
+ <Table.HeaderCell>Port</Table.HeaderCell>
97
+ <Table.HeaderCell>Uptime</Table.HeaderCell>
98
+ </Table.Row>
99
+ </Table.Header>
100
+ <Table.Body>
101
+ {services.map((s) => (
102
+ <Table.Row key={s.name}>
103
+ <Table.Cell>{s.name}</Table.Cell>
104
+ <Table.Cell>
105
+ <Badge variant={statusVariant(s.status)} size="sm">
106
+ {s.status}
107
+ </Badge>
108
+ </Table.Cell>
109
+ <Table.Cell>{s.port}</Table.Cell>
110
+ <Table.Cell>{s.uptime}</Table.Cell>
111
+ </Table.Row>
112
+ ))}
113
+ </Table.Body>
114
+ </Table.Root>
115
+ ),
116
+ play: async ({canvas}) => {
117
+ await expect(canvas.getByRole('table')).toBeInTheDocument()
118
+ await expect(canvas.getAllByRole('row').length).toBe(6)
119
+ },
120
+ }
121
+
122
+ export const Bordered: Story = {
123
+ render: () => (
124
+ <Table.Root variant="bordered" columns={4}>
125
+ <Table.Header>
126
+ <Table.Row>
127
+ <Table.HeaderCell>Service</Table.HeaderCell>
128
+ <Table.HeaderCell>Status</Table.HeaderCell>
129
+ <Table.HeaderCell>Port</Table.HeaderCell>
130
+ <Table.HeaderCell>Uptime</Table.HeaderCell>
131
+ </Table.Row>
132
+ </Table.Header>
133
+ <Table.Body>
134
+ {services.map((s) => (
135
+ <Table.Row key={s.name}>
136
+ <Table.Cell>{s.name}</Table.Cell>
137
+ <Table.Cell>
138
+ <Badge variant={statusVariant(s.status)} size="sm">
139
+ {s.status}
140
+ </Badge>
141
+ </Table.Cell>
142
+ <Table.Cell>{s.port}</Table.Cell>
143
+ <Table.Cell>{s.uptime}</Table.Cell>
144
+ </Table.Row>
145
+ ))}
146
+ </Table.Body>
147
+ </Table.Root>
148
+ ),
149
+ play: async ({canvas}) => {
150
+ await expect(canvas.getByRole('table')).toBeInTheDocument()
151
+ await expect(canvas.getAllByRole('columnheader').length).toBe(4)
152
+ await expect(canvas.getAllByRole('cell').length).toBe(20)
153
+ },
154
+ }
155
+
156
+ export const Compact: Story = {
157
+ render: () => (
158
+ <Table.Root size="sm" columns={4}>
159
+ <Table.Header>
160
+ <Table.Row>
161
+ <Table.HeaderCell>Service</Table.HeaderCell>
162
+ <Table.HeaderCell>Status</Table.HeaderCell>
163
+ <Table.HeaderCell>Port</Table.HeaderCell>
164
+ <Table.HeaderCell>Uptime</Table.HeaderCell>
165
+ </Table.Row>
166
+ </Table.Header>
167
+ <Table.Body>
168
+ {services.map((s) => (
169
+ <Table.Row key={s.name}>
170
+ <Table.Cell>{s.name}</Table.Cell>
171
+ <Table.Cell>
172
+ <Badge variant={statusVariant(s.status)} size="sm">
173
+ {s.status}
174
+ </Badge>
175
+ </Table.Cell>
176
+ <Table.Cell>{s.port}</Table.Cell>
177
+ <Table.Cell>{s.uptime}</Table.Cell>
178
+ </Table.Row>
179
+ ))}
180
+ </Table.Body>
181
+ </Table.Root>
182
+ ),
183
+ play: async ({canvas}) => {
184
+ await expect(canvas.getByRole('table')).toBeInTheDocument()
185
+ await expect(canvas.getByText('Traefik')).toBeInTheDocument()
186
+ },
187
+ }
188
+
189
+ const storyStyles = css.create({
190
+ stack: {display: 'flex', flexDirection: 'column', gap: 24},
191
+ label: {color: colors.textMuted, fontSize: '0.875rem', fontWeight: 600},
192
+ })
193
+
194
+ export const AllVariants: Story = {
195
+ render: () => (
196
+ <html.div style={storyStyles.stack}>
197
+ <html.div>
198
+ <html.span style={storyStyles.label}>Default</html.span>
199
+ <Table.Root columns={3}>
200
+ <Table.Header>
201
+ <Table.Row>
202
+ <Table.HeaderCell>Name</Table.HeaderCell>
203
+ <Table.HeaderCell>Role</Table.HeaderCell>
204
+ <Table.HeaderCell>Status</Table.HeaderCell>
205
+ </Table.Row>
206
+ </Table.Header>
207
+ <Table.Body>
208
+ <Table.Row>
209
+ <Table.Cell>Traefik</Table.Cell>
210
+ <Table.Cell>Reverse Proxy</Table.Cell>
211
+ <Table.Cell>Active</Table.Cell>
212
+ </Table.Row>
213
+ <Table.Row>
214
+ <Table.Cell>Pi-hole</Table.Cell>
215
+ <Table.Cell>DNS</Table.Cell>
216
+ <Table.Cell>Active</Table.Cell>
217
+ </Table.Row>
218
+ </Table.Body>
219
+ </Table.Root>
220
+ </html.div>
221
+
222
+ <html.div>
223
+ <html.span style={storyStyles.label}>Striped</html.span>
224
+ <Table.Root variant="striped" columns={3}>
225
+ <Table.Header>
226
+ <Table.Row>
227
+ <Table.HeaderCell>Name</Table.HeaderCell>
228
+ <Table.HeaderCell>Role</Table.HeaderCell>
229
+ <Table.HeaderCell>Status</Table.HeaderCell>
230
+ </Table.Row>
231
+ </Table.Header>
232
+ <Table.Body>
233
+ <Table.Row>
234
+ <Table.Cell>Traefik</Table.Cell>
235
+ <Table.Cell>Reverse Proxy</Table.Cell>
236
+ <Table.Cell>Active</Table.Cell>
237
+ </Table.Row>
238
+ <Table.Row>
239
+ <Table.Cell>Pi-hole</Table.Cell>
240
+ <Table.Cell>DNS</Table.Cell>
241
+ <Table.Cell>Active</Table.Cell>
242
+ </Table.Row>
243
+ <Table.Row>
244
+ <Table.Cell>Grafana</Table.Cell>
245
+ <Table.Cell>Monitoring</Table.Cell>
246
+ <Table.Cell>Active</Table.Cell>
247
+ </Table.Row>
248
+ </Table.Body>
249
+ </Table.Root>
250
+ </html.div>
251
+
252
+ <html.div>
253
+ <html.span style={storyStyles.label}>Bordered</html.span>
254
+ <Table.Root variant="bordered" columns={3}>
255
+ <Table.Header>
256
+ <Table.Row>
257
+ <Table.HeaderCell>Name</Table.HeaderCell>
258
+ <Table.HeaderCell>Role</Table.HeaderCell>
259
+ <Table.HeaderCell>Status</Table.HeaderCell>
260
+ </Table.Row>
261
+ </Table.Header>
262
+ <Table.Body>
263
+ <Table.Row>
264
+ <Table.Cell>Traefik</Table.Cell>
265
+ <Table.Cell>Reverse Proxy</Table.Cell>
266
+ <Table.Cell>Active</Table.Cell>
267
+ </Table.Row>
268
+ <Table.Row>
269
+ <Table.Cell>Pi-hole</Table.Cell>
270
+ <Table.Cell>DNS</Table.Cell>
271
+ <Table.Cell>Active</Table.Cell>
272
+ </Table.Row>
273
+ </Table.Body>
274
+ </Table.Root>
275
+ </html.div>
276
+
277
+ <html.div>
278
+ <html.span style={storyStyles.label}>Compact (sm)</html.span>
279
+ <Table.Root size="sm" columns={3}>
280
+ <Table.Header>
281
+ <Table.Row>
282
+ <Table.HeaderCell>Name</Table.HeaderCell>
283
+ <Table.HeaderCell>Role</Table.HeaderCell>
284
+ <Table.HeaderCell>Status</Table.HeaderCell>
285
+ </Table.Row>
286
+ </Table.Header>
287
+ <Table.Body>
288
+ <Table.Row>
289
+ <Table.Cell>Traefik</Table.Cell>
290
+ <Table.Cell>Reverse Proxy</Table.Cell>
291
+ <Table.Cell>Active</Table.Cell>
292
+ </Table.Row>
293
+ <Table.Row>
294
+ <Table.Cell>Pi-hole</Table.Cell>
295
+ <Table.Cell>DNS</Table.Cell>
296
+ <Table.Cell>Active</Table.Cell>
297
+ </Table.Row>
298
+ </Table.Body>
299
+ </Table.Root>
300
+ </html.div>
301
+ </html.div>
302
+ ),
303
+ play: async ({canvas}) => {
304
+ // All four tables render
305
+ const tables = canvas.getAllByRole('table')
306
+ await expect(tables.length).toBe(4)
307
+ },
308
+ }
@@ -0,0 +1,179 @@
1
+ import {type ReactNode, createContext, useContext, Children} from 'react'
2
+ import {html} from 'react-strict-dom'
3
+ import {styles} from './styles.css'
4
+
5
+ // --- Types ---
6
+
7
+ export type TableVariant = 'default' | 'striped' | 'bordered'
8
+ export type TableSize = 'sm' | 'md'
9
+
10
+ // --- Context ---
11
+
12
+ interface TableContextValue {
13
+ variant: TableVariant
14
+ size: TableSize
15
+ columns: number
16
+ isHeader: boolean
17
+ }
18
+
19
+ const TableContext = createContext<TableContextValue | null>(null)
20
+
21
+ function useTable() {
22
+ const ctx = useContext(TableContext)
23
+ if (!ctx) throw new Error('Table compound components must be used within Table.Root')
24
+ return ctx
25
+ }
26
+
27
+ // --- HeaderContext (to distinguish header vs body rowgroup) ---
28
+
29
+ const HeaderContext = createContext(false)
30
+
31
+ // --- Root ---
32
+
33
+ interface RootProps {
34
+ children: ReactNode
35
+ variant?: TableVariant
36
+ size?: TableSize
37
+ columns: number
38
+ }
39
+
40
+ function Root({children, variant = 'default', size = 'md', columns}: RootProps) {
41
+ return (
42
+ <TableContext.Provider value={{variant, size, columns, isHeader: false}}>
43
+ <html.div role="table" style={styles.root}>
44
+ {children}
45
+ </html.div>
46
+ </TableContext.Provider>
47
+ )
48
+ }
49
+
50
+ // --- Header ---
51
+
52
+ function Header({children}: {children: ReactNode}) {
53
+ return (
54
+ <HeaderContext.Provider value={true}>
55
+ <html.div role="rowgroup" style={styles.header}>
56
+ {children}
57
+ </html.div>
58
+ </HeaderContext.Provider>
59
+ )
60
+ }
61
+
62
+ // --- Body ---
63
+
64
+ function Body({children}: {children: ReactNode}) {
65
+ const {variant} = useTable()
66
+ const childArray = Children.toArray(children)
67
+
68
+ return (
69
+ <HeaderContext.Provider value={false}>
70
+ <html.div role="rowgroup">
71
+ {childArray.map((child, index) => {
72
+ if (variant === 'striped') {
73
+ return (
74
+ <RowIndexContext.Provider key={index} value={index}>
75
+ {child}
76
+ </RowIndexContext.Provider>
77
+ )
78
+ }
79
+ return child
80
+ })}
81
+ </html.div>
82
+ </HeaderContext.Provider>
83
+ )
84
+ }
85
+
86
+ // Row index context for striped variant
87
+ const RowIndexContext = createContext<number>(-1)
88
+
89
+ // --- Row ---
90
+
91
+ function Row({children}: {children: ReactNode}) {
92
+ const {variant, columns} = useTable()
93
+ const isHeader = useContext(HeaderContext)
94
+ const rowIndex = useContext(RowIndexContext)
95
+ const isEvenRow = rowIndex >= 0 && rowIndex % 2 === 1
96
+ const childArray = Children.toArray(children)
97
+ const isLastRow = false // handled by CSS or parent
98
+
99
+ return (
100
+ <html.div
101
+ role="row"
102
+ style={[
103
+ styles.row,
104
+ styles.gridColumns(columns),
105
+ !isHeader && styles.bodyRow,
106
+ !isHeader && variant === 'striped' && isEvenRow && styles.stripedEven,
107
+ ]}
108
+ >
109
+ {variant === 'bordered'
110
+ ? childArray.map((child, index) => (
111
+ <CellIndexContext.Provider key={index} value={{index, total: childArray.length}}>
112
+ {child}
113
+ </CellIndexContext.Provider>
114
+ ))
115
+ : children}
116
+ </html.div>
117
+ )
118
+ }
119
+
120
+ // Cell index context for bordered variant
121
+ const CellIndexContext = createContext<{index: number; total: number}>({
122
+ index: 0,
123
+ total: 0,
124
+ })
125
+
126
+ // --- HeaderCell ---
127
+
128
+ function HeaderCell({children}: {children: ReactNode}) {
129
+ const {size, variant} = useTable()
130
+ const {index, total} = useContext(CellIndexContext)
131
+ const isLast = variant === 'bordered' && index === total - 1
132
+
133
+ return (
134
+ <html.div
135
+ role="columnheader"
136
+ style={[
137
+ styles.headerCell,
138
+ size === 'sm' ? styles.cellSm : styles.cellMd,
139
+ variant === 'bordered' && styles.borderedCell,
140
+ isLast && styles.borderedCellLast,
141
+ ]}
142
+ >
143
+ {children}
144
+ </html.div>
145
+ )
146
+ }
147
+
148
+ // --- Cell ---
149
+
150
+ function Cell({children}: {children: ReactNode}) {
151
+ const {size, variant} = useTable()
152
+ const {index, total} = useContext(CellIndexContext)
153
+ const isLast = variant === 'bordered' && index === total - 1
154
+
155
+ return (
156
+ <html.div
157
+ role="cell"
158
+ style={[
159
+ styles.cell,
160
+ size === 'sm' ? styles.cellSm : styles.cellMd,
161
+ variant === 'bordered' && styles.borderedCell,
162
+ isLast && styles.borderedCellLast,
163
+ ]}
164
+ >
165
+ {children}
166
+ </html.div>
167
+ )
168
+ }
169
+
170
+ // --- Export ---
171
+
172
+ export const Table = {
173
+ Root,
174
+ Header,
175
+ Body,
176
+ Row,
177
+ HeaderCell,
178
+ Cell,
179
+ }
@@ -0,0 +1,97 @@
1
+ import {css} from 'react-strict-dom'
2
+ import {colors} from '@duro-app/tokens/tokens/colors.css'
3
+ import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
4
+ import {typography} from '@duro-app/tokens/tokens/typography.css'
5
+
6
+ export const styles = css.create({
7
+ // Root
8
+ root: {
9
+ borderWidth: 1,
10
+ borderStyle: 'solid',
11
+ borderColor: colors.border,
12
+ borderRadius: radii.md,
13
+ overflow: 'hidden',
14
+ backgroundColor: colors.bgCard,
15
+ fontFamily: typography.fontFamily,
16
+ color: colors.text,
17
+ },
18
+
19
+ // Header group
20
+ header: {
21
+ backgroundColor: colors.bgCard,
22
+ },
23
+
24
+ // Row
25
+ row: {
26
+ display: 'grid',
27
+ borderBottomWidth: 1,
28
+ borderBottomStyle: 'solid',
29
+ borderBottomColor: colors.border,
30
+ },
31
+ rowLastChild: {
32
+ borderBottomWidth: 0,
33
+ },
34
+ bodyRow: {
35
+ backgroundColor: {
36
+ default: 'transparent',
37
+ ':hover': colors.bgCardHover,
38
+ },
39
+ transitionProperty: 'background-color',
40
+ transitionDuration: '150ms',
41
+ transitionTimingFunction: 'ease',
42
+ },
43
+
44
+ // Header cell
45
+ headerCell: {
46
+ fontWeight: typography.fontWeightSemibold,
47
+ color: colors.textMuted,
48
+ textAlign: 'start',
49
+ },
50
+
51
+ // Body cell
52
+ cell: {
53
+ color: colors.text,
54
+ display: 'flex',
55
+ alignItems: 'center',
56
+ },
57
+
58
+ // Size: sm
59
+ cellSm: {
60
+ paddingTop: spacing.xs,
61
+ paddingBottom: spacing.xs,
62
+ paddingLeft: spacing.sm,
63
+ paddingRight: spacing.sm,
64
+ fontSize: typography.fontSizeXs,
65
+ },
66
+ // Size: md
67
+ cellMd: {
68
+ paddingTop: spacing.sm,
69
+ paddingBottom: spacing.sm,
70
+ paddingLeft: spacing.md,
71
+ paddingRight: spacing.md,
72
+ fontSize: typography.fontSizeSm,
73
+ },
74
+
75
+ // Variant: striped (even body rows)
76
+ stripedEven: {
77
+ backgroundColor: {
78
+ default: colors.bgCardHover,
79
+ ':hover': colors.bgCardHover,
80
+ },
81
+ },
82
+
83
+ // Variant: bordered (cells get side borders)
84
+ borderedCell: {
85
+ borderRightWidth: 1,
86
+ borderRightStyle: 'solid',
87
+ borderRightColor: colors.border,
88
+ },
89
+ borderedCellLast: {
90
+ borderRightWidth: 0,
91
+ },
92
+
93
+ // Dynamic styles — simple identifier params only (StyleX constraint)
94
+ gridColumns: (columns: number) => ({
95
+ gridTemplateColumns: `repeat(${columns}, 1fr)`,
96
+ }),
97
+ })