@duro-app/ui 0.46.0 → 0.47.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 +1 -8
- package/dist/components/Table/SortChip.d.ts.map +1 -1
- package/dist/components/Table/Table.d.ts +10 -2
- package/dist/components/Table/Table.d.ts.map +1 -1
- package/dist/components/Table/Table.stories.d.ts +3 -3
- package/dist/components/Table/Table.stories.d.ts.map +1 -1
- package/dist/components/Table/styles.css.d.ts +5 -40
- package/dist/components/Table/styles.css.d.ts.map +1 -1
- package/dist/components/VirtualTable/VirtualTable.d.ts +4 -5
- package/dist/components/VirtualTable/VirtualTable.d.ts.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.js +2342 -2431
- 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 +2 -15
- package/src/components/Table/Table.stories.tsx +91 -132
- package/src/components/Table/Table.tsx +38 -151
- package/src/components/Table/styles.css.ts +38 -65
- package/src/components/VirtualTable/VirtualTable.tsx +6 -6
|
@@ -3,12 +3,8 @@ import {
|
|
|
3
3
|
type ReactNode,
|
|
4
4
|
type MutableRefObject,
|
|
5
5
|
createContext,
|
|
6
|
-
cloneElement,
|
|
7
6
|
useContext,
|
|
8
|
-
useEffect,
|
|
9
|
-
useLayoutEffect,
|
|
10
7
|
useRef,
|
|
11
|
-
useState,
|
|
12
8
|
Children,
|
|
13
9
|
isValidElement,
|
|
14
10
|
} from 'react'
|
|
@@ -35,43 +31,6 @@ interface TableContextValue {
|
|
|
35
31
|
labels: ReadonlyArray<string>
|
|
36
32
|
/** Mutable ref: header Row writes inferred template, Root reads it */
|
|
37
33
|
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
|
|
43
|
-
}
|
|
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
|
-
|
|
52
|
-
// Does any column's content no longer fit its box? We sample the header row
|
|
53
|
-
// plus the first body row (bounded cost). Free text wraps rather than
|
|
54
|
-
// overflowing (min-width:0 + overflow-wrap), so this only trips on content
|
|
55
|
-
// that genuinely can't shrink — action buttons, badges, or nowrap header
|
|
56
|
-
// labels — i.e. the real "columns won't fit" signal, independent of how many
|
|
57
|
-
// columns there are. Structural overflow is consistent down a column, so the
|
|
58
|
-
// first body row is a faithful sample.
|
|
59
|
-
function gridOverflows(grid: HTMLDivElement | null): boolean {
|
|
60
|
-
if (!grid) return false
|
|
61
|
-
const TOL = 2 // sub-pixel layout rounding slack
|
|
62
|
-
const over = (el: Element) => el.scrollWidth - el.clientWidth > TOL
|
|
63
|
-
for (const h of grid.querySelectorAll('[role="columnheader"]')) {
|
|
64
|
-
if (over(h)) return true
|
|
65
|
-
}
|
|
66
|
-
const rowgroups = grid.querySelectorAll('[role="rowgroup"]')
|
|
67
|
-
const body = rowgroups[rowgroups.length - 1] // header is first, body last
|
|
68
|
-
const firstRow = body?.querySelector('[role="row"]')
|
|
69
|
-
if (firstRow) {
|
|
70
|
-
for (const c of firstRow.querySelectorAll('[role="cell"]')) {
|
|
71
|
-
if (over(c)) return true
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
return false
|
|
75
34
|
}
|
|
76
35
|
|
|
77
36
|
const TableContext = createContext<TableContextValue | null>(null)
|
|
@@ -148,7 +107,10 @@ function unwrapElementType(t: unknown): unknown {
|
|
|
148
107
|
return current
|
|
149
108
|
}
|
|
150
109
|
|
|
151
|
-
function extractColumnMeta(
|
|
110
|
+
function extractColumnMeta(
|
|
111
|
+
children: ReactNode,
|
|
112
|
+
minColumnWidth: number,
|
|
113
|
+
): {
|
|
152
114
|
template: string
|
|
153
115
|
compactTemplate: string
|
|
154
116
|
labels: string[]
|
|
@@ -167,14 +129,14 @@ function extractColumnMeta(children: ReactNode): {
|
|
|
167
129
|
(unwrappedType as {name?: string} | null)?.name ||
|
|
168
130
|
''
|
|
169
131
|
if (unwrappedType === HeaderCell || displayName === 'HeaderCell') {
|
|
170
|
-
// Default to minmax(
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
const width = props.width ||
|
|
132
|
+
// Default each column to `minmax(<minColumnWidth>px, 1fr)`: columns
|
|
133
|
+
// share the row evenly (1fr) but never shrink below a readable floor.
|
|
134
|
+
// That floor is what makes a dense table overflow its container
|
|
135
|
+
// instead of crushing — Root's scroll wrapper then scrolls sideways.
|
|
136
|
+
// Narrow columns (a checkbox) or naturally-wide ones (an actions
|
|
137
|
+
// cell) override this with an explicit `width` (e.g. '40px',
|
|
138
|
+
// 'max-content').
|
|
139
|
+
const width = props.width || `minmax(${minColumnWidth}px, 1fr)`
|
|
178
140
|
widths.push(width)
|
|
179
141
|
// `compactWidth` lets the consumer switch to a content-aware
|
|
180
142
|
// layout in the compact band while staying evenly distributed
|
|
@@ -213,8 +175,16 @@ interface RootProps {
|
|
|
213
175
|
children: ReactNode
|
|
214
176
|
variant?: TableVariant
|
|
215
177
|
size?: TableSize
|
|
216
|
-
/** Opt out of responsive behavior (
|
|
178
|
+
/** Opt out of responsive behavior (horizontal scroll + card breakpoint). Default true. */
|
|
217
179
|
responsive?: boolean
|
|
180
|
+
/**
|
|
181
|
+
* Minimum width (px) each flexible column keeps before the table scrolls
|
|
182
|
+
* horizontally rather than crushing its cells. Columns still share the row
|
|
183
|
+
* evenly (`minmax(<minColumnWidth>px, 1fr)`); this is only the floor. Raise
|
|
184
|
+
* it for roomier columns (more scrolling), lower it to fit more before
|
|
185
|
+
* scrolling. Columns with an explicit `width` are unaffected. Default 120.
|
|
186
|
+
*/
|
|
187
|
+
minColumnWidth?: number
|
|
218
188
|
/**
|
|
219
189
|
* Optional sort UI rendered above the grid. Visible only in stack mode
|
|
220
190
|
* (SortChip carries its own `display: none → inline-flex` rule). Typical
|
|
@@ -233,76 +203,19 @@ export function Root({
|
|
|
233
203
|
variant = 'default',
|
|
234
204
|
size = 'md',
|
|
235
205
|
responsive = true,
|
|
206
|
+
minColumnWidth = 120,
|
|
236
207
|
sortChip,
|
|
237
208
|
pagination,
|
|
238
209
|
}: RootProps) {
|
|
239
210
|
const inferredTemplateRef = useRef<string | null>(null)
|
|
240
|
-
const {template, compactTemplate, labels} = extractColumnMeta(children)
|
|
211
|
+
const {template, compactTemplate, labels} = extractColumnMeta(children, minColumnWidth)
|
|
241
212
|
if (template) {
|
|
242
213
|
inferredTemplateRef.current = template
|
|
243
214
|
}
|
|
244
215
|
|
|
245
|
-
// Content-aware stacking driven by ACTUAL overflow, not a width heuristic.
|
|
246
|
-
// Column count alone is a poor proxy — a table with 8 short-text columns
|
|
247
|
-
// fits fine on a laptop, while 4 columns of buttons + long emails can be
|
|
248
|
-
// crushed. So instead of guessing, we render tabular and check whether any
|
|
249
|
-
// cell's content actually overflows its box (scrollWidth > clientWidth);
|
|
250
|
-
// when it does, the columns can't fit and we card up. The @container base
|
|
251
|
-
// (styles.*: STACK_BP) still stacks true-mobile widths without JS.
|
|
252
|
-
//
|
|
253
|
-
// Oscillation guard: stacking changes the layout (cards never overflow), so
|
|
254
|
-
// we can't re-measure overflow while stacked. We latch the container width
|
|
255
|
-
// at the moment we stacked and only return to tabular once the container
|
|
256
|
-
// has grown comfortably past it (hysteresis), then re-check overflow fresh.
|
|
257
|
-
const containerRef = useRef<HTMLDivElement>(null)
|
|
258
|
-
const gridRef = useRef<HTMLDivElement>(null)
|
|
259
|
-
const [stacked, setStacked] = useState(false)
|
|
260
|
-
const stackedAtWidthRef = useRef(0)
|
|
261
|
-
|
|
262
|
-
// Re-runs whenever `stacked` flips: after we drop back to tabular the
|
|
263
|
-
// container width is unchanged, so the ResizeObserver won't fire — the
|
|
264
|
-
// effect re-running is what re-checks overflow in the fresh tabular layout.
|
|
265
|
-
useIsoLayoutEffect(() => {
|
|
266
|
-
if (!responsive) {
|
|
267
|
-
setStacked(false)
|
|
268
|
-
return
|
|
269
|
-
}
|
|
270
|
-
const el = containerRef.current
|
|
271
|
-
if (!el || typeof ResizeObserver === 'undefined') return
|
|
272
|
-
|
|
273
|
-
// Grows-back margin: only leave stack mode once we're well clear of the
|
|
274
|
-
// width that triggered it, so a 1px jiggle at the boundary can't flip-flop.
|
|
275
|
-
const HYSTERESIS = 48
|
|
276
|
-
|
|
277
|
-
const measure = (width: number) => {
|
|
278
|
-
if (width <= 0) return
|
|
279
|
-
if (!stacked) {
|
|
280
|
-
if (gridOverflows(gridRef.current)) {
|
|
281
|
-
stackedAtWidthRef.current = width
|
|
282
|
-
setStacked(true)
|
|
283
|
-
}
|
|
284
|
-
} else if (width > stackedAtWidthRef.current + HYSTERESIS) {
|
|
285
|
-
// Drop back to tabular; this effect re-runs and re-checks overflow.
|
|
286
|
-
setStacked(false)
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
measure(el.getBoundingClientRect().width)
|
|
291
|
-
const ro = new ResizeObserver((entries) => {
|
|
292
|
-
const entry = entries[0]
|
|
293
|
-
if (!entry) return
|
|
294
|
-
const box = entry.contentBoxSize?.[0]
|
|
295
|
-
measure(box ? box.inlineSize : entry.contentRect.width)
|
|
296
|
-
})
|
|
297
|
-
ro.observe(el)
|
|
298
|
-
return () => ro.disconnect()
|
|
299
|
-
// labels.length re-runs detection when the column set changes.
|
|
300
|
-
}, [responsive, labels.length, stacked])
|
|
301
|
-
|
|
302
216
|
const grid = (
|
|
303
217
|
<html.div
|
|
304
218
|
role="table"
|
|
305
|
-
ref={gridRef}
|
|
306
219
|
style={[
|
|
307
220
|
styles.root,
|
|
308
221
|
// When responsive, use gridColumnsResponsive so the explicit column
|
|
@@ -317,45 +230,33 @@ export function Root({
|
|
|
317
230
|
: styles.gridColumns(template)
|
|
318
231
|
: undefined,
|
|
319
232
|
responsive && styles.rootResponsive,
|
|
320
|
-
// Force-stack override — collapses the grid to one column. Applied
|
|
321
|
-
// last so its gridTemplateColumns wins over gridColumnsResponsive.
|
|
322
|
-
responsive && stacked && styles.rootStacked,
|
|
323
233
|
]}
|
|
324
234
|
>
|
|
325
235
|
{children}
|
|
326
236
|
</html.div>
|
|
327
237
|
)
|
|
328
238
|
|
|
329
|
-
//
|
|
330
|
-
//
|
|
331
|
-
//
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
? cloneElement(sortChip as ReactElement<{forceShow?: boolean}>, {forceShow: true})
|
|
335
|
-
: sortChip
|
|
239
|
+
// Above the stack breakpoint the grid keeps each column ≥ minColumnWidth, so
|
|
240
|
+
// a dense table overflows this wrapper and scrolls sideways instead of
|
|
241
|
+
// crushing. Below it (@container ≤ sm) the grid is one column and there's
|
|
242
|
+
// nothing to scroll. Non-responsive tables opt out of the frame entirely.
|
|
243
|
+
const framedGrid = responsive ? <html.div style={styles.scrollX}>{grid}</html.div> : grid
|
|
336
244
|
|
|
337
245
|
// Slots render unconditionally — `responsive=false` still wants its sort/
|
|
338
246
|
// pagination chrome. Only the containerType:inline-size wrapper is
|
|
339
|
-
// conditional, since it only matters when @container queries fire.
|
|
247
|
+
// conditional, since it only matters when @container queries fire. SortChip
|
|
248
|
+
// reveals itself in card mode via its own @container rule — no JS needed.
|
|
340
249
|
const body = (
|
|
341
250
|
<>
|
|
342
|
-
{
|
|
343
|
-
{
|
|
251
|
+
{sortChip}
|
|
252
|
+
{framedGrid}
|
|
344
253
|
{pagination}
|
|
345
254
|
</>
|
|
346
255
|
)
|
|
347
256
|
|
|
348
257
|
return (
|
|
349
|
-
<TableContext.Provider
|
|
350
|
-
|
|
351
|
-
>
|
|
352
|
-
{responsive ? (
|
|
353
|
-
<html.div ref={containerRef} style={styles.rootContainer}>
|
|
354
|
-
{body}
|
|
355
|
-
</html.div>
|
|
356
|
-
) : (
|
|
357
|
-
body
|
|
358
|
-
)}
|
|
258
|
+
<TableContext.Provider value={{variant, size, responsive, labels, inferredTemplateRef}}>
|
|
259
|
+
{responsive ? <html.div style={styles.rootContainer}>{body}</html.div> : body}
|
|
359
260
|
</TableContext.Provider>
|
|
360
261
|
)
|
|
361
262
|
}
|
|
@@ -363,10 +264,9 @@ export function Root({
|
|
|
363
264
|
// --- Header ---
|
|
364
265
|
|
|
365
266
|
export function Header({children}: {children: ReactNode}) {
|
|
366
|
-
const {stacked} = useTable()
|
|
367
267
|
return (
|
|
368
268
|
<HeaderContext.Provider value={true}>
|
|
369
|
-
<html.div role="rowgroup" style={
|
|
269
|
+
<html.div role="rowgroup" style={styles.header}>
|
|
370
270
|
{children}
|
|
371
271
|
</html.div>
|
|
372
272
|
</HeaderContext.Provider>
|
|
@@ -430,7 +330,7 @@ const INTERACTIVE_SELECTOR =
|
|
|
430
330
|
'button, a, input, select, textarea, [role="button"], [role="link"], [role="checkbox"], [role="menuitem"], [role="switch"], [role="tab"], [contenteditable="true"]'
|
|
431
331
|
|
|
432
332
|
export function Row({children, onClick, 'aria-label': ariaLabel}: RowProps) {
|
|
433
|
-
const {variant
|
|
333
|
+
const {variant} = useTable()
|
|
434
334
|
const isHeader = useContext(HeaderContext)
|
|
435
335
|
const rowIndex = useContext(RowIndexContext)
|
|
436
336
|
const isEvenRow = rowIndex >= 0 && rowIndex % 2 === 1
|
|
@@ -472,9 +372,6 @@ export function Row({children, onClick, 'aria-label': ariaLabel}: RowProps) {
|
|
|
472
372
|
!isHeader && styles.bodyRow,
|
|
473
373
|
!isHeader && variant === 'striped' && isEvenRow && styles.stripedEven,
|
|
474
374
|
isClickable && styles.clickableRow,
|
|
475
|
-
stacked && styles.rowStacked,
|
|
476
|
-
// bodyRowStacked last so its card background wins over stripedEven.
|
|
477
|
-
!isHeader && stacked && styles.bodyRowStacked,
|
|
478
375
|
]}
|
|
479
376
|
>
|
|
480
377
|
{childArray.map((child, index) => (
|
|
@@ -563,7 +460,7 @@ HeaderCell.displayName = 'HeaderCell'
|
|
|
563
460
|
// isActions=true — actions footers don't show a label.
|
|
564
461
|
|
|
565
462
|
export function Cell({children, isActions}: {children: ReactNode; isActions?: boolean}) {
|
|
566
|
-
const {size, variant, labels, responsive
|
|
463
|
+
const {size, variant, labels, responsive} = useTable()
|
|
567
464
|
const {index, total} = useContext(CellIndexContext)
|
|
568
465
|
const isLast = variant === 'bordered' && index === total - 1
|
|
569
466
|
const label = labels[index] ?? ''
|
|
@@ -577,20 +474,10 @@ export function Cell({children, isActions}: {children: ReactNode; isActions?: bo
|
|
|
577
474
|
variant === 'bordered' && styles.borderedCell,
|
|
578
475
|
isLast && styles.borderedCellLast,
|
|
579
476
|
isActions && styles.cellActions,
|
|
580
|
-
// Force-stack: cells become a label|value grid; the actions cell then
|
|
581
|
-
// overrides its template to a single track and turns into the card's
|
|
582
|
-
// right-aligned footer. Mirrors the @container (STACK_BP) branch, so
|
|
583
|
-
// cellStacked applies for every cell and cellActionsStacked layers on
|
|
584
|
-
// top for actions (its gridTemplateColumns wins as the later entry).
|
|
585
|
-
stacked && styles.cellStacked,
|
|
586
|
-
isActions && stacked && styles.cellActionsStacked,
|
|
587
|
-
variant === 'bordered' && stacked && styles.borderedCellStacked,
|
|
588
477
|
]}
|
|
589
478
|
>
|
|
590
479
|
{responsive && !isActions && label !== '' ? (
|
|
591
|
-
<html.span style={
|
|
592
|
-
{label}
|
|
593
|
-
</html.span>
|
|
480
|
+
<html.span style={styles.cellLabel}>{label}</html.span>
|
|
594
481
|
) : null}
|
|
595
482
|
{isActions ? children : <html.div style={styles.cellValue}>{children}</html.div>}
|
|
596
483
|
</html.div>
|
|
@@ -3,13 +3,15 @@ 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
|
import {duration, easing} from '@duro-app/tokens/tokens/motion.css'
|
|
6
|
+
import {breakpoints} from '@duro-app/tokens/tokens/breakpoints.css'
|
|
6
7
|
|
|
7
|
-
// Container-query thresholds
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
const
|
|
8
|
+
// Container-query thresholds, from the shared breakpoint scale. `sm` (640px)
|
|
9
|
+
// is the single "card up" line used across every table (Table + VirtualTable);
|
|
10
|
+
// below it dense tables become cards, above it they stay tabular and scroll
|
|
11
|
+
// horizontally. `md` (768px) is the compact band (tighter padding, nowrap
|
|
12
|
+
// headers). These are inlined into the @container query text at build time.
|
|
13
|
+
const COMPACT_BP = breakpoints.md
|
|
14
|
+
const STACK_BP = breakpoints.sm
|
|
13
15
|
|
|
14
16
|
export const styles = css.create({
|
|
15
17
|
// Outer wrapper that hosts the @container query. Wraps SortChip + Root
|
|
@@ -68,6 +70,16 @@ export const styles = css.create({
|
|
|
68
70
|
// always wins, so combining gridColumns + rootResponsive in one array would
|
|
69
71
|
// silently drop the column template).
|
|
70
72
|
rootResponsive: {
|
|
73
|
+
// Size the grid to its content so it can OVERFLOW the scroll wrapper
|
|
74
|
+
// rather than shrink: `min-content` here is the sum of each column's
|
|
75
|
+
// minimum track (the minmax() floor / its content), so a dense table
|
|
76
|
+
// grows past the container and Root's `scrollX` wrapper scrolls sideways.
|
|
77
|
+
// In stack mode the grid is a single column, so drop the floor to let the
|
|
78
|
+
// card fill the width instead of forcing a horizontal scrollbar.
|
|
79
|
+
minWidth: {
|
|
80
|
+
default: 'min-content',
|
|
81
|
+
[`@container (max-width: ${STACK_BP})`]: 0,
|
|
82
|
+
},
|
|
71
83
|
// The card-list look needs the outer border to disappear in stack
|
|
72
84
|
// mode — each row paints its own border.
|
|
73
85
|
borderWidth: {
|
|
@@ -320,61 +332,19 @@ export const styles = css.create({
|
|
|
320
332
|
alignSelf: 'flex-start',
|
|
321
333
|
},
|
|
322
334
|
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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',
|
|
335
|
+
// Horizontal-scroll frame around the grid (responsive path). Above the
|
|
336
|
+
// stack breakpoint the grid keeps each column at least `minColumnWidth`
|
|
337
|
+
// wide (see the minmax() template), so a dense table overflows and this
|
|
338
|
+
// wrapper scrolls sideways instead of crushing its cells — the standard
|
|
339
|
+
// data-table behavior. In stack mode the grid collapses to one column, so
|
|
340
|
+
// nothing overflows and no scrollbar appears.
|
|
341
|
+
scrollX: {
|
|
342
|
+
overflowX: 'auto',
|
|
343
|
+
// Contain the scroll to this axis; vertical growth (rows) is natural.
|
|
344
|
+
overflowY: 'visible',
|
|
345
|
+
// Momentum scroll on touch + a stable gutter so the layout doesn't jump
|
|
346
|
+
// when the scrollbar shows.
|
|
347
|
+
WebkitOverflowScrolling: 'touch',
|
|
378
348
|
},
|
|
379
349
|
|
|
380
350
|
// Dynamic: grid columns applied on Root (non-responsive path only).
|
|
@@ -389,15 +359,18 @@ export const styles = css.create({
|
|
|
389
359
|
// earlier classes (the LAST style wins per property key).
|
|
390
360
|
//
|
|
391
361
|
// - default: balanced multi-column layout for desktop widths
|
|
392
|
-
// -
|
|
393
|
-
// template
|
|
394
|
-
//
|
|
362
|
+
// - compact band (STACK_BP < width ≤ COMPACT_BP): consumer-supplied
|
|
363
|
+
// "compact" template. This is written as an explicit RANGE, not just
|
|
364
|
+
// `max-width: COMPACT_BP`, so it does NOT also match below STACK_BP —
|
|
365
|
+
// otherwise both the compact and the stack rule would set
|
|
366
|
+
// gridTemplateColumns at card widths and StyleX's ordering could let the
|
|
367
|
+
// (wider, 4-column) compact template shadow the single-column stack one.
|
|
395
368
|
// - @container (max-width: STACK_BP): collapse to a single column so
|
|
396
369
|
// each row becomes a card
|
|
397
370
|
gridColumnsResponsive: (template: string, compactTemplate: string) => ({
|
|
398
371
|
gridTemplateColumns: {
|
|
399
372
|
default: template,
|
|
400
|
-
[`@container (
|
|
373
|
+
[`@container (${STACK_BP} < width <= ${COMPACT_BP})`]: compactTemplate,
|
|
401
374
|
[`@container (max-width: ${STACK_BP})`]: '1fr',
|
|
402
375
|
},
|
|
403
376
|
}),
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
type Row,
|
|
12
12
|
} from '@tanstack/react-table'
|
|
13
13
|
import {useVirtualizer} from '@tanstack/react-virtual'
|
|
14
|
+
import {breakpointsPx} from '@duro-app/tokens/tokens/breakpoints.css'
|
|
14
15
|
import {styles} from './styles.css'
|
|
15
16
|
|
|
16
17
|
// Measure + commit before paint on the client; no-op-safe on the server.
|
|
@@ -61,11 +62,10 @@ interface VirtualTableProps<TData> {
|
|
|
61
62
|
virtualizeThreshold?: number
|
|
62
63
|
/**
|
|
63
64
|
* Below this container width (px) the table cards up: the header hides and
|
|
64
|
-
* each row becomes a label/value card (like Table's stack mode).
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* Default 640. Set `responsive={false}` to disable entirely.
|
|
65
|
+
* each row becomes a label/value card (like Table's stack mode). Defaults to
|
|
66
|
+
* the shared `sm` breakpoint (640px) so every table cards up at the same
|
|
67
|
+
* line. Cards disable windowing (variable heights), so keep phone lists
|
|
68
|
+
* reasonably short. Set `responsive={false}` to disable entirely.
|
|
69
69
|
*/
|
|
70
70
|
stackBelow?: number
|
|
71
71
|
/** Opt out of the card-up-on-narrow behavior. Default true. */
|
|
@@ -98,7 +98,7 @@ export function VirtualTable<TData>({
|
|
|
98
98
|
estimateRowHeight = 44,
|
|
99
99
|
maxHeight = '70vh',
|
|
100
100
|
virtualizeThreshold = 150,
|
|
101
|
-
stackBelow =
|
|
101
|
+
stackBelow = breakpointsPx.sm,
|
|
102
102
|
responsive = true,
|
|
103
103
|
emptyLabel,
|
|
104
104
|
}: VirtualTableProps<TData>) {
|