basalt-ui 1.31.0 → 1.32.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/CHANGELOG.md +23 -0
- package/MIGRATING.md +101 -17
- package/agent/rules/basalt-mantine.md +4 -4
- package/configs/oxlint-plugin.js +159 -42
- package/configs/oxlint-plugin.test.ts +182 -0
- package/dist/charts/kinds/BandStrip.d.ts.map +1 -1
- package/dist/charts/kinds/BandStrip.js +18 -66
- package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
- package/dist/charts/kinds/DualPanel.js +5 -26
- package/dist/charts/kinds/MirroredBars.d.ts.map +1 -1
- package/dist/charts/kinds/MirroredBars.js +10 -68
- package/dist/charts/kinds/band-chrome.d.ts +31 -0
- package/dist/charts/kinds/band-chrome.d.ts.map +1 -0
- package/dist/charts/kinds/band-chrome.js +71 -0
- package/dist/charts/primitives/CartesianChart.d.ts.map +1 -1
- package/dist/charts/primitives/CartesianChart.js +49 -76
- package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
- package/dist/charts/primitives/ChartCard.js +5 -23
- package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
- package/dist/charts/primitives/ChartFrame.js +23 -40
- package/dist/charts/primitives/ChartLegend.d.ts.map +1 -1
- package/dist/charts/primitives/ChartLegend.js +5 -133
- package/dist/charts/primitives/ChartTooltip.d.ts +5 -0
- package/dist/charts/primitives/ChartTooltip.d.ts.map +1 -1
- package/dist/charts/primitives/ChartTooltip.js +16 -1
- package/dist/charts/primitives/HoverOverlay.d.ts +24 -9
- package/dist/charts/primitives/HoverOverlay.d.ts.map +1 -1
- package/dist/charts/primitives/HoverOverlay.js +17 -1
- package/dist/charts/primitives/chart-card-context.d.ts +17 -13
- package/dist/charts/primitives/chart-card-context.d.ts.map +1 -1
- package/dist/charts/primitives/chart-card-context.js +10 -5
- package/dist/charts/primitives/chart-frame-layout.d.ts.map +1 -1
- package/dist/charts/primitives/chart-frame-layout.js +25 -31
- package/dist/charts/primitives/legend-disclosure.d.ts +27 -0
- package/dist/charts/primitives/legend-disclosure.d.ts.map +1 -0
- package/dist/charts/primitives/legend-disclosure.js +153 -0
- package/dist/controls/controls.module.css +5 -0
- package/dist/controls/sync-button.module.css +1 -0
- package/dist/dashboard/stat-group.module.css +19 -64
- package/dist/dashboard/widget-grid.module.css +11 -38
- package/dist/data/data-table-state.d.ts +83 -0
- package/dist/data/data-table-state.d.ts.map +1 -0
- package/dist/data/data-table-state.js +139 -0
- package/dist/data/data-table.d.ts +33 -2
- package/dist/data/data-table.d.ts.map +1 -1
- package/dist/data/data-table.js +179 -277
- package/dist/forms/form-layout.module.css +3 -13
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +6 -5
- package/dist/notifications/center.d.ts.map +1 -1
- package/dist/provider/index.d.ts.map +1 -1
- package/dist/provider/index.js +12 -10
- package/dist/shell/aside-docking.d.ts +31 -0
- package/dist/shell/aside-docking.d.ts.map +1 -0
- package/dist/shell/aside-docking.js +19 -0
- package/dist/shell/index.d.ts +0 -11
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +13 -14
- package/dist/shell/use-breakpoint.d.ts +1 -1
- package/dist/theme/floating.module.css +1 -0
- package/dist/theme-lab/derive-controls.d.ts.map +1 -1
- package/llms.txt +2 -2
- package/package.json +2 -1
- package/src/charts/kinds/BandStrip.tsx +18 -64
- package/src/charts/kinds/DualPanel.tsx +4 -24
- package/src/charts/kinds/MirroredBars.tsx +12 -66
- package/src/charts/kinds/band-chrome.tsx +91 -0
- package/src/charts/primitives/CartesianChart.tsx +79 -122
- package/src/charts/primitives/ChartCard.tsx +5 -27
- package/src/charts/primitives/ChartFrame.tsx +37 -76
- package/src/charts/primitives/ChartLegend.tsx +7 -154
- package/src/charts/primitives/ChartTooltip.tsx +16 -0
- package/src/charts/primitives/HoverOverlay.tsx +52 -21
- package/src/charts/primitives/chart-card-context.ts +25 -16
- package/src/charts/primitives/chart-frame-layout.ts +44 -56
- package/src/charts/primitives/legend-disclosure.tsx +177 -0
- package/src/controls/controls.module.css +5 -0
- package/src/controls/sync-button.module.css +1 -0
- package/src/dashboard/stat-group.module.css +19 -64
- package/src/dashboard/widget-grid.module.css +11 -38
- package/src/data/data-table-state.ts +218 -0
- package/src/data/data-table.tsx +127 -218
- package/src/forms/form-layout.module.css +3 -13
- package/src/guard/index.ts +21 -16
- package/src/notifications/center.tsx +3 -0
- package/src/provider/index.tsx +18 -17
- package/src/shell/aside-docking.ts +54 -0
- package/src/shell/index.tsx +15 -37
- package/src/shell/use-breakpoint.ts +2 -2
- package/src/theme/floating.module.css +1 -0
- package/src/theme-lab/derive-controls.tsx +2 -0
package/src/data/data-table.tsx
CHANGED
|
@@ -37,8 +37,10 @@ import {
|
|
|
37
37
|
Checkbox,
|
|
38
38
|
Group,
|
|
39
39
|
Pagination,
|
|
40
|
+
ScrollArea,
|
|
40
41
|
Select,
|
|
41
42
|
Skeleton,
|
|
43
|
+
Stack,
|
|
42
44
|
Table,
|
|
43
45
|
Text,
|
|
44
46
|
TextInput,
|
|
@@ -48,7 +50,6 @@ import type {
|
|
|
48
50
|
Column,
|
|
49
51
|
ColumnDef,
|
|
50
52
|
ColumnFiltersState,
|
|
51
|
-
ColumnPinningState,
|
|
52
53
|
FilterFn,
|
|
53
54
|
PaginationState,
|
|
54
55
|
Row,
|
|
@@ -56,7 +57,6 @@ import type {
|
|
|
56
57
|
RowSelectionState,
|
|
57
58
|
SortingState,
|
|
58
59
|
Table as TanstackTable,
|
|
59
|
-
Updater,
|
|
60
60
|
VisibilityState,
|
|
61
61
|
} from '@tanstack/react-table'
|
|
62
62
|
import {
|
|
@@ -67,14 +67,8 @@ import {
|
|
|
67
67
|
getSortedRowModel,
|
|
68
68
|
useReactTable,
|
|
69
69
|
} from '@tanstack/react-table'
|
|
70
|
-
import type {
|
|
71
|
-
|
|
72
|
-
KeyboardEvent as ReactKeyboardEvent,
|
|
73
|
-
MouseEvent as ReactMouseEvent,
|
|
74
|
-
ReactNode,
|
|
75
|
-
RefObject,
|
|
76
|
-
} from 'react'
|
|
77
|
-
import { Fragment, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
|
70
|
+
import type { CSSProperties, MouseEvent as ReactMouseEvent, ReactNode, RefObject } from 'react'
|
|
71
|
+
import { Fragment, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
|
78
72
|
import { cx } from '../common/props'
|
|
79
73
|
import type { BasaltProps, SlotStylesProps } from '../common/props'
|
|
80
74
|
import { BASALT_PREFIX } from '../common/errors'
|
|
@@ -96,6 +90,13 @@ import type { EnumField, FieldHandle, MultiField } from '../state'
|
|
|
96
90
|
import { WidgetHeader } from '../dashboard'
|
|
97
91
|
import { ErrorState } from '../dashboard/query-state'
|
|
98
92
|
import type { QueryStateLike } from '../dashboard/query-state'
|
|
93
|
+
import {
|
|
94
|
+
activationProps,
|
|
95
|
+
EMPTY_FOLD_SET,
|
|
96
|
+
useCardProjection,
|
|
97
|
+
useDataTableState,
|
|
98
|
+
useRowDisclosure,
|
|
99
|
+
} from './data-table-state'
|
|
99
100
|
import { dataQueryBranch } from './query-branch'
|
|
100
101
|
import { isDev } from '../common/is-dev'
|
|
101
102
|
import classes from './data-table.module.css'
|
|
@@ -388,8 +389,9 @@ function BulkActionBar({ count, actions }: { count: number; actions: BarAction[]
|
|
|
388
389
|
|
|
389
390
|
/**
|
|
390
391
|
* The four boxes `BasaltDataTable` paints: `root` is the outer fragment's replacement wrapper,
|
|
391
|
-
* `toolbar` is the search/facets/actions row, `table` is
|
|
392
|
-
* the pagination
|
|
392
|
+
* `toolbar` is the search/facets/actions row, `table` is whichever body renders — the Mantine
|
|
393
|
+
* `Table`, or the card list's `Stack` while `renderCard` projects — and `footer` is the pagination
|
|
394
|
+
* bar.
|
|
393
395
|
*/
|
|
394
396
|
export type BasaltDataTableSlot = 'root' | 'toolbar' | 'table' | 'footer'
|
|
395
397
|
|
|
@@ -595,6 +597,37 @@ export type BasaltDataTableProps<T> = BasaltProps &
|
|
|
595
597
|
* <BasaltDataTable data={rows} columns={columns} onRowActivate={(row) => setSelected(row)} />
|
|
596
598
|
*/
|
|
597
599
|
onRowActivate?: (row: T) => void
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* A compact card projection of one row, rendered INSTEAD of the table while the table's own box
|
|
603
|
+
* is narrower than the `regular` container class (`CONTAINER_CLASSES`, 480px) — keyed on the
|
|
604
|
+
* width this table actually has, never the viewport, so a table in a narrow aside or a split
|
|
605
|
+
* column swaps on a desktop too, and a full-width one does not swap early on a tablet.
|
|
606
|
+
*
|
|
607
|
+
* The cards are the SAME processed rows the table would draw — sorted, filtered and paged — so a
|
|
608
|
+
* list can never disagree with its table; a fixed card order is `initialSorting`, not a second
|
|
609
|
+
* sort. Everything around the body stays: the header (title · count · toolbar), the bulk bar,
|
|
610
|
+
* the pager, and `query` / `isLoading` / `emptyState`. `onRowActivate` makes each card
|
|
611
|
+
* activatable exactly as it does a row (click, or Enter on the focused card). The selection
|
|
612
|
+
* checkbox is not projected — a card list with a selection keeps its bulk bar but no ticks.
|
|
613
|
+
*
|
|
614
|
+
* An activatable card takes `tabIndex=0` + `data-activatable`, never `role="button"`: a card is
|
|
615
|
+
* where a row's actions and links land (argo's all do), and a `button` role makes every
|
|
616
|
+
* descendant presentational, so those nested controls would vanish from the accessibility tree.
|
|
617
|
+
* The affordance is the cursor and the focus ring, as on the row. `maxHeight` caps the card list
|
|
618
|
+
* in a `ScrollArea`; `minWidth` does not apply — it is a horizontal floor for columns, and a
|
|
619
|
+
* card list has none.
|
|
620
|
+
*
|
|
621
|
+
* Reach for it when the row carries more than the columns a phone can hold (a status + label +
|
|
622
|
+
* age header, a clamped title, a prose line). A table that only needs to shed a column or two
|
|
623
|
+
* already has that: the column fold hides the lowest-priority columns behind a per-row
|
|
624
|
+
* disclosure with no prop at all. Server render and first paint are the table.
|
|
625
|
+
*
|
|
626
|
+
* @example
|
|
627
|
+
* <BasaltDataTable data={items} columns={columns} onRowActivate={open}
|
|
628
|
+
* renderCard={(item) => <ItemCard item={item} />} />
|
|
629
|
+
*/
|
|
630
|
+
renderCard?: (row: T) => ReactNode
|
|
598
631
|
/**
|
|
599
632
|
* Prepends a checkbox column (header = select-all on the page) and arms TanStack's row-selection
|
|
600
633
|
* feature. Selection is uncontrolled unless `rowSelection` is passed.
|
|
@@ -795,6 +828,8 @@ function SortIndicator({ direction }: { direction: 'asc' | 'desc' | false }) {
|
|
|
795
828
|
)
|
|
796
829
|
}
|
|
797
830
|
|
|
831
|
+
const ARIA_SORT = { asc: 'ascending', desc: 'descending' } as const
|
|
832
|
+
|
|
798
833
|
// ── Numeral cell idiom (docs/DESIGN-SPEC.md §3: "Stat/table numeral: mono, 12-12.5px, weight
|
|
799
834
|
// 500, ink") — auto-detected from the cell's raw value (no column-level opt-in prop needed) so
|
|
800
835
|
// every numeric column gets tabular, mono figures with zero call-site configuration.
|
|
@@ -940,8 +975,6 @@ function foldOrder<T>(columns: readonly ColumnDef<T, unknown>[]): string[] {
|
|
|
940
975
|
* that hook, so it carries no styling and needs no CSS-module counterpart. */
|
|
941
976
|
const FOLD_ID_ATTR = 'data-basalt-fold-id'
|
|
942
977
|
|
|
943
|
-
const EMPTY_FOLD_SET: ReadonlySet<string> = new Set()
|
|
944
|
-
|
|
945
978
|
/**
|
|
946
979
|
* Pure decision: fold columns off the FRONT of `order` (already fold-first sorted) until the
|
|
947
980
|
* still-unfolded ones — plus `overhead` (the columns excluded from candidacy altogether — pinned,
|
|
@@ -1294,32 +1327,23 @@ export function BasaltDataTable<T>(props: BasaltDataTableProps<T>) {
|
|
|
1294
1327
|
isLoading = false,
|
|
1295
1328
|
query,
|
|
1296
1329
|
skeletonRows = 5,
|
|
1297
|
-
initialSorting,
|
|
1298
|
-
onSortingChange,
|
|
1299
1330
|
enableGlobalFilter = false,
|
|
1300
1331
|
globalFilterPlaceholder = 'Search…',
|
|
1301
1332
|
searchIcon,
|
|
1302
|
-
initialGlobalFilter,
|
|
1303
|
-
onGlobalFilterChange,
|
|
1304
1333
|
facets,
|
|
1305
|
-
onColumnFiltersChange,
|
|
1306
1334
|
manualFiltering = false,
|
|
1307
1335
|
actions,
|
|
1308
1336
|
onRowActivate,
|
|
1337
|
+
renderCard,
|
|
1309
1338
|
enableRowSelection = false,
|
|
1310
|
-
rowSelection,
|
|
1311
|
-
onRowSelectionChange,
|
|
1312
1339
|
getRowId,
|
|
1313
1340
|
bulkActions,
|
|
1314
1341
|
enablePagination = false,
|
|
1315
1342
|
pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS,
|
|
1316
|
-
initialPagination,
|
|
1317
|
-
onPaginationChange,
|
|
1318
1343
|
manualPagination = false,
|
|
1319
1344
|
rowCount,
|
|
1320
1345
|
pageCount,
|
|
1321
1346
|
enablePinning = false,
|
|
1322
|
-
initialColumnPinning,
|
|
1323
1347
|
maxHeight,
|
|
1324
1348
|
minWidth,
|
|
1325
1349
|
stickyHeader,
|
|
@@ -1332,17 +1356,8 @@ export function BasaltDataTable<T>(props: BasaltDataTableProps<T>) {
|
|
|
1332
1356
|
style,
|
|
1333
1357
|
classNames,
|
|
1334
1358
|
} = props
|
|
1335
|
-
const
|
|
1336
|
-
const
|
|
1337
|
-
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
|
|
1338
|
-
const [pagination, setPagination] = useState<PaginationState>(
|
|
1339
|
-
initialPagination ?? { pageIndex: 0, pageSize: pageSizeOptions[0] ?? 10 },
|
|
1340
|
-
)
|
|
1341
|
-
const [columnPinning, setColumnPinning] = useState<ColumnPinningState>(initialColumnPinning ?? {})
|
|
1342
|
-
// Uncontrolled by default; `rowSelection`, when passed, is the truth and this only mirrors it so
|
|
1343
|
-
// the updater below has a base to apply against.
|
|
1344
|
-
const [internalRowSelection, setInternalRowSelection] = useState<RowSelectionState>({})
|
|
1345
|
-
const rowSelectionState = rowSelection ?? internalRowSelection
|
|
1359
|
+
const tableState = useDataTableState(props, pageSizeOptions[0] ?? 10)
|
|
1360
|
+
const { globalFilter, columnPinning, onGlobalFilterChange: handleGlobalFilterChange } = tableState
|
|
1346
1361
|
|
|
1347
1362
|
// `stickyHeaderOffset` is DROPPED whenever the table owns a scroll container — see
|
|
1348
1363
|
// `resolvedStickyHeaderOffset` below for why it is always wrong there. Dropping it silently is
|
|
@@ -1385,69 +1400,6 @@ export function BasaltDataTable<T>(props: BasaltDataTableProps<T>) {
|
|
|
1385
1400
|
],
|
|
1386
1401
|
)
|
|
1387
1402
|
|
|
1388
|
-
const handleSortingChange = useCallback(
|
|
1389
|
-
(updater: Updater<SortingState>) => {
|
|
1390
|
-
setSorting((prev) => {
|
|
1391
|
-
const next = typeof updater === 'function' ? updater(prev) : updater
|
|
1392
|
-
onSortingChange?.(next)
|
|
1393
|
-
return next
|
|
1394
|
-
})
|
|
1395
|
-
},
|
|
1396
|
-
[onSortingChange],
|
|
1397
|
-
)
|
|
1398
|
-
|
|
1399
|
-
const handleGlobalFilterChange = useCallback(
|
|
1400
|
-
(updater: Updater<string>) => {
|
|
1401
|
-
setGlobalFilter((prev) => {
|
|
1402
|
-
const next = typeof updater === 'function' ? updater(prev) : updater
|
|
1403
|
-
onGlobalFilterChange?.(next)
|
|
1404
|
-
return next
|
|
1405
|
-
})
|
|
1406
|
-
},
|
|
1407
|
-
[onGlobalFilterChange],
|
|
1408
|
-
)
|
|
1409
|
-
|
|
1410
|
-
const handleColumnFiltersChange = useCallback(
|
|
1411
|
-
(updater: Updater<ColumnFiltersState>) => {
|
|
1412
|
-
setColumnFilters((prev) => {
|
|
1413
|
-
const next = typeof updater === 'function' ? updater(prev) : updater
|
|
1414
|
-
onColumnFiltersChange?.(next)
|
|
1415
|
-
return next
|
|
1416
|
-
})
|
|
1417
|
-
},
|
|
1418
|
-
[onColumnFiltersChange],
|
|
1419
|
-
)
|
|
1420
|
-
|
|
1421
|
-
const handlePaginationChange = useCallback(
|
|
1422
|
-
(updater: Updater<PaginationState>) => {
|
|
1423
|
-
setPagination((prev) => {
|
|
1424
|
-
const next = typeof updater === 'function' ? updater(prev) : updater
|
|
1425
|
-
onPaginationChange?.(next)
|
|
1426
|
-
return next
|
|
1427
|
-
})
|
|
1428
|
-
},
|
|
1429
|
-
[onPaginationChange],
|
|
1430
|
-
)
|
|
1431
|
-
|
|
1432
|
-
const handleRowSelectionChange = useCallback(
|
|
1433
|
-
(updater: Updater<RowSelectionState>) => {
|
|
1434
|
-
// CONTROLLED: the caller's map is the base AND the only writer. Mirroring it into state too
|
|
1435
|
-
// would leave a copy that the caller never moves, and that stale copy becomes the selection
|
|
1436
|
-
// the moment `rowSelection` goes back to `undefined` — plus every tick would render twice for
|
|
1437
|
-
// one change. Report the next map and let the caller own it.
|
|
1438
|
-
if (rowSelection !== undefined) {
|
|
1439
|
-
onRowSelectionChange?.(typeof updater === 'function' ? updater(rowSelection) : updater)
|
|
1440
|
-
return
|
|
1441
|
-
}
|
|
1442
|
-
setInternalRowSelection((prev) => {
|
|
1443
|
-
const next = typeof updater === 'function' ? updater(prev) : updater
|
|
1444
|
-
onRowSelectionChange?.(next)
|
|
1445
|
-
return next
|
|
1446
|
-
})
|
|
1447
|
-
},
|
|
1448
|
-
[rowSelection, onRowSelectionChange],
|
|
1449
|
-
)
|
|
1450
|
-
|
|
1451
1403
|
// Facets are TanStack `columnFilters` entries under the hood — inject an exact-match (or
|
|
1452
1404
|
// any-of, for `multiple`) filterFn onto the matching column so the built-in `includesString`
|
|
1453
1405
|
// auto-filter (substring match) never misfires against a Select's exact option value.
|
|
@@ -1538,8 +1490,11 @@ export function BasaltDataTable<T>(props: BasaltDataTableProps<T>) {
|
|
|
1538
1490
|
columnFoldFixedIds: [...excluded],
|
|
1539
1491
|
}
|
|
1540
1492
|
}, [columns, columnPinning])
|
|
1493
|
+
// The card projection unmounts the table, so the fold and the containment wrapper go inactive with
|
|
1494
|
+
// it — their measuring effects re-attach to the remounted table when the width returns.
|
|
1495
|
+
const projection = useCardProjection(renderCard !== undefined)
|
|
1541
1496
|
const columnFold = useColumnFold({
|
|
1542
|
-
active: foldEligible,
|
|
1497
|
+
active: foldEligible && !projection.cards,
|
|
1543
1498
|
order: columnFoldOrder,
|
|
1544
1499
|
fixedIds: columnFoldFixedIds,
|
|
1545
1500
|
})
|
|
@@ -1548,74 +1503,28 @@ export function BasaltDataTable<T>(props: BasaltDataTableProps<T>) {
|
|
|
1548
1503
|
[columnFold.folded],
|
|
1549
1504
|
)
|
|
1550
1505
|
const hasFolded = columnFold.folded.size > 0
|
|
1551
|
-
const
|
|
1552
|
-
const toggleFoldRow = useCallback((rowId: string) => {
|
|
1553
|
-
setExpandedFoldRows((current) => {
|
|
1554
|
-
const next = new Set(current)
|
|
1555
|
-
if (next.has(rowId)) next.delete(rowId)
|
|
1556
|
-
else next.add(rowId)
|
|
1557
|
-
return next
|
|
1558
|
-
})
|
|
1559
|
-
}, [])
|
|
1560
|
-
// `row.id` defaults to the row's index in `data` (no `getRowId` passed) — stable across a
|
|
1561
|
-
// client-side sort/filter/pagination (they reorder or subset the SAME core rows). PRUNED rather
|
|
1562
|
-
// than cleared on every `data` change: a non-memoized `data` prop (an inline `.map()`, an
|
|
1563
|
-
// unstable query result) hands a brand-new array of the SAME rows on every parent re-render, and
|
|
1564
|
-
// clearing on that reference alone collapsed every open disclosure the instant anything upstream
|
|
1565
|
-
// re-rendered.
|
|
1566
|
-
//
|
|
1567
|
-
// Pruning by ID ALONE is only safe with `getRowId` — a caller-declared identity that genuinely
|
|
1568
|
-
// survives a page turn or a refetch, so an id no longer present (a manual-pagination page turn, a
|
|
1569
|
-
// delete) still has to go, or a differently-shaped record seated at a reused id would silently
|
|
1570
|
-
// read as pre-expanded. Without `getRowId` the id IS the array index, and an index survives a
|
|
1571
|
-
// refetch trivially — it is still "0", "1", … even when every object behind it is now a
|
|
1572
|
-
// completely different record. Keeping an expanded index across THAT would silently attach the
|
|
1573
|
-
// old disclosure to a stranger's row. So the index-id lane instead keeps a `prevData` snapshot and
|
|
1574
|
-
// asks the narrower, correct question: is the object AT this index still the same reference as
|
|
1575
|
-
// last time? A non-memoized array of the SAME rows answers yes at every index (the case above);
|
|
1576
|
-
// a page turn or a refetch answers no.
|
|
1577
|
-
const prevDataRef = useRef(data)
|
|
1578
|
-
useEffect(() => {
|
|
1579
|
-
const prevData = prevDataRef.current
|
|
1580
|
-
prevDataRef.current = data
|
|
1581
|
-
setExpandedFoldRows((current) => {
|
|
1582
|
-
if (current.size === 0) return current
|
|
1583
|
-
let next: ReadonlySet<string>
|
|
1584
|
-
if (getRowId === undefined) {
|
|
1585
|
-
next = new Set(
|
|
1586
|
-
[...current].filter((id) => {
|
|
1587
|
-
const index = Number(id)
|
|
1588
|
-
return data[index] !== undefined && data[index] === prevData[index]
|
|
1589
|
-
}),
|
|
1590
|
-
)
|
|
1591
|
-
} else {
|
|
1592
|
-
const liveIds = new Set(data.map((row, index) => getRowId(row, index)))
|
|
1593
|
-
next = new Set([...current].filter((id) => liveIds.has(id)))
|
|
1594
|
-
}
|
|
1595
|
-
return next.size === current.size ? current : next
|
|
1596
|
-
})
|
|
1597
|
-
}, [data, getRowId])
|
|
1506
|
+
const foldRows = useRowDisclosure({ data, getRowId })
|
|
1598
1507
|
|
|
1599
1508
|
const table = useReactTable<T>({
|
|
1600
1509
|
data,
|
|
1601
1510
|
columns: tableColumns,
|
|
1602
1511
|
state: {
|
|
1603
|
-
sorting,
|
|
1512
|
+
sorting: tableState.sorting,
|
|
1604
1513
|
globalFilter,
|
|
1605
|
-
columnFilters,
|
|
1514
|
+
columnFilters: tableState.columnFilters,
|
|
1606
1515
|
columnPinning,
|
|
1607
1516
|
columnVisibility,
|
|
1608
|
-
...(enablePagination && { pagination }),
|
|
1609
|
-
...(enableRowSelection && { rowSelection:
|
|
1517
|
+
...(enablePagination && { pagination: tableState.pagination }),
|
|
1518
|
+
...(enableRowSelection && { rowSelection: tableState.rowSelection }),
|
|
1610
1519
|
},
|
|
1611
|
-
onSortingChange:
|
|
1520
|
+
onSortingChange: tableState.onSortingChange,
|
|
1612
1521
|
onGlobalFilterChange: handleGlobalFilterChange,
|
|
1613
|
-
onColumnFiltersChange:
|
|
1614
|
-
onColumnPinningChange:
|
|
1615
|
-
...(enablePagination && { onPaginationChange:
|
|
1522
|
+
onColumnFiltersChange: tableState.onColumnFiltersChange,
|
|
1523
|
+
onColumnPinningChange: tableState.onColumnPinningChange,
|
|
1524
|
+
...(enablePagination && { onPaginationChange: tableState.onPaginationChange }),
|
|
1616
1525
|
...(enableRowSelection && {
|
|
1617
1526
|
enableRowSelection: true,
|
|
1618
|
-
onRowSelectionChange:
|
|
1527
|
+
onRowSelectionChange: tableState.onRowSelectionChange,
|
|
1619
1528
|
}),
|
|
1620
1529
|
...(getRowId !== undefined && { getRowId }),
|
|
1621
1530
|
getCoreRowModel: getCoreRowModel(),
|
|
@@ -1678,10 +1587,6 @@ export function BasaltDataTable<T>(props: BasaltDataTableProps<T>) {
|
|
|
1678
1587
|
// `data` non-empty while the body has nothing in it, and a blank body reads as a broken table.
|
|
1679
1588
|
const rows = table.getRowModel().rows
|
|
1680
1589
|
|
|
1681
|
-
// A capped body or a horizontal floor both need the same native scroll node. Everything else —
|
|
1682
|
-
// pinning included — either takes that same container (the uncapped, non-sticky default) or the
|
|
1683
|
-
// measured wrapper below; there is no third overflow box. (`scrolls` itself is computed earlier,
|
|
1684
|
-
// ahead of the table, because the column fold above needs it too.)
|
|
1685
1590
|
// `stickyHeaderOffset` is a WINDOW-scroll concept — the height of whatever fixed chrome the page
|
|
1686
1591
|
// scrolls under (the AppShell header plus `PageBar` row 2). Inside the scroll container it is
|
|
1687
1592
|
// always wrong: that box is the sticky header's own scrollport, so the offset parks the `<thead>`
|
|
@@ -1700,9 +1605,27 @@ export function BasaltDataTable<T>(props: BasaltDataTableProps<T>) {
|
|
|
1700
1605
|
// `useMeasuredContainment` drives instead: bare while the table fits, `overflow-x: auto` once it
|
|
1701
1606
|
// does not. Its docblock carries the full accounting.
|
|
1702
1607
|
const pageStickyHeader = !scrolls && stickyHeader === true
|
|
1703
|
-
const containment = useMeasuredContainment(pageStickyHeader)
|
|
1608
|
+
const containment = useMeasuredContainment(pageStickyHeader && !projection.cards)
|
|
1704
1609
|
const resolvedStickyHeaderOffset = scrolls ? undefined : stickyHeaderOffset
|
|
1705
1610
|
|
|
1611
|
+
// The error branch the table did not have (components audit #3) carries the query's OWN `refetch`
|
|
1612
|
+
// behind Retry, so the failure is reported where the rows would have been — in a row spanning
|
|
1613
|
+
// every rendered column, or in place of the card list.
|
|
1614
|
+
const errorNode = queryError !== undefined && (
|
|
1615
|
+
<ErrorState
|
|
1616
|
+
error={queryError.error}
|
|
1617
|
+
title="Could not load"
|
|
1618
|
+
tier="section"
|
|
1619
|
+
retrying={queryError.fetchStatus === 'fetching'}
|
|
1620
|
+
onRetry={() => void queryError.refetch()}
|
|
1621
|
+
/>
|
|
1622
|
+
)
|
|
1623
|
+
const emptyNode = emptyState ?? (
|
|
1624
|
+
<Text c="dimmed" ta="center" size="sm" py="sm">
|
|
1625
|
+
No data to display.
|
|
1626
|
+
</Text>
|
|
1627
|
+
)
|
|
1628
|
+
|
|
1706
1629
|
const tableNode = (
|
|
1707
1630
|
<Table
|
|
1708
1631
|
className={cx(classNames?.table)}
|
|
@@ -1750,15 +1673,7 @@ export function BasaltDataTable<T>(props: BasaltDataTableProps<T>) {
|
|
|
1750
1673
|
: undefined
|
|
1751
1674
|
}
|
|
1752
1675
|
tabIndex={canSort ? 0 : undefined}
|
|
1753
|
-
aria-sort={
|
|
1754
|
-
canSort
|
|
1755
|
-
? sorted === 'asc'
|
|
1756
|
-
? 'ascending'
|
|
1757
|
-
: sorted === 'desc'
|
|
1758
|
-
? 'descending'
|
|
1759
|
-
: 'none'
|
|
1760
|
-
: undefined
|
|
1761
|
-
}
|
|
1676
|
+
aria-sort={canSort ? (sorted ? ARIA_SORT[sorted] : 'none') : undefined}
|
|
1762
1677
|
style={{
|
|
1763
1678
|
...(canSort ? { cursor: 'pointer', userSelect: 'none' } : undefined),
|
|
1764
1679
|
...(align !== undefined && { textAlign: align }),
|
|
@@ -1777,19 +1692,8 @@ export function BasaltDataTable<T>(props: BasaltDataTableProps<T>) {
|
|
|
1777
1692
|
</Table.Thead>
|
|
1778
1693
|
<Table.Tbody>
|
|
1779
1694
|
{queryError !== undefined ? (
|
|
1780
|
-
// The branch the table did not have (components audit #3). It spans every rendered
|
|
1781
|
-
// column and carries the query's OWN `refetch` behind Retry, so the failure is reported
|
|
1782
|
-
// where the rows would have been — not as an empty table saying nothing failed.
|
|
1783
1695
|
<Table.Tr>
|
|
1784
|
-
<Table.Td colSpan={columnCount}>
|
|
1785
|
-
<ErrorState
|
|
1786
|
-
error={queryError.error}
|
|
1787
|
-
title="Could not load"
|
|
1788
|
-
tier="section"
|
|
1789
|
-
retrying={queryError.fetchStatus === 'fetching'}
|
|
1790
|
-
onRetry={() => void queryError.refetch()}
|
|
1791
|
-
/>
|
|
1792
|
-
</Table.Td>
|
|
1696
|
+
<Table.Td colSpan={columnCount}>{errorNode}</Table.Td>
|
|
1793
1697
|
</Table.Tr>
|
|
1794
1698
|
) : showSkeleton ? (
|
|
1795
1699
|
Array.from({ length: skeletonRows }, (_, rowIndex) => (
|
|
@@ -1803,13 +1707,7 @@ export function BasaltDataTable<T>(props: BasaltDataTableProps<T>) {
|
|
|
1803
1707
|
))
|
|
1804
1708
|
) : rows.length === 0 ? (
|
|
1805
1709
|
<Table.Tr>
|
|
1806
|
-
<Table.Td colSpan={columnCount}>
|
|
1807
|
-
{emptyState ?? (
|
|
1808
|
-
<Text c="dimmed" ta="center" size="sm" py="sm">
|
|
1809
|
-
No data to display.
|
|
1810
|
-
</Text>
|
|
1811
|
-
)}
|
|
1812
|
-
</Table.Td>
|
|
1710
|
+
<Table.Td colSpan={columnCount}>{emptyNode}</Table.Td>
|
|
1813
1711
|
</Table.Tr>
|
|
1814
1712
|
) : (
|
|
1815
1713
|
rows.map((row) => {
|
|
@@ -1820,36 +1718,19 @@ export function BasaltDataTable<T>(props: BasaltDataTableProps<T>) {
|
|
|
1820
1718
|
...row.getRightVisibleCells(),
|
|
1821
1719
|
]
|
|
1822
1720
|
: row.getVisibleCells()
|
|
1823
|
-
const rowExpanded = hasFolded &&
|
|
1721
|
+
const rowExpanded = hasFolded && foldRows.expanded.has(row.id)
|
|
1824
1722
|
return (
|
|
1825
1723
|
<Fragment key={row.id}>
|
|
1826
1724
|
<Table.Tr
|
|
1827
1725
|
{...(enableRowSelection && row.getIsSelected() && { 'data-selected': true })}
|
|
1828
|
-
{...(onRowActivate
|
|
1829
|
-
className: classes.activatable,
|
|
1830
|
-
'data-activatable': true,
|
|
1831
|
-
tabIndex: 0,
|
|
1832
|
-
onClick: () => onRowActivate(row.original),
|
|
1833
|
-
// Enter only. Space is the browser's own page-scroll on a focused non-button, and
|
|
1834
|
-
// stealing it from a keyboard reader moving down a long table costs more than the
|
|
1835
|
-
// second activation key buys.
|
|
1836
|
-
onKeyDown: (event: ReactKeyboardEvent<HTMLTableRowElement>) => {
|
|
1837
|
-
if (event.key !== 'Enter') return
|
|
1838
|
-
// Only the ROW's own Enter. A cell may hold a button, a link or the selection
|
|
1839
|
-
// checkbox, and keydown bubbles — so without this an Enter on a nested control
|
|
1840
|
-
// fired that control AND opened the row's detail behind it.
|
|
1841
|
-
if (event.target !== event.currentTarget) return
|
|
1842
|
-
event.preventDefault()
|
|
1843
|
-
onRowActivate(row.original)
|
|
1844
|
-
},
|
|
1845
|
-
})}
|
|
1726
|
+
{...activationProps(onRowActivate, row.original)}
|
|
1846
1727
|
>
|
|
1847
1728
|
{hasFolded && (
|
|
1848
1729
|
<Table.Td className={classes.foldToggleCell}>
|
|
1849
1730
|
<FoldToggle
|
|
1850
1731
|
expanded={rowExpanded}
|
|
1851
1732
|
controlsId={`${row.id}-fold`}
|
|
1852
|
-
onToggle={() =>
|
|
1733
|
+
onToggle={() => foldRows.toggle(row.id)}
|
|
1853
1734
|
/>
|
|
1854
1735
|
</Table.Td>
|
|
1855
1736
|
)}
|
|
@@ -1898,6 +1779,25 @@ export function BasaltDataTable<T>(props: BasaltDataTableProps<T>) {
|
|
|
1898
1779
|
</Table>
|
|
1899
1780
|
)
|
|
1900
1781
|
|
|
1782
|
+
// The `renderCard` body — the same processed rows, states and activation as the table's.
|
|
1783
|
+
const renderCardList = () => (
|
|
1784
|
+
<Stack gap="xs" className={cx(classNames?.table)}>
|
|
1785
|
+
{queryError !== undefined
|
|
1786
|
+
? errorNode
|
|
1787
|
+
: showSkeleton
|
|
1788
|
+
? Array.from({ length: skeletonRows }, (_, index) => (
|
|
1789
|
+
<Skeleton key={`skeleton-${index}`} height={48} radius="md" />
|
|
1790
|
+
))
|
|
1791
|
+
: rows.length === 0
|
|
1792
|
+
? emptyNode
|
|
1793
|
+
: rows.map((row) => (
|
|
1794
|
+
<Box key={row.id} {...activationProps(onRowActivate, row.original)}>
|
|
1795
|
+
{renderCard?.(row.original)}
|
|
1796
|
+
</Box>
|
|
1797
|
+
))}
|
|
1798
|
+
</Stack>
|
|
1799
|
+
)
|
|
1800
|
+
|
|
1901
1801
|
const paginationState = table.getState().pagination
|
|
1902
1802
|
// The CURRENT page size is always an option, even when it is not in `pageSizeOptions`. Mantine's
|
|
1903
1803
|
// Select renders EMPTY when its `value` matches no row, so a table opened at
|
|
@@ -1911,7 +1811,11 @@ export function BasaltDataTable<T>(props: BasaltDataTableProps<T>) {
|
|
|
1911
1811
|
const rangeEnd = Math.min((paginationState.pageIndex + 1) * paginationState.pageSize, total)
|
|
1912
1812
|
|
|
1913
1813
|
return (
|
|
1914
|
-
<div
|
|
1814
|
+
<div
|
|
1815
|
+
ref={projection.rootRef}
|
|
1816
|
+
className={cx(classNames?.root, className)}
|
|
1817
|
+
{...(style !== undefined && { style })}
|
|
1818
|
+
>
|
|
1915
1819
|
{/*
|
|
1916
1820
|
* ONE header row: the `WidgetHeader` (title · count) on the left, the toolbar right-aligned in
|
|
1917
1821
|
* the SAME row. It was two stacked rows with `mb="xs"` on each, so a titled table with search
|
|
@@ -1985,7 +1889,15 @@ export function BasaltDataTable<T>(props: BasaltDataTableProps<T>) {
|
|
|
1985
1889
|
{bulkBarActions !== undefined && (
|
|
1986
1890
|
<BulkActionBar count={selectedRows.length} actions={bulkBarActions} />
|
|
1987
1891
|
)}
|
|
1988
|
-
{
|
|
1892
|
+
{projection.cards ? (
|
|
1893
|
+
maxHeight === undefined ? (
|
|
1894
|
+
renderCardList()
|
|
1895
|
+
) : (
|
|
1896
|
+
<ScrollArea.Autosize mah={maxHeight} type="hover" scrollbars="y">
|
|
1897
|
+
{renderCardList()}
|
|
1898
|
+
</ScrollArea.Autosize>
|
|
1899
|
+
)
|
|
1900
|
+
) : !foldEligible ? (
|
|
1989
1901
|
// An explicit `minWidth` is the one genuinely DECLARED horizontal floor — the column fold
|
|
1990
1902
|
// never engages here (see `foldEligible`, above), so this shape keeps its exact
|
|
1991
1903
|
// pre-existing DOM: no extra measuring wrapper for a table nothing folds.
|
|
@@ -2010,15 +1922,10 @@ export function BasaltDataTable<T>(props: BasaltDataTableProps<T>) {
|
|
|
2010
1922
|
>
|
|
2011
1923
|
{tableNode}
|
|
2012
1924
|
</div>
|
|
2013
|
-
) : maxHeight !== undefined ? (
|
|
2014
|
-
// `pageStickyHeader` is already false whenever `maxHeight` is set (`scrolls` above), so
|
|
2015
|
-
// this is the ONE remaining case: a vertical cap with no sticky header to measure —
|
|
2016
|
-
// still wants its own `ScrollArea` scrollport, just with no `minWidth` floor.
|
|
2017
|
-
<Table.ScrollContainer type="native" minWidth={0} maxHeight={maxHeight}>
|
|
2018
|
-
{tableNode}
|
|
2019
|
-
</Table.ScrollContainer>
|
|
2020
1925
|
) : (
|
|
2021
|
-
|
|
1926
|
+
// `pageStickyHeader` is false whenever `maxHeight` is set (`scrolls` above), so a vertical
|
|
1927
|
+
// cap lands here — its own `ScrollArea` scrollport, just with no `minWidth` floor.
|
|
1928
|
+
<Table.ScrollContainer type="native" minWidth={0} maxHeight={maxHeight}>
|
|
2022
1929
|
{tableNode}
|
|
2023
1930
|
</Table.ScrollContainer>
|
|
2024
1931
|
)}
|
|
@@ -2038,6 +1945,8 @@ export function BasaltDataTable<T>(props: BasaltDataTableProps<T>) {
|
|
|
2038
1945
|
{totalIsAuthoritative ? ` of ${total}` : ''}
|
|
2039
1946
|
</Text>
|
|
2040
1947
|
<Group gap="xs" align="center">
|
|
1948
|
+
{/* theme-allow control-outside-home — the table's own page-size setting, not a page
|
|
1949
|
+
filter: its state is the table's pagination, owned inside this component. */}
|
|
2041
1950
|
<Select
|
|
2042
1951
|
size="ctl"
|
|
2043
1952
|
radius="md"
|
|
@@ -14,9 +14,10 @@
|
|
|
14
14
|
* containment and `.grid` is the queried child, the same split `StatGroup`'s `.container`/`.root`
|
|
15
15
|
* uses — a container cannot reliably query ITSELF for a layout-affecting property (measured here:
|
|
16
16
|
* Chrome silently keeps the un-queried value even once the container's own width crosses the
|
|
17
|
-
* condition), only a descendant. The `599.9px
|
|
17
|
+
* condition), only a descendant. The `599.9px` literal: 600px is roughly the point
|
|
18
18
|
* two ~250px+ cells stop feeling cramped; `@container`/`@media` conditions cannot read a custom
|
|
19
|
-
* property, so
|
|
19
|
+
* property, so it is a written number, not a `--vx-*` ref. No viewport fallback — see
|
|
20
|
+
* `stat-group.module.css`: nothing that renders basalt lacks `@container`.
|
|
20
21
|
*/
|
|
21
22
|
|
|
22
23
|
/* ── FormSection ─────────────────────────────────────────────────────────────────────────────── */
|
|
@@ -102,17 +103,6 @@
|
|
|
102
103
|
}
|
|
103
104
|
}
|
|
104
105
|
|
|
105
|
-
/* Pre-container-query fallback (Safari < 16, Chrome < 106, Firefox < 110) — the old viewport law,
|
|
106
|
-
* reachable only where `@container` itself cannot parse (same reasoning as `stat-group.module.css`). */
|
|
107
|
-
@supports not (container-type: inline-size) {
|
|
108
|
-
@media (max-width: 47.99375em) {
|
|
109
|
-
.grid {
|
|
110
|
-
grid-template-columns: minmax(0, 1fr);
|
|
111
|
-
gap: var(--vx-space-stack-xs);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
106
|
/* ── FormGroup ───────────────────────────────────────────────────────────────────────────────── */
|
|
117
107
|
|
|
118
108
|
/* Same UA reset as `.control`, same reason. */
|
package/src/guard/index.ts
CHANGED
|
@@ -152,7 +152,7 @@ const CSS_WIDTH_MEDIA_QUERY = new RegExp(
|
|
|
152
152
|
* An `@container` condition — the declared NAME (optional: `@container (min-width: …)` with no
|
|
153
153
|
* name is legal CSS, scoping to the nearest ancestor container regardless of its name) plus every
|
|
154
154
|
* raw boundary across the WHOLE at-rule, chained `and (...)` clauses included
|
|
155
|
-
* (`@container basalt-
|
|
155
|
+
* (`@container basalt-grid (min-width: 800px) and (max-width: 1199.9px)` —
|
|
156
156
|
* `stat-group.module.css` ships exactly this shape). Capturing only the first parenthesized clause
|
|
157
157
|
* would silently skip every boundary after the first `and`; group 2 here is the full run of
|
|
158
158
|
* `(...)`/`and` text so `CSS_CONTAINER_LITERAL`'s own `matchAll` below finds all of them.
|
|
@@ -180,11 +180,8 @@ const CSS_NON_COARSE_POINTER_QUERY =
|
|
|
180
180
|
* `basalt-form`/`basalt-main`/`basalt-grid` are the declared remaining three, reserved here so a
|
|
181
181
|
* consumer adopting one ahead of the framework does not immediately trip this guard.
|
|
182
182
|
*
|
|
183
|
-
*
|
|
184
|
-
* `basalt-widget-grid`
|
|
185
|
-
* names them) — predate the declared table and are deliberately NOT in this set: they surface as
|
|
186
|
-
* `warn` findings against basalt's own source the same way any other kind's incumbent violations do
|
|
187
|
-
* at grace time, rather than being silently grandfathered into a shipped rule's allowlist.
|
|
183
|
+
* `basalt-grid` is also what `StatGroup`/`WidgetGrid` declare (`src/dashboard/*.module.css`) —
|
|
184
|
+
* they shipped as `basalt-stat-group`/`basalt-widget-grid` until 1.32.0 moved them onto this table.
|
|
188
185
|
*/
|
|
189
186
|
const DECLARED_CONTAINER_NAMES = new Set([
|
|
190
187
|
'basalt-card',
|
|
@@ -229,7 +226,14 @@ function isSanctionedShellMediaQuery(relPath: string, query: string): boolean {
|
|
|
229
226
|
// the comparison/chained spellings too, with no shape-specific regex of its own. All four units
|
|
230
227
|
// are captured (not `em` only) so a `px` width — never a `SIZE_CLASS_LITERALS` member — correctly
|
|
231
228
|
// fails the check below, rather than `.every()` vacuously passing on an empty match.
|
|
232
|
-
|
|
229
|
+
//
|
|
230
|
+
// But `query` is the whole at-rule prelude up to the width condition, so it can also carry a
|
|
231
|
+
// NON-width clause — the shell's own landscape rule is `(max-height: 480px) and (orientation:
|
|
232
|
+
// landscape) and (max-width: 52.49375em)`, and reading its `480px` height as an off-table width
|
|
233
|
+
// flagged the framework's own sanctioned implementation. Only the parenthesized clauses that name
|
|
234
|
+
// `width` are read.
|
|
235
|
+
const widthClauses = query.match(/\([^()]*\bwidth\b[^()]*\)/g) ?? []
|
|
236
|
+
const widths = widthClauses.flatMap((c) => c.match(new RegExp(CSS_WIDTH_NUM, 'g')) ?? [])
|
|
233
237
|
return widths.length > 0 && widths.every((w) => SIZE_CLASS_LITERALS.has(w))
|
|
234
238
|
}
|
|
235
239
|
|
|
@@ -860,8 +864,9 @@ export type GraceEntry = { since: string; promote: string; why: string }
|
|
|
860
864
|
export const GRACE_PERIOD_KINDS: Partial<Record<GuardKind, GraceEntry>> = {
|
|
861
865
|
'raw-selection-control': {
|
|
862
866
|
since: '1.26.0',
|
|
863
|
-
promote: '1.
|
|
867
|
+
promote: '1.33.0',
|
|
864
868
|
why:
|
|
869
|
+
"Extended 1.32.0 -> 1.33.0 at the 1.32.0 release, the last extension: basalt's own tree is at 0 (consumer-loop r1 dogfood) and argo is at 2, but RE-MEASURED 2026-09-30 the fleet carries ~33 (image-gen 11, rb 7, image-share 6, weatherorb 5, email-gateway 4, argo 2). The 1.32.0 fleet upgrade clears them while they warn, together with raw-breakpoint, so 1.33.0 promotes all four ids against a measured-clean fleet. " +
|
|
865
870
|
'Extended 1.31.0 -> 1.32.0 at the 1.31.0 release: the basalt tree still carries incumbents (data-table, notifications center, theme-lab) and the argo sweep has not run. ' +
|
|
866
871
|
'new in the wave-6 control guards (docs/CONTROLS-SPEC.md §6, law C1). The text lane cannot ' +
|
|
867
872
|
'see ancestry, so "no home" is approximated by a 12-line host-tag window — the loosest ' +
|
|
@@ -880,15 +885,15 @@ export const GRACE_PERIOD_KINDS: Partial<Record<GuardKind, GraceEntry>> = {
|
|
|
880
885
|
},
|
|
881
886
|
'raw-media-query': {
|
|
882
887
|
since: '1.31.0',
|
|
883
|
-
promote: '1.
|
|
888
|
+
promote: '1.33.0',
|
|
884
889
|
why:
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
'
|
|
890
|
+
"The CSS-text twin of the oxlint plugin's `basalt/raw-breakpoint` — ONE LAW, TWO LANES, ONE " +
|
|
891
|
+
'PROMOTION, so it moves with that rule and never alone (`configs/oxlint-plugin.test.ts` › twin lanes share one promotion pins the pair). New in ' +
|
|
892
|
+
'the wave-11 responsive/touch guards; reaching CSS is a NEW file type for this check, same ' +
|
|
893
|
+
"reasoning as `css-raw-surface`, with incumbents in basalt's own tree at ship time, so it " +
|
|
894
|
+
'shipped warn. Extended 1.32.0 -> 1.33.0 with its twin: 1.31.0 named ' +
|
|
895
|
+
'no reachable container primitive, and 1.32.0 is the first minor that documents one ' +
|
|
896
|
+
'(`CONTAINER_CLASSES` + the declared `@container` names, MIGRATING § `basalt/raw-breakpoint`).',
|
|
892
897
|
},
|
|
893
898
|
}
|
|
894
899
|
|