@duro-app/ui 0.44.0 → 0.46.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/SortChip.d.ts +8 -1
- package/dist/components/Table/SortChip.d.ts.map +1 -1
- package/dist/components/Table/Table.d.ts +1 -1
- package/dist/components/Table/Table.d.ts.map +1 -1
- package/dist/components/Table/Table.stories.d.ts +2 -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/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 +12 -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 +2793 -2607
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/Table/SortChip.tsx +15 -2
- package/src/components/Table/Table.stories.tsx +125 -1
- package/src/components/Table/Table.tsx +140 -8
- package/src/components/Table/styles.css.ts +57 -0
- 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
|
@@ -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,
|
|
@@ -13,6 +13,9 @@ import {
|
|
|
13
13
|
import {useVirtualizer} from '@tanstack/react-virtual'
|
|
14
14
|
import {styles} from './styles.css'
|
|
15
15
|
|
|
16
|
+
// Measure + commit before paint on the client; no-op-safe on the server.
|
|
17
|
+
const useIsoLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect
|
|
18
|
+
|
|
16
19
|
export interface VirtualTableRange {
|
|
17
20
|
/** 1-based index of the first visible row (0 when empty). */
|
|
18
21
|
from: number
|
|
@@ -56,6 +59,17 @@ interface VirtualTableProps<TData> {
|
|
|
56
59
|
maxHeight?: number | string
|
|
57
60
|
/** Render every row (no windowing) at or below this count. Default 150. */
|
|
58
61
|
virtualizeThreshold?: number
|
|
62
|
+
/**
|
|
63
|
+
* 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). Since the
|
|
65
|
+
* flex columns truncate rather than overflow, this is a deliberate
|
|
66
|
+
* phone-width breakpoint, not an auto content measurement. Cards disable
|
|
67
|
+
* windowing (variable heights), so keep phone lists reasonably short.
|
|
68
|
+
* Default 640. 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 = 640,
|
|
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,
|