@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
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,6 +22,9 @@ interface FromTanstackBaseProps<TData> {
|
|
|
22
22
|
readonly variant?: TableVariant
|
|
23
23
|
readonly size?: TableSize
|
|
24
24
|
readonly responsive?: boolean
|
|
25
|
+
/** Min width (px) each flexible column keeps before the table scrolls
|
|
26
|
+
* horizontally. Forwarded to Table.Root — see its `minColumnWidth`. */
|
|
27
|
+
readonly minColumnWidth?: number
|
|
25
28
|
/** Render a SortChip above the grid (stack-mode only). Options derive from
|
|
26
29
|
* columns where `column.getCanSort()` returns true. Wires value/onChange
|
|
27
30
|
* through the TanStack sorting state automatically. */
|
|
@@ -68,6 +71,7 @@ export function FromTanstack<TData>({
|
|
|
68
71
|
variant = 'default',
|
|
69
72
|
size = 'md',
|
|
70
73
|
responsive = true,
|
|
74
|
+
minColumnWidth,
|
|
71
75
|
sortChip,
|
|
72
76
|
pagination,
|
|
73
77
|
rowKey,
|
|
@@ -82,6 +86,7 @@ export function FromTanstack<TData>({
|
|
|
82
86
|
variant={variant}
|
|
83
87
|
size={size}
|
|
84
88
|
responsive={responsive}
|
|
89
|
+
minColumnWidth={minColumnWidth}
|
|
85
90
|
sortChip={sortChipNode}
|
|
86
91
|
pagination={paginationNode}
|
|
87
92
|
>
|
|
@@ -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,167 +1137,123 @@ 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
|
-
|
|
1149
|
-
// 8 short-text columns with room to breathe: nothing overflows, so a
|
|
1150
|
-
// wide "grants-style" table must stay tabular even though it has many
|
|
1151
|
-
// columns (regression guard against the old column-count heuristic).
|
|
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.
|
|
1152
1149
|
roomy: {width: 1120},
|
|
1150
|
+
// Below the 640px card breakpoint → cards.
|
|
1151
|
+
phone: {width: 560},
|
|
1153
1152
|
})
|
|
1154
1153
|
|
|
1155
1154
|
const LONG_EMAIL = 'a-really-long-service-account.address@subdomain.example-company.com'
|
|
1156
1155
|
|
|
1157
|
-
|
|
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)',
|
|
1158
1190
|
render: () => (
|
|
1159
|
-
<html.div style={
|
|
1191
|
+
<html.div style={respStyles.scrollDense}>
|
|
1160
1192
|
<Table.Root>
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
<Table.HeaderCell>Display Name</Table.HeaderCell>
|
|
1164
|
-
<Table.HeaderCell>Type</Table.HeaderCell>
|
|
1165
|
-
<Table.HeaderCell>Email</Table.HeaderCell>
|
|
1166
|
-
<Table.HeaderCell>Status</Table.HeaderCell>
|
|
1167
|
-
<Table.HeaderCell width="max-content">Actions</Table.HeaderCell>
|
|
1168
|
-
</Table.Row>
|
|
1169
|
-
</Table.Header>
|
|
1170
|
-
<Table.Body>
|
|
1171
|
-
<Table.Row>
|
|
1172
|
-
<Table.Cell>SMTP submission service account</Table.Cell>
|
|
1173
|
-
<Table.Cell>
|
|
1174
|
-
<Badge>User</Badge>
|
|
1175
|
-
</Table.Cell>
|
|
1176
|
-
<Table.Cell>{LONG_EMAIL}</Table.Cell>
|
|
1177
|
-
<Table.Cell>
|
|
1178
|
-
<Badge variant="warning">No certs</Badge>
|
|
1179
|
-
</Table.Cell>
|
|
1180
|
-
<Table.Cell isActions>
|
|
1181
|
-
<Button size="small">Send Cert</Button>
|
|
1182
|
-
<Button size="small" variant="secondary">
|
|
1183
|
-
Certificates
|
|
1184
|
-
</Button>
|
|
1185
|
-
<Button size="small" variant="danger">
|
|
1186
|
-
Revoke All
|
|
1187
|
-
</Button>
|
|
1188
|
-
</Table.Cell>
|
|
1189
|
-
</Table.Row>
|
|
1190
|
-
</Table.Body>
|
|
1193
|
+
{grantsColumns}
|
|
1194
|
+
<Table.Body>{grantsRow}</Table.Body>
|
|
1191
1195
|
</Table.Root>
|
|
1192
1196
|
</html.div>
|
|
1193
1197
|
),
|
|
1194
1198
|
play: async ({canvas}) => {
|
|
1195
|
-
const
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
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)
|
|
1200
1207
|
},
|
|
1201
1208
|
}
|
|
1202
1209
|
|
|
1203
|
-
//
|
|
1204
|
-
//
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
export const AutoStacksWhenCramped: Story = {
|
|
1208
|
-
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',
|
|
1209
1214
|
render: () => (
|
|
1210
|
-
<html.div style={
|
|
1215
|
+
<html.div style={respStyles.roomy}>
|
|
1211
1216
|
<Table.Root>
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
<Table.HeaderCell>Display Name</Table.HeaderCell>
|
|
1215
|
-
<Table.HeaderCell>Type</Table.HeaderCell>
|
|
1216
|
-
<Table.HeaderCell>Email</Table.HeaderCell>
|
|
1217
|
-
<Table.HeaderCell>Status</Table.HeaderCell>
|
|
1218
|
-
<Table.HeaderCell>Owner</Table.HeaderCell>
|
|
1219
|
-
{/* Normal 1fr track (no max-content) so the buttons get squeezed
|
|
1220
|
-
and overflow — the exact condition the auto-stack detects. */}
|
|
1221
|
-
<Table.HeaderCell>Actions</Table.HeaderCell>
|
|
1222
|
-
</Table.Row>
|
|
1223
|
-
</Table.Header>
|
|
1224
|
-
<Table.Body>
|
|
1225
|
-
<Table.Row>
|
|
1226
|
-
<Table.Cell>SMTP submission service account</Table.Cell>
|
|
1227
|
-
<Table.Cell>
|
|
1228
|
-
<Badge>User</Badge>
|
|
1229
|
-
</Table.Cell>
|
|
1230
|
-
<Table.Cell>{LONG_EMAIL}</Table.Cell>
|
|
1231
|
-
<Table.Cell>
|
|
1232
|
-
<Badge variant="warning">No certs</Badge>
|
|
1233
|
-
</Table.Cell>
|
|
1234
|
-
<Table.Cell>daddy</Table.Cell>
|
|
1235
|
-
<Table.Cell isActions>
|
|
1236
|
-
<Button size="small">Send Cert</Button>
|
|
1237
|
-
<Button size="small" variant="secondary">
|
|
1238
|
-
Revoke
|
|
1239
|
-
</Button>
|
|
1240
|
-
</Table.Cell>
|
|
1241
|
-
</Table.Row>
|
|
1242
|
-
</Table.Body>
|
|
1217
|
+
{grantsColumns}
|
|
1218
|
+
<Table.Body>{grantsRow}</Table.Body>
|
|
1243
1219
|
</Table.Root>
|
|
1244
1220
|
</html.div>
|
|
1245
1221
|
),
|
|
1246
1222
|
play: async ({canvas}) => {
|
|
1247
1223
|
const grid = canvas.getByRole('table')
|
|
1248
|
-
|
|
1224
|
+
const scroller = grid.parentElement as HTMLElement
|
|
1249
1225
|
await waitFor(async () => {
|
|
1250
|
-
|
|
1251
|
-
const tracks = getComputedStyle(grid).gridTemplateColumns.trim().split(/\s+/)
|
|
1252
|
-
await expect(tracks.length).toBe(1)
|
|
1226
|
+
await expect(canvas.queryAllByRole('columnheader')).toHaveLength(8)
|
|
1253
1227
|
})
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
await expect(
|
|
1258
|
-
// Each body cell now surfaces its label span (label|value card rows). The
|
|
1259
|
-
// hidden header 'Owner' stays in the DOM, so pick the visible <span>.
|
|
1260
|
-
const ownerLabel = canvas
|
|
1261
|
-
.getAllByText('Owner')
|
|
1262
|
-
.find((el) => el.tagName.toLowerCase() === 'span') as HTMLElement
|
|
1263
|
-
await expect(ownerLabel).toBeTruthy()
|
|
1264
|
-
await expect(getComputedStyle(ownerLabel).display).toBe('block')
|
|
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)
|
|
1265
1232
|
},
|
|
1266
1233
|
}
|
|
1267
1234
|
|
|
1268
|
-
//
|
|
1269
|
-
//
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
// tabular because nothing actually overflows.
|
|
1273
|
-
export const ManyColumnsStayTabularWhenFitting: Story = {
|
|
1274
|
-
name: 'Many columns stay tabular when they fit',
|
|
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',
|
|
1275
1239
|
render: () => (
|
|
1276
|
-
<html.div style={
|
|
1240
|
+
<html.div style={respStyles.phone}>
|
|
1277
1241
|
<Table.Root>
|
|
1278
1242
|
<Table.Header>
|
|
1279
1243
|
<Table.Row>
|
|
1280
|
-
<Table.HeaderCell>
|
|
1281
|
-
<Table.HeaderCell>
|
|
1282
|
-
<Table.HeaderCell>
|
|
1283
|
-
<Table.HeaderCell>
|
|
1284
|
-
<Table.HeaderCell>Resource</Table.HeaderCell>
|
|
1285
|
-
<Table.HeaderCell>Granted by</Table.HeaderCell>
|
|
1286
|
-
<Table.HeaderCell>Expires</Table.HeaderCell>
|
|
1287
|
-
<Table.HeaderCell>Status</Table.HeaderCell>
|
|
1244
|
+
<Table.HeaderCell>Display Name</Table.HeaderCell>
|
|
1245
|
+
<Table.HeaderCell>Email</Table.HeaderCell>
|
|
1246
|
+
<Table.HeaderCell>Owner</Table.HeaderCell>
|
|
1247
|
+
<Table.HeaderCell width="max-content">Actions</Table.HeaderCell>
|
|
1288
1248
|
</Table.Row>
|
|
1289
1249
|
</Table.Header>
|
|
1290
1250
|
<Table.Body>
|
|
1291
1251
|
<Table.Row>
|
|
1292
|
-
<Table.Cell>
|
|
1293
|
-
<Table.Cell>
|
|
1294
|
-
<Table.Cell>Editor</Table.Cell>
|
|
1295
|
-
<Table.Cell>library</Table.Cell>
|
|
1296
|
-
<Table.Cell>all</Table.Cell>
|
|
1252
|
+
<Table.Cell>SMTP submission service account</Table.Cell>
|
|
1253
|
+
<Table.Cell>{LONG_EMAIL}</Table.Cell>
|
|
1297
1254
|
<Table.Cell>daddy</Table.Cell>
|
|
1298
|
-
<Table.Cell>
|
|
1299
|
-
|
|
1300
|
-
<Badge variant="success">Active</Badge>
|
|
1255
|
+
<Table.Cell isActions>
|
|
1256
|
+
<Button size="small">Send Cert</Button>
|
|
1301
1257
|
</Table.Cell>
|
|
1302
1258
|
</Table.Row>
|
|
1303
1259
|
</Table.Body>
|
|
@@ -1306,12 +1262,15 @@ export const ManyColumnsStayTabularWhenFitting: Story = {
|
|
|
1306
1262
|
),
|
|
1307
1263
|
play: async ({canvas}) => {
|
|
1308
1264
|
const grid = canvas.getByRole('table')
|
|
1309
|
-
//
|
|
1310
|
-
// tabular: 8 header cells still in the a11y tree, grid keeps 8 tracks.
|
|
1311
|
-
await waitFor(async () => {
|
|
1312
|
-
await expect(canvas.queryAllByRole('columnheader')).toHaveLength(8)
|
|
1313
|
-
})
|
|
1265
|
+
// Single-track grid = card layout (the @container rule fires below 640).
|
|
1314
1266
|
const tracks = getComputedStyle(grid).gridTemplateColumns.trim().split(/\s+/)
|
|
1315
|
-
await expect(tracks.length).toBe(
|
|
1267
|
+
await expect(tracks.length).toBe(1)
|
|
1268
|
+
// Header hidden → out of the a11y tree; each cell surfaces its label span.
|
|
1269
|
+
await expect(canvas.queryAllByRole('columnheader')).toHaveLength(0)
|
|
1270
|
+
const ownerLabel = canvas
|
|
1271
|
+
.getAllByText('Owner')
|
|
1272
|
+
.find((el) => el.tagName.toLowerCase() === 'span') as HTMLElement
|
|
1273
|
+
await expect(ownerLabel).toBeTruthy()
|
|
1274
|
+
await expect(getComputedStyle(ownerLabel).display).toBe('block')
|
|
1316
1275
|
},
|
|
1317
1276
|
}
|