@duro-app/ui 0.44.0 → 0.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Table/FromTanstack.d.ts +4 -1
- package/dist/components/Table/FromTanstack.d.ts.map +1 -1
- package/dist/components/Table/SortChip.d.ts +8 -1
- package/dist/components/Table/SortChip.d.ts.map +1 -1
- package/dist/components/Table/Table.d.ts +9 -1
- package/dist/components/Table/Table.d.ts.map +1 -1
- package/dist/components/Table/Table.stories.d.ts +1 -0
- package/dist/components/Table/Table.stories.d.ts.map +1 -1
- package/dist/components/Table/styles.css.d.ts +41 -0
- package/dist/components/Table/styles.css.d.ts.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.js +1859 -1772
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/Table/FromTanstack.tsx +5 -0
- package/src/components/Table/SortChip.tsx +15 -2
- package/src/components/Table/Table.stories.tsx +68 -1
- package/src/components/Table/Table.tsx +94 -7
- package/src/components/Table/styles.css.ts +57 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@duro-app/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.45.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"@tanstack/react-virtual": "^3.14.6",
|
|
58
|
-
"@duro-app/tokens": "^0.
|
|
58
|
+
"@duro-app/tokens": "^0.45.0"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@babel/preset-typescript": "^7.28.0",
|
|
@@ -22,6 +22,9 @@ interface FromTanstackBaseProps<TData> {
|
|
|
22
22
|
readonly variant?: TableVariant
|
|
23
23
|
readonly size?: TableSize
|
|
24
24
|
readonly responsive?: boolean
|
|
25
|
+
/** Min comfortable column width (px) before the table cards up. Forwarded
|
|
26
|
+
* to Table.Root — see its `minColumnWidth`. Default 128. */
|
|
27
|
+
readonly minColumnWidth?: number
|
|
25
28
|
/** Render a SortChip above the grid (stack-mode only). Options derive from
|
|
26
29
|
* columns where `column.getCanSort()` returns true. Wires value/onChange
|
|
27
30
|
* through the TanStack sorting state automatically. */
|
|
@@ -68,6 +71,7 @@ export function FromTanstack<TData>({
|
|
|
68
71
|
variant = 'default',
|
|
69
72
|
size = 'md',
|
|
70
73
|
responsive = true,
|
|
74
|
+
minColumnWidth,
|
|
71
75
|
sortChip,
|
|
72
76
|
pagination,
|
|
73
77
|
rowKey,
|
|
@@ -82,6 +86,7 @@ export function FromTanstack<TData>({
|
|
|
82
86
|
variant={variant}
|
|
83
87
|
size={size}
|
|
84
88
|
responsive={responsive}
|
|
89
|
+
minColumnWidth={minColumnWidth}
|
|
85
90
|
sortChip={sortChipNode}
|
|
86
91
|
pagination={paginationNode}
|
|
87
92
|
>
|
|
@@ -13,6 +13,13 @@ export interface SortChipProps {
|
|
|
13
13
|
readonly onChange: (next: SortValue | null) => void
|
|
14
14
|
/** Visible label preceding the sort field, e.g. "Sort by". */
|
|
15
15
|
readonly label?: string
|
|
16
|
+
/**
|
|
17
|
+
* Reveal the chip regardless of the container query. Injected by
|
|
18
|
+
* `Table.Root` when it force-stacks a cramped medium-width table (headers
|
|
19
|
+
* hidden but container wider than the mobile breakpoint, so the chip's own
|
|
20
|
+
* `@container` rule hasn't fired). Not part of the public API.
|
|
21
|
+
*/
|
|
22
|
+
readonly forceShow?: boolean
|
|
16
23
|
}
|
|
17
24
|
|
|
18
25
|
/**
|
|
@@ -24,12 +31,18 @@ export interface SortChipProps {
|
|
|
24
31
|
* couple to @tanstack/react-table — the consumer wires `value` and
|
|
25
32
|
* `onChange` from whatever sort state they manage.
|
|
26
33
|
*/
|
|
27
|
-
export function SortChip({
|
|
34
|
+
export function SortChip({
|
|
35
|
+
options,
|
|
36
|
+
value,
|
|
37
|
+
onChange,
|
|
38
|
+
label = 'Sort by',
|
|
39
|
+
forceShow = false,
|
|
40
|
+
}: SortChipProps) {
|
|
28
41
|
const current = value ? options.find((o) => o.id === value.id) : null
|
|
29
42
|
const triggerLabel = current ? `${label}: ${current.label} ${value!.desc ? '↓' : '↑'}` : label
|
|
30
43
|
|
|
31
44
|
return (
|
|
32
|
-
<html.div style={styles.sortChip}>
|
|
45
|
+
<html.div style={[styles.sortChip, forceShow && styles.sortChipStacked]}>
|
|
33
46
|
<Menu.Root>
|
|
34
47
|
<Menu.Trigger>{triggerLabel}</Menu.Trigger>
|
|
35
48
|
<Menu.Popup>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type {Meta, StoryObj} from '@storybook/react'
|
|
2
|
-
import {expect} from 'storybook/test'
|
|
2
|
+
import {expect, waitFor} from 'storybook/test'
|
|
3
3
|
import {css, html} from 'react-strict-dom'
|
|
4
4
|
import {useState} from 'react'
|
|
5
5
|
import {Table} from './Table'
|
|
@@ -1142,6 +1142,10 @@ export const SlotPropsOnRoot: Story = {
|
|
|
1142
1142
|
// grid track into the next column. Cells now shrink to their track and wrap.
|
|
1143
1143
|
const overlapWrapStyles = css.create({
|
|
1144
1144
|
wide: {width: 760},
|
|
1145
|
+
// 6 columns in 560px → each column would get < 128px (the min comfortable
|
|
1146
|
+
// width), well above the 440px mobile breakpoint, so only the JS
|
|
1147
|
+
// ResizeObserver — not the @container query — can catch the cramping.
|
|
1148
|
+
cramped: {width: 560},
|
|
1145
1149
|
})
|
|
1146
1150
|
|
|
1147
1151
|
const LONG_EMAIL = 'a-really-long-service-account.address@subdomain.example-company.com'
|
|
@@ -1191,3 +1195,66 @@ export const ManyColumnsWrapNoOverlap: Story = {
|
|
|
1191
1195
|
await expect(emailCell.scrollWidth).toBeLessThanOrEqual(emailCell.clientWidth + 2)
|
|
1192
1196
|
},
|
|
1193
1197
|
}
|
|
1198
|
+
|
|
1199
|
+
// A dense table wider than the 440px mobile breakpoint but too narrow to give
|
|
1200
|
+
// each column a comfortable width. The @container query never fires (560 >
|
|
1201
|
+
// 440), so this asserts the JS ResizeObserver force-stacks it into cards:
|
|
1202
|
+
// 6 columns × 128px default = 768px threshold > 560px container.
|
|
1203
|
+
export const AutoStacksWhenCramped: Story = {
|
|
1204
|
+
name: 'Auto-stacks when columns get cramped',
|
|
1205
|
+
render: () => (
|
|
1206
|
+
<html.div style={overlapWrapStyles.cramped}>
|
|
1207
|
+
<Table.Root>
|
|
1208
|
+
<Table.Header>
|
|
1209
|
+
<Table.Row>
|
|
1210
|
+
<Table.HeaderCell>Display Name</Table.HeaderCell>
|
|
1211
|
+
<Table.HeaderCell>Type</Table.HeaderCell>
|
|
1212
|
+
<Table.HeaderCell>Email</Table.HeaderCell>
|
|
1213
|
+
<Table.HeaderCell>Status</Table.HeaderCell>
|
|
1214
|
+
<Table.HeaderCell>Owner</Table.HeaderCell>
|
|
1215
|
+
<Table.HeaderCell width="max-content">Actions</Table.HeaderCell>
|
|
1216
|
+
</Table.Row>
|
|
1217
|
+
</Table.Header>
|
|
1218
|
+
<Table.Body>
|
|
1219
|
+
<Table.Row>
|
|
1220
|
+
<Table.Cell>SMTP submission service account</Table.Cell>
|
|
1221
|
+
<Table.Cell>
|
|
1222
|
+
<Badge>User</Badge>
|
|
1223
|
+
</Table.Cell>
|
|
1224
|
+
<Table.Cell>{LONG_EMAIL}</Table.Cell>
|
|
1225
|
+
<Table.Cell>
|
|
1226
|
+
<Badge variant="warning">No certs</Badge>
|
|
1227
|
+
</Table.Cell>
|
|
1228
|
+
<Table.Cell>daddy</Table.Cell>
|
|
1229
|
+
<Table.Cell isActions>
|
|
1230
|
+
<Button size="small">Send Cert</Button>
|
|
1231
|
+
<Button size="small" variant="secondary">
|
|
1232
|
+
Revoke
|
|
1233
|
+
</Button>
|
|
1234
|
+
</Table.Cell>
|
|
1235
|
+
</Table.Row>
|
|
1236
|
+
</Table.Body>
|
|
1237
|
+
</Table.Root>
|
|
1238
|
+
</html.div>
|
|
1239
|
+
),
|
|
1240
|
+
play: async ({canvas}) => {
|
|
1241
|
+
const grid = canvas.getByRole('table')
|
|
1242
|
+
// ResizeObserver fires post-layout; wait for the stack decision to commit.
|
|
1243
|
+
await waitFor(async () => {
|
|
1244
|
+
// The grid collapses to a single column — one resolved track, no spaces.
|
|
1245
|
+
const tracks = getComputedStyle(grid).gridTemplateColumns.trim().split(/\s+/)
|
|
1246
|
+
await expect(tracks.length).toBe(1)
|
|
1247
|
+
})
|
|
1248
|
+
// Header group is display:none, so it drops out of the accessibility tree:
|
|
1249
|
+
// no column headers remain queryable, leaving only the body rowgroup.
|
|
1250
|
+
await expect(canvas.queryAllByRole('columnheader')).toHaveLength(0)
|
|
1251
|
+
await expect(canvas.getAllByRole('rowgroup')).toHaveLength(1)
|
|
1252
|
+
// Each body cell now surfaces its label span (label|value card rows). The
|
|
1253
|
+
// hidden header 'Owner' stays in the DOM, so pick the visible <span>.
|
|
1254
|
+
const ownerLabel = canvas
|
|
1255
|
+
.getAllByText('Owner')
|
|
1256
|
+
.find((el) => el.tagName.toLowerCase() === 'span') as HTMLElement
|
|
1257
|
+
await expect(ownerLabel).toBeTruthy()
|
|
1258
|
+
await expect(getComputedStyle(ownerLabel).display).toBe('block')
|
|
1259
|
+
},
|
|
1260
|
+
}
|
|
@@ -3,8 +3,12 @@ import {
|
|
|
3
3
|
type ReactNode,
|
|
4
4
|
type MutableRefObject,
|
|
5
5
|
createContext,
|
|
6
|
+
cloneElement,
|
|
6
7
|
useContext,
|
|
8
|
+
useEffect,
|
|
9
|
+
useLayoutEffect,
|
|
7
10
|
useRef,
|
|
11
|
+
useState,
|
|
8
12
|
Children,
|
|
9
13
|
isValidElement,
|
|
10
14
|
} from 'react'
|
|
@@ -31,8 +35,20 @@ interface TableContextValue {
|
|
|
31
35
|
labels: ReadonlyArray<string>
|
|
32
36
|
/** Mutable ref: header Row writes inferred template, Root reads it */
|
|
33
37
|
inferredTemplateRef: MutableRefObject<string | null>
|
|
38
|
+
/** JS-measured force-stack flag. True when Root's ResizeObserver finds the
|
|
39
|
+
* container too narrow for its column count (cells would be crushed), so
|
|
40
|
+
* every cell/row/header also applies its `*Stacked` variant. Independent
|
|
41
|
+
* of the @container base, which still handles genuinely narrow widths. */
|
|
42
|
+
stacked: boolean
|
|
34
43
|
}
|
|
35
44
|
|
|
45
|
+
// useLayoutEffect measures + commits the stack decision before the browser
|
|
46
|
+
// paints, so a client-rendered narrow table shows cards on first frame
|
|
47
|
+
// rather than flashing tabular. On the server it would warn (no layout), so
|
|
48
|
+
// fall back to useEffect there — SSR always emits the non-stacked markup and
|
|
49
|
+
// the @container CSS covers true mobile without JS.
|
|
50
|
+
const useIsoLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect
|
|
51
|
+
|
|
36
52
|
const TableContext = createContext<TableContextValue | null>(null)
|
|
37
53
|
|
|
38
54
|
function useTable() {
|
|
@@ -174,6 +190,14 @@ interface RootProps {
|
|
|
174
190
|
size?: TableSize
|
|
175
191
|
/** Opt out of responsive container-query behavior. Default true. */
|
|
176
192
|
responsive?: boolean
|
|
193
|
+
/**
|
|
194
|
+
* Minimum comfortable width (px) for a single column. When responsive,
|
|
195
|
+
* Root measures its container and switches to the stacked card layout as
|
|
196
|
+
* soon as `containerWidth < columnCount × minColumnWidth` — i.e. before
|
|
197
|
+
* cells get crushed, not only at the fixed mobile breakpoint. Raise it to
|
|
198
|
+
* card up sooner, lower it to keep the grid at tighter widths. Default 128.
|
|
199
|
+
*/
|
|
200
|
+
minColumnWidth?: number
|
|
177
201
|
/**
|
|
178
202
|
* Optional sort UI rendered above the grid. Visible only in stack mode
|
|
179
203
|
* (SortChip carries its own `display: none → inline-flex` rule). Typical
|
|
@@ -192,6 +216,7 @@ export function Root({
|
|
|
192
216
|
variant = 'default',
|
|
193
217
|
size = 'md',
|
|
194
218
|
responsive = true,
|
|
219
|
+
minColumnWidth = 128,
|
|
195
220
|
sortChip,
|
|
196
221
|
pagination,
|
|
197
222
|
}: RootProps) {
|
|
@@ -201,6 +226,35 @@ export function Root({
|
|
|
201
226
|
inferredTemplateRef.current = template
|
|
202
227
|
}
|
|
203
228
|
|
|
229
|
+
// Content-aware stacking. A container query can't see the column count, so
|
|
230
|
+
// measure the container and card up as soon as each column would fall
|
|
231
|
+
// below `minColumnWidth`. Keyed on labels.length so it tracks the actual
|
|
232
|
+
// number of columns. The @container base (styles.*: STACK_BP) still fires
|
|
233
|
+
// independently for genuinely narrow widths without needing JS.
|
|
234
|
+
const columnCount = labels.length
|
|
235
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
236
|
+
const [stacked, setStacked] = useState(false)
|
|
237
|
+
|
|
238
|
+
useIsoLayoutEffect(() => {
|
|
239
|
+
if (!responsive || columnCount === 0) {
|
|
240
|
+
setStacked(false)
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
const el = containerRef.current
|
|
244
|
+
if (!el || typeof ResizeObserver === 'undefined') return
|
|
245
|
+
const threshold = columnCount * minColumnWidth
|
|
246
|
+
const apply = (width: number) => setStacked(width > 0 && width < threshold)
|
|
247
|
+
apply(el.getBoundingClientRect().width)
|
|
248
|
+
const ro = new ResizeObserver((entries) => {
|
|
249
|
+
const entry = entries[0]
|
|
250
|
+
if (!entry) return
|
|
251
|
+
const box = entry.contentBoxSize?.[0]
|
|
252
|
+
apply(box ? box.inlineSize : entry.contentRect.width)
|
|
253
|
+
})
|
|
254
|
+
ro.observe(el)
|
|
255
|
+
return () => ro.disconnect()
|
|
256
|
+
}, [responsive, columnCount, minColumnWidth])
|
|
257
|
+
|
|
204
258
|
const grid = (
|
|
205
259
|
<html.div
|
|
206
260
|
role="table"
|
|
@@ -218,26 +272,45 @@ export function Root({
|
|
|
218
272
|
: styles.gridColumns(template)
|
|
219
273
|
: undefined,
|
|
220
274
|
responsive && styles.rootResponsive,
|
|
275
|
+
// Force-stack override — collapses the grid to one column. Applied
|
|
276
|
+
// last so its gridTemplateColumns wins over gridColumnsResponsive.
|
|
277
|
+
responsive && stacked && styles.rootStacked,
|
|
221
278
|
]}
|
|
222
279
|
>
|
|
223
280
|
{children}
|
|
224
281
|
</html.div>
|
|
225
282
|
)
|
|
226
283
|
|
|
284
|
+
// The sort chip is normally revealed only by the @container query. When
|
|
285
|
+
// JS force-stacks (headers hidden, but container wider than STACK_BP) the
|
|
286
|
+
// chip's own query hasn't fired, so inject forceShow to reveal it.
|
|
287
|
+
const sortChipEl =
|
|
288
|
+
stacked && isValidElement(sortChip)
|
|
289
|
+
? cloneElement(sortChip as ReactElement<{forceShow?: boolean}>, {forceShow: true})
|
|
290
|
+
: sortChip
|
|
291
|
+
|
|
227
292
|
// Slots render unconditionally — `responsive=false` still wants its sort/
|
|
228
293
|
// pagination chrome. Only the containerType:inline-size wrapper is
|
|
229
294
|
// conditional, since it only matters when @container queries fire.
|
|
230
295
|
const body = (
|
|
231
296
|
<>
|
|
232
|
-
{
|
|
297
|
+
{sortChipEl}
|
|
233
298
|
{grid}
|
|
234
299
|
{pagination}
|
|
235
300
|
</>
|
|
236
301
|
)
|
|
237
302
|
|
|
238
303
|
return (
|
|
239
|
-
<TableContext.Provider
|
|
240
|
-
{responsive
|
|
304
|
+
<TableContext.Provider
|
|
305
|
+
value={{variant, size, responsive, labels, inferredTemplateRef, stacked}}
|
|
306
|
+
>
|
|
307
|
+
{responsive ? (
|
|
308
|
+
<html.div ref={containerRef} style={styles.rootContainer}>
|
|
309
|
+
{body}
|
|
310
|
+
</html.div>
|
|
311
|
+
) : (
|
|
312
|
+
body
|
|
313
|
+
)}
|
|
241
314
|
</TableContext.Provider>
|
|
242
315
|
)
|
|
243
316
|
}
|
|
@@ -245,9 +318,10 @@ export function Root({
|
|
|
245
318
|
// --- Header ---
|
|
246
319
|
|
|
247
320
|
export function Header({children}: {children: ReactNode}) {
|
|
321
|
+
const {stacked} = useTable()
|
|
248
322
|
return (
|
|
249
323
|
<HeaderContext.Provider value={true}>
|
|
250
|
-
<html.div role="rowgroup" style={styles.header}>
|
|
324
|
+
<html.div role="rowgroup" style={[styles.header, stacked && styles.headerStacked]}>
|
|
251
325
|
{children}
|
|
252
326
|
</html.div>
|
|
253
327
|
</HeaderContext.Provider>
|
|
@@ -311,7 +385,7 @@ const INTERACTIVE_SELECTOR =
|
|
|
311
385
|
'button, a, input, select, textarea, [role="button"], [role="link"], [role="checkbox"], [role="menuitem"], [role="switch"], [role="tab"], [contenteditable="true"]'
|
|
312
386
|
|
|
313
387
|
export function Row({children, onClick, 'aria-label': ariaLabel}: RowProps) {
|
|
314
|
-
const {variant} = useTable()
|
|
388
|
+
const {variant, stacked} = useTable()
|
|
315
389
|
const isHeader = useContext(HeaderContext)
|
|
316
390
|
const rowIndex = useContext(RowIndexContext)
|
|
317
391
|
const isEvenRow = rowIndex >= 0 && rowIndex % 2 === 1
|
|
@@ -353,6 +427,9 @@ export function Row({children, onClick, 'aria-label': ariaLabel}: RowProps) {
|
|
|
353
427
|
!isHeader && styles.bodyRow,
|
|
354
428
|
!isHeader && variant === 'striped' && isEvenRow && styles.stripedEven,
|
|
355
429
|
isClickable && styles.clickableRow,
|
|
430
|
+
stacked && styles.rowStacked,
|
|
431
|
+
// bodyRowStacked last so its card background wins over stripedEven.
|
|
432
|
+
!isHeader && stacked && styles.bodyRowStacked,
|
|
356
433
|
]}
|
|
357
434
|
>
|
|
358
435
|
{childArray.map((child, index) => (
|
|
@@ -441,7 +518,7 @@ HeaderCell.displayName = 'HeaderCell'
|
|
|
441
518
|
// isActions=true — actions footers don't show a label.
|
|
442
519
|
|
|
443
520
|
export function Cell({children, isActions}: {children: ReactNode; isActions?: boolean}) {
|
|
444
|
-
const {size, variant, labels, responsive} = useTable()
|
|
521
|
+
const {size, variant, labels, responsive, stacked} = useTable()
|
|
445
522
|
const {index, total} = useContext(CellIndexContext)
|
|
446
523
|
const isLast = variant === 'bordered' && index === total - 1
|
|
447
524
|
const label = labels[index] ?? ''
|
|
@@ -455,10 +532,20 @@ export function Cell({children, isActions}: {children: ReactNode; isActions?: bo
|
|
|
455
532
|
variant === 'bordered' && styles.borderedCell,
|
|
456
533
|
isLast && styles.borderedCellLast,
|
|
457
534
|
isActions && styles.cellActions,
|
|
535
|
+
// Force-stack: cells become a label|value grid; the actions cell then
|
|
536
|
+
// overrides its template to a single track and turns into the card's
|
|
537
|
+
// right-aligned footer. Mirrors the @container (STACK_BP) branch, so
|
|
538
|
+
// cellStacked applies for every cell and cellActionsStacked layers on
|
|
539
|
+
// top for actions (its gridTemplateColumns wins as the later entry).
|
|
540
|
+
stacked && styles.cellStacked,
|
|
541
|
+
isActions && stacked && styles.cellActionsStacked,
|
|
542
|
+
variant === 'bordered' && stacked && styles.borderedCellStacked,
|
|
458
543
|
]}
|
|
459
544
|
>
|
|
460
545
|
{responsive && !isActions && label !== '' ? (
|
|
461
|
-
<html.span style={styles.cellLabel}>
|
|
546
|
+
<html.span style={[styles.cellLabel, stacked && styles.cellLabelStacked]}>
|
|
547
|
+
{label}
|
|
548
|
+
</html.span>
|
|
462
549
|
) : null}
|
|
463
550
|
{isActions ? children : <html.div style={styles.cellValue}>{children}</html.div>}
|
|
464
551
|
</html.div>
|
|
@@ -320,6 +320,63 @@ export const styles = css.create({
|
|
|
320
320
|
alignSelf: 'flex-start',
|
|
321
321
|
},
|
|
322
322
|
|
|
323
|
+
// --- JS-measured force-stack variants ---
|
|
324
|
+
//
|
|
325
|
+
// A container query can only test the container's WIDTH — it can't know
|
|
326
|
+
// "6 columns won't fit here". So a dense table can be wider than STACK_BP
|
|
327
|
+
// yet still crush every cell to a few characters. Table.Root measures the
|
|
328
|
+
// container with a ResizeObserver and, when `width < columnCount ×
|
|
329
|
+
// minColumnWidth`, sets `stacked` in context; each component then also
|
|
330
|
+
// applies its `*Stacked` variant here. These mirror the values in the
|
|
331
|
+
// `@container (max-width: ${STACK_BP})` branches above, so JS-forced and
|
|
332
|
+
// CSS-driven stacking render identically. The @container base is kept as
|
|
333
|
+
// the SSR-safe path for genuinely narrow (mobile) widths — no JS, no
|
|
334
|
+
// hydration flash — while these handle the cramped medium-width case.
|
|
335
|
+
rootStacked: {
|
|
336
|
+
gridTemplateColumns: '1fr',
|
|
337
|
+
borderWidth: 0,
|
|
338
|
+
backgroundColor: 'transparent',
|
|
339
|
+
overflow: 'visible',
|
|
340
|
+
rowGap: spacing.sm,
|
|
341
|
+
},
|
|
342
|
+
headerStacked: {
|
|
343
|
+
display: 'none',
|
|
344
|
+
},
|
|
345
|
+
rowStacked: {
|
|
346
|
+
gridTemplateColumns: '1fr',
|
|
347
|
+
borderBottomWidth: 0,
|
|
348
|
+
},
|
|
349
|
+
bodyRowStacked: {
|
|
350
|
+
backgroundColor: {
|
|
351
|
+
default: colors.bgCard,
|
|
352
|
+
':hover': colors.bgCardHover,
|
|
353
|
+
},
|
|
354
|
+
padding: spacing.sm,
|
|
355
|
+
borderRadius: radii.sm,
|
|
356
|
+
borderWidth: 1,
|
|
357
|
+
},
|
|
358
|
+
cellStacked: {
|
|
359
|
+
display: 'grid',
|
|
360
|
+
gridTemplateColumns: '1fr 2fr',
|
|
361
|
+
gap: spacing.sm,
|
|
362
|
+
},
|
|
363
|
+
cellLabelStacked: {
|
|
364
|
+
display: 'block',
|
|
365
|
+
},
|
|
366
|
+
cellActionsStacked: {
|
|
367
|
+
gridTemplateColumns: '1fr',
|
|
368
|
+
justifyContent: 'flex-end',
|
|
369
|
+
marginTop: spacing.sm,
|
|
370
|
+
paddingTop: spacing.sm,
|
|
371
|
+
borderTopWidth: 1,
|
|
372
|
+
},
|
|
373
|
+
borderedCellStacked: {
|
|
374
|
+
borderRightWidth: 0,
|
|
375
|
+
},
|
|
376
|
+
sortChipStacked: {
|
|
377
|
+
display: 'inline-flex',
|
|
378
|
+
},
|
|
379
|
+
|
|
323
380
|
// Dynamic: grid columns applied on Root (non-responsive path only).
|
|
324
381
|
gridColumns: (template: string) => ({
|
|
325
382
|
gridTemplateColumns: template,
|