@duro-app/ui 0.32.0 → 0.34.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.
@@ -3,8 +3,50 @@ import {colors} from '@duro-app/tokens/tokens/colors.css'
3
3
  import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
4
4
  import {typography} from '@duro-app/tokens/tokens/typography.css'
5
5
 
6
+ // Container-query thresholds. Picked for typical 5-7 column admin tables —
7
+ // see Table.stories.tsx "Responsive" for the visual reasoning. Defined
8
+ // once here so it's clear they're a design-system constant, not a magic
9
+ // number sprinkled across files.
10
+ const COMPACT_BP = '720px'
11
+ const STACK_BP = '440px'
12
+
6
13
  export const styles = css.create({
7
- // Root — the single grid container for the entire table
14
+ // Outer wrapper that hosts the @container query. Wraps SortChip + Root
15
+ // + Pagination so all three react to the same width. Root itself does
16
+ // NOT carry containerType — keeping a single query target per Table.
17
+ //
18
+ // @deprecated Use Table.Root's sortChip/pagination slot props instead.
19
+ // This style backs the deprecated <Table.Container> wrapper.
20
+ container: {
21
+ containerType: 'inline-size',
22
+ display: 'flex',
23
+ flexDirection: 'column',
24
+ gap: spacing.sm,
25
+ },
26
+
27
+ // The same containment chrome, but applied by Root when responsive=true.
28
+ // Mirrors `container` so consumers don't need to wrap manually.
29
+ rootContainer: {
30
+ containerType: 'inline-size',
31
+ display: 'flex',
32
+ flexDirection: 'column',
33
+ gap: spacing.sm,
34
+ },
35
+
36
+ // Applied when Row receives an `onClick` handler. Cursor signals clickability;
37
+ // the :focus-visible outline meets WCAG 2.4.7 for keyboard navigation.
38
+ clickableRow: {
39
+ cursor: 'pointer',
40
+ outlineWidth: {
41
+ default: 0,
42
+ ':focus-visible': 2,
43
+ },
44
+ outlineStyle: 'solid',
45
+ outlineColor: colors.accent,
46
+ outlineOffset: -2,
47
+ },
48
+
49
+ // Root — the single grid container for the table itself
8
50
  root: {
9
51
  display: 'grid',
10
52
  borderWidth: 1,
@@ -17,9 +59,40 @@ export const styles = css.create({
17
59
  color: colors.text,
18
60
  },
19
61
 
20
- // Header group
62
+ // Stack-mode override: the Root's grid collapses to a single column so
63
+ // each Row spans full width and stacks vertically. Applied AFTER the
64
+ // dynamic gridColumns(template) so this rule wins inside the @container.
65
+ rootResponsive: {
66
+ gridTemplateColumns: {
67
+ default: null,
68
+ [`@container (max-width: ${STACK_BP})`]: '1fr',
69
+ },
70
+ // The card-list look needs the outer border to disappear in stack
71
+ // mode — each row paints its own border.
72
+ borderWidth: {
73
+ default: 1,
74
+ [`@container (max-width: ${STACK_BP})`]: 0,
75
+ },
76
+ backgroundColor: {
77
+ default: colors.bgCard,
78
+ [`@container (max-width: ${STACK_BP})`]: 'transparent',
79
+ },
80
+ overflow: {
81
+ default: 'hidden',
82
+ [`@container (max-width: ${STACK_BP})`]: 'visible',
83
+ },
84
+ rowGap: {
85
+ default: 0,
86
+ [`@container (max-width: ${STACK_BP})`]: spacing.sm,
87
+ },
88
+ },
89
+
90
+ // Header group — hidden entirely in stack mode
21
91
  header: {
22
- display: 'grid',
92
+ display: {
93
+ default: 'grid',
94
+ [`@container (max-width: ${STACK_BP})`]: 'none',
95
+ },
23
96
  gridColumn: '1 / -1',
24
97
  gridTemplateColumns: 'subgrid',
25
98
  backgroundColor: colors.bgCard,
@@ -32,43 +105,148 @@ export const styles = css.create({
32
105
  gridTemplateColumns: 'subgrid',
33
106
  },
34
107
 
35
- // Row — spans all columns, uses subgrid to share parent tracks
108
+ // Row — spans all columns. In stack mode its template flips to 1fr so
109
+ // children stack as grid items in a single column (we keep grid context
110
+ // so cells with `gridColumn` overrides still compute).
36
111
  row: {
37
112
  display: 'grid',
38
113
  gridColumn: '1 / -1',
39
- gridTemplateColumns: 'subgrid',
40
- borderBottomWidth: 1,
114
+ gridTemplateColumns: {
115
+ default: 'subgrid',
116
+ [`@container (max-width: ${STACK_BP})`]: '1fr',
117
+ },
118
+ borderBottomWidth: {
119
+ default: 1,
120
+ [`@container (max-width: ${STACK_BP})`]: 0,
121
+ },
41
122
  borderBottomStyle: 'solid',
42
123
  borderBottomColor: colors.border,
43
124
  },
44
125
  bodyRow: {
45
126
  backgroundColor: {
46
127
  default: 'transparent',
128
+ [`@container (max-width: ${STACK_BP})`]: colors.bgCard,
47
129
  ':hover': colors.bgCardHover,
48
130
  },
49
131
  transitionProperty: 'background-color',
50
132
  transitionDuration: '150ms',
51
133
  transitionTimingFunction: 'ease',
134
+ // Stack-mode card chrome
135
+ padding: {
136
+ default: 0,
137
+ [`@container (max-width: ${STACK_BP})`]: spacing.sm,
138
+ },
139
+ borderRadius: {
140
+ default: 0,
141
+ [`@container (max-width: ${STACK_BP})`]: radii.sm,
142
+ },
143
+ borderWidth: {
144
+ default: 0,
145
+ [`@container (max-width: ${STACK_BP})`]: 1,
146
+ },
147
+ borderStyle: 'solid',
148
+ borderColor: colors.border,
52
149
  },
53
150
 
54
- // Header cell
151
+ // Header cell — keeps headers on a single line in compact mode
55
152
  headerCell: {
56
153
  fontWeight: typography.fontWeightSemibold,
57
154
  color: colors.textMuted,
58
155
  textAlign: 'start',
156
+ whiteSpace: {
157
+ default: 'normal',
158
+ [`@container (max-width: ${COMPACT_BP})`]: 'nowrap',
159
+ },
59
160
  },
60
161
 
61
- // Body cell
162
+ // Body cell. In compact mode: nowrap + truncate. In stack mode:
163
+ // becomes a 2-column grid for label / value.
62
164
  cell: {
63
165
  color: colors.text,
64
- display: 'flex',
166
+ display: {
167
+ default: 'flex',
168
+ [`@container (max-width: ${STACK_BP})`]: 'grid',
169
+ },
65
170
  alignItems: 'center',
171
+ minWidth: {
172
+ default: 'auto',
173
+ [`@container (max-width: ${COMPACT_BP})`]: 0,
174
+ },
175
+ overflow: {
176
+ default: 'visible',
177
+ [`@container (max-width: ${COMPACT_BP})`]: 'hidden',
178
+ },
179
+ textOverflow: {
180
+ default: 'clip',
181
+ [`@container (max-width: ${COMPACT_BP})`]: 'ellipsis',
182
+ },
183
+ whiteSpace: {
184
+ default: 'normal',
185
+ [`@container (max-width: ${COMPACT_BP})`]: 'nowrap',
186
+ [`@container (max-width: ${STACK_BP})`]: 'normal',
187
+ },
188
+ gridTemplateColumns: {
189
+ default: null,
190
+ [`@container (max-width: ${STACK_BP})`]: '1fr 2fr',
191
+ },
192
+ gap: {
193
+ default: 0,
194
+ [`@container (max-width: ${STACK_BP})`]: spacing.sm,
195
+ },
196
+ },
197
+
198
+ // Label rendered as a real <span> inside each cell. Display:none in
199
+ // default/compact, shown only in stack — matches StyleX guidance to
200
+ // prefer real elements over ::before/::after for non-shadow-DOM cases.
201
+ cellLabel: {
202
+ display: {
203
+ default: 'none',
204
+ [`@container (max-width: ${STACK_BP})`]: 'block',
205
+ },
206
+ color: colors.textMuted,
207
+ fontWeight: typography.fontWeightMedium,
208
+ fontSize: typography.fontSizeXs,
209
+ },
210
+
211
+ // Actions cell. In default/compact behaves like any cell. In stack
212
+ // mode: full-width footer of the card, right-aligned, with a top
213
+ // separator. Label span is suppressed in JSX (Cell does not render it
214
+ // when isActions=true).
215
+ cellActions: {
216
+ gridTemplateColumns: {
217
+ default: null,
218
+ [`@container (max-width: ${STACK_BP})`]: '1fr',
219
+ },
220
+ justifyContent: {
221
+ default: null,
222
+ [`@container (max-width: ${STACK_BP})`]: 'flex-end',
223
+ },
224
+ marginTop: {
225
+ default: 0,
226
+ [`@container (max-width: ${STACK_BP})`]: spacing.sm,
227
+ },
228
+ paddingTop: {
229
+ default: null,
230
+ [`@container (max-width: ${STACK_BP})`]: spacing.sm,
231
+ },
232
+ borderTopWidth: {
233
+ default: 0,
234
+ [`@container (max-width: ${STACK_BP})`]: 1,
235
+ },
236
+ borderTopStyle: 'solid',
237
+ borderTopColor: colors.border,
66
238
  },
67
239
 
68
240
  // Size: sm
69
241
  cellSm: {
70
- paddingTop: spacing.xs,
71
- paddingBottom: spacing.xs,
242
+ paddingTop: {
243
+ default: spacing.xs,
244
+ [`@container (max-width: ${COMPACT_BP})`]: spacing.xs,
245
+ },
246
+ paddingBottom: {
247
+ default: spacing.xs,
248
+ [`@container (max-width: ${COMPACT_BP})`]: spacing.xs,
249
+ },
72
250
  paddingLeft: spacing.sm,
73
251
  paddingRight: spacing.sm,
74
252
  fontSize: typography.fontSizeXs,
@@ -77,22 +255,33 @@ export const styles = css.create({
77
255
  cellMd: {
78
256
  paddingTop: spacing.sm,
79
257
  paddingBottom: spacing.sm,
80
- paddingLeft: spacing.md,
81
- paddingRight: spacing.md,
258
+ paddingLeft: {
259
+ default: spacing.md,
260
+ [`@container (max-width: ${COMPACT_BP})`]: spacing.sm,
261
+ },
262
+ paddingRight: {
263
+ default: spacing.md,
264
+ [`@container (max-width: ${COMPACT_BP})`]: spacing.sm,
265
+ },
82
266
  fontSize: typography.fontSizeSm,
83
267
  },
84
268
 
85
- // Variant: striped (even body rows)
269
+ // Variant: striped (even body rows). Suppressed in stack — cards have
270
+ // their own surface and zebra-striping reads as a bug there.
86
271
  stripedEven: {
87
272
  backgroundColor: {
88
273
  default: colors.bgCardHover,
274
+ [`@container (max-width: ${STACK_BP})`]: colors.bgCard,
89
275
  ':hover': colors.bgCardHover,
90
276
  },
91
277
  },
92
278
 
93
- // Variant: bordered (cells get side borders)
279
+ // Variant: bordered (cells get side borders). Suppressed in stack.
94
280
  borderedCell: {
95
- borderRightWidth: 1,
281
+ borderRightWidth: {
282
+ default: 1,
283
+ [`@container (max-width: ${STACK_BP})`]: 0,
284
+ },
96
285
  borderRightStyle: 'solid',
97
286
  borderRightColor: colors.border,
98
287
  },
@@ -100,6 +289,15 @@ export const styles = css.create({
100
289
  borderRightWidth: 0,
101
290
  },
102
291
 
292
+ // SortChip — visible only in stack mode
293
+ sortChip: {
294
+ display: {
295
+ default: 'none',
296
+ [`@container (max-width: ${STACK_BP})`]: 'inline-flex',
297
+ },
298
+ alignSelf: 'flex-start',
299
+ },
300
+
103
301
  // Dynamic: grid columns applied on Root
104
302
  gridColumns: (template: string) => ({
105
303
  gridTemplateColumns: template,
@@ -0,0 +1,28 @@
1
+ // Side-effect-only module: registers `@duro-app/ui`'s additions to
2
+ // TanStack's `ColumnMeta` so consumers can write
3
+ // meta: { stackLabel: 'X', isActions: true }
4
+ // on column defs and have it type-check, regardless of whether they
5
+ // import from Table/FromTanstack in that particular file.
6
+ //
7
+ // `index.ts` re-exports this module so the augmentation registers as
8
+ // soon as any file in the consumer's TS program imports from
9
+ // `@duro-app/ui`. Keeping the augmentation in a standalone file (instead
10
+ // of co-located with `FromTanstack.tsx`) also keeps it from being
11
+ // tree-shaken out of the published types.
12
+
13
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
14
+ declare module '@tanstack/react-table' {
15
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
16
+ interface ColumnMeta<TData extends unknown, TValue> {
17
+ /** Override the stack-mode label when columnDef.header is JSX (icon + text, etc). */
18
+ stackLabel?: string
19
+ /**
20
+ * Marks this column as the actions column. In stack mode the cells
21
+ * render as a full-width footer instead of a labelled key/value row.
22
+ */
23
+ isActions?: boolean
24
+ }
25
+ }
26
+
27
+ // Empty export so this file is treated as a module by TS and ts-up/vite.
28
+ export {}
package/src/index.ts CHANGED
@@ -46,6 +46,11 @@ export {
46
46
  export {Tag, type TagVariant, type TagSize} from './components/Tag/Tag'
47
47
  export {TagGroup} from './components/TagGroup/TagGroup'
48
48
  export {Table, type TableVariant, type TableSize} from './components/Table/Table'
49
+ // Side-effect import registers our augmentation of TanStack's ColumnMeta
50
+ // (stackLabel / isActions). Standalone module so the augmentation lands
51
+ // whenever this package is in a consumer's TS program, even if FromTanstack
52
+ // itself isn't imported.
53
+ import './components/Table/tanstack-augmentation'
49
54
  // TanStack Table: consumers install @tanstack/react-table directly and use
50
55
  // Table.Pagination, Table.SortIndicator, Table.ColumnFilter for styled integration
51
56
  export {Tabs} from './components/Tabs/Tabs'