basalt-ui 2.1.0 → 2.3.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 +31 -0
- package/MIGRATING.md +46 -12
- package/configs/oxlint.json +1 -0
- package/dist/charts/BandStrip.d.ts +7 -7
- package/dist/charts/BandStrip.d.ts.map +1 -1
- package/dist/charts/Bars.d.ts +4 -4
- package/dist/charts/Bars.d.ts.map +1 -1
- package/dist/charts/ChartCard.d.ts +10 -10
- package/dist/charts/ChartCard.d.ts.map +1 -1
- package/dist/charts/ChartCard.js +2 -1
- package/dist/charts/Donut.d.ts +5 -5
- package/dist/charts/Donut.d.ts.map +1 -1
- package/dist/charts/DualPanel.d.ts +9 -9
- package/dist/charts/DualPanel.d.ts.map +1 -1
- package/dist/charts/Heatmap.d.ts +18 -16
- package/dist/charts/Heatmap.d.ts.map +1 -1
- package/dist/charts/Heatmap.js +8 -2
- package/dist/charts/MirroredBars.d.ts +9 -9
- package/dist/charts/MirroredBars.d.ts.map +1 -1
- package/dist/charts/Sparkline.d.ts +9 -5
- package/dist/charts/Sparkline.d.ts.map +1 -1
- package/dist/charts/Sparkline.js +18 -12
- package/dist/charts/StackedArea.d.ts +2 -2
- package/dist/charts/StackedArea.d.ts.map +1 -1
- package/dist/charts/compile.d.ts +17 -1
- package/dist/charts/compile.d.ts.map +1 -1
- package/dist/charts/compile.js +56 -2
- package/dist/charts/index.d.ts +1 -0
- package/dist/charts/index.d.ts.map +1 -1
- package/dist/charts/index.js +3 -1
- package/dist/charts/state.d.ts +3 -0
- package/dist/charts/state.d.ts.map +1 -1
- package/dist/charts/state.js +14 -3
- package/dist/charts/types.d.ts +64 -61
- package/dist/charts/types.d.ts.map +1 -1
- package/dist/cli/doctor.js +2 -2
- package/dist/dashboard/index.d.ts +1 -1
- package/dist/dashboard/index.d.ts.map +1 -1
- package/dist/dashboard/index.js +3 -1
- package/dist/dashboard/stat-card.d.ts +16 -1
- package/dist/dashboard/stat-card.d.ts.map +1 -1
- package/dist/dashboard/stat-card.js +14 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/src/charts/BandStrip.tsx +12 -10
- package/src/charts/Bars.tsx +4 -4
- package/src/charts/ChartCard.tsx +17 -11
- package/src/charts/Donut.tsx +5 -5
- package/src/charts/DualPanel.tsx +13 -6
- package/src/charts/Heatmap.tsx +23 -15
- package/src/charts/MirroredBars.tsx +8 -8
- package/src/charts/Sparkline.tsx +14 -6
- package/src/charts/StackedArea.tsx +2 -2
- package/src/charts/compile.ts +92 -1
- package/src/charts/index.ts +1 -0
- package/src/charts/state.tsx +15 -3
- package/src/charts/types.ts +70 -57
- package/src/cli/doctor.ts +2 -2
- package/src/dashboard/index.ts +6 -1
- package/src/dashboard/stat-card.tsx +34 -3
- package/src/index.ts +1 -0
package/src/charts/state.tsx
CHANGED
|
@@ -45,6 +45,9 @@ export function ChartPlaceholder(props: {
|
|
|
45
45
|
state: ChartState | undefined
|
|
46
46
|
height?: ChartBoxHeight
|
|
47
47
|
action?: ReactNode
|
|
48
|
+
/** For a box too small to hold a sentence (a sparkline): error and empty show a `—` and carry the
|
|
49
|
+
* label as the accessible name and the hover title. */
|
|
50
|
+
compact?: boolean
|
|
48
51
|
}): ReactElement | null {
|
|
49
52
|
const kind = resolveChartState(props.state)
|
|
50
53
|
if (kind === null) return null
|
|
@@ -60,20 +63,29 @@ export function ChartPlaceholder(props: {
|
|
|
60
63
|
kind === 'error'
|
|
61
64
|
? toErrorMessage(props.state?.error, 'The chart failed to load.')
|
|
62
65
|
: emptyLabel(props.state)
|
|
63
|
-
const body = (
|
|
66
|
+
const body = props.compact ? (
|
|
67
|
+
<span aria-hidden="true">—</span>
|
|
68
|
+
) : (
|
|
64
69
|
<>
|
|
65
70
|
<span>{label}</span>
|
|
66
71
|
{props.action}
|
|
67
72
|
</>
|
|
68
73
|
)
|
|
74
|
+
const name = props.compact ? { 'aria-label': label, title: label } : {}
|
|
69
75
|
if (kind === 'error')
|
|
70
76
|
return fit(
|
|
71
|
-
<div
|
|
77
|
+
<div
|
|
78
|
+
role="alert"
|
|
79
|
+
data-basalt-chart-state={kind}
|
|
80
|
+
className={stateClass}
|
|
81
|
+
style={style}
|
|
82
|
+
{...name}
|
|
83
|
+
>
|
|
72
84
|
{body}
|
|
73
85
|
</div>,
|
|
74
86
|
)
|
|
75
87
|
return fit(
|
|
76
|
-
<output data-basalt-chart-state={kind} className={stateClass} style={style}>
|
|
88
|
+
<output data-basalt-chart-state={kind} className={stateClass} style={style} {...name}>
|
|
77
89
|
{body}
|
|
78
90
|
</output>,
|
|
79
91
|
)
|
package/src/charts/types.ts
CHANGED
|
@@ -24,35 +24,44 @@ export type ChartSeries<T> = {
|
|
|
24
24
|
* series whose `getValue` is always `null` is a legend-only entry (a zone or reference key). */
|
|
25
25
|
getValue: (d: T) => number | null
|
|
26
26
|
/** The legend swatch shape. Default: the kind's own mark (`line` in `Lines`, `bar` in `Bars`). */
|
|
27
|
-
mark?: 'line' | 'bar' | 'area'
|
|
27
|
+
mark?: 'line' | 'bar' | 'area' | undefined
|
|
28
28
|
/** Default `'solid'`; `'dashed'` is `VX.dashArray` on the line, the legend and the tooltip. */
|
|
29
|
-
dash?: 'solid' | 'dashed'
|
|
29
|
+
dash?: 'solid' | 'dashed' | undefined
|
|
30
30
|
/** Interpolation between points. Default `'monotone'`. A piecewise-constant quantity (a price
|
|
31
31
|
* tier, a config value) is a step: a smoothed curve draws values that never existed. */
|
|
32
|
-
curve?: CurveName
|
|
33
|
-
strokeWidth?: number
|
|
32
|
+
curve?: CurveName | undefined
|
|
33
|
+
strokeWidth?: number | undefined
|
|
34
34
|
/** Dims the plotted stroke (a faint companion line). Strokes only. */
|
|
35
|
-
strokeOpacity?: number
|
|
35
|
+
strokeOpacity?: number | undefined
|
|
36
36
|
/** Bars and areas. */
|
|
37
|
-
fillOpacity?: number
|
|
38
|
-
/** `Lines`: a soft gradient fill under the line — `true` in the series color,
|
|
39
|
-
|
|
37
|
+
fillOpacity?: number | undefined
|
|
38
|
+
/** `Lines`: a soft gradient fill under the line — `true` in the series color, a color ref, or
|
|
39
|
+
* `'zones'`: a vertical gradient coloured by the chart's `zones` on this series' axis, hard stops
|
|
40
|
+
* at the zone boundaries (no zone in reach: the series color). Linear axes only. */
|
|
41
|
+
area?: boolean | 'zones' | (string & {}) | undefined
|
|
40
42
|
/** Which y axis the series is measured against. `'right'` needs the chart's `y2`. */
|
|
41
|
-
axis?: 'left' | 'right'
|
|
43
|
+
axis?: 'left' | 'right' | undefined
|
|
42
44
|
/** Legend grouping: series → overlay → reference. Default `'series'`. */
|
|
43
|
-
role?: 'series' | 'overlay' | 'reference'
|
|
45
|
+
role?: 'series' | 'overlay' | 'reference' | undefined
|
|
44
46
|
/** `false` keeps it out of the legend (a companion line folded under `parent`). Default `true`. */
|
|
45
|
-
legend?: boolean
|
|
47
|
+
legend?: boolean | undefined
|
|
46
48
|
/** `false` draws and legends it but never lists it as a tooltip row. Default `true`. */
|
|
47
|
-
tooltip?: boolean
|
|
49
|
+
tooltip?: boolean | undefined
|
|
48
50
|
/** A companion (an MA line) names its parent, so a legend toggle and hover keep the pair. */
|
|
49
|
-
parent?: string
|
|
51
|
+
parent?: string | undefined
|
|
50
52
|
/** A short qualifier after the label in the legend and the tooltip row, in muted text. */
|
|
51
|
-
note?: string
|
|
53
|
+
note?: string | undefined
|
|
52
54
|
/** Per-series tooltip value format; receives the datum so a row can read more than the value. */
|
|
53
|
-
formatValue?: (v: number, d: T) => string
|
|
55
|
+
formatValue?: ((v: number, d: T) => string) | undefined
|
|
54
56
|
/** A marker at a point (a PR star, a status dot); `null` for none. */
|
|
55
|
-
getMarker?:
|
|
57
|
+
getMarker?:
|
|
58
|
+
| ((d: T) => {
|
|
59
|
+
color?: string | undefined
|
|
60
|
+
r?: number | undefined
|
|
61
|
+
fillOpacity?: number | undefined
|
|
62
|
+
ring?: boolean | undefined
|
|
63
|
+
} | null)
|
|
64
|
+
| undefined
|
|
56
65
|
}
|
|
57
66
|
|
|
58
67
|
/** One y axis. */
|
|
@@ -66,23 +75,24 @@ export type AxisConfig<T> = {
|
|
|
66
75
|
| readonly [number, number]
|
|
67
76
|
| 'auto'
|
|
68
77
|
| ((data: readonly T[], visible: readonly ChartSeries<T>[]) => readonly [number, number])
|
|
78
|
+
| undefined
|
|
69
79
|
/** When `'auto'`: the raw upper bound is at least this; padding applies after. */
|
|
70
|
-
autoMaxFloor?: number
|
|
80
|
+
autoMaxFloor?: number | undefined
|
|
71
81
|
/** When `'auto'`: the lower bound is at most this. Default `0` (a zero baseline); `Infinity` pads
|
|
72
82
|
* down from the data minimum instead. Ignored on a log axis. */
|
|
73
|
-
autoMinCeil?: number
|
|
83
|
+
autoMinCeil?: number | undefined
|
|
74
84
|
/** When `'auto'`: padding multiplier away from the data. Default `1.1`. */
|
|
75
|
-
autoPad?: number
|
|
85
|
+
autoPad?: number | undefined
|
|
76
86
|
/** A tick count, or the exact tick values. Default: one per ~48px of plot height, ≤ 5. */
|
|
77
|
-
ticks?: number | readonly number[]
|
|
87
|
+
ticks?: number | readonly number[] | undefined
|
|
78
88
|
/** Tick and tooltip formatter. Default: compact numbers (`12.5k`). */
|
|
79
|
-
format?: (v: number) => string
|
|
89
|
+
format?: ((v: number) => string) | undefined
|
|
80
90
|
/** Horizontal grid rules. Default: on for `y`, off for `y2`. */
|
|
81
|
-
grid?: boolean
|
|
91
|
+
grid?: boolean | undefined
|
|
82
92
|
/** Round the domain outward to nice ticks. Default `false` (a fixed domain stays fixed). */
|
|
83
|
-
nice?: boolean
|
|
93
|
+
nice?: boolean | undefined
|
|
84
94
|
/** Default `'linear'`. A log axis floors at the smallest positive visible value. */
|
|
85
|
-
scale?: 'linear' | 'log'
|
|
95
|
+
scale?: 'linear' | 'log' | undefined
|
|
86
96
|
}
|
|
87
97
|
|
|
88
98
|
/** A horizontal band on one y axis. `±Infinity` reaches the plot edge. */
|
|
@@ -90,7 +100,7 @@ export type ZoneSpec = {
|
|
|
90
100
|
from: number
|
|
91
101
|
to: number
|
|
92
102
|
fill: string
|
|
93
|
-
axisSide?: 'left' | 'right'
|
|
103
|
+
axisSide?: 'left' | 'right' | undefined
|
|
94
104
|
}
|
|
95
105
|
|
|
96
106
|
/**
|
|
@@ -99,18 +109,18 @@ export type ZoneSpec = {
|
|
|
99
109
|
* from one hour to another, both hours included).
|
|
100
110
|
*/
|
|
101
111
|
export type XZoneSpec<X extends ChartX = ChartX> = {
|
|
102
|
-
from?: X
|
|
103
|
-
to?: X
|
|
112
|
+
from?: X | undefined
|
|
113
|
+
to?: X | undefined
|
|
104
114
|
fill: string
|
|
105
|
-
align?: 'center' | 'edge'
|
|
115
|
+
align?: 'center' | 'edge' | undefined
|
|
106
116
|
}
|
|
107
117
|
|
|
108
118
|
/** A horizontal reference line — annotation only, no domain role beyond staying inside the plot. */
|
|
109
119
|
export type RefLine = {
|
|
110
120
|
value: number
|
|
111
121
|
color: string
|
|
112
|
-
dashed?: boolean
|
|
113
|
-
axis?: 'left' | 'right'
|
|
122
|
+
dashed?: boolean | undefined
|
|
123
|
+
axis?: 'left' | 'right' | undefined
|
|
114
124
|
}
|
|
115
125
|
|
|
116
126
|
/** A fill between the line and a value, on one side of it, tracking the line. */
|
|
@@ -134,43 +144,46 @@ export type ChartTooltipRow = {
|
|
|
134
144
|
export type ChartTooltipConfig<T, X extends ChartX = ChartX> = {
|
|
135
145
|
/** The tooltip title. Default: the x value through `formatX`'s long form (a date reads
|
|
136
146
|
* `Mon Sep 1 2026`). */
|
|
137
|
-
formatHeader?: (x: X, d: T) => string
|
|
147
|
+
formatHeader?: ((x: X, d: T) => string) | undefined
|
|
138
148
|
/** Rows after the per-series rows (a total, a zone badge, a context line). `visible`/`hidden` are
|
|
139
149
|
* the legend's split, so a row never names a series the plot no longer draws. */
|
|
140
|
-
rows?:
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
150
|
+
rows?:
|
|
151
|
+
| ((
|
|
152
|
+
d: T,
|
|
153
|
+
ctx: { visible: readonly ChartSeries<T>[]; hidden: readonly ChartSeries<T>[] },
|
|
154
|
+
) => readonly ChartTooltipRow[])
|
|
155
|
+
| undefined
|
|
144
156
|
/** Show this chart's tooltip while it FOLLOWS a sibling's cursor. Default `false`: only the chart
|
|
145
157
|
* under the pointer or keyboard focus shows a tooltip and a live region. */
|
|
146
|
-
onFollow?: boolean
|
|
158
|
+
onFollow?: boolean | undefined
|
|
147
159
|
}
|
|
148
160
|
|
|
149
161
|
export type ChartLegendConfig = {
|
|
150
162
|
/** Default `'bottom'`. `'header'` draws the legend at the top of the chart body, spaced as the
|
|
151
163
|
* enclosing `ChartCard`'s header legend row (in the chart's own markup, so it is in the server
|
|
152
164
|
* HTML and the header never grows); outside a card it is `'top'`. */
|
|
153
|
-
placement?: 'top' | 'bottom' | 'header'
|
|
165
|
+
placement?: 'top' | 'bottom' | 'header' | undefined
|
|
154
166
|
/** Visually separate roles: series | overlay | reference. */
|
|
155
|
-
groups?: boolean
|
|
167
|
+
groups?: boolean | undefined
|
|
156
168
|
/** Clicking an entry hides that series. Default: on whenever there are two or more entries. */
|
|
157
|
-
toggle?: boolean
|
|
169
|
+
toggle?: boolean | undefined
|
|
158
170
|
}
|
|
159
171
|
|
|
160
172
|
/** A height per container class, `base` below `regular`. A plain number is fixed. */
|
|
161
173
|
export type ResponsiveChartHeight = {
|
|
162
174
|
base: number
|
|
163
|
-
regular?: number
|
|
164
|
-
wide?: number
|
|
175
|
+
regular?: number | undefined
|
|
176
|
+
wide?: number | undefined
|
|
165
177
|
}
|
|
166
178
|
|
|
167
179
|
/** The three "nothing to draw" states, pending → error → empty. */
|
|
168
180
|
export type ChartState = {
|
|
169
|
-
pending?: boolean
|
|
181
|
+
pending?: boolean | undefined
|
|
170
182
|
/** Truthy = the query failed; an `Error`'s message is the label. */
|
|
171
183
|
error?: unknown
|
|
172
|
-
/** `true`, or the empty label itself.
|
|
173
|
-
|
|
184
|
+
/** `true`, or the empty label itself. The label is the one way to say WHY (`'No runs match the
|
|
185
|
+
* filter'` vs `'No runs yet'`): § 9 fixes the state shape, so there is no reason field. */
|
|
186
|
+
empty?: boolean | string | undefined
|
|
174
187
|
}
|
|
175
188
|
|
|
176
189
|
/** A row of `plot.rows(key)`: one per datum, `y: null` kept as a gap. */
|
|
@@ -214,27 +227,27 @@ export type CartesianChartProps<T, X extends ChartX = ChartX> = BasaltProps & {
|
|
|
214
227
|
series: readonly ChartSeries<T>[]
|
|
215
228
|
/** The plot's marks. Draw `plot.rows(key)` / `plot.visible`, never `series` directly. */
|
|
216
229
|
marks: (plot: PlotContext<T, X>) => ChartMarks
|
|
217
|
-
y?: AxisConfig<T>
|
|
230
|
+
y?: AxisConfig<T> | undefined
|
|
218
231
|
/** A right axis; series with `axis: 'right'` and marks bound to `plot.y2` read it. */
|
|
219
|
-
y2?: AxisConfig<T>
|
|
232
|
+
y2?: AxisConfig<T> | undefined
|
|
220
233
|
/** A tick count, or the exact x tick values. Default: by container class. */
|
|
221
|
-
xTicks?: number | readonly X[]
|
|
234
|
+
xTicks?: number | readonly X[] | undefined
|
|
222
235
|
/** Tick label for an x value. Default: `DD.MM` for dates, compact numbers, strings verbatim. */
|
|
223
|
-
formatX?: (x: X) => string
|
|
224
|
-
zones?: readonly ZoneSpec[]
|
|
225
|
-
xZones?: readonly XZoneSpec<X>[]
|
|
226
|
-
refLines?: readonly RefLine[]
|
|
236
|
+
formatX?: ((x: X) => string) | undefined
|
|
237
|
+
zones?: readonly ZoneSpec[] | undefined
|
|
238
|
+
xZones?: readonly XZoneSpec<X>[] | undefined
|
|
239
|
+
refLines?: readonly RefLine[] | undefined
|
|
227
240
|
/** `false` hides the legend. */
|
|
228
|
-
legend?: ChartLegendConfig | false
|
|
241
|
+
legend?: ChartLegendConfig | false | undefined
|
|
229
242
|
/** Default: the height law (measured width × class ratio, 160–420px, ≤ 45vh on touch). */
|
|
230
|
-
height?: number | ResponsiveChartHeight
|
|
231
|
-
state?: ChartState
|
|
243
|
+
height?: number | ResponsiveChartHeight | undefined
|
|
244
|
+
state?: ChartState | undefined
|
|
232
245
|
/** Joins a named cursor group instead of the page-shared default. */
|
|
233
|
-
cursorGroup?: string
|
|
246
|
+
cursorGroup?: string | undefined
|
|
234
247
|
/** Default `'nearest'`. `'leading'` for a series keyed by a bucket's leading edge (a weekly
|
|
235
248
|
* value keyed by its Monday). */
|
|
236
|
-
cursorResolution?: CursorResolution
|
|
237
|
-
tooltip?: ChartTooltipConfig<T, X> | false
|
|
249
|
+
cursorResolution?: CursorResolution | undefined
|
|
250
|
+
tooltip?: ChartTooltipConfig<T, X> | false | undefined
|
|
238
251
|
}
|
|
239
252
|
|
|
240
253
|
/** The x channel of a `plot.rows()` row, as an accessor (a field name does not type-check against a
|
package/src/cli/doctor.ts
CHANGED
|
@@ -395,7 +395,7 @@ export function doctor(invocationCwd: string = process.cwd(), flags: string[] =
|
|
|
395
395
|
],
|
|
396
396
|
[
|
|
397
397
|
'dropped-subpaths: no import of a basalt-ui subpath the install lacks',
|
|
398
|
-
droppedSubpathsCheck(
|
|
398
|
+
droppedSubpathsCheck(appDir, cfg, install),
|
|
399
399
|
],
|
|
400
400
|
[
|
|
401
401
|
'app-wiring: the entry mounts BasaltProvider and the router tree is generated',
|
|
@@ -404,7 +404,7 @@ export function doctor(invocationCwd: string = process.cwd(), flags: string[] =
|
|
|
404
404
|
['exact-peers: installed exact-pinned peers match basalt-ui', exactPeersCheck(cwd, install)],
|
|
405
405
|
[
|
|
406
406
|
'subpath-peers: every imported basalt-ui subpath has its required peers installed',
|
|
407
|
-
subpathPeersCheck(
|
|
407
|
+
subpathPeersCheck(appDir, cfg, install),
|
|
408
408
|
],
|
|
409
409
|
[
|
|
410
410
|
'config-keys: the basalt config block sets only keys basalt-ui reads',
|
package/src/dashboard/index.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
export { WidgetHeader } from './widget-header'
|
|
2
2
|
export { DeltaBadge } from './delta-badge'
|
|
3
3
|
export { Section } from './section'
|
|
4
|
-
export {
|
|
4
|
+
export {
|
|
5
|
+
StatCard,
|
|
6
|
+
type StatCardBreakdownRow,
|
|
7
|
+
type StatCardSparklineSize,
|
|
8
|
+
type StatCardTone,
|
|
9
|
+
} from './stat-card'
|
|
5
10
|
export { StatGroup } from './stat-group'
|
|
6
11
|
export { EmptyState } from './empty-state'
|
|
7
12
|
export { SettingsSection, SettingsRow, DangerZone } from './settings-section'
|
|
@@ -23,7 +23,9 @@
|
|
|
23
23
|
* here. Pass your own sparkline at the call site (basalt ships no `Sparkline` kind in 2.0.0).
|
|
24
24
|
*
|
|
25
25
|
* `query` hands the card's pending/error/empty branch to `QueryState` at the `'section'` tier,
|
|
26
|
-
* rendered under the header rather than over it — the title row is chrome and must not flicker.
|
|
26
|
+
* rendered under the header rather than over it — the title row is chrome and must not flicker. The
|
|
27
|
+
* same query also reaches a render-prop `sparkline` as `size.state`, so `<Sparkline {...size} />`
|
|
28
|
+
* shows its sized pending / error placeholder with no hand-rolled `'—'`.
|
|
27
29
|
*
|
|
28
30
|
* `tone` marks the card as past a threshold with an accent rail down its leading edge. It exists
|
|
29
31
|
* because `value` is typed `string`: without it, a card whose number has crossed a threshold reads
|
|
@@ -106,6 +108,7 @@ import { cx } from '../common/props'
|
|
|
106
108
|
import type { BasaltProps, SlotStylesProps, Tone } from '../common/props'
|
|
107
109
|
import { QueryState } from './query-state'
|
|
108
110
|
import type { QueryStateLike } from './query-state'
|
|
111
|
+
import { assertQueryStateLike } from '../common/query-state-like'
|
|
109
112
|
import classes from './stat-card.module.css'
|
|
110
113
|
|
|
111
114
|
/**
|
|
@@ -160,6 +163,13 @@ const SPARKLINE_RIGHT_HEIGHT = 26
|
|
|
160
163
|
export type StatCardSparklineSize = {
|
|
161
164
|
width: number
|
|
162
165
|
height: number
|
|
166
|
+
/**
|
|
167
|
+
* The card's `query` as a chart state — present only while the sparkline has nothing to draw
|
|
168
|
+
* (pending, or failed with no data), so `<Sparkline {...size} />` forwards it and the tile shows a
|
|
169
|
+
* sized placeholder instead of a hand-rolled `'—'`. Structurally a `ChartState`: this module
|
|
170
|
+
* stays chart-free. Absent without a `query`, and once data has arrived.
|
|
171
|
+
*/
|
|
172
|
+
state?: { pending?: boolean; error?: unknown; empty?: boolean }
|
|
163
173
|
}
|
|
164
174
|
|
|
165
175
|
/**
|
|
@@ -199,6 +209,18 @@ function useSlotWidth(active: boolean): {
|
|
|
199
209
|
}
|
|
200
210
|
}
|
|
201
211
|
|
|
212
|
+
/** The `query` as a sparkline state, by the same precedence as {@link QueryState}: an error with no
|
|
213
|
+
* data is an error, no data and not idle is pending, anything else draws. */
|
|
214
|
+
function sparklineState(
|
|
215
|
+
query: QueryStateLike<unknown> | undefined,
|
|
216
|
+
): StatCardSparklineSize['state'] {
|
|
217
|
+
if (query === undefined) return undefined
|
|
218
|
+
assertQueryStateLike('StatCard', query)
|
|
219
|
+
if (query.data !== undefined) return undefined
|
|
220
|
+
if (query.isError) return { error: query.error ?? true }
|
|
221
|
+
return query.fetchStatus === 'idle' ? undefined : { pending: true }
|
|
222
|
+
}
|
|
223
|
+
|
|
202
224
|
/** The rail's width, in px. Wide enough to read as a deliberate mark at a glance, narrow enough
|
|
203
225
|
* that it cannot be mistaken for the card's own edge. */
|
|
204
226
|
const TONE_RAIL_WIDTH = 3
|
|
@@ -270,8 +292,12 @@ export type StatCardProps = BasaltProps &
|
|
|
270
292
|
* measured width is the card's own inner width including the bled inset; with `'right'` it is the
|
|
271
293
|
* fixed 72×26 slot (see {@link SPARKLINE_RIGHT_WIDTH} for why that one is not measured).
|
|
272
294
|
*
|
|
295
|
+
* A render prop is a sanctioned pattern here, so `react/no-unstable-nested-components` is
|
|
296
|
+
* configured with `allowAsProps` in the shipped preset. Spread the size (`{...size}`) to forward
|
|
297
|
+
* `state` along with the box.
|
|
298
|
+
*
|
|
273
299
|
* @example
|
|
274
|
-
* sparkline={(
|
|
300
|
+
* sparkline={(size) => <Sparkline ariaLabel="Active users" data={history} {...size} />}
|
|
275
301
|
*/
|
|
276
302
|
sparkline?: ReactNode | ((size: StatCardSparklineSize) => ReactNode)
|
|
277
303
|
/** Where `sparkline` sits. `'bleed'` (default) is today's full-width row bled to the card edges;
|
|
@@ -313,7 +339,12 @@ export function StatCard({
|
|
|
313
339
|
const bleeds = sparklinePlacement === 'bleed'
|
|
314
340
|
// Only a render prop needs a measurement; a plain node sizes itself.
|
|
315
341
|
const { ref: slotRef, width: slotWidth } = useSlotWidth(isRender)
|
|
316
|
-
const
|
|
342
|
+
const state = sparklineState(query)
|
|
343
|
+
const measured: StatCardSparklineSize = {
|
|
344
|
+
width: slotWidth,
|
|
345
|
+
height: SPARKLINE_RIGHT_HEIGHT,
|
|
346
|
+
...(state !== undefined && { state }),
|
|
347
|
+
}
|
|
317
348
|
|
|
318
349
|
// A `<dl>`, not a table and not a stack of divs: each row IS a term and its value, which is the
|
|
319
350
|
// one semantic that survives a reader meeting the card out of context.
|