@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@duro-app/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.47.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.47.0"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@babel/preset-typescript": "^7.28.0",
|
|
@@ -22,8 +22,8 @@ interface FromTanstackBaseProps<TData> {
|
|
|
22
22
|
readonly variant?: TableVariant
|
|
23
23
|
readonly size?: TableSize
|
|
24
24
|
readonly responsive?: boolean
|
|
25
|
-
/** Min
|
|
26
|
-
* to Table.Root — see its `minColumnWidth`.
|
|
25
|
+
/** Min width (px) each flexible column keeps before the table scrolls
|
|
26
|
+
* horizontally. Forwarded to Table.Root — see its `minColumnWidth`. */
|
|
27
27
|
readonly minColumnWidth?: number
|
|
28
28
|
/** Render a SortChip above the grid (stack-mode only). Options derive from
|
|
29
29
|
* columns where `column.getCanSort()` returns true. Wires value/onChange
|
|
@@ -13,13 +13,6 @@ 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
|
|
23
16
|
}
|
|
24
17
|
|
|
25
18
|
/**
|
|
@@ -31,18 +24,12 @@ export interface SortChipProps {
|
|
|
31
24
|
* couple to @tanstack/react-table — the consumer wires `value` and
|
|
32
25
|
* `onChange` from whatever sort state they manage.
|
|
33
26
|
*/
|
|
34
|
-
export function SortChip({
|
|
35
|
-
options,
|
|
36
|
-
value,
|
|
37
|
-
onChange,
|
|
38
|
-
label = 'Sort by',
|
|
39
|
-
forceShow = false,
|
|
40
|
-
}: SortChipProps) {
|
|
27
|
+
export function SortChip({options, value, onChange, label = 'Sort by'}: SortChipProps) {
|
|
41
28
|
const current = value ? options.find((o) => o.id === value.id) : null
|
|
42
29
|
const triggerLabel = current ? `${label}: ${current.label} ${value!.desc ? '↓' : '↑'}` : label
|
|
43
30
|
|
|
44
31
|
return (
|
|
45
|
-
<html.div style={
|
|
32
|
+
<html.div style={styles.sortChip}>
|
|
46
33
|
<Menu.Root>
|
|
47
34
|
<Menu.Trigger>{triggerLabel}</Menu.Trigger>
|
|
48
35
|
<Menu.Popup>
|
|
@@ -1137,80 +1137,112 @@ export const SlotPropsOnRoot: Story = {
|
|
|
1137
1137
|
|
|
1138
1138
|
// --- Regression: many columns + long content must wrap, not overlap ---
|
|
1139
1139
|
//
|
|
1140
|
-
//
|
|
1141
|
-
//
|
|
1142
|
-
//
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
//
|
|
1146
|
-
//
|
|
1147
|
-
|
|
1148
|
-
|
|
1140
|
+
// The responsive model: above the `sm` (640px) breakpoint a table stays
|
|
1141
|
+
// tabular and, if its columns don't fit, scrolls horizontally (each column
|
|
1142
|
+
// keeps a `minColumnWidth` floor); below `sm` it cards up. One uniform
|
|
1143
|
+
// breakpoint for every table — no per-table content measurement.
|
|
1144
|
+
const respStyles = css.create({
|
|
1145
|
+
// 8 columns × 120px floor = 960px min. At 800px the grid overflows → the
|
|
1146
|
+
// wrapper scrolls sideways, but it's still tabular (above 640).
|
|
1147
|
+
scrollDense: {width: 800},
|
|
1148
|
+
// Same table at 1120px: 960 < 1120, so it fits with no scrollbar.
|
|
1149
|
+
roomy: {width: 1120},
|
|
1150
|
+
// Below the 640px card breakpoint → cards.
|
|
1151
|
+
phone: {width: 560},
|
|
1149
1152
|
})
|
|
1150
1153
|
|
|
1151
1154
|
const LONG_EMAIL = 'a-really-long-service-account.address@subdomain.example-company.com'
|
|
1152
1155
|
|
|
1153
|
-
|
|
1156
|
+
const grantsColumns = (
|
|
1157
|
+
<Table.Header>
|
|
1158
|
+
<Table.Row>
|
|
1159
|
+
<Table.HeaderCell>Principal</Table.HeaderCell>
|
|
1160
|
+
<Table.HeaderCell>Application</Table.HeaderCell>
|
|
1161
|
+
<Table.HeaderCell>Role</Table.HeaderCell>
|
|
1162
|
+
<Table.HeaderCell>Entitlement</Table.HeaderCell>
|
|
1163
|
+
<Table.HeaderCell>Resource</Table.HeaderCell>
|
|
1164
|
+
<Table.HeaderCell>Granted by</Table.HeaderCell>
|
|
1165
|
+
<Table.HeaderCell>Expires</Table.HeaderCell>
|
|
1166
|
+
<Table.HeaderCell>Status</Table.HeaderCell>
|
|
1167
|
+
</Table.Row>
|
|
1168
|
+
</Table.Header>
|
|
1169
|
+
)
|
|
1170
|
+
const grantsRow = (
|
|
1171
|
+
<Table.Row>
|
|
1172
|
+
<Table.Cell>alice</Table.Cell>
|
|
1173
|
+
<Table.Cell>Immich</Table.Cell>
|
|
1174
|
+
<Table.Cell>Editor</Table.Cell>
|
|
1175
|
+
<Table.Cell>library</Table.Cell>
|
|
1176
|
+
<Table.Cell>all</Table.Cell>
|
|
1177
|
+
<Table.Cell>daddy</Table.Cell>
|
|
1178
|
+
<Table.Cell>Never</Table.Cell>
|
|
1179
|
+
<Table.Cell>
|
|
1180
|
+
<Badge variant="success">Active</Badge>
|
|
1181
|
+
</Table.Cell>
|
|
1182
|
+
</Table.Row>
|
|
1183
|
+
)
|
|
1184
|
+
|
|
1185
|
+
// A dense table too narrow to fit but WIDER than the card breakpoint stays
|
|
1186
|
+
// tabular and scrolls sideways — the standard data-table behavior, and the
|
|
1187
|
+
// fix for grants carding up on a laptop.
|
|
1188
|
+
export const ScrollsHorizontallyWhenDense: Story = {
|
|
1189
|
+
name: 'Scrolls horizontally when dense (not cards)',
|
|
1154
1190
|
render: () => (
|
|
1155
|
-
<html.div style={
|
|
1191
|
+
<html.div style={respStyles.scrollDense}>
|
|
1156
1192
|
<Table.Root>
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
<Table.HeaderCell>Display Name</Table.HeaderCell>
|
|
1160
|
-
<Table.HeaderCell>Type</Table.HeaderCell>
|
|
1161
|
-
<Table.HeaderCell>Email</Table.HeaderCell>
|
|
1162
|
-
<Table.HeaderCell>Status</Table.HeaderCell>
|
|
1163
|
-
<Table.HeaderCell width="max-content">Actions</Table.HeaderCell>
|
|
1164
|
-
</Table.Row>
|
|
1165
|
-
</Table.Header>
|
|
1166
|
-
<Table.Body>
|
|
1167
|
-
<Table.Row>
|
|
1168
|
-
<Table.Cell>SMTP submission service account</Table.Cell>
|
|
1169
|
-
<Table.Cell>
|
|
1170
|
-
<Badge>User</Badge>
|
|
1171
|
-
</Table.Cell>
|
|
1172
|
-
<Table.Cell>{LONG_EMAIL}</Table.Cell>
|
|
1173
|
-
<Table.Cell>
|
|
1174
|
-
<Badge variant="warning">No certs</Badge>
|
|
1175
|
-
</Table.Cell>
|
|
1176
|
-
<Table.Cell isActions>
|
|
1177
|
-
<Button size="small">Send Cert</Button>
|
|
1178
|
-
<Button size="small" variant="secondary">
|
|
1179
|
-
Certificates
|
|
1180
|
-
</Button>
|
|
1181
|
-
<Button size="small" variant="danger">
|
|
1182
|
-
Revoke All
|
|
1183
|
-
</Button>
|
|
1184
|
-
</Table.Cell>
|
|
1185
|
-
</Table.Row>
|
|
1186
|
-
</Table.Body>
|
|
1193
|
+
{grantsColumns}
|
|
1194
|
+
<Table.Body>{grantsRow}</Table.Body>
|
|
1187
1195
|
</Table.Root>
|
|
1188
1196
|
</html.div>
|
|
1189
1197
|
),
|
|
1190
1198
|
play: async ({canvas}) => {
|
|
1191
|
-
const
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1199
|
+
const grid = canvas.getByRole('table')
|
|
1200
|
+
const scroller = grid.parentElement as HTMLElement
|
|
1201
|
+
await waitFor(async () => {
|
|
1202
|
+
// Still tabular: all 8 headers present (NOT carded up).
|
|
1203
|
+
await expect(canvas.queryAllByRole('columnheader')).toHaveLength(8)
|
|
1204
|
+
})
|
|
1205
|
+
// The wrapper scrolls: content (8 × 120px ≈ 960) exceeds its 800px box.
|
|
1206
|
+
await expect(scroller.scrollWidth).toBeGreaterThan(scroller.clientWidth + 2)
|
|
1196
1207
|
},
|
|
1197
1208
|
}
|
|
1198
1209
|
|
|
1199
|
-
//
|
|
1200
|
-
//
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
export const AutoStacksWhenCramped: Story = {
|
|
1204
|
-
name: 'Auto-stacks when columns get cramped',
|
|
1210
|
+
// Same table with room: fits, no horizontal scrollbar, stays tabular. Guards
|
|
1211
|
+
// against the old column-count heuristic that carded this up on a laptop.
|
|
1212
|
+
export const ManyColumnsFitNoScroll: Story = {
|
|
1213
|
+
name: 'Many columns fit — no scroll, no cards',
|
|
1205
1214
|
render: () => (
|
|
1206
|
-
<html.div style={
|
|
1215
|
+
<html.div style={respStyles.roomy}>
|
|
1216
|
+
<Table.Root>
|
|
1217
|
+
{grantsColumns}
|
|
1218
|
+
<Table.Body>{grantsRow}</Table.Body>
|
|
1219
|
+
</Table.Root>
|
|
1220
|
+
</html.div>
|
|
1221
|
+
),
|
|
1222
|
+
play: async ({canvas}) => {
|
|
1223
|
+
const grid = canvas.getByRole('table')
|
|
1224
|
+
const scroller = grid.parentElement as HTMLElement
|
|
1225
|
+
await waitFor(async () => {
|
|
1226
|
+
await expect(canvas.queryAllByRole('columnheader')).toHaveLength(8)
|
|
1227
|
+
})
|
|
1228
|
+
const tracks = getComputedStyle(grid).gridTemplateColumns.trim().split(/\s+/)
|
|
1229
|
+
await expect(tracks.length).toBe(8)
|
|
1230
|
+
// No horizontal overflow: the grid fits its container.
|
|
1231
|
+
await expect(scroller.scrollWidth).toBeLessThanOrEqual(scroller.clientWidth + 2)
|
|
1232
|
+
},
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// Below the single 640px card breakpoint every table cards up: header hides,
|
|
1236
|
+
// each row becomes a label/value card.
|
|
1237
|
+
export const CardsBelowMobileBreakpoint: Story = {
|
|
1238
|
+
name: 'Cards below the mobile breakpoint',
|
|
1239
|
+
render: () => (
|
|
1240
|
+
<html.div style={respStyles.phone}>
|
|
1207
1241
|
<Table.Root>
|
|
1208
1242
|
<Table.Header>
|
|
1209
1243
|
<Table.Row>
|
|
1210
1244
|
<Table.HeaderCell>Display Name</Table.HeaderCell>
|
|
1211
|
-
<Table.HeaderCell>Type</Table.HeaderCell>
|
|
1212
1245
|
<Table.HeaderCell>Email</Table.HeaderCell>
|
|
1213
|
-
<Table.HeaderCell>Status</Table.HeaderCell>
|
|
1214
1246
|
<Table.HeaderCell>Owner</Table.HeaderCell>
|
|
1215
1247
|
<Table.HeaderCell width="max-content">Actions</Table.HeaderCell>
|
|
1216
1248
|
</Table.Row>
|
|
@@ -1218,19 +1250,10 @@ export const AutoStacksWhenCramped: Story = {
|
|
|
1218
1250
|
<Table.Body>
|
|
1219
1251
|
<Table.Row>
|
|
1220
1252
|
<Table.Cell>SMTP submission service account</Table.Cell>
|
|
1221
|
-
<Table.Cell>
|
|
1222
|
-
<Badge>User</Badge>
|
|
1223
|
-
</Table.Cell>
|
|
1224
1253
|
<Table.Cell>{LONG_EMAIL}</Table.Cell>
|
|
1225
|
-
<Table.Cell>
|
|
1226
|
-
<Badge variant="warning">No certs</Badge>
|
|
1227
|
-
</Table.Cell>
|
|
1228
1254
|
<Table.Cell>daddy</Table.Cell>
|
|
1229
1255
|
<Table.Cell isActions>
|
|
1230
1256
|
<Button size="small">Send Cert</Button>
|
|
1231
|
-
<Button size="small" variant="secondary">
|
|
1232
|
-
Revoke
|
|
1233
|
-
</Button>
|
|
1234
1257
|
</Table.Cell>
|
|
1235
1258
|
</Table.Row>
|
|
1236
1259
|
</Table.Body>
|
|
@@ -1239,18 +1262,11 @@ export const AutoStacksWhenCramped: Story = {
|
|
|
1239
1262
|
),
|
|
1240
1263
|
play: async ({canvas}) => {
|
|
1241
1264
|
const grid = canvas.getByRole('table')
|
|
1242
|
-
//
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
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.
|
|
1265
|
+
// Single-track grid = card layout (the @container rule fires below 640).
|
|
1266
|
+
const tracks = getComputedStyle(grid).gridTemplateColumns.trim().split(/\s+/)
|
|
1267
|
+
await expect(tracks.length).toBe(1)
|
|
1268
|
+
// Header hidden → out of the a11y tree; each cell surfaces its label span.
|
|
1250
1269
|
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
1270
|
const ownerLabel = canvas
|
|
1255
1271
|
.getAllByText('Owner')
|
|
1256
1272
|
.find((el) => el.tagName.toLowerCase() === 'span') as HTMLElement
|
|
@@ -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,20 +31,8 @@ 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
34
|
}
|
|
44
35
|
|
|
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
36
|
const TableContext = createContext<TableContextValue | null>(null)
|
|
53
37
|
|
|
54
38
|
function useTable() {
|
|
@@ -123,7 +107,10 @@ function unwrapElementType(t: unknown): unknown {
|
|
|
123
107
|
return current
|
|
124
108
|
}
|
|
125
109
|
|
|
126
|
-
function extractColumnMeta(
|
|
110
|
+
function extractColumnMeta(
|
|
111
|
+
children: ReactNode,
|
|
112
|
+
minColumnWidth: number,
|
|
113
|
+
): {
|
|
127
114
|
template: string
|
|
128
115
|
compactTemplate: string
|
|
129
116
|
labels: string[]
|
|
@@ -142,14 +129,14 @@ function extractColumnMeta(children: ReactNode): {
|
|
|
142
129
|
(unwrappedType as {name?: string} | null)?.name ||
|
|
143
130
|
''
|
|
144
131
|
if (unwrappedType === HeaderCell || displayName === 'HeaderCell') {
|
|
145
|
-
// Default to minmax(
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
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)`
|
|
153
140
|
widths.push(width)
|
|
154
141
|
// `compactWidth` lets the consumer switch to a content-aware
|
|
155
142
|
// layout in the compact band while staying evenly distributed
|
|
@@ -188,14 +175,14 @@ interface RootProps {
|
|
|
188
175
|
children: ReactNode
|
|
189
176
|
variant?: TableVariant
|
|
190
177
|
size?: TableSize
|
|
191
|
-
/** Opt out of responsive
|
|
178
|
+
/** Opt out of responsive behavior (horizontal scroll + card breakpoint). Default true. */
|
|
192
179
|
responsive?: boolean
|
|
193
180
|
/**
|
|
194
|
-
* Minimum
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
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.
|
|
199
186
|
*/
|
|
200
187
|
minColumnWidth?: number
|
|
201
188
|
/**
|
|
@@ -216,45 +203,16 @@ export function Root({
|
|
|
216
203
|
variant = 'default',
|
|
217
204
|
size = 'md',
|
|
218
205
|
responsive = true,
|
|
219
|
-
minColumnWidth =
|
|
206
|
+
minColumnWidth = 120,
|
|
220
207
|
sortChip,
|
|
221
208
|
pagination,
|
|
222
209
|
}: RootProps) {
|
|
223
210
|
const inferredTemplateRef = useRef<string | null>(null)
|
|
224
|
-
const {template, compactTemplate, labels} = extractColumnMeta(children)
|
|
211
|
+
const {template, compactTemplate, labels} = extractColumnMeta(children, minColumnWidth)
|
|
225
212
|
if (template) {
|
|
226
213
|
inferredTemplateRef.current = template
|
|
227
214
|
}
|
|
228
215
|
|
|
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
|
-
|
|
258
216
|
const grid = (
|
|
259
217
|
<html.div
|
|
260
218
|
role="table"
|
|
@@ -272,45 +230,33 @@ export function Root({
|
|
|
272
230
|
: styles.gridColumns(template)
|
|
273
231
|
: undefined,
|
|
274
232
|
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,
|
|
278
233
|
]}
|
|
279
234
|
>
|
|
280
235
|
{children}
|
|
281
236
|
</html.div>
|
|
282
237
|
)
|
|
283
238
|
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
//
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
? cloneElement(sortChip as ReactElement<{forceShow?: boolean}>, {forceShow: true})
|
|
290
|
-
: 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
|
|
291
244
|
|
|
292
245
|
// Slots render unconditionally — `responsive=false` still wants its sort/
|
|
293
246
|
// pagination chrome. Only the containerType:inline-size wrapper is
|
|
294
|
-
// 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.
|
|
295
249
|
const body = (
|
|
296
250
|
<>
|
|
297
|
-
{
|
|
298
|
-
{
|
|
251
|
+
{sortChip}
|
|
252
|
+
{framedGrid}
|
|
299
253
|
{pagination}
|
|
300
254
|
</>
|
|
301
255
|
)
|
|
302
256
|
|
|
303
257
|
return (
|
|
304
|
-
<TableContext.Provider
|
|
305
|
-
|
|
306
|
-
>
|
|
307
|
-
{responsive ? (
|
|
308
|
-
<html.div ref={containerRef} style={styles.rootContainer}>
|
|
309
|
-
{body}
|
|
310
|
-
</html.div>
|
|
311
|
-
) : (
|
|
312
|
-
body
|
|
313
|
-
)}
|
|
258
|
+
<TableContext.Provider value={{variant, size, responsive, labels, inferredTemplateRef}}>
|
|
259
|
+
{responsive ? <html.div style={styles.rootContainer}>{body}</html.div> : body}
|
|
314
260
|
</TableContext.Provider>
|
|
315
261
|
)
|
|
316
262
|
}
|
|
@@ -318,10 +264,9 @@ export function Root({
|
|
|
318
264
|
// --- Header ---
|
|
319
265
|
|
|
320
266
|
export function Header({children}: {children: ReactNode}) {
|
|
321
|
-
const {stacked} = useTable()
|
|
322
267
|
return (
|
|
323
268
|
<HeaderContext.Provider value={true}>
|
|
324
|
-
<html.div role="rowgroup" style={
|
|
269
|
+
<html.div role="rowgroup" style={styles.header}>
|
|
325
270
|
{children}
|
|
326
271
|
</html.div>
|
|
327
272
|
</HeaderContext.Provider>
|
|
@@ -385,7 +330,7 @@ const INTERACTIVE_SELECTOR =
|
|
|
385
330
|
'button, a, input, select, textarea, [role="button"], [role="link"], [role="checkbox"], [role="menuitem"], [role="switch"], [role="tab"], [contenteditable="true"]'
|
|
386
331
|
|
|
387
332
|
export function Row({children, onClick, 'aria-label': ariaLabel}: RowProps) {
|
|
388
|
-
const {variant
|
|
333
|
+
const {variant} = useTable()
|
|
389
334
|
const isHeader = useContext(HeaderContext)
|
|
390
335
|
const rowIndex = useContext(RowIndexContext)
|
|
391
336
|
const isEvenRow = rowIndex >= 0 && rowIndex % 2 === 1
|
|
@@ -427,9 +372,6 @@ export function Row({children, onClick, 'aria-label': ariaLabel}: RowProps) {
|
|
|
427
372
|
!isHeader && styles.bodyRow,
|
|
428
373
|
!isHeader && variant === 'striped' && isEvenRow && styles.stripedEven,
|
|
429
374
|
isClickable && styles.clickableRow,
|
|
430
|
-
stacked && styles.rowStacked,
|
|
431
|
-
// bodyRowStacked last so its card background wins over stripedEven.
|
|
432
|
-
!isHeader && stacked && styles.bodyRowStacked,
|
|
433
375
|
]}
|
|
434
376
|
>
|
|
435
377
|
{childArray.map((child, index) => (
|
|
@@ -518,7 +460,7 @@ HeaderCell.displayName = 'HeaderCell'
|
|
|
518
460
|
// isActions=true — actions footers don't show a label.
|
|
519
461
|
|
|
520
462
|
export function Cell({children, isActions}: {children: ReactNode; isActions?: boolean}) {
|
|
521
|
-
const {size, variant, labels, responsive
|
|
463
|
+
const {size, variant, labels, responsive} = useTable()
|
|
522
464
|
const {index, total} = useContext(CellIndexContext)
|
|
523
465
|
const isLast = variant === 'bordered' && index === total - 1
|
|
524
466
|
const label = labels[index] ?? ''
|
|
@@ -532,20 +474,10 @@ export function Cell({children, isActions}: {children: ReactNode; isActions?: bo
|
|
|
532
474
|
variant === 'bordered' && styles.borderedCell,
|
|
533
475
|
isLast && styles.borderedCellLast,
|
|
534
476
|
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,
|
|
543
477
|
]}
|
|
544
478
|
>
|
|
545
479
|
{responsive && !isActions && label !== '' ? (
|
|
546
|
-
<html.span style={
|
|
547
|
-
{label}
|
|
548
|
-
</html.span>
|
|
480
|
+
<html.span style={styles.cellLabel}>{label}</html.span>
|
|
549
481
|
) : null}
|
|
550
482
|
{isActions ? children : <html.div style={styles.cellValue}>{children}</html.div>}
|
|
551
483
|
</html.div>
|