@duro-app/ui 0.45.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 +2 -2
- 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 +6 -6
- package/dist/components/Table/Table.d.ts.map +1 -1
- package/dist/components/Table/Table.stories.d.ts +3 -2
- 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/Tag/Tag.d.ts +9 -1
- package/dist/components/Tag/Tag.d.ts.map +1 -1
- package/dist/components/Tag/Tag.stories.d.ts +1 -0
- package/dist/components/Tag/Tag.stories.d.ts.map +1 -1
- package/dist/components/Tag/styles.css.d.ts +7 -0
- package/dist/components/Tag/styles.css.d.ts.map +1 -1
- package/dist/components/VirtualTable/VirtualTable.d.ts +11 -1
- package/dist/components/VirtualTable/VirtualTable.d.ts.map +1 -1
- package/dist/components/VirtualTable/VirtualTable.stories.d.ts +1 -0
- package/dist/components/VirtualTable/VirtualTable.stories.d.ts.map +1 -1
- package/dist/components/VirtualTable/styles.css.d.ts +40 -0
- package/dist/components/VirtualTable/styles.css.d.ts.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.js +2633 -2623
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/Table/FromTanstack.tsx +2 -2
- package/src/components/Table/SortChip.tsx +2 -15
- package/src/components/Table/Table.stories.tsx +91 -75
- package/src/components/Table/Table.tsx +35 -103
- package/src/components/Table/styles.css.ts +38 -65
- package/src/components/Tag/Tag.stories.tsx +22 -0
- package/src/components/Tag/Tag.tsx +10 -0
- package/src/components/Tag/styles.css.ts +12 -0
- package/src/components/VirtualTable/VirtualTable.stories.tsx +29 -0
- package/src/components/VirtualTable/VirtualTable.tsx +152 -58
- package/src/components/VirtualTable/styles.css.ts +43 -0
|
@@ -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
|
}),
|
|
@@ -49,8 +49,30 @@ export const Small: Story = {
|
|
|
49
49
|
const layoutStyles = css.create({
|
|
50
50
|
row: {display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap'},
|
|
51
51
|
stack: {display: 'flex', flexDirection: 'column', gap: 16},
|
|
52
|
+
// Narrow column that a long identifier tag would overflow without `wrap`.
|
|
53
|
+
narrowCol: {width: 120, borderWidth: 1, borderStyle: 'solid', borderColor: '#333', padding: 8},
|
|
52
54
|
})
|
|
53
55
|
|
|
56
|
+
const LONG_CAP = 'lldap.group.member.remove'
|
|
57
|
+
|
|
58
|
+
// A long identifier-style tag in a narrow column: with `wrap` it breaks
|
|
59
|
+
// within the column instead of overflowing (its scrollWidth stays within the
|
|
60
|
+
// box), fixing the "capability leaks into the next column" bug.
|
|
61
|
+
export const WrapLongLabel: Story = {
|
|
62
|
+
render: () => (
|
|
63
|
+
<html.div style={layoutStyles.narrowCol}>
|
|
64
|
+
<Tag size="sm" wrap>
|
|
65
|
+
{LONG_CAP}
|
|
66
|
+
</Tag>
|
|
67
|
+
</html.div>
|
|
68
|
+
),
|
|
69
|
+
play: async ({canvas}) => {
|
|
70
|
+
const tag = canvas.getByText(LONG_CAP)
|
|
71
|
+
// Content wraps inside the tag; the tag doesn't spill past its column.
|
|
72
|
+
await expect(tag.scrollWidth).toBeLessThanOrEqual(tag.clientWidth + 2)
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
|
|
54
76
|
export const AllVariants: Story = {
|
|
55
77
|
render: () => (
|
|
56
78
|
<html.div style={layoutStyles.stack}>
|
|
@@ -16,6 +16,14 @@ interface TagProps {
|
|
|
16
16
|
/** Called when remove button is clicked. Auto-wired to TagGroup when inside one. */
|
|
17
17
|
onRemove?: () => void
|
|
18
18
|
disabled?: boolean
|
|
19
|
+
/**
|
|
20
|
+
* Allow the label to wrap / break instead of staying on one line. Tags are
|
|
21
|
+
* `white-space: nowrap` by default (they read as pills); set this for long
|
|
22
|
+
* identifier-style values (e.g. `lldap.group.member.remove`) sitting in a
|
|
23
|
+
* constrained column, so they break within their cell instead of leaking
|
|
24
|
+
* into the next one.
|
|
25
|
+
*/
|
|
26
|
+
wrap?: boolean
|
|
19
27
|
children: ReactNode
|
|
20
28
|
}
|
|
21
29
|
|
|
@@ -36,6 +44,7 @@ export function Tag({
|
|
|
36
44
|
removable: removableProp,
|
|
37
45
|
onRemove: onRemoveProp,
|
|
38
46
|
disabled: disabledProp = false,
|
|
47
|
+
wrap = false,
|
|
39
48
|
children,
|
|
40
49
|
}: TagProps) {
|
|
41
50
|
const group = useTagGroup()
|
|
@@ -133,6 +142,7 @@ export function Tag({
|
|
|
133
142
|
removable && removableSizeMap[size],
|
|
134
143
|
styles[variant],
|
|
135
144
|
disabled && styles.disabled,
|
|
145
|
+
wrap && styles.wrap,
|
|
136
146
|
]}
|
|
137
147
|
>
|
|
138
148
|
{tagContent}
|
|
@@ -13,6 +13,7 @@ export const styles = css.create({
|
|
|
13
13
|
lineHeight: 1,
|
|
14
14
|
borderRadius: 4,
|
|
15
15
|
whiteSpace: 'nowrap',
|
|
16
|
+
minWidth: 0,
|
|
16
17
|
borderWidth: 1,
|
|
17
18
|
borderStyle: 'solid',
|
|
18
19
|
borderColor: colors.border,
|
|
@@ -47,6 +48,17 @@ export const styles = css.create({
|
|
|
47
48
|
paddingRight: spacing.sm,
|
|
48
49
|
fontSize: typography.fontSizeXs,
|
|
49
50
|
},
|
|
51
|
+
// Opt-in wrapping for long identifier-style labels. Overrides the base
|
|
52
|
+
// nowrap so the text breaks within its container (e.g. a table cell)
|
|
53
|
+
// instead of overflowing into the next column. `anywhere` also lets the
|
|
54
|
+
// tag shrink below its content's intrinsic width so a flex/grid parent can
|
|
55
|
+
// actually give it a narrow track.
|
|
56
|
+
wrap: {
|
|
57
|
+
whiteSpace: 'normal',
|
|
58
|
+
overflowWrap: 'anywhere',
|
|
59
|
+
lineHeight: 1.35,
|
|
60
|
+
textAlign: 'start',
|
|
61
|
+
},
|
|
50
62
|
// Reduce right padding when removable to keep visual balance with the button
|
|
51
63
|
removableMd: {
|
|
52
64
|
paddingRight: spacing.sm,
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import type {Meta, StoryObj} from '@storybook/react'
|
|
2
2
|
import {expect, fn, waitFor} from 'storybook/test'
|
|
3
3
|
import {createColumnHelper} from '@tanstack/react-table'
|
|
4
|
+
import {css, html} from 'react-strict-dom'
|
|
4
5
|
import {VirtualTable} from './VirtualTable'
|
|
5
6
|
|
|
7
|
+
const storyStyles = css.create({
|
|
8
|
+
// Phone-width wrapper: below VirtualTable's 640px stackBelow default.
|
|
9
|
+
narrow: {width: 400},
|
|
10
|
+
})
|
|
11
|
+
|
|
6
12
|
interface Item {
|
|
7
13
|
id: string
|
|
8
14
|
name: string
|
|
@@ -87,3 +93,26 @@ export const ScrollReportsPage: Story = {
|
|
|
87
93
|
await expect(maxPage).toBeGreaterThan(1)
|
|
88
94
|
},
|
|
89
95
|
}
|
|
96
|
+
|
|
97
|
+
// In a narrow (phone-width) container the table cards up: header hides and
|
|
98
|
+
// each row becomes a label/value card, like Table's stack mode.
|
|
99
|
+
export const StacksOnNarrow: Story = {
|
|
100
|
+
args: {data: makeData(6), onRowClick: fn(), rowLabel: (r: Item) => r.name},
|
|
101
|
+
decorators: [
|
|
102
|
+
(Story) => (
|
|
103
|
+
<html.div style={storyStyles.narrow}>
|
|
104
|
+
<Story />
|
|
105
|
+
</html.div>
|
|
106
|
+
),
|
|
107
|
+
],
|
|
108
|
+
play: async ({canvas}) => {
|
|
109
|
+
// Header group not rendered → no column headers in the a11y tree.
|
|
110
|
+
await waitFor(async () => {
|
|
111
|
+
await expect(canvas.queryAllByRole('columnheader')).toHaveLength(0)
|
|
112
|
+
})
|
|
113
|
+
// Each card surfaces the column labels ('Name'/'Value') next to values.
|
|
114
|
+
await expect(canvas.getAllByText('Name').length).toBe(6)
|
|
115
|
+
await expect(canvas.getByText('Row 0')).toBeInTheDocument()
|
|
116
|
+
await expect(canvas.getByText('Row 5')).toBeInTheDocument()
|
|
117
|
+
},
|
|
118
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {useEffect, useRef, useState, type ReactNode} from 'react'
|
|
1
|
+
import {useEffect, useLayoutEffect, useRef, useState, type ReactNode} from 'react'
|
|
2
2
|
import {html} from 'react-strict-dom'
|
|
3
3
|
import {
|
|
4
4
|
useReactTable,
|
|
@@ -11,8 +11,12 @@ 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
|
|
|
17
|
+
// Measure + commit before paint on the client; no-op-safe on the server.
|
|
18
|
+
const useIsoLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect
|
|
19
|
+
|
|
16
20
|
export interface VirtualTableRange {
|
|
17
21
|
/** 1-based index of the first visible row (0 when empty). */
|
|
18
22
|
from: number
|
|
@@ -56,6 +60,16 @@ interface VirtualTableProps<TData> {
|
|
|
56
60
|
maxHeight?: number | string
|
|
57
61
|
/** Render every row (no windowing) at or below this count. Default 150. */
|
|
58
62
|
virtualizeThreshold?: number
|
|
63
|
+
/**
|
|
64
|
+
* Below this container width (px) the table cards up: the header hides and
|
|
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
|
+
*/
|
|
70
|
+
stackBelow?: number
|
|
71
|
+
/** Opt out of the card-up-on-narrow behavior. Default true. */
|
|
72
|
+
responsive?: boolean
|
|
59
73
|
emptyLabel?: ReactNode
|
|
60
74
|
}
|
|
61
75
|
|
|
@@ -84,6 +98,8 @@ export function VirtualTable<TData>({
|
|
|
84
98
|
estimateRowHeight = 44,
|
|
85
99
|
maxHeight = '70vh',
|
|
86
100
|
virtualizeThreshold = 150,
|
|
101
|
+
stackBelow = breakpointsPx.sm,
|
|
102
|
+
responsive = true,
|
|
87
103
|
emptyLabel,
|
|
88
104
|
}: VirtualTableProps<TData>) {
|
|
89
105
|
// Support both controlled sorting (SortChip in the caller) and standalone use.
|
|
@@ -102,7 +118,35 @@ export function VirtualTable<TData>({
|
|
|
102
118
|
})
|
|
103
119
|
|
|
104
120
|
const rows = table.getRowModel().rows
|
|
105
|
-
|
|
121
|
+
|
|
122
|
+
// Card-up on narrow containers. Flex cells truncate rather than overflow, so
|
|
123
|
+
// there's no content-overflow signal to measure (as in Table) — we use a
|
|
124
|
+
// deliberate phone-width breakpoint. Cards render every row (no windowing),
|
|
125
|
+
// so `virtualize` is forced off while stacked.
|
|
126
|
+
const wrapRef = useRef<HTMLDivElement>(null)
|
|
127
|
+
const [stacked, setStacked] = useState(false)
|
|
128
|
+
useIsoLayoutEffect(() => {
|
|
129
|
+
if (!responsive) {
|
|
130
|
+
setStacked(false)
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
const el = wrapRef.current
|
|
134
|
+
if (!el || typeof ResizeObserver === 'undefined') return
|
|
135
|
+
const apply = (width: number) => {
|
|
136
|
+
if (width > 0) setStacked(width < stackBelow)
|
|
137
|
+
}
|
|
138
|
+
apply(el.getBoundingClientRect().width)
|
|
139
|
+
const ro = new ResizeObserver((entries) => {
|
|
140
|
+
const entry = entries[0]
|
|
141
|
+
if (!entry) return
|
|
142
|
+
const box = entry.contentBoxSize?.[0]
|
|
143
|
+
apply(box ? box.inlineSize : entry.contentRect.width)
|
|
144
|
+
})
|
|
145
|
+
ro.observe(el)
|
|
146
|
+
return () => ro.disconnect()
|
|
147
|
+
}, [responsive, stackBelow])
|
|
148
|
+
|
|
149
|
+
const virtualize = rows.length > virtualizeThreshold && !stacked
|
|
106
150
|
|
|
107
151
|
const scrollRef = useRef<HTMLDivElement>(null)
|
|
108
152
|
const rv = useVirtualizer({
|
|
@@ -174,82 +218,132 @@ export function VirtualTable<TData>({
|
|
|
174
218
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
175
219
|
}, [curPage, virtualize])
|
|
176
220
|
|
|
221
|
+
// Stack-mode label for a column: explicit meta override, else the string
|
|
222
|
+
// header, else nothing (value renders label-less, e.g. an actions column).
|
|
223
|
+
const columnStackLabel = (columnDef: {
|
|
224
|
+
header?: unknown
|
|
225
|
+
meta?: {stackLabel?: string; label?: string}
|
|
226
|
+
}): string => {
|
|
227
|
+
const meta = columnDef.meta
|
|
228
|
+
if (typeof meta?.stackLabel === 'string') return meta.stackLabel
|
|
229
|
+
if (typeof meta?.label === 'string') return meta.label
|
|
230
|
+
return typeof columnDef.header === 'string' ? columnDef.header : ''
|
|
231
|
+
}
|
|
232
|
+
|
|
177
233
|
const renderRow = (
|
|
178
234
|
row: Row<TData>,
|
|
179
235
|
index: number,
|
|
180
236
|
positioned?: ReturnType<typeof styles.rowAbsolute>,
|
|
181
237
|
measureRef?: (node: Element | null) => void,
|
|
182
|
-
) =>
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
238
|
+
) =>
|
|
239
|
+
stacked ? (
|
|
240
|
+
<html.div
|
|
241
|
+
key={row.id}
|
|
242
|
+
role="row"
|
|
243
|
+
aria-label={rowLabel?.(row.original)}
|
|
244
|
+
onClick={onRowClick ? () => onRowClick(row.original) : undefined}
|
|
245
|
+
style={[styles.stackRow, Boolean(onRowClick) && styles.rowClickable]}
|
|
246
|
+
>
|
|
247
|
+
{row.getVisibleCells().map((cell) => {
|
|
248
|
+
const lbl = columnStackLabel(cell.column.columnDef)
|
|
249
|
+
return (
|
|
250
|
+
<html.div key={cell.id} role="cell" style={styles.stackCell}>
|
|
251
|
+
{lbl ? <html.span style={styles.stackCellLabel}>{lbl}</html.span> : null}
|
|
252
|
+
<html.div style={styles.stackCellValue}>
|
|
253
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
254
|
+
</html.div>
|
|
255
|
+
</html.div>
|
|
256
|
+
)
|
|
257
|
+
})}
|
|
258
|
+
</html.div>
|
|
259
|
+
) : (
|
|
260
|
+
<html.div
|
|
261
|
+
key={row.id}
|
|
262
|
+
ref={measureRef}
|
|
263
|
+
data-index={index}
|
|
264
|
+
role="row"
|
|
265
|
+
aria-label={rowLabel?.(row.original)}
|
|
266
|
+
onClick={onRowClick ? () => onRowClick(row.original) : undefined}
|
|
267
|
+
style={[styles.row, Boolean(onRowClick) && styles.rowClickable, positioned]}
|
|
268
|
+
>
|
|
269
|
+
{row.getVisibleCells().map((cell) => (
|
|
270
|
+
<html.div
|
|
271
|
+
key={cell.id}
|
|
272
|
+
role="cell"
|
|
273
|
+
style={[styles.cell, styles.cellFlex(cell.column.getSize())]}
|
|
274
|
+
>
|
|
275
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
276
|
+
</html.div>
|
|
277
|
+
))}
|
|
278
|
+
</html.div>
|
|
279
|
+
)
|
|
203
280
|
|
|
204
281
|
const label =
|
|
205
282
|
rangeLabel?.(range) ?? (rows.length ? `${range.from}–${range.to} / ${range.total}` : '')
|
|
206
283
|
|
|
207
284
|
return (
|
|
208
|
-
<html.div style={styles.wrap}>
|
|
285
|
+
<html.div ref={wrapRef} style={styles.wrap}>
|
|
209
286
|
<html.div
|
|
210
287
|
ref={scrollRef}
|
|
211
288
|
role="table"
|
|
212
|
-
style={[
|
|
289
|
+
style={[
|
|
290
|
+
stacked ? styles.stackScroll : styles.scroll,
|
|
291
|
+
virtualize && styles.scrollMax(maxHeight),
|
|
292
|
+
]}
|
|
213
293
|
>
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
{headerGroup.
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
294
|
+
{stacked ? null : (
|
|
295
|
+
<html.div role="rowgroup" style={styles.head}>
|
|
296
|
+
{table.getHeaderGroups().map((headerGroup) => (
|
|
297
|
+
<html.div key={headerGroup.id} role="row" style={styles.headRow}>
|
|
298
|
+
{headerGroup.headers.map((header) => {
|
|
299
|
+
const col = header.column
|
|
300
|
+
const sorted = col.getIsSorted()
|
|
301
|
+
return (
|
|
302
|
+
<html.div
|
|
303
|
+
key={header.id}
|
|
304
|
+
role="columnheader"
|
|
305
|
+
aria-sort={
|
|
306
|
+
sorted === 'asc'
|
|
307
|
+
? 'ascending'
|
|
308
|
+
: sorted === 'desc'
|
|
309
|
+
? 'descending'
|
|
310
|
+
: undefined
|
|
311
|
+
}
|
|
312
|
+
onClick={col.getCanSort() ? col.getToggleSortingHandler() : undefined}
|
|
313
|
+
style={[
|
|
314
|
+
styles.headCell,
|
|
315
|
+
col.getCanSort() && styles.headCellSortable,
|
|
316
|
+
styles.cellFlex(header.getSize()),
|
|
317
|
+
]}
|
|
318
|
+
>
|
|
319
|
+
{header.isPlaceholder
|
|
320
|
+
? null
|
|
321
|
+
: flexRender(col.columnDef.header, header.getContext())}
|
|
322
|
+
{sorted ? (
|
|
323
|
+
<html.span style={styles.sortMark}>
|
|
324
|
+
{sorted === 'asc' ? '↑' : '↓'}
|
|
325
|
+
</html.span>
|
|
326
|
+
) : null}
|
|
327
|
+
</html.div>
|
|
328
|
+
)
|
|
329
|
+
})}
|
|
330
|
+
</html.div>
|
|
331
|
+
))}
|
|
332
|
+
</html.div>
|
|
333
|
+
)}
|
|
246
334
|
|
|
247
335
|
{rows.length === 0 ? (
|
|
248
336
|
<html.div style={styles.empty}>{emptyLabel}</html.div>
|
|
249
337
|
) : (
|
|
250
338
|
<html.div
|
|
251
339
|
role="rowgroup"
|
|
252
|
-
style={
|
|
340
|
+
style={
|
|
341
|
+
stacked
|
|
342
|
+
? styles.stackBody
|
|
343
|
+
: virtualize
|
|
344
|
+
? styles.bodyTotal(rv.getTotalSize())
|
|
345
|
+
: undefined
|
|
346
|
+
}
|
|
253
347
|
>
|
|
254
348
|
{virtualize
|
|
255
349
|
? vItems.map((vi) =>
|
|
@@ -105,6 +105,49 @@ export const styles = css.create({
|
|
|
105
105
|
flexBasis: 0,
|
|
106
106
|
minWidth: 0,
|
|
107
107
|
}),
|
|
108
|
+
// --- Stack (card) mode: narrow containers ---
|
|
109
|
+
// The scroll frame drops its border/background so each row card stands on
|
|
110
|
+
// its own; windowing is off in this mode so no capped viewport.
|
|
111
|
+
stackScroll: {
|
|
112
|
+
position: 'relative',
|
|
113
|
+
width: '100%',
|
|
114
|
+
backgroundColor: 'transparent',
|
|
115
|
+
},
|
|
116
|
+
stackBody: {
|
|
117
|
+
display: 'flex',
|
|
118
|
+
flexDirection: 'column',
|
|
119
|
+
gap: spacing.sm,
|
|
120
|
+
},
|
|
121
|
+
stackRow: {
|
|
122
|
+
display: 'flex',
|
|
123
|
+
flexDirection: 'column',
|
|
124
|
+
gap: spacing.xs,
|
|
125
|
+
width: '100%',
|
|
126
|
+
padding: spacing.sm,
|
|
127
|
+
borderWidth: 1,
|
|
128
|
+
borderStyle: 'solid',
|
|
129
|
+
borderColor: colors.border,
|
|
130
|
+
borderRadius: radii.sm,
|
|
131
|
+
backgroundColor: colors.bgCard,
|
|
132
|
+
},
|
|
133
|
+
stackCell: {
|
|
134
|
+
display: 'grid',
|
|
135
|
+
gridTemplateColumns: '1fr 2fr',
|
|
136
|
+
gap: spacing.sm,
|
|
137
|
+
alignItems: 'center',
|
|
138
|
+
fontFamily: typography.fontFamily,
|
|
139
|
+
fontSize: typography.fontSizeSm,
|
|
140
|
+
color: colors.text,
|
|
141
|
+
},
|
|
142
|
+
stackCellLabel: {
|
|
143
|
+
color: colors.textMuted,
|
|
144
|
+
fontWeight: typography.fontWeightMedium,
|
|
145
|
+
fontSize: typography.fontSizeXs,
|
|
146
|
+
},
|
|
147
|
+
stackCellValue: {
|
|
148
|
+
minWidth: 0,
|
|
149
|
+
overflowWrap: 'break-word',
|
|
150
|
+
},
|
|
108
151
|
empty: {
|
|
109
152
|
paddingTop: spacing.xl,
|
|
110
153
|
paddingBottom: spacing.xl,
|