basalt-ui 1.31.0 → 1.32.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/CHANGELOG.md +34 -0
- package/MIGRATING.md +212 -20
- package/agent/rules/basalt-mantine.md +4 -4
- package/configs/oxlint-plugin.js +240 -59
- package/configs/oxlint-plugin.test.ts +329 -1
- 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 +40 -2
- package/dist/data/data-table.d.ts.map +1 -1
- package/dist/data/data-table.js +179 -277
- package/dist/forms/form-layout.d.ts +6 -1
- package/dist/forms/form-layout.d.ts.map +1 -1
- package/dist/forms/form-layout.module.css +6 -13
- package/dist/guard/index.d.ts +2 -2
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +10 -14
- 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/page-bar.d.ts +24 -0
- package/dist/shell/page-bar.d.ts.map +1 -1
- 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/dist/tokens/index.d.ts +2 -0
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +4 -0
- 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 +134 -218
- package/src/forms/form-layout.module.css +6 -13
- package/src/forms/form-layout.tsx +6 -1
- package/src/guard/index.ts +38 -34
- 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/page-bar.tsx +24 -0
- 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/tokens/index.ts +5 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useId, useRef, useState } from "react";
|
|
3
|
+
import { createPortal } from "react-dom";
|
|
4
|
+
import { alpha, VX } from "../../tokens/index.js";
|
|
5
|
+
import { useDismissOnOutside } from "../cursor/touch-pin.js";
|
|
6
|
+
import { LEGEND_ROW_GAP } from "./chart-frame-layout.js";
|
|
7
|
+
const DISCLOSURE_PANEL = {
|
|
8
|
+
position: "fixed",
|
|
9
|
+
zIndex: VX.zIndexFloating,
|
|
10
|
+
display: "flex",
|
|
11
|
+
flexDirection: "column",
|
|
12
|
+
gap: LEGEND_ROW_GAP,
|
|
13
|
+
// theme-allow raw-scroll-container — a fixed floating panel of one legend, not app chrome
|
|
14
|
+
overflowY: "auto",
|
|
15
|
+
// `<dialog open>` brings UA chrome (centring margin, border, fit-content box) this resets.
|
|
16
|
+
margin: 0,
|
|
17
|
+
border: "none",
|
|
18
|
+
width: "auto",
|
|
19
|
+
height: "auto",
|
|
20
|
+
padding: 10,
|
|
21
|
+
boxSizing: "border-box",
|
|
22
|
+
backgroundColor: VX.surface.overlay,
|
|
23
|
+
boxShadow: VX.shadowOverlay,
|
|
24
|
+
color: VX.muted,
|
|
25
|
+
borderRadius: VX.radiusCard
|
|
26
|
+
};
|
|
27
|
+
const DISCLOSURE_MIN_HEIGHT = 96;
|
|
28
|
+
function disclosurePlacement(chip, coarse) {
|
|
29
|
+
if (coarse) {
|
|
30
|
+
return {
|
|
31
|
+
left: 0,
|
|
32
|
+
right: 0,
|
|
33
|
+
bottom: 0,
|
|
34
|
+
maxHeight: "60dvh",
|
|
35
|
+
borderBottomLeftRadius: 0,
|
|
36
|
+
borderBottomRightRadius: 0,
|
|
37
|
+
padding: "12px 16px calc(12px + env(safe-area-inset-bottom, 0px))"
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const below = window.innerHeight - chip.bottom;
|
|
41
|
+
const left = Math.max(8, Math.min(chip.left, window.innerWidth - 248));
|
|
42
|
+
const base = { left, right: "auto", minWidth: 200, maxWidth: "calc(100vw - 16px)" };
|
|
43
|
+
return below >= 200 || below >= chip.top ? { ...base, top: chip.bottom + 4, maxHeight: Math.max(below - 12, DISCLOSURE_MIN_HEIGHT) } : {
|
|
44
|
+
...base,
|
|
45
|
+
bottom: window.innerHeight - chip.top + 4,
|
|
46
|
+
maxHeight: Math.max(chip.top - 12, DISCLOSURE_MIN_HEIGHT)
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function useLegendDisclosure(coarse) {
|
|
50
|
+
const [placement, setPlacement] = useState(null);
|
|
51
|
+
const triggerRef = useRef(null);
|
|
52
|
+
const panelRef = useRef(null);
|
|
53
|
+
const panelId = useId();
|
|
54
|
+
const wasOpen = useRef(false);
|
|
55
|
+
const open = placement !== null;
|
|
56
|
+
const close = useCallback(() => setPlacement(null), []);
|
|
57
|
+
const openFrom = (trigger) => {
|
|
58
|
+
triggerRef.current = trigger;
|
|
59
|
+
setPlacement(disclosurePlacement(trigger.getBoundingClientRect(), coarse));
|
|
60
|
+
};
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (open) panelRef.current?.focus();
|
|
63
|
+
else if (wasOpen.current) triggerRef.current?.focus();
|
|
64
|
+
wasOpen.current = open;
|
|
65
|
+
}, [open]);
|
|
66
|
+
const isInsidePanel = useCallback(
|
|
67
|
+
(target) => panelRef.current?.contains(target) === true || triggerRef.current?.contains(target) === true,
|
|
68
|
+
[]
|
|
69
|
+
);
|
|
70
|
+
useDismissOnOutside({
|
|
71
|
+
enabled: open,
|
|
72
|
+
isInside: isInsidePanel,
|
|
73
|
+
onDismiss: close,
|
|
74
|
+
dismissOnResize: true,
|
|
75
|
+
scrollExemptInside: true
|
|
76
|
+
});
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (!open || !coarse) return;
|
|
79
|
+
const panel = panelRef.current;
|
|
80
|
+
if (panel === null) return;
|
|
81
|
+
const onKeyDown = (e) => {
|
|
82
|
+
if (e.key !== "Tab") return;
|
|
83
|
+
const focusable = panel.querySelectorAll(
|
|
84
|
+
'button, [tabindex]:not([tabindex="-1"])'
|
|
85
|
+
);
|
|
86
|
+
const first = focusable[0];
|
|
87
|
+
const last = focusable[focusable.length - 1];
|
|
88
|
+
if (first === void 0 || last === void 0) return;
|
|
89
|
+
if (e.shiftKey && document.activeElement === first) {
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
last.focus();
|
|
92
|
+
} else if (!e.shiftKey && document.activeElement === last) {
|
|
93
|
+
e.preventDefault();
|
|
94
|
+
first.focus();
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
panel.addEventListener("keydown", onKeyDown);
|
|
98
|
+
return () => panel.removeEventListener("keydown", onKeyDown);
|
|
99
|
+
}, [open, coarse]);
|
|
100
|
+
return { open, panelId, openFrom, close, panel: { panelRef, panelId, coarse, placement, close } };
|
|
101
|
+
}
|
|
102
|
+
function LegendDisclosure({
|
|
103
|
+
panelRef,
|
|
104
|
+
panelId,
|
|
105
|
+
coarse,
|
|
106
|
+
placement,
|
|
107
|
+
close,
|
|
108
|
+
fontSize,
|
|
109
|
+
children
|
|
110
|
+
}) {
|
|
111
|
+
if (placement === null) return null;
|
|
112
|
+
return createPortal(
|
|
113
|
+
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
114
|
+
coarse && /* @__PURE__ */ jsx(
|
|
115
|
+
"div",
|
|
116
|
+
{
|
|
117
|
+
"aria-hidden": true,
|
|
118
|
+
onPointerDown: (e) => {
|
|
119
|
+
e.stopPropagation();
|
|
120
|
+
close();
|
|
121
|
+
},
|
|
122
|
+
style: {
|
|
123
|
+
position: "fixed",
|
|
124
|
+
inset: 0,
|
|
125
|
+
// One below the panel it sits behind — still `VX.zIndexFloating`-derived, never a
|
|
126
|
+
// second unrelated literal (B14).
|
|
127
|
+
zIndex: VX.zIndexFloating - 1,
|
|
128
|
+
backgroundColor: alpha(VX.neutral, 0.35)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
),
|
|
132
|
+
/* @__PURE__ */ jsx(
|
|
133
|
+
"dialog",
|
|
134
|
+
{
|
|
135
|
+
ref: panelRef,
|
|
136
|
+
id: panelId,
|
|
137
|
+
open: true,
|
|
138
|
+
tabIndex: -1,
|
|
139
|
+
"aria-label": "Legend",
|
|
140
|
+
...coarse && { "aria-modal": true },
|
|
141
|
+
"data-basalt-legend-disclosure": coarse ? "sheet" : "popover",
|
|
142
|
+
style: { ...DISCLOSURE_PANEL, fontSize, ...placement },
|
|
143
|
+
children
|
|
144
|
+
}
|
|
145
|
+
)
|
|
146
|
+
] }),
|
|
147
|
+
document.body
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
export {
|
|
151
|
+
LegendDisclosure,
|
|
152
|
+
useLegendDisclosure
|
|
153
|
+
};
|
|
@@ -49,6 +49,11 @@
|
|
|
49
49
|
flex-shrink: 0;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/* theme-allow-file raw-media-query — the ControlRow's own C9 swap (pills inline, the rest in a
|
|
53
|
+
sheet) keys on the SIZE CLASS by design: the sheet is compact-class chrome, the same axis the
|
|
54
|
+
shell's bottom bar reads. Every width query here is `SIZE_CLASSES.medium`, pinned by
|
|
55
|
+
`breakpoint-literals.test.ts`. */
|
|
56
|
+
|
|
52
57
|
/* Below `sm` only the first `inline` slots are pills; the rest live in the sheet. */
|
|
53
58
|
@media (max-width: 52.49375em) {
|
|
54
59
|
.pillSlot:not([data-inline]) {
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
* width itself, pinned to the `ctl` height so the box is square. This module is unlayered, so it
|
|
46
46
|
* outranks `@layer mantine` regardless of specificity.
|
|
47
47
|
*/
|
|
48
|
+
/* theme-allow raw-media-query — the control's own C9 icon-only swap at the compact size class */
|
|
48
49
|
@media (max-width: 52.49375em) {
|
|
49
50
|
.label,
|
|
50
51
|
.age {
|
|
@@ -15,24 +15,24 @@
|
|
|
15
15
|
* one: a query with no container resolves to false, so a shell-only context would have silently
|
|
16
16
|
* pinned every non-shell consumer to the base count.
|
|
17
17
|
*
|
|
18
|
-
* The
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
18
|
+
* The literals are the container-class law (`CONTAINER_CLASSES`, `tokens/size-classes.ts`): the
|
|
19
|
+
* `sm` count opens at `wide` (800px), not at the viewport-era 768. Two are StatGroup's own and say
|
|
20
|
+
* so: 260px (one column, below) and 1200px, where the full `cols` count opens. The class table
|
|
21
|
+
* stops at 800, and 800 is too early for a full KPI row — four cells of ~190px clamp a value a real
|
|
22
|
+
* dashboard prints (`tests/layout/page-aside.layout.test.ts` measures ~195px against the 844px row
|
|
23
|
+
* a claimed aside leaves). Conditions are px, never `em`: `em` inside a `@container` condition
|
|
24
|
+
* resolves against the CONTAINER's font-size, so a consumer panel with smaller text would silently
|
|
25
|
+
* move the boundary. A condition cannot read a custom property, so the numbers are written; the
|
|
26
|
+
* COUNTS are custom properties the component sets per instance.
|
|
23
27
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* condition resolves against the CONTAINER's font-size rather than the root's, so a consumer panel
|
|
28
|
-
* with smaller text would silently move the breakpoint. px is the only spelling that cannot drift.
|
|
29
|
-
* (`47.99375em` → `767.9px`, `74.99375em` → `1199.9px`.) A condition still cannot read a custom property either way, so the numbers are written;
|
|
30
|
-
* the COUNTS are custom properties the component sets per instance.
|
|
28
|
+
* No viewport fallback: basalt's own CSS already requires `color-mix()` (Safari 16.2, Chrome 111,
|
|
29
|
+
* Firefox 113), and every one of those parses `@container` — a `@supports not (container-type)`
|
|
30
|
+
* block is unreachable in any browser that renders basalt at all.
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
33
|
.container {
|
|
34
34
|
container-type: inline-size;
|
|
35
|
-
container-name: basalt-
|
|
35
|
+
container-name: basalt-grid;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
.root {
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
|
|
46
46
|
/* 260px is StatGroup's own literal (docs/DESIGN-CORE.md § Layout, elevation, shapes), not a container class: one column below it,
|
|
47
47
|
* so every cell leads a row and the rail draws no leading hairline at all. */
|
|
48
|
-
@container basalt-
|
|
48
|
+
@container basalt-grid (max-width: 259.9px) {
|
|
49
49
|
.root {
|
|
50
50
|
grid-template-columns: minmax(0, 1fr);
|
|
51
51
|
}
|
|
@@ -56,13 +56,13 @@
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
@container basalt-
|
|
59
|
+
@container basalt-grid (min-width: 800px) {
|
|
60
60
|
.root {
|
|
61
61
|
grid-template-columns: repeat(var(--basalt-stat-group-cols-sm), minmax(0, 1fr));
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
@container basalt-
|
|
65
|
+
@container basalt-grid (min-width: 1200px) {
|
|
66
66
|
.root {
|
|
67
67
|
grid-template-columns: repeat(var(--basalt-stat-group-cols-lg), minmax(0, 1fr));
|
|
68
68
|
}
|
|
@@ -83,14 +83,14 @@
|
|
|
83
83
|
padding-inline-start: var(--vx-space-stack-md);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
@container basalt-
|
|
86
|
+
@container basalt-grid (max-width: 799.9px) {
|
|
87
87
|
.root[data-divided] > *:nth-child(2n + 1) {
|
|
88
88
|
border-inline-start: 0;
|
|
89
89
|
padding-inline-start: 0;
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
@container basalt-
|
|
93
|
+
@container basalt-grid (min-width: 800px) and (max-width: 1199.9px) {
|
|
94
94
|
.root[data-divided][data-sm-cols='2'] > *:nth-child(2n + 1),
|
|
95
95
|
.root[data-divided][data-sm-cols='3'] > *:nth-child(3n + 1) {
|
|
96
96
|
border-inline-start: 0;
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
@container basalt-
|
|
101
|
+
@container basalt-grid (min-width: 1200px) {
|
|
102
102
|
.root[data-divided][data-cols='2'] > *:nth-child(2n + 1),
|
|
103
103
|
.root[data-divided][data-cols='3'] > *:nth-child(3n + 1),
|
|
104
104
|
.root[data-divided][data-cols='4'] > *:nth-child(4n + 1),
|
|
@@ -107,48 +107,3 @@
|
|
|
107
107
|
padding-inline-start: 0;
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
|
-
|
|
111
|
-
/*
|
|
112
|
-
* The pre-container-query fallback — Safari < 16, Chrome < 106, Firefox < 110. Verbatim the law
|
|
113
|
-
* this file shipped before, keyed on the viewport, and reachable ONLY where `@container` is not.
|
|
114
|
-
* A browser that cannot parse `container-type` also cannot parse the `@container` blocks above, so
|
|
115
|
-
* the two sets are never live at once.
|
|
116
|
-
*/
|
|
117
|
-
@supports not (container-type: inline-size) {
|
|
118
|
-
@media (min-width: 48em) {
|
|
119
|
-
.root {
|
|
120
|
-
grid-template-columns: repeat(var(--basalt-stat-group-cols-sm), minmax(0, 1fr));
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
@media (min-width: 75em) {
|
|
125
|
-
.root {
|
|
126
|
-
grid-template-columns: repeat(var(--basalt-stat-group-cols-lg), minmax(0, 1fr));
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
@media (max-width: 47.99375em) {
|
|
131
|
-
.root[data-divided] > *:nth-child(2n + 1) {
|
|
132
|
-
border-inline-start: 0;
|
|
133
|
-
padding-inline-start: 0;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
@media (min-width: 48em) and (max-width: 74.99375em) {
|
|
138
|
-
.root[data-divided][data-sm-cols='2'] > *:nth-child(2n + 1),
|
|
139
|
-
.root[data-divided][data-sm-cols='3'] > *:nth-child(3n + 1) {
|
|
140
|
-
border-inline-start: 0;
|
|
141
|
-
padding-inline-start: 0;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
@media (min-width: 75em) {
|
|
146
|
-
.root[data-divided][data-cols='2'] > *:nth-child(2n + 1),
|
|
147
|
-
.root[data-divided][data-cols='3'] > *:nth-child(3n + 1),
|
|
148
|
-
.root[data-divided][data-cols='4'] > *:nth-child(4n + 1),
|
|
149
|
-
.root[data-divided][data-cols='5'] > *:nth-child(5n + 1) {
|
|
150
|
-
border-inline-start: 0;
|
|
151
|
-
padding-inline-start: 0;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
@@ -15,18 +15,19 @@
|
|
|
15
15
|
* container. A span resolved against the viewport while the tracks follow the container is exactly
|
|
16
16
|
* the phantom-track defect `WidgetGrid.Item`'s context clamp exists to prevent.
|
|
17
17
|
*
|
|
18
|
-
* The literals
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
18
|
+
* The literals are the container-class law, as in `stat-group.module.css`: the `sm` count opens
|
|
19
|
+
* at `wide` (800px); 1200px, where the full `cols` count opens, is the grid's own boundary above
|
|
20
|
+
* the class table's top step. px, never `em` (an `em` condition resolves against the container's
|
|
21
|
+
* font-size). A condition cannot read a custom property, so the numbers are written; the COUNTS
|
|
22
|
+
* are custom properties, set once per instance by the component — three hand-written `repeat()`
|
|
23
|
+
* blocks would have to enumerate every `cols` value, and the whole point of this primitive is that
|
|
24
|
+
* a consumer states one desktop number and basalt owns the rest. No viewport fallback, for the
|
|
25
|
+
* reason `stat-group.module.css` states: nothing that renders basalt lacks `@container`.
|
|
25
26
|
*/
|
|
26
27
|
|
|
27
28
|
.container {
|
|
28
29
|
container-type: inline-size;
|
|
29
|
-
container-name: basalt-
|
|
30
|
+
container-name: basalt-grid;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
.root {
|
|
@@ -47,7 +48,7 @@
|
|
|
47
48
|
min-width: 0;
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
@container basalt-
|
|
51
|
+
@container basalt-grid (min-width: 800px) {
|
|
51
52
|
.root {
|
|
52
53
|
grid-template-columns: repeat(var(--basalt-widget-grid-cols-sm), minmax(0, 1fr));
|
|
53
54
|
}
|
|
@@ -57,7 +58,7 @@
|
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
@container basalt-
|
|
61
|
+
@container basalt-grid (min-width: 1200px) {
|
|
61
62
|
.root {
|
|
62
63
|
grid-template-columns: repeat(var(--basalt-widget-grid-cols-lg), minmax(0, 1fr));
|
|
63
64
|
}
|
|
@@ -66,31 +67,3 @@
|
|
|
66
67
|
grid-column: span var(--basalt-widget-grid-span-lg);
|
|
67
68
|
}
|
|
68
69
|
}
|
|
69
|
-
|
|
70
|
-
/*
|
|
71
|
-
* The pre-container-query fallback — Safari < 16, Chrome < 106, Firefox < 110. Verbatim the law
|
|
72
|
-
* this file shipped before, keyed on the viewport, and reachable ONLY where `@container` is not: a
|
|
73
|
-
* browser that cannot parse `container-type` cannot parse the blocks above either, so the two sets
|
|
74
|
-
* are never live at once.
|
|
75
|
-
*/
|
|
76
|
-
@supports not (container-type: inline-size) {
|
|
77
|
-
@media (min-width: 48em) {
|
|
78
|
-
.root {
|
|
79
|
-
grid-template-columns: repeat(var(--basalt-widget-grid-cols-sm), minmax(0, 1fr));
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
.item {
|
|
83
|
-
grid-column: span var(--basalt-widget-grid-span-sm);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
@media (min-width: 75em) {
|
|
88
|
-
.root {
|
|
89
|
-
grid-template-columns: repeat(var(--basalt-widget-grid-cols-lg), minmax(0, 1fr));
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
.item {
|
|
93
|
-
grid-column: span var(--basalt-widget-grid-span-lg);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `BasaltDataTable`'s state hooks — the table's own interactive state, the fold row disclosure, the
|
|
3
|
+
* card projection and the row/card activation props. Internal to `./data/table`; nothing here is on a
|
|
4
|
+
* published subpath.
|
|
5
|
+
*/
|
|
6
|
+
import type { ColumnFiltersState, ColumnPinningState, PaginationState, RowSelectionState, SortingState, Updater } from '@tanstack/react-table';
|
|
7
|
+
import type { KeyboardEvent as ReactKeyboardEvent, RefObject } from 'react';
|
|
8
|
+
export declare const EMPTY_FOLD_SET: ReadonlySet<string>;
|
|
9
|
+
/**
|
|
10
|
+
* Which rows have their fold disclosure open. `row.id` defaults to the row's index in `data` (no
|
|
11
|
+
* `getRowId`) — stable across a client-side sort/filter/pagination, which reorder or subset the SAME
|
|
12
|
+
* core rows. PRUNED rather than cleared on every `data` change: a non-memoized `data` prop (an inline
|
|
13
|
+
* `.map()`, an unstable query result) hands a brand-new array of the SAME rows on every parent
|
|
14
|
+
* re-render, and clearing on that reference alone collapsed every open disclosure the instant
|
|
15
|
+
* anything upstream re-rendered.
|
|
16
|
+
*
|
|
17
|
+
* Pruning by ID ALONE is only safe with `getRowId` — a caller-declared identity that genuinely
|
|
18
|
+
* survives a page turn or a refetch, so an id no longer present (a manual-pagination page turn, a
|
|
19
|
+
* delete) still has to go, or a differently-shaped record seated at a reused id would silently read
|
|
20
|
+
* as pre-expanded. Without `getRowId` the id IS the array index, and an index survives a refetch
|
|
21
|
+
* trivially — it is still "0", "1", … even when every object behind it is a different record.
|
|
22
|
+
* Keeping an expanded index across THAT would silently attach the old disclosure to a stranger's
|
|
23
|
+
* row. So the index-id lane keeps a `prevData` snapshot and asks the narrower, correct question: is
|
|
24
|
+
* the object AT this index still the same reference as last time? A non-memoized array of the SAME
|
|
25
|
+
* rows answers yes at every index (the case above); a page turn or a refetch answers no.
|
|
26
|
+
*/
|
|
27
|
+
export declare function useRowDisclosure<T>({ data, getRowId, }: {
|
|
28
|
+
data: readonly T[];
|
|
29
|
+
getRowId: ((row: T, index: number) => string) | undefined;
|
|
30
|
+
}): {
|
|
31
|
+
expanded: ReadonlySet<string>;
|
|
32
|
+
toggle: (rowId: string) => void;
|
|
33
|
+
};
|
|
34
|
+
/** The `BasaltDataTableProps` fields `useDataTableState` reads — declared here rather than picked
|
|
35
|
+
* off the props type, so the hook owns its contract and this module never imports the component. */
|
|
36
|
+
export type DataTableStateProps = {
|
|
37
|
+
initialSorting?: SortingState | undefined;
|
|
38
|
+
onSortingChange?: ((sorting: SortingState) => void) | undefined;
|
|
39
|
+
initialGlobalFilter?: string | undefined;
|
|
40
|
+
onGlobalFilterChange?: ((value: string) => void) | undefined;
|
|
41
|
+
onColumnFiltersChange?: ((filters: ColumnFiltersState) => void) | undefined;
|
|
42
|
+
initialPagination?: PaginationState | undefined;
|
|
43
|
+
onPaginationChange?: ((pagination: PaginationState) => void) | undefined;
|
|
44
|
+
initialColumnPinning?: ColumnPinningState | undefined;
|
|
45
|
+
rowSelection?: RowSelectionState | undefined;
|
|
46
|
+
onRowSelectionChange?: ((selection: RowSelectionState) => void) | undefined;
|
|
47
|
+
};
|
|
48
|
+
/** The table's own interactive state and the TanStack `on*Change` handlers that write it — every
|
|
49
|
+
* piece uncontrolled with an optional change callback, except row selection, which is controlled
|
|
50
|
+
* whenever `rowSelection` is passed. */
|
|
51
|
+
export declare function useDataTableState(props: DataTableStateProps, defaultPageSize: number): {
|
|
52
|
+
sorting: SortingState;
|
|
53
|
+
onSortingChange: (updater: Updater<SortingState>) => void;
|
|
54
|
+
globalFilter: string;
|
|
55
|
+
onGlobalFilterChange: (updater: Updater<string>) => void;
|
|
56
|
+
columnFilters: ColumnFiltersState;
|
|
57
|
+
onColumnFiltersChange: (updater: Updater<ColumnFiltersState>) => void;
|
|
58
|
+
pagination: PaginationState;
|
|
59
|
+
onPaginationChange: (updater: Updater<PaginationState>) => void;
|
|
60
|
+
columnPinning: ColumnPinningState;
|
|
61
|
+
onColumnPinningChange: import("react").Dispatch<import("react").SetStateAction<ColumnPinningState>>;
|
|
62
|
+
rowSelection: RowSelectionState;
|
|
63
|
+
onRowSelectionChange: (updater: Updater<RowSelectionState>) => void;
|
|
64
|
+
};
|
|
65
|
+
/** Whether the table renders as `renderCard` projections: its own root measured below the `regular`
|
|
66
|
+
* container class. A zero width is an un-laid-out ancestor, not a narrow table, so it moves nothing
|
|
67
|
+
* — the default (and the SSR answer) is the table. The root's width never depends on which body it
|
|
68
|
+
* holds, so the swap cannot oscillate. */
|
|
69
|
+
export declare function useCardProjection(enabled: boolean): {
|
|
70
|
+
rootRef: RefObject<HTMLDivElement | null>;
|
|
71
|
+
cards: boolean;
|
|
72
|
+
};
|
|
73
|
+
/** Click + Enter activation, shared by a row and its card. Enter only: Space is the browser's own
|
|
74
|
+
* page-scroll on a focused non-button, and stealing it from a keyboard reader moving down a long
|
|
75
|
+
* table costs more than the second activation key buys. */
|
|
76
|
+
export declare function activationProps<T>(onRowActivate: ((row: T) => void) | undefined, row: T): {
|
|
77
|
+
className: string;
|
|
78
|
+
'data-activatable': boolean;
|
|
79
|
+
tabIndex: number;
|
|
80
|
+
onClick: () => void;
|
|
81
|
+
onKeyDown: (event: ReactKeyboardEvent<HTMLElement>) => void;
|
|
82
|
+
} | undefined;
|
|
83
|
+
//# sourceMappingURL=data-table-state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"data-table-state.d.ts","sourceRoot":"","sources":["../../src/data/data-table-state.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EACV,kBAAkB,EAClB,kBAAkB,EAClB,eAAe,EACf,iBAAiB,EACjB,YAAY,EACZ,OAAO,EACR,MAAM,uBAAuB,CAAA;AAE9B,OAAO,KAAK,EAAE,aAAa,IAAI,kBAAkB,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAM3E,eAAO,MAAM,cAAc,EAAE,WAAW,CAAC,MAAM,CAAa,CAAA;AAE5D;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,EAClC,IAAI,EACJ,QAAQ,GACT,EAAE;IACD,IAAI,EAAE,SAAS,CAAC,EAAE,CAAA;IAClB,QAAQ,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC,GAAG,SAAS,CAAA;CAC1D,GAAG;IAAE,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAAC,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,CA2BrE;AAuBD;oGACoG;AACpG,MAAM,MAAM,mBAAmB,GAAG;IAChC,cAAc,CAAC,EAAE,YAAY,GAAG,SAAS,CAAA;IACzC,eAAe,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,YAAY,KAAK,IAAI,CAAC,GAAG,SAAS,CAAA;IAC/D,mBAAmB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACxC,oBAAoB,CAAC,EAAE,CAAC,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,SAAS,CAAA;IAC5D,qBAAqB,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,kBAAkB,KAAK,IAAI,CAAC,GAAG,SAAS,CAAA;IAC3E,iBAAiB,CAAC,EAAE,eAAe,GAAG,SAAS,CAAA;IAC/C,kBAAkB,CAAC,EAAE,CAAC,CAAC,UAAU,EAAE,eAAe,KAAK,IAAI,CAAC,GAAG,SAAS,CAAA;IACxE,oBAAoB,CAAC,EAAE,kBAAkB,GAAG,SAAS,CAAA;IACrD,YAAY,CAAC,EAAE,iBAAiB,GAAG,SAAS,CAAA;IAC5C,oBAAoB,CAAC,EAAE,CAAC,CAAC,SAAS,EAAE,iBAAiB,KAAK,IAAI,CAAC,GAAG,SAAS,CAAA;CAC5E,CAAA;AAED;;wCAEwC;AACxC,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM;;yDAjCrD,IAAI;;wDAAJ,IAAI;;qEAAJ,IAAI;;+DAAJ,IAAI;;;;oCA2DtB,OAAO,CAAC,iBAAiB,CAAC;EAgCvC;AAED;;;0CAG0C;AAC1C,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,GAAG;IACnD,OAAO,EAAE,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAA;IACzC,KAAK,EAAE,OAAO,CAAA;CACf,CAcA;AAED;;2DAE2D;AAC3D,wBAAgB,eAAe,CAAC,CAAC,EAAE,aAAa,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,KAAK,IAAI,CAAC,GAAG,SAAS,EAAE,GAAG,EAAE,CAAC;;;;;uBAOjE,kBAAkB,CAAC,WAAW,CAAC;cAUrD"}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { functionalUpdate } from "@tanstack/react-table";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
import { useMeasuredWidths } from "../common/use-measured-widths.js";
|
|
4
|
+
import { CONTAINER_CLASSES } from "../tokens/size-classes.js";
|
|
5
|
+
import classes from "./data-table.module.css";
|
|
6
|
+
const EMPTY_FOLD_SET = /* @__PURE__ */ new Set();
|
|
7
|
+
function useRowDisclosure({
|
|
8
|
+
data,
|
|
9
|
+
getRowId
|
|
10
|
+
}) {
|
|
11
|
+
const [expanded, setExpanded] = useState(EMPTY_FOLD_SET);
|
|
12
|
+
const toggle = useCallback((rowId) => {
|
|
13
|
+
setExpanded((current) => {
|
|
14
|
+
const next = new Set(current);
|
|
15
|
+
if (next.has(rowId)) next.delete(rowId);
|
|
16
|
+
else next.add(rowId);
|
|
17
|
+
return next;
|
|
18
|
+
});
|
|
19
|
+
}, []);
|
|
20
|
+
const prevDataRef = useRef(data);
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
const prevData = prevDataRef.current;
|
|
23
|
+
prevDataRef.current = data;
|
|
24
|
+
setExpanded((current) => {
|
|
25
|
+
if (current.size === 0) return current;
|
|
26
|
+
const liveIds = getRowId && new Set(data.map((row, index) => getRowId(row, index)));
|
|
27
|
+
const isLive = (id) => {
|
|
28
|
+
if (liveIds) return liveIds.has(id);
|
|
29
|
+
const index = Number(id);
|
|
30
|
+
return data[index] !== void 0 && data[index] === prevData[index];
|
|
31
|
+
};
|
|
32
|
+
const next = new Set([...current].filter(isLive));
|
|
33
|
+
return next.size === current.size ? current : next;
|
|
34
|
+
});
|
|
35
|
+
}, [data, getRowId]);
|
|
36
|
+
return { expanded, toggle };
|
|
37
|
+
}
|
|
38
|
+
function useReportedState(initial, onChange) {
|
|
39
|
+
const [value, setValue] = useState(initial);
|
|
40
|
+
const update = useCallback(
|
|
41
|
+
(updater) => {
|
|
42
|
+
setValue((prev) => {
|
|
43
|
+
const next = functionalUpdate(updater, prev);
|
|
44
|
+
onChange?.(next);
|
|
45
|
+
return next;
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
[onChange]
|
|
49
|
+
);
|
|
50
|
+
return [value, update];
|
|
51
|
+
}
|
|
52
|
+
function useDataTableState(props, defaultPageSize) {
|
|
53
|
+
const { rowSelection, onRowSelectionChange } = props;
|
|
54
|
+
const [sorting, onSortingChange] = useReportedState(
|
|
55
|
+
props.initialSorting ?? [],
|
|
56
|
+
props.onSortingChange
|
|
57
|
+
);
|
|
58
|
+
const [globalFilter, onGlobalFilterChange] = useReportedState(
|
|
59
|
+
props.initialGlobalFilter ?? "",
|
|
60
|
+
props.onGlobalFilterChange
|
|
61
|
+
);
|
|
62
|
+
const [columnFilters, onColumnFiltersChange] = useReportedState(
|
|
63
|
+
[],
|
|
64
|
+
props.onColumnFiltersChange
|
|
65
|
+
);
|
|
66
|
+
const [pagination, onPaginationChange] = useReportedState(
|
|
67
|
+
props.initialPagination ?? { pageIndex: 0, pageSize: defaultPageSize },
|
|
68
|
+
props.onPaginationChange
|
|
69
|
+
);
|
|
70
|
+
const [columnPinning, setColumnPinning] = useState(
|
|
71
|
+
props.initialColumnPinning ?? {}
|
|
72
|
+
);
|
|
73
|
+
const [internalRowSelection, setInternalRowSelection] = useState({});
|
|
74
|
+
const handleRowSelectionChange = useCallback(
|
|
75
|
+
(updater) => {
|
|
76
|
+
if (rowSelection !== void 0) {
|
|
77
|
+
onRowSelectionChange?.(functionalUpdate(updater, rowSelection));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
setInternalRowSelection((prev) => {
|
|
81
|
+
const next = functionalUpdate(updater, prev);
|
|
82
|
+
onRowSelectionChange?.(next);
|
|
83
|
+
return next;
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
[rowSelection, onRowSelectionChange]
|
|
87
|
+
);
|
|
88
|
+
return {
|
|
89
|
+
sorting,
|
|
90
|
+
onSortingChange,
|
|
91
|
+
globalFilter,
|
|
92
|
+
onGlobalFilterChange,
|
|
93
|
+
columnFilters,
|
|
94
|
+
onColumnFiltersChange,
|
|
95
|
+
pagination,
|
|
96
|
+
onPaginationChange,
|
|
97
|
+
columnPinning,
|
|
98
|
+
onColumnPinningChange: setColumnPinning,
|
|
99
|
+
rowSelection: rowSelection ?? internalRowSelection,
|
|
100
|
+
onRowSelectionChange: handleRowSelectionChange
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function useCardProjection(enabled) {
|
|
104
|
+
const rootRef = useRef(null);
|
|
105
|
+
const [narrow, setNarrow] = useState(false);
|
|
106
|
+
useMeasuredWidths({
|
|
107
|
+
resolveRoot: () => rootRef.current,
|
|
108
|
+
signature: "",
|
|
109
|
+
enabled,
|
|
110
|
+
onMeasure: (root) => {
|
|
111
|
+
if (root.clientWidth === 0) return;
|
|
112
|
+
const next = root.clientWidth < CONTAINER_CLASSES.regular;
|
|
113
|
+
setNarrow((current) => current === next ? current : next);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
return { rootRef, cards: enabled && narrow };
|
|
117
|
+
}
|
|
118
|
+
function activationProps(onRowActivate, row) {
|
|
119
|
+
if (onRowActivate === void 0) return void 0;
|
|
120
|
+
return {
|
|
121
|
+
className: classes.activatable,
|
|
122
|
+
"data-activatable": true,
|
|
123
|
+
tabIndex: 0,
|
|
124
|
+
onClick: () => onRowActivate(row),
|
|
125
|
+
onKeyDown: (event) => {
|
|
126
|
+
if (event.key !== "Enter") return;
|
|
127
|
+
if (event.target !== event.currentTarget) return;
|
|
128
|
+
event.preventDefault();
|
|
129
|
+
onRowActivate(row);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
export {
|
|
134
|
+
EMPTY_FOLD_SET,
|
|
135
|
+
activationProps,
|
|
136
|
+
useCardProjection,
|
|
137
|
+
useDataTableState,
|
|
138
|
+
useRowDisclosure
|
|
139
|
+
};
|