@duro-app/ui 0.33.0 → 0.34.1
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 +45 -0
- package/dist/components/Table/FromTanstack.d.ts.map +1 -0
- package/dist/components/Table/Table.d.ts +37 -13
- 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 +16 -1
- package/dist/components/Table/styles.css.d.ts.map +1 -1
- package/dist/components/Table/tanstack-augmentation.d.ts +13 -0
- package/dist/components/Table/tanstack-augmentation.d.ts.map +1 -0
- package/dist/index.css +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2293 -2160
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/Table/FromTanstack.tsx +159 -0
- package/src/components/Table/Table.stories.tsx +164 -0
- package/src/components/Table/Table.tsx +204 -42
- package/src/components/Table/styles.css.ts +45 -7
- package/src/components/Table/tanstack-augmentation.ts +28 -0
- package/src/index.ts +5 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
type ReactElement,
|
|
2
3
|
type ReactNode,
|
|
3
4
|
type MutableRefObject,
|
|
4
5
|
createContext,
|
|
@@ -44,13 +45,26 @@ function useTable() {
|
|
|
44
45
|
|
|
45
46
|
const HeaderContext = createContext(false)
|
|
46
47
|
|
|
48
|
+
// --- Dev-only warn registry ---
|
|
49
|
+
//
|
|
50
|
+
// One-shot per warning code per process: long-running dev sessions
|
|
51
|
+
// shouldn't drown the console on every re-render of a misconfigured table.
|
|
52
|
+
const _IS_PROD = typeof process !== 'undefined' && process.env?.NODE_ENV === 'production'
|
|
53
|
+
const _devWarned = new Set<string>()
|
|
54
|
+
function devWarnOnce(code: string, message: string) {
|
|
55
|
+
if (_IS_PROD || _devWarned.has(code)) return
|
|
56
|
+
_devWarned.add(code)
|
|
57
|
+
// eslint-disable-next-line no-console
|
|
58
|
+
console.warn(`[duro-app/ui Table] ${message}`)
|
|
59
|
+
}
|
|
60
|
+
|
|
47
61
|
// --- Container — owns the @container query target ---
|
|
48
62
|
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
63
|
+
// @deprecated Root sets up its own container query and accepts `sortChip`
|
|
64
|
+
// / `pagination` as slot props. Container is kept exported as a passthrough
|
|
65
|
+
// so existing call sites continue to work; new code should use Root alone.
|
|
52
66
|
|
|
53
|
-
function Container({children}: {children: ReactNode}) {
|
|
67
|
+
export function Container({children}: {children: ReactNode}) {
|
|
54
68
|
return <html.div style={styles.container}>{children}</html.div>
|
|
55
69
|
}
|
|
56
70
|
|
|
@@ -62,19 +76,37 @@ function Container({children}: {children: ReactNode}) {
|
|
|
62
76
|
// render — cheap (small N, no DOM) — but worth memoising on `children`
|
|
63
77
|
// reference if profiling flags it.
|
|
64
78
|
|
|
79
|
+
// Shallow string-children extraction. We deliberately do NOT recurse into
|
|
80
|
+
// child elements: an icon, tooltip trigger, or sort indicator nested inside
|
|
81
|
+
// a HeaderCell would otherwise pollute the stack-mode label with its own
|
|
82
|
+
// text content. JSX-only headers must set `label` explicitly.
|
|
65
83
|
function extractText(node: ReactNode): string {
|
|
66
84
|
let out = ''
|
|
67
85
|
Children.forEach(node, (child) => {
|
|
68
86
|
if (typeof child === 'string' || typeof child === 'number') {
|
|
69
87
|
out += String(child)
|
|
70
|
-
} else if (isValidElement(child)) {
|
|
71
|
-
const props = child.props as {children?: ReactNode}
|
|
72
|
-
if (props.children) out += extractText(props.children)
|
|
73
88
|
}
|
|
74
89
|
})
|
|
75
90
|
return out
|
|
76
91
|
}
|
|
77
92
|
|
|
93
|
+
// Unwrap memo(...) / forwardRef(...) wrappers so JSX like
|
|
94
|
+
// `<MemoizedHeaderCell ...>` still identifies as HeaderCell. Both wrappers
|
|
95
|
+
// expose the inner component via a `.type` (memo) or `.render` (forwardRef)
|
|
96
|
+
// property on the element-type descriptor. We walk both fields until we
|
|
97
|
+
// reach a leaf, which is the underlying function/class component.
|
|
98
|
+
function unwrapElementType(t: unknown): unknown {
|
|
99
|
+
let current = t
|
|
100
|
+
// Bounded loop — defensive cap against pathological infinite-nesting.
|
|
101
|
+
for (let i = 0; i < 8 && current && typeof current === 'object'; i++) {
|
|
102
|
+
const next =
|
|
103
|
+
(current as {type?: unknown; render?: unknown}).type ?? (current as {render?: unknown}).render
|
|
104
|
+
if (next == null) break
|
|
105
|
+
current = next
|
|
106
|
+
}
|
|
107
|
+
return current
|
|
108
|
+
}
|
|
109
|
+
|
|
78
110
|
function extractColumnMeta(children: ReactNode): {template: string; labels: string[]} {
|
|
79
111
|
const widths: string[] = []
|
|
80
112
|
const labels: string[] = []
|
|
@@ -83,10 +115,24 @@ function extractColumnMeta(children: ReactNode): {template: string; labels: stri
|
|
|
83
115
|
Children.forEach(node, (child) => {
|
|
84
116
|
if (!isValidElement(child)) return
|
|
85
117
|
const props = child.props as Record<string, any>
|
|
86
|
-
const
|
|
87
|
-
|
|
118
|
+
const unwrappedType = unwrapElementType(child.type)
|
|
119
|
+
const displayName =
|
|
120
|
+
(unwrappedType as {name?: string; displayName?: string} | null)?.displayName ||
|
|
121
|
+
(unwrappedType as {name?: string} | null)?.name ||
|
|
122
|
+
''
|
|
123
|
+
if (unwrappedType === HeaderCell || displayName === 'HeaderCell') {
|
|
88
124
|
widths.push(props.width || '1fr')
|
|
89
|
-
const
|
|
125
|
+
const explicit = typeof props.label === 'string' ? props.label : undefined
|
|
126
|
+
const fallback = extractText(props.children).trim()
|
|
127
|
+
const label = explicit ?? fallback
|
|
128
|
+
if (!explicit && fallback === '' && props.children != null) {
|
|
129
|
+
// Children are JSX with no string content — stack mode will render
|
|
130
|
+
// an unlabeled cell. Tell the developer once.
|
|
131
|
+
devWarnOnce(
|
|
132
|
+
'headerCell-missing-label',
|
|
133
|
+
'Table.HeaderCell with JSX children must set `label` for stack-mode rendering.',
|
|
134
|
+
)
|
|
135
|
+
}
|
|
90
136
|
labels.push(label)
|
|
91
137
|
} else if (props.children) {
|
|
92
138
|
walk(props.children)
|
|
@@ -106,34 +152,77 @@ interface RootProps {
|
|
|
106
152
|
size?: TableSize
|
|
107
153
|
/** Opt out of responsive container-query behavior. Default true. */
|
|
108
154
|
responsive?: boolean
|
|
155
|
+
/**
|
|
156
|
+
* Optional sort UI rendered above the grid. Visible only in stack mode
|
|
157
|
+
* (SortChip carries its own `display: none → inline-flex` rule). Typical
|
|
158
|
+
* value: `<Table.SortChip options={...} value={...} onChange={...} />`.
|
|
159
|
+
*/
|
|
160
|
+
sortChip?: ReactNode
|
|
161
|
+
/**
|
|
162
|
+
* Optional pagination UI rendered below the grid. Typical value:
|
|
163
|
+
* `<Table.Pagination table={tanstackTable} />`.
|
|
164
|
+
*/
|
|
165
|
+
pagination?: ReactNode
|
|
109
166
|
}
|
|
110
167
|
|
|
111
|
-
function Root({
|
|
168
|
+
export function Root({
|
|
169
|
+
children,
|
|
170
|
+
variant = 'default',
|
|
171
|
+
size = 'md',
|
|
172
|
+
responsive = true,
|
|
173
|
+
sortChip,
|
|
174
|
+
pagination,
|
|
175
|
+
}: RootProps) {
|
|
112
176
|
const inferredTemplateRef = useRef<string | null>(null)
|
|
113
177
|
const {template, labels} = extractColumnMeta(children)
|
|
114
178
|
if (template) {
|
|
115
179
|
inferredTemplateRef.current = template
|
|
116
180
|
}
|
|
117
181
|
|
|
182
|
+
const grid = (
|
|
183
|
+
<html.div
|
|
184
|
+
role="table"
|
|
185
|
+
style={[
|
|
186
|
+
styles.root,
|
|
187
|
+
// When responsive, use gridColumnsResponsive so the explicit column
|
|
188
|
+
// template and the stack-mode @container override live in one StyleX
|
|
189
|
+
// style object. Splitting them across two styles (gridColumns +
|
|
190
|
+
// rootResponsive) causes StyleX conflict resolution to drop the
|
|
191
|
+
// column template because rootResponsive's gridTemplateColumns key
|
|
192
|
+
// always wins as the later entry in the array.
|
|
193
|
+
template
|
|
194
|
+
? responsive
|
|
195
|
+
? styles.gridColumnsResponsive(template)
|
|
196
|
+
: styles.gridColumns(template)
|
|
197
|
+
: undefined,
|
|
198
|
+
responsive && styles.rootResponsive,
|
|
199
|
+
]}
|
|
200
|
+
>
|
|
201
|
+
{children}
|
|
202
|
+
</html.div>
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
// Slots render unconditionally — `responsive=false` still wants its sort/
|
|
206
|
+
// pagination chrome. Only the containerType:inline-size wrapper is
|
|
207
|
+
// conditional, since it only matters when @container queries fire.
|
|
208
|
+
const body = (
|
|
209
|
+
<>
|
|
210
|
+
{sortChip}
|
|
211
|
+
{grid}
|
|
212
|
+
{pagination}
|
|
213
|
+
</>
|
|
214
|
+
)
|
|
215
|
+
|
|
118
216
|
return (
|
|
119
217
|
<TableContext.Provider value={{variant, size, responsive, labels, inferredTemplateRef}}>
|
|
120
|
-
<html.div
|
|
121
|
-
role="table"
|
|
122
|
-
style={[
|
|
123
|
-
styles.root,
|
|
124
|
-
template ? styles.gridColumns(template) : undefined,
|
|
125
|
-
responsive && styles.rootResponsive,
|
|
126
|
-
]}
|
|
127
|
-
>
|
|
128
|
-
{children}
|
|
129
|
-
</html.div>
|
|
218
|
+
{responsive ? <html.div style={styles.rootContainer}>{body}</html.div> : body}
|
|
130
219
|
</TableContext.Provider>
|
|
131
220
|
)
|
|
132
221
|
}
|
|
133
222
|
|
|
134
223
|
// --- Header ---
|
|
135
224
|
|
|
136
|
-
function Header({children}: {children: ReactNode}) {
|
|
225
|
+
export function Header({children}: {children: ReactNode}) {
|
|
137
226
|
return (
|
|
138
227
|
<HeaderContext.Provider value={true}>
|
|
139
228
|
<html.div role="rowgroup" style={styles.header}>
|
|
@@ -145,22 +234,24 @@ function Header({children}: {children: ReactNode}) {
|
|
|
145
234
|
|
|
146
235
|
// --- Body ---
|
|
147
236
|
|
|
148
|
-
function Body({children}: {children: ReactNode}) {
|
|
237
|
+
export function Body({children}: {children: ReactNode}) {
|
|
149
238
|
const {variant} = useTable()
|
|
150
|
-
const childArray = Children.toArray(children)
|
|
239
|
+
const childArray = Children.toArray(children) as ReactElement[]
|
|
151
240
|
|
|
152
241
|
return (
|
|
153
242
|
<HeaderContext.Provider value={false}>
|
|
154
243
|
<html.div role="rowgroup" style={styles.body}>
|
|
155
244
|
{childArray.map((child, index) => {
|
|
156
|
-
if (variant
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
245
|
+
if (variant !== 'striped') return child
|
|
246
|
+
// Key the provider by the child's React key (Children.toArray
|
|
247
|
+
// guarantees one) so reordered rows carry their providers along
|
|
248
|
+
// with them. Position is passed as the value because striping is
|
|
249
|
+
// a positional concern even when row identity is stable.
|
|
250
|
+
return (
|
|
251
|
+
<RowIndexContext.Provider key={child.key ?? index} value={index}>
|
|
252
|
+
{child}
|
|
253
|
+
</RowIndexContext.Provider>
|
|
254
|
+
)
|
|
164
255
|
})}
|
|
165
256
|
</html.div>
|
|
166
257
|
</HeaderContext.Provider>
|
|
@@ -175,25 +266,81 @@ const RowIndexContext = createContext<number>(-1)
|
|
|
175
266
|
// was only set up for `variant === 'bordered'`, which meant non-bordered
|
|
176
267
|
// tables had every cell reading {index: 0} from the default — fine until
|
|
177
268
|
// we needed to look up labels by column index.
|
|
269
|
+
//
|
|
270
|
+
// Interactive rows: pass `onClick` to make the row a navigation target.
|
|
271
|
+
// The role stays `row` (per ARIA grid pattern) — we don't wrap in an outer
|
|
272
|
+
// element with role=button, which would break the rowgroup → row hierarchy.
|
|
273
|
+
// A click that originates inside an interactive descendant (button, link,
|
|
274
|
+
// input, etc.) is treated as that descendant's click and does not fire
|
|
275
|
+
// onClick — prevents double-fire when a row contains action buttons.
|
|
276
|
+
|
|
277
|
+
interface RowProps {
|
|
278
|
+
children: ReactNode
|
|
279
|
+
/** When set, the row becomes focusable + clickable. */
|
|
280
|
+
onClick?: () => void
|
|
281
|
+
/** Per-row accessible name — required when onClick is set, recommended otherwise. */
|
|
282
|
+
'aria-label'?: string
|
|
283
|
+
}
|
|
178
284
|
|
|
179
|
-
|
|
285
|
+
// Heuristic for "clicking a descendant means the descendant, not the row".
|
|
286
|
+
// Matches the elements the WAI-ARIA grid pattern lists as "widgets" plus the
|
|
287
|
+
// usual native focus-stealers.
|
|
288
|
+
const INTERACTIVE_SELECTOR =
|
|
289
|
+
'button, a, input, select, textarea, [role="button"], [role="link"], [role="checkbox"], [role="menuitem"], [role="switch"], [role="tab"], [contenteditable="true"]'
|
|
290
|
+
|
|
291
|
+
export function Row({children, onClick, 'aria-label': ariaLabel}: RowProps) {
|
|
180
292
|
const {variant} = useTable()
|
|
181
293
|
const isHeader = useContext(HeaderContext)
|
|
182
294
|
const rowIndex = useContext(RowIndexContext)
|
|
183
295
|
const isEvenRow = rowIndex >= 0 && rowIndex % 2 === 1
|
|
184
|
-
const childArray = Children.toArray(children)
|
|
296
|
+
const childArray = Children.toArray(children) as ReactElement[]
|
|
297
|
+
const isClickable = onClick !== undefined && !isHeader
|
|
185
298
|
|
|
186
299
|
return (
|
|
187
300
|
<html.div
|
|
188
301
|
role="row"
|
|
302
|
+
tabIndex={isClickable ? 0 : undefined}
|
|
303
|
+
aria-label={isClickable ? ariaLabel : undefined}
|
|
304
|
+
onClick={
|
|
305
|
+
isClickable
|
|
306
|
+
? (e) => {
|
|
307
|
+
// react-strict-dom's typed click event omits `target`, but the
|
|
308
|
+
// underlying SyntheticEvent still carries it. Cast through so
|
|
309
|
+
// we can detect clicks that originate inside an interactive
|
|
310
|
+
// descendant (action button, link, input) and let them be
|
|
311
|
+
// attributed to that widget instead of the row.
|
|
312
|
+
const target = (e as unknown as {target?: EventTarget | null}).target
|
|
313
|
+
if (target instanceof Element && target.closest(INTERACTIVE_SELECTOR)) {
|
|
314
|
+
return
|
|
315
|
+
}
|
|
316
|
+
onClick()
|
|
317
|
+
}
|
|
318
|
+
: undefined
|
|
319
|
+
}
|
|
320
|
+
onKeyDown={
|
|
321
|
+
isClickable
|
|
322
|
+
? (e) => {
|
|
323
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
324
|
+
onClick()
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
: undefined
|
|
328
|
+
}
|
|
189
329
|
style={[
|
|
190
330
|
styles.row,
|
|
191
331
|
!isHeader && styles.bodyRow,
|
|
192
332
|
!isHeader && variant === 'striped' && isEvenRow && styles.stripedEven,
|
|
333
|
+
isClickable && styles.clickableRow,
|
|
193
334
|
]}
|
|
194
335
|
>
|
|
195
336
|
{childArray.map((child, index) => (
|
|
196
|
-
|
|
337
|
+
// Key by the child's React key (Children.toArray assigns one) so a
|
|
338
|
+
// reordered column carries its CellIndexContext provider along.
|
|
339
|
+
// The positional `index` lives in the provider's value, not its key.
|
|
340
|
+
<CellIndexContext.Provider
|
|
341
|
+
key={child.key ?? index}
|
|
342
|
+
value={{index, total: childArray.length}}
|
|
343
|
+
>
|
|
197
344
|
{child}
|
|
198
345
|
</CellIndexContext.Provider>
|
|
199
346
|
))}
|
|
@@ -205,24 +352,34 @@ const CellIndexContext = createContext<{index: number; total: number}>({index: 0
|
|
|
205
352
|
|
|
206
353
|
// --- HeaderCell ---
|
|
207
354
|
|
|
208
|
-
function HeaderCell({
|
|
355
|
+
export function HeaderCell({
|
|
209
356
|
children,
|
|
210
357
|
width: _width,
|
|
211
358
|
label: _label,
|
|
212
|
-
isActions
|
|
359
|
+
isActions,
|
|
213
360
|
'aria-label': ariaLabel,
|
|
214
361
|
}: {
|
|
215
362
|
children?: ReactNode
|
|
216
363
|
/** Column width: CSS value like '40px', '2fr', 'max-content'. Defaults to '1fr'. */
|
|
217
364
|
width?: string
|
|
218
|
-
/** Stack-mode label string.
|
|
219
|
-
* text
|
|
365
|
+
/** Stack-mode label string. Optional when children is a plain string — the
|
|
366
|
+
* text content is used as the label automatically. Required when children
|
|
367
|
+
* contain JSX (icon + text, sort indicator, etc.). */
|
|
220
368
|
label?: string
|
|
221
|
-
/**
|
|
222
|
-
*
|
|
369
|
+
/**
|
|
370
|
+
* @deprecated Has no effect on HeaderCell. Pass `isActions` on the
|
|
371
|
+
* matching `Table.Cell` instead — that's where the stack-mode footer
|
|
372
|
+
* layout actually applies.
|
|
373
|
+
*/
|
|
223
374
|
isActions?: boolean
|
|
224
375
|
'aria-label'?: string
|
|
225
376
|
}) {
|
|
377
|
+
if (isActions === true) {
|
|
378
|
+
devWarnOnce(
|
|
379
|
+
'headerCell-isActions',
|
|
380
|
+
'HeaderCell `isActions` prop has no effect and is deprecated. Pass `isActions` on the matching Table.Cell instead.',
|
|
381
|
+
)
|
|
382
|
+
}
|
|
226
383
|
const {size, variant} = useTable()
|
|
227
384
|
const {index, total} = useContext(CellIndexContext)
|
|
228
385
|
const isLast = variant === 'bordered' && index === total - 1
|
|
@@ -250,7 +407,7 @@ HeaderCell.displayName = 'HeaderCell'
|
|
|
250
407
|
// container query in non-stack modes. The span is omitted entirely when
|
|
251
408
|
// isActions=true — actions footers don't show a label.
|
|
252
409
|
|
|
253
|
-
function Cell({children, isActions}: {children: ReactNode; isActions?: boolean}) {
|
|
410
|
+
export function Cell({children, isActions}: {children: ReactNode; isActions?: boolean}) {
|
|
254
411
|
const {size, variant, labels, responsive} = useTable()
|
|
255
412
|
const {index, total} = useContext(CellIndexContext)
|
|
256
413
|
const isLast = variant === 'bordered' && index === total - 1
|
|
@@ -277,7 +434,10 @@ function Cell({children, isActions}: {children: ReactNode; isActions?: boolean})
|
|
|
277
434
|
|
|
278
435
|
// --- Export ---
|
|
279
436
|
|
|
437
|
+
import {FromTanstack} from './FromTanstack'
|
|
438
|
+
|
|
280
439
|
export const Table = {
|
|
440
|
+
/** @deprecated Wrap behaviour is built into Table.Root. Drop this wrapper from new code. */
|
|
281
441
|
Container,
|
|
282
442
|
Root,
|
|
283
443
|
Header,
|
|
@@ -289,4 +449,6 @@ export const Table = {
|
|
|
289
449
|
SortIndicator,
|
|
290
450
|
ColumnFilter,
|
|
291
451
|
SortChip,
|
|
452
|
+
/** Renders a styled Table directly from a TanStack table instance. */
|
|
453
|
+
FromTanstack,
|
|
292
454
|
}
|
|
@@ -14,6 +14,9 @@ export const styles = css.create({
|
|
|
14
14
|
// Outer wrapper that hosts the @container query. Wraps SortChip + Root
|
|
15
15
|
// + Pagination so all three react to the same width. Root itself does
|
|
16
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.
|
|
17
20
|
container: {
|
|
18
21
|
containerType: 'inline-size',
|
|
19
22
|
display: 'flex',
|
|
@@ -21,6 +24,28 @@ export const styles = css.create({
|
|
|
21
24
|
gap: spacing.sm,
|
|
22
25
|
},
|
|
23
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
|
+
|
|
24
49
|
// Root — the single grid container for the table itself
|
|
25
50
|
root: {
|
|
26
51
|
display: 'grid',
|
|
@@ -35,13 +60,13 @@ export const styles = css.create({
|
|
|
35
60
|
},
|
|
36
61
|
|
|
37
62
|
// Stack-mode override: the Root's grid collapses to a single column so
|
|
38
|
-
// each Row spans full width and stacks vertically.
|
|
39
|
-
//
|
|
63
|
+
// each Row spans full width and stacks vertically.
|
|
64
|
+
// NOTE: gridTemplateColumns is intentionally absent here. It is handled by
|
|
65
|
+
// gridColumnsResponsive() so that StyleX conflict resolution does not remove
|
|
66
|
+
// the explicit column template (a later style with the same property key
|
|
67
|
+
// always wins, so combining gridColumns + rootResponsive in one array would
|
|
68
|
+
// silently drop the column template).
|
|
40
69
|
rootResponsive: {
|
|
41
|
-
gridTemplateColumns: {
|
|
42
|
-
default: null,
|
|
43
|
-
[`@container (max-width: ${STACK_BP})`]: '1fr',
|
|
44
|
-
},
|
|
45
70
|
// The card-list look needs the outer border to disappear in stack
|
|
46
71
|
// mode — each row paints its own border.
|
|
47
72
|
borderWidth: {
|
|
@@ -273,8 +298,21 @@ export const styles = css.create({
|
|
|
273
298
|
alignSelf: 'flex-start',
|
|
274
299
|
},
|
|
275
300
|
|
|
276
|
-
// Dynamic: grid columns applied on Root
|
|
301
|
+
// Dynamic: grid columns applied on Root (non-responsive path only).
|
|
277
302
|
gridColumns: (template: string) => ({
|
|
278
303
|
gridTemplateColumns: template,
|
|
279
304
|
}),
|
|
305
|
+
|
|
306
|
+
// Dynamic: grid columns + stack-mode collapse in one style.
|
|
307
|
+
// Used instead of gridColumns() when responsive=true so that both the
|
|
308
|
+
// explicit column template and the @container override live in the same
|
|
309
|
+
// StyleX style object. If they were in separate styles (gridColumns +
|
|
310
|
+
// rootResponsive), StyleX conflict resolution would keep only the LAST
|
|
311
|
+
// style's gridTemplateColumns class and silently discard the earlier one.
|
|
312
|
+
gridColumnsResponsive: (template: string) => ({
|
|
313
|
+
gridTemplateColumns: {
|
|
314
|
+
default: template,
|
|
315
|
+
[`@container (max-width: ${STACK_BP})`]: '1fr',
|
|
316
|
+
},
|
|
317
|
+
}),
|
|
280
318
|
})
|
|
@@ -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'
|