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,218 @@
|
|
|
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 {
|
|
7
|
+
ColumnFiltersState,
|
|
8
|
+
ColumnPinningState,
|
|
9
|
+
PaginationState,
|
|
10
|
+
RowSelectionState,
|
|
11
|
+
SortingState,
|
|
12
|
+
Updater,
|
|
13
|
+
} from '@tanstack/react-table'
|
|
14
|
+
import { functionalUpdate } from '@tanstack/react-table'
|
|
15
|
+
import type { KeyboardEvent as ReactKeyboardEvent, RefObject } from 'react'
|
|
16
|
+
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
17
|
+
import { useMeasuredWidths } from '../common/use-measured-widths'
|
|
18
|
+
import { CONTAINER_CLASSES } from '../tokens/size-classes'
|
|
19
|
+
import classes from './data-table.module.css'
|
|
20
|
+
|
|
21
|
+
export const EMPTY_FOLD_SET: ReadonlySet<string> = new Set()
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Which rows have their fold disclosure open. `row.id` defaults to the row's index in `data` (no
|
|
25
|
+
* `getRowId`) — stable across a client-side sort/filter/pagination, which reorder or subset the SAME
|
|
26
|
+
* core rows. PRUNED rather than cleared on every `data` change: a non-memoized `data` prop (an inline
|
|
27
|
+
* `.map()`, an unstable query result) hands a brand-new array of the SAME rows on every parent
|
|
28
|
+
* re-render, and clearing on that reference alone collapsed every open disclosure the instant
|
|
29
|
+
* anything upstream re-rendered.
|
|
30
|
+
*
|
|
31
|
+
* Pruning by ID ALONE is only safe with `getRowId` — a caller-declared identity that genuinely
|
|
32
|
+
* survives a page turn or a refetch, so an id no longer present (a manual-pagination page turn, a
|
|
33
|
+
* delete) still has to go, or a differently-shaped record seated at a reused id would silently read
|
|
34
|
+
* as pre-expanded. Without `getRowId` the id IS the array index, and an index survives a refetch
|
|
35
|
+
* trivially — it is still "0", "1", … even when every object behind it is a different record.
|
|
36
|
+
* Keeping an expanded index across THAT would silently attach the old disclosure to a stranger's
|
|
37
|
+
* row. So the index-id lane keeps a `prevData` snapshot and asks the narrower, correct question: is
|
|
38
|
+
* the object AT this index still the same reference as last time? A non-memoized array of the SAME
|
|
39
|
+
* rows answers yes at every index (the case above); a page turn or a refetch answers no.
|
|
40
|
+
*/
|
|
41
|
+
export function useRowDisclosure<T>({
|
|
42
|
+
data,
|
|
43
|
+
getRowId,
|
|
44
|
+
}: {
|
|
45
|
+
data: readonly T[]
|
|
46
|
+
getRowId: ((row: T, index: number) => string) | undefined
|
|
47
|
+
}): { expanded: ReadonlySet<string>; toggle: (rowId: string) => void } {
|
|
48
|
+
const [expanded, setExpanded] = useState<ReadonlySet<string>>(EMPTY_FOLD_SET)
|
|
49
|
+
const toggle = useCallback((rowId: string) => {
|
|
50
|
+
setExpanded((current) => {
|
|
51
|
+
const next = new Set(current)
|
|
52
|
+
if (next.has(rowId)) next.delete(rowId)
|
|
53
|
+
else next.add(rowId)
|
|
54
|
+
return next
|
|
55
|
+
})
|
|
56
|
+
}, [])
|
|
57
|
+
const prevDataRef = useRef(data)
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
const prevData = prevDataRef.current
|
|
60
|
+
prevDataRef.current = data
|
|
61
|
+
setExpanded((current) => {
|
|
62
|
+
if (current.size === 0) return current
|
|
63
|
+
const liveIds = getRowId && new Set(data.map((row, index) => getRowId(row, index)))
|
|
64
|
+
const isLive = (id: string) => {
|
|
65
|
+
if (liveIds) return liveIds.has(id)
|
|
66
|
+
const index = Number(id)
|
|
67
|
+
return data[index] !== undefined && data[index] === prevData[index]
|
|
68
|
+
}
|
|
69
|
+
const next = new Set([...current].filter(isLive))
|
|
70
|
+
return next.size === current.size ? current : next
|
|
71
|
+
})
|
|
72
|
+
}, [data, getRowId])
|
|
73
|
+
return { expanded, toggle }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** `useState` whose every write also reports the settled value — the one shape sorting, the global
|
|
77
|
+
* filter, the column filters and pagination all share. The callback runs inside the updater, so it
|
|
78
|
+
* sees exactly the value TanStack applied. */
|
|
79
|
+
function useReportedState<S>(
|
|
80
|
+
initial: S,
|
|
81
|
+
onChange: ((next: S) => void) | undefined,
|
|
82
|
+
): [S, (updater: Updater<S>) => void] {
|
|
83
|
+
const [value, setValue] = useState(initial)
|
|
84
|
+
const update = useCallback(
|
|
85
|
+
(updater: Updater<S>) => {
|
|
86
|
+
setValue((prev) => {
|
|
87
|
+
const next = functionalUpdate(updater, prev)
|
|
88
|
+
onChange?.(next)
|
|
89
|
+
return next
|
|
90
|
+
})
|
|
91
|
+
},
|
|
92
|
+
[onChange],
|
|
93
|
+
)
|
|
94
|
+
return [value, update]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The `BasaltDataTableProps` fields `useDataTableState` reads — declared here rather than picked
|
|
98
|
+
* off the props type, so the hook owns its contract and this module never imports the component. */
|
|
99
|
+
export type DataTableStateProps = {
|
|
100
|
+
initialSorting?: SortingState | undefined
|
|
101
|
+
onSortingChange?: ((sorting: SortingState) => void) | undefined
|
|
102
|
+
initialGlobalFilter?: string | undefined
|
|
103
|
+
onGlobalFilterChange?: ((value: string) => void) | undefined
|
|
104
|
+
onColumnFiltersChange?: ((filters: ColumnFiltersState) => void) | undefined
|
|
105
|
+
initialPagination?: PaginationState | undefined
|
|
106
|
+
onPaginationChange?: ((pagination: PaginationState) => void) | undefined
|
|
107
|
+
initialColumnPinning?: ColumnPinningState | undefined
|
|
108
|
+
rowSelection?: RowSelectionState | undefined
|
|
109
|
+
onRowSelectionChange?: ((selection: RowSelectionState) => void) | undefined
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The table's own interactive state and the TanStack `on*Change` handlers that write it — every
|
|
113
|
+
* piece uncontrolled with an optional change callback, except row selection, which is controlled
|
|
114
|
+
* whenever `rowSelection` is passed. */
|
|
115
|
+
export function useDataTableState(props: DataTableStateProps, defaultPageSize: number) {
|
|
116
|
+
const { rowSelection, onRowSelectionChange } = props
|
|
117
|
+
const [sorting, onSortingChange] = useReportedState(
|
|
118
|
+
props.initialSorting ?? [],
|
|
119
|
+
props.onSortingChange,
|
|
120
|
+
)
|
|
121
|
+
const [globalFilter, onGlobalFilterChange] = useReportedState(
|
|
122
|
+
props.initialGlobalFilter ?? '',
|
|
123
|
+
props.onGlobalFilterChange,
|
|
124
|
+
)
|
|
125
|
+
const [columnFilters, onColumnFiltersChange] = useReportedState<ColumnFiltersState>(
|
|
126
|
+
[],
|
|
127
|
+
props.onColumnFiltersChange,
|
|
128
|
+
)
|
|
129
|
+
const [pagination, onPaginationChange] = useReportedState(
|
|
130
|
+
props.initialPagination ?? { pageIndex: 0, pageSize: defaultPageSize },
|
|
131
|
+
props.onPaginationChange,
|
|
132
|
+
)
|
|
133
|
+
const [columnPinning, setColumnPinning] = useState<ColumnPinningState>(
|
|
134
|
+
props.initialColumnPinning ?? {},
|
|
135
|
+
)
|
|
136
|
+
// Uncontrolled by default; `rowSelection`, when passed, is the truth and this only mirrors it so
|
|
137
|
+
// the updater below has a base to apply against.
|
|
138
|
+
const [internalRowSelection, setInternalRowSelection] = useState<RowSelectionState>({})
|
|
139
|
+
|
|
140
|
+
const handleRowSelectionChange = useCallback(
|
|
141
|
+
(updater: Updater<RowSelectionState>) => {
|
|
142
|
+
// CONTROLLED: the caller's map is the base AND the only writer. Mirroring it into state too
|
|
143
|
+
// would leave a copy that the caller never moves, and that stale copy becomes the selection
|
|
144
|
+
// the moment `rowSelection` goes back to `undefined` — plus every tick would render twice for
|
|
145
|
+
// one change. Report the next map and let the caller own it.
|
|
146
|
+
if (rowSelection !== undefined) {
|
|
147
|
+
onRowSelectionChange?.(functionalUpdate(updater, rowSelection))
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
setInternalRowSelection((prev) => {
|
|
151
|
+
const next = functionalUpdate(updater, prev)
|
|
152
|
+
onRowSelectionChange?.(next)
|
|
153
|
+
return next
|
|
154
|
+
})
|
|
155
|
+
},
|
|
156
|
+
[rowSelection, onRowSelectionChange],
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
sorting,
|
|
161
|
+
onSortingChange,
|
|
162
|
+
globalFilter,
|
|
163
|
+
onGlobalFilterChange,
|
|
164
|
+
columnFilters,
|
|
165
|
+
onColumnFiltersChange,
|
|
166
|
+
pagination,
|
|
167
|
+
onPaginationChange,
|
|
168
|
+
columnPinning,
|
|
169
|
+
onColumnPinningChange: setColumnPinning,
|
|
170
|
+
rowSelection: rowSelection ?? internalRowSelection,
|
|
171
|
+
onRowSelectionChange: handleRowSelectionChange,
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Whether the table renders as `renderCard` projections: its own root measured below the `regular`
|
|
176
|
+
* container class. A zero width is an un-laid-out ancestor, not a narrow table, so it moves nothing
|
|
177
|
+
* — the default (and the SSR answer) is the table. The root's width never depends on which body it
|
|
178
|
+
* holds, so the swap cannot oscillate. */
|
|
179
|
+
export function useCardProjection(enabled: boolean): {
|
|
180
|
+
rootRef: RefObject<HTMLDivElement | null>
|
|
181
|
+
cards: boolean
|
|
182
|
+
} {
|
|
183
|
+
const rootRef = useRef<HTMLDivElement>(null)
|
|
184
|
+
const [narrow, setNarrow] = useState(false)
|
|
185
|
+
useMeasuredWidths({
|
|
186
|
+
resolveRoot: () => rootRef.current,
|
|
187
|
+
signature: '',
|
|
188
|
+
enabled,
|
|
189
|
+
onMeasure: (root) => {
|
|
190
|
+
if (root.clientWidth === 0) return
|
|
191
|
+
const next = root.clientWidth < CONTAINER_CLASSES.regular
|
|
192
|
+
setNarrow((current) => (current === next ? current : next))
|
|
193
|
+
},
|
|
194
|
+
})
|
|
195
|
+
return { rootRef, cards: enabled && narrow }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Click + Enter activation, shared by a row and its card. Enter only: Space is the browser's own
|
|
199
|
+
* page-scroll on a focused non-button, and stealing it from a keyboard reader moving down a long
|
|
200
|
+
* table costs more than the second activation key buys. */
|
|
201
|
+
export function activationProps<T>(onRowActivate: ((row: T) => void) | undefined, row: T) {
|
|
202
|
+
if (onRowActivate === undefined) return undefined
|
|
203
|
+
return {
|
|
204
|
+
className: classes.activatable,
|
|
205
|
+
'data-activatable': true,
|
|
206
|
+
tabIndex: 0,
|
|
207
|
+
onClick: () => onRowActivate(row),
|
|
208
|
+
onKeyDown: (event: ReactKeyboardEvent<HTMLElement>) => {
|
|
209
|
+
if (event.key !== 'Enter') return
|
|
210
|
+
// Only the element's own Enter. A cell or card may hold a button, a link or the selection
|
|
211
|
+
// checkbox, and keydown bubbles — so without this an Enter on a nested control fired that
|
|
212
|
+
// control AND opened the row's detail behind it.
|
|
213
|
+
if (event.target !== event.currentTarget) return
|
|
214
|
+
event.preventDefault()
|
|
215
|
+
onRowActivate(row)
|
|
216
|
+
},
|
|
217
|
+
}
|
|
218
|
+
}
|