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.
Files changed (63) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/MIGRATING.md +46 -12
  3. package/configs/oxlint.json +1 -0
  4. package/dist/charts/BandStrip.d.ts +7 -7
  5. package/dist/charts/BandStrip.d.ts.map +1 -1
  6. package/dist/charts/Bars.d.ts +4 -4
  7. package/dist/charts/Bars.d.ts.map +1 -1
  8. package/dist/charts/ChartCard.d.ts +10 -10
  9. package/dist/charts/ChartCard.d.ts.map +1 -1
  10. package/dist/charts/ChartCard.js +2 -1
  11. package/dist/charts/Donut.d.ts +5 -5
  12. package/dist/charts/Donut.d.ts.map +1 -1
  13. package/dist/charts/DualPanel.d.ts +9 -9
  14. package/dist/charts/DualPanel.d.ts.map +1 -1
  15. package/dist/charts/Heatmap.d.ts +18 -16
  16. package/dist/charts/Heatmap.d.ts.map +1 -1
  17. package/dist/charts/Heatmap.js +8 -2
  18. package/dist/charts/MirroredBars.d.ts +9 -9
  19. package/dist/charts/MirroredBars.d.ts.map +1 -1
  20. package/dist/charts/Sparkline.d.ts +9 -5
  21. package/dist/charts/Sparkline.d.ts.map +1 -1
  22. package/dist/charts/Sparkline.js +18 -12
  23. package/dist/charts/StackedArea.d.ts +2 -2
  24. package/dist/charts/StackedArea.d.ts.map +1 -1
  25. package/dist/charts/compile.d.ts +17 -1
  26. package/dist/charts/compile.d.ts.map +1 -1
  27. package/dist/charts/compile.js +56 -2
  28. package/dist/charts/index.d.ts +1 -0
  29. package/dist/charts/index.d.ts.map +1 -1
  30. package/dist/charts/index.js +3 -1
  31. package/dist/charts/state.d.ts +3 -0
  32. package/dist/charts/state.d.ts.map +1 -1
  33. package/dist/charts/state.js +14 -3
  34. package/dist/charts/types.d.ts +64 -61
  35. package/dist/charts/types.d.ts.map +1 -1
  36. package/dist/cli/doctor.js +2 -2
  37. package/dist/dashboard/index.d.ts +1 -1
  38. package/dist/dashboard/index.d.ts.map +1 -1
  39. package/dist/dashboard/index.js +3 -1
  40. package/dist/dashboard/stat-card.d.ts +16 -1
  41. package/dist/dashboard/stat-card.d.ts.map +1 -1
  42. package/dist/dashboard/stat-card.js +14 -1
  43. package/dist/index.d.ts +1 -1
  44. package/dist/index.d.ts.map +1 -1
  45. package/llms.txt +2 -2
  46. package/package.json +1 -1
  47. package/src/charts/BandStrip.tsx +12 -10
  48. package/src/charts/Bars.tsx +4 -4
  49. package/src/charts/ChartCard.tsx +17 -11
  50. package/src/charts/Donut.tsx +5 -5
  51. package/src/charts/DualPanel.tsx +13 -6
  52. package/src/charts/Heatmap.tsx +23 -15
  53. package/src/charts/MirroredBars.tsx +8 -8
  54. package/src/charts/Sparkline.tsx +14 -6
  55. package/src/charts/StackedArea.tsx +2 -2
  56. package/src/charts/compile.ts +92 -1
  57. package/src/charts/index.ts +1 -0
  58. package/src/charts/state.tsx +15 -3
  59. package/src/charts/types.ts +70 -57
  60. package/src/cli/doctor.ts +2 -2
  61. package/src/dashboard/index.ts +6 -1
  62. package/src/dashboard/stat-card.tsx +34 -3
  63. package/src/index.ts +1 -0
@@ -39,6 +39,17 @@ export type StatCardBreakdownRow = {
39
39
  export type StatCardSparklineSize = {
40
40
  width: number;
41
41
  height: number;
42
+ /**
43
+ * The card's `query` as a chart state — present only while the sparkline has nothing to draw
44
+ * (pending, or failed with no data), so `<Sparkline {...size} />` forwards it and the tile shows a
45
+ * sized placeholder instead of a hand-rolled `'—'`. Structurally a `ChartState`: this module
46
+ * stays chart-free. Absent without a `query`, and once data has arrived.
47
+ */
48
+ state?: {
49
+ pending?: boolean;
50
+ error?: unknown;
51
+ empty?: boolean;
52
+ };
42
53
  };
43
54
  /**
44
55
  * `StatCard` re-publishes three NAMED `WidgetHeader` slices (`WidgetHeaderTitleProps` /
@@ -97,8 +108,12 @@ export type StatCardProps = BasaltProps & SlotStylesProps<StatCardSlot> & Widget
97
108
  * measured width is the card's own inner width including the bled inset; with `'right'` it is the
98
109
  * fixed 72×26 slot (see {@link SPARKLINE_RIGHT_WIDTH} for why that one is not measured).
99
110
  *
111
+ * A render prop is a sanctioned pattern here, so `react/no-unstable-nested-components` is
112
+ * configured with `allowAsProps` in the shipped preset. Spread the size (`{...size}`) to forward
113
+ * `state` along with the box.
114
+ *
100
115
  * @example
101
- * sparkline={({ width, height }) => <MySparkline data={history} width={width} height={height} />}
116
+ * sparkline={(size) => <Sparkline ariaLabel="Active users" data={history} {...size} />}
102
117
  */
103
118
  sparkline?: ReactNode | ((size: StatCardSparklineSize) => ReactNode);
104
119
  /** Where `sparkline` sits. `'bleed'` (default) is today's full-width row bled to the card edges;
@@ -1 +1 @@
1
- {"version":3,"file":"stat-card.d.ts","sourceRoot":"","sources":["../../src/dashboard/stat-card.tsx"],"names":[],"mappings":"AA+FA,OAAO,KAAK,EAAiB,SAAS,EAAE,MAAM,OAAO,CAAA;AAErD,OAAO,KAAK,EACV,sBAAsB,EACtB,uBAAuB,EACvB,sBAAsB,EACvB,MAAM,iBAAiB,CAAA;AAIxB,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAA;AAEzE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAGnD;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG,IAAI,CAAA;AAE/B;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,GAAG,WAAW,CAAA;AAEnE;;;;;;;GAOG;AACH,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,YAAY,CAAA;CAC7B,CAAA;AAcD;;;;;;;;GAQG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;CACf,CAAA;AAiDD;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,aAAa,GAAG,WAAW,GACrC,eAAe,CAAC,YAAY,CAAC,GAC7B,sBAAsB,GACtB,IAAI,CAAC,uBAAuB,EAAE,OAAO,CAAC,GACtC,sBAAsB,GAAG;IACvB;iEAC6D;IAC7D,KAAK,EAAE,MAAM,CAAA;IACb;;;;;;;;;;;;OAYG;IACH,SAAS,CAAC,EAAE,SAAS,oBAAoB,EAAE,CAAA;IAC3C;;;;;;;;;;;;;OAaG;IACH,KAAK,CAAC,EAAE,cAAc,CAAC,OAAO,CAAC,CAAA;IAC/B;;;;;;;;;;;OAWG;IACH,SAAS,CAAC,EAAE,SAAS,GAAG,CAAC,CAAC,IAAI,EAAE,qBAAqB,KAAK,SAAS,CAAC,CAAA;IACpE;4GACwG;IACxG,kBAAkB,CAAC,EAAE,OAAO,GAAG,OAAO,CAAA;IACtC;mGAC+F;IAC/F,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB;;;+EAG2E;IAC3E,IAAI,CAAC,EAAE,YAAY,CAAA;CACpB,CAAA;AAEH,wBAAgB,QAAQ,CAAC,EACvB,KAAK,EACL,IAAI,EACJ,QAAQ,EACR,IAAI,EACJ,KAAK,EACL,IAAI,EACJ,SAAS,EACT,KAAK,EACL,KAAK,EACL,WAAW,EACX,aAAa,EACb,WAAW,EACX,UAAU,EACV,SAAS,EACT,kBAA4B,EAC5B,OAAO,EACP,IAAI,EACJ,SAAS,EACT,KAAK,EACL,UAAU,GACX,EAAE,aAAa,+BAqKf"}
1
+ {"version":3,"file":"stat-card.d.ts","sourceRoot":"","sources":["../../src/dashboard/stat-card.tsx"],"names":[],"mappings":"AAiGA,OAAO,KAAK,EAAiB,SAAS,EAAE,MAAM,OAAO,CAAA;AAErD,OAAO,KAAK,EACV,sBAAsB,EACtB,uBAAuB,EACvB,sBAAsB,EACvB,MAAM,iBAAiB,CAAA;AAIxB,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAA;AAEzE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAInD;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG,IAAI,CAAA;AAE/B;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,GAAG,WAAW,CAAA;AAEnE;;;;;;;GAOG;AACH,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,YAAY,CAAA;CAC7B,CAAA;AAcD;;;;;;;;GAQG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd;;;;;OAKG;IACH,KAAK,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,CAAA;CAChE,CAAA;AA6DD;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,aAAa,GAAG,WAAW,GACrC,eAAe,CAAC,YAAY,CAAC,GAC7B,sBAAsB,GACtB,IAAI,CAAC,uBAAuB,EAAE,OAAO,CAAC,GACtC,sBAAsB,GAAG;IACvB;iEAC6D;IAC7D,KAAK,EAAE,MAAM,CAAA;IACb;;;;;;;;;;;;OAYG;IACH,SAAS,CAAC,EAAE,SAAS,oBAAoB,EAAE,CAAA;IAC3C;;;;;;;;;;;;;OAaG;IACH,KAAK,CAAC,EAAE,cAAc,CAAC,OAAO,CAAC,CAAA;IAC/B;;;;;;;;;;;;;;;OAeG;IACH,SAAS,CAAC,EAAE,SAAS,GAAG,CAAC,CAAC,IAAI,EAAE,qBAAqB,KAAK,SAAS,CAAC,CAAA;IACpE;4GACwG;IACxG,kBAAkB,CAAC,EAAE,OAAO,GAAG,OAAO,CAAA;IACtC;mGAC+F;IAC/F,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB;;;+EAG2E;IAC3E,IAAI,CAAC,EAAE,YAAY,CAAA;CACpB,CAAA;AAEH,wBAAgB,QAAQ,CAAC,EACvB,KAAK,EACL,IAAI,EACJ,QAAQ,EACR,IAAI,EACJ,KAAK,EACL,IAAI,EACJ,SAAS,EACT,KAAK,EACL,KAAK,EACL,WAAW,EACX,aAAa,EACb,WAAW,EACX,UAAU,EACV,SAAS,EACT,kBAA4B,EAC5B,OAAO,EACP,IAAI,EACJ,SAAS,EACT,KAAK,EACL,UAAU,GACX,EAAE,aAAa,+BA0Kf"}
@@ -6,6 +6,7 @@ import { CtlSlot } from "../theme/index.js";
6
6
  import { VX } from "../tokens/index.js";
7
7
  import { cx } from "../common/props.js";
8
8
  import { QueryState } from "./query-state.js";
9
+ import { assertQueryStateLike } from "../common/query-state-like.js";
9
10
  import classes from "./stat-card.module.css";
10
11
  const SPARKLINE_RIGHT_WIDTH = 72;
11
12
  const SPARKLINE_RIGHT_HEIGHT = 26;
@@ -33,6 +34,13 @@ function useSlotWidth(active) {
33
34
  width
34
35
  };
35
36
  }
37
+ function sparklineState(query) {
38
+ if (query === void 0) return void 0;
39
+ assertQueryStateLike("StatCard", query);
40
+ if (query.data !== void 0) return void 0;
41
+ if (query.isError) return { error: query.error ?? true };
42
+ return query.fetchStatus === "idle" ? void 0 : { pending: true };
43
+ }
36
44
  const TONE_RAIL_WIDTH = 3;
37
45
  const TONE_LABEL = {
38
46
  good: "Within the good threshold",
@@ -64,7 +72,12 @@ function StatCard({
64
72
  const isRender = typeof sparkline === "function";
65
73
  const bleeds = sparklinePlacement === "bleed";
66
74
  const { ref: slotRef, width: slotWidth } = useSlotWidth(isRender);
67
- const measured = { width: slotWidth, height: SPARKLINE_RIGHT_HEIGHT };
75
+ const state = sparklineState(query);
76
+ const measured = {
77
+ width: slotWidth,
78
+ height: SPARKLINE_RIGHT_HEIGHT,
79
+ ...state !== void 0 && { state }
80
+ };
68
81
  const breakdownRows = breakdown === void 0 || breakdown.length === 0 ? null : /* @__PURE__ */ jsx("dl", { className: classes.breakdown, children: breakdown.map((row) => /* @__PURE__ */ jsxs(
69
82
  "div",
70
83
  {
package/dist/index.d.ts CHANGED
@@ -13,7 +13,7 @@ export { scrollParentOf, useCoarsePointer } from './common/index.js';
13
13
  export { createBasaltTheme } from './theme/index.js';
14
14
  export { ThemeToggle } from './theme/theme-toggle.js';
15
15
  export { BasaltShell, PageBar, PageAside, useSizeClass, PageTitle, type ShellBaseProps, type SidebarBlock, type SettingsMenuItem, } from './shell/index.js';
16
- export { WidgetHeader, DeltaBadge, Section, StatCard, type StatCardBreakdownRow, type StatCardTone, StatGroup, EmptyState, SettingsSection, SettingsRow, DangerZone, } from './dashboard/index.js';
16
+ export { WidgetHeader, DeltaBadge, Section, StatCard, type StatCardBreakdownRow, type StatCardSparklineSize, type StatCardTone, StatGroup, EmptyState, SettingsSection, SettingsRow, DangerZone, } from './dashboard/index.js';
17
17
  export { unwrap } from './query-client.js';
18
18
  export type { BasaltRegister, SeriesKey } from './register.js';
19
19
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAC3C,OAAO,EACL,kBAAkB,EAClB,KAAK,YAAY,EACjB,KAAK,mBAAmB,GACzB,MAAM,0BAA0B,CAAA;AACjC,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,EACL,SAAS,EACT,QAAQ,EACR,SAAS,EACT,UAAU,EACV,YAAY,EACZ,KAAK,SAAS,GACf,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAC3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAA;AAC3C,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAClD,OAAO,EACL,WAAW,EACX,OAAO,EACP,SAAS,EACT,YAAY,EACZ,SAAS,EACT,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,gBAAgB,GACtB,MAAM,SAAS,CAAA;AAChB,OAAO,EACL,YAAY,EACZ,UAAU,EACV,OAAO,EACP,QAAQ,EACR,KAAK,oBAAoB,EACzB,KAAK,YAAY,EACjB,SAAS,EACT,UAAU,EACV,eAAe,EACf,WAAW,EACX,UAAU,GACX,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAMvC,YAAY,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAC3C,OAAO,EACL,kBAAkB,EAClB,KAAK,YAAY,EACjB,KAAK,mBAAmB,GACzB,MAAM,0BAA0B,CAAA;AACjC,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,EACL,SAAS,EACT,QAAQ,EACR,SAAS,EACT,UAAU,EACV,YAAY,EACZ,KAAK,SAAS,GACf,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAC3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAA;AAC3C,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAClD,OAAO,EACL,WAAW,EACX,OAAO,EACP,SAAS,EACT,YAAY,EACZ,SAAS,EACT,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,gBAAgB,GACtB,MAAM,SAAS,CAAA;AAChB,OAAO,EACL,YAAY,EACZ,UAAU,EACV,OAAO,EACP,QAAQ,EACR,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC1B,KAAK,YAAY,EACjB,SAAS,EACT,UAAU,EACV,eAAe,EACf,WAAW,EACX,UAAU,GACX,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAMvC,YAAY,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA"}
package/llms.txt CHANGED
@@ -1,4 +1,4 @@
1
- # basalt-ui v2.1.0
1
+ # basalt-ui v2.3.0
2
2
  # Opinionated framework for Mantine-based React apps — theme, app shell, TanStack Charts, toolchain presets
3
3
  #
4
4
  # Entry points: use the exact import specifier below; do not guess.
@@ -13,7 +13,7 @@
13
13
  # A subpath's `Install:` line is its required peers; `OptionalPeers:` are import()-only, install on use.
14
14
 
15
15
  Package: basalt-ui
16
- Version: 2.1.0
16
+ Version: 2.3.0
17
17
 
18
18
  ## basalt-ui
19
19
  Import: import { ... } from 'basalt-ui'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "basalt-ui",
3
- "version": "2.1.0",
3
+ "version": "2.3.0",
4
4
  "description": "Opinionated framework for Mantine-based React apps — theme, app shell, TanStack Charts, toolchain presets",
5
5
  "keywords": [
6
6
  "charts",
@@ -39,7 +39,7 @@ export type BandStripSeries<T> = Pick<
39
39
  * The tooltip row's value for a band in this state. `null` is an ABSENT reading and renders an
40
40
  * em dash; omit the function when the state's name is the whole reading ("Not measured").
41
41
  */
42
- formatValue?: (d: T) => string | null
42
+ formatValue?: ((d: T) => string | null) | undefined
43
43
  }
44
44
 
45
45
  /** One band: its state, and the two ways to qualify it. */
@@ -47,23 +47,25 @@ export type BandSpan = {
47
47
  /** A `states` key: the band's legend entry, fill and tooltip row. */
48
48
  state: string
49
49
  /** Overrides the state's fill — an intensity ramp WITHIN a state. `alpha(VX.*, …)`, never a hex. */
50
- fill?: string
50
+ fill?: string | undefined
51
51
  /**
52
52
  * A shorter, inset bar over the band, for a fact that must not be read off the fill ramp (a
53
53
  * renegotiation, a restart). Its color is a `states` entry, so the legend names it.
54
54
  */
55
- marker?: {
56
- /** Defaults to the band's own `state`. */
57
- state?: string
58
- /** Vertical inset from the band's top and bottom, in px. Default 6. */
59
- inset?: number
60
- }
55
+ marker?:
56
+ | {
57
+ /** Defaults to the band's own `state`. */
58
+ state?: string | undefined
59
+ /** Vertical inset from the band's top and bottom, in px. Default 6. */
60
+ inset?: number | undefined
61
+ }
62
+ | undefined
61
63
  }
62
64
 
63
65
  /** Collapse adjacent data where one band per datum would be thinner than `minBandPx`. */
64
66
  export type BandFold<T> = {
65
67
  /** Floor on a drawn band's px width. Default 3. */
66
- minBandPx?: number
68
+ minBandPx?: number | undefined
67
69
  /**
68
70
  * The consumer's merge of a group of adjacent data into the datum drawn in its place — whether
69
71
  * that is a max, a sum or a recomputed rate is a question about the measurement. Keep the first
@@ -79,7 +81,7 @@ export type BandStripProps<T, X extends ChartX = ChartX> = Omit<
79
81
  states: readonly BandStripSeries<T>[]
80
82
  /** Which state, fill and marker each datum has. Memoize it: a new function recompiles the chart. */
81
83
  getBand: (d: T) => BandSpan
82
- fold?: BandFold<T>
84
+ fold?: BandFold<T> | undefined
83
85
  }
84
86
 
85
87
  const DEFAULT_MIN_BAND_PX = 3
@@ -27,7 +27,7 @@ export type BarsBar<T> = Pick<
27
27
  'key' | 'label' | 'color' | 'getValue' | 'formatValue' | 'tooltip' | 'legend' | 'role' | 'note'
28
28
  > & {
29
29
  /** Default 0.85. */
30
- fillOpacity?: number
30
+ fillOpacity?: number | undefined
31
31
  }
32
32
 
33
33
  /** A line overlay; `axis: 'right'` reads the chart's `y2`. */
@@ -54,11 +54,11 @@ export type BarsProps<T, X extends ChartX = ChartX> = Omit<
54
54
  'marks' | 'series'
55
55
  > & {
56
56
  bars: readonly BarsBar<T>[]
57
- lines?: readonly BarsLine<T>[]
57
+ lines?: readonly BarsLine<T>[] | undefined
58
58
  /** Default `'stacked'`. `'normalized'` stacks each x to its share of the total. */
59
- layout?: 'stacked' | 'grouped' | 'normalized'
59
+ layout?: 'stacked' | 'grouped' | 'normalized' | undefined
60
60
  /** Per-bar opacity (a partial week, an estimate). Overrides the series' `fillOpacity`. */
61
- barOpacity?: (d: T, key: string) => number
61
+ barOpacity?: ((d: T, key: string) => number) | undefined
62
62
  }
63
63
 
64
64
  const DEFAULT_BAR_OPACITY = 0.85
@@ -92,32 +92,38 @@ export type ChartCardSlot = 'root' | 'header' | 'body'
92
92
 
93
93
  export type ChartCardProps = BasaltProps &
94
94
  SlotStylesProps<ChartCardSlot> & {
95
- title?: string
95
+ title?: string | undefined
96
96
  icon?: ReactNode
97
97
  /** A muted line below the title row; does not by itself render the header. */
98
- subtitle?: string
98
+ subtitle?: string | undefined
99
99
  /** `WidgetHeader`'s info glyph beside the title. */
100
- info?: string
100
+ info?: string | undefined
101
101
  /** A pre-formatted metric value, with `delta` on the hero row. */
102
- value?: string
103
- delta?: number
104
- deltaPeriod?: string
105
- deltaPolarity?: DeltaPolarity
102
+ value?: string | undefined
103
+ delta?: number | undefined
104
+ deltaPeriod?: string | undefined
105
+ deltaPolarity?: DeltaPolarity | undefined
106
106
  /** A mono count tag after the title. */
107
- count?: number
107
+ count?: number | undefined
108
108
  /** Right-aligned controls. */
109
109
  actions?: ReactNode
110
110
  /** A resolved state replaces `children` with its placeholder; the header stays. */
111
- state?: ChartState
111
+ state?: ChartState | undefined
112
112
  /** Body height while a placeholder shows. Default: the chart height law, i.e. exactly the box an
113
113
  * unsized chart takes — pass the chart's own `height` when it states one, so a `<Suspense
114
114
  * fallback={<ChartCard state={{ pending: true }} …/>}>` reserves the final size. */
115
- placeholderHeight?: number | ResponsiveChartHeight
115
+ placeholderHeight?: number | ResponsiveChartHeight | undefined
116
116
  /** A retry button or a "clear filters" link under the placeholder. */
117
117
  stateAction?: ReactNode
118
118
  children?: ReactNode
119
119
  }
120
120
 
121
+ /** Drops explicit `undefined`s: `WidgetHeader` is exact-optional, this props type is not. */
122
+ const definedOnly = <T extends object>(o: T): { [K in keyof T]?: Exclude<T[K], undefined> } =>
123
+ Object.fromEntries(Object.entries(o).filter(([, v]) => v !== undefined)) as {
124
+ [K in keyof T]?: Exclude<T[K], undefined>
125
+ }
126
+
121
127
  /** A header renders when one of these is set (a subtitle or delta alone does not). */
122
128
  const HEADER_TRIGGERS = ['title', 'info', 'value', 'icon', 'count'] as const
123
129
 
@@ -150,7 +156,7 @@ export function ChartCard({
150
156
  <div className={cx(classes.header, classNames?.header)}>
151
157
  <WidgetHeader
152
158
  tier="widget"
153
- {...header}
159
+ {...definedOnly(header)}
154
160
  title={header.title ?? ''}
155
161
  {...(actions !== undefined && {
156
162
  actions: <span data-basalt-tier="widget">{actions}</span>,
@@ -34,13 +34,13 @@ export type DonutProps<K extends string = string> = BasaltProps & {
34
34
  colorForKey: (key: K) => string
35
35
  formatValue: (v: number) => string
36
36
  /** Legend and tooltip label for a key. Default: the key. */
37
- seriesLabel?: (key: K) => string
37
+ seriesLabel?: ((key: K) => string) | undefined
38
38
  /** The ring's centre text, and the smaller line under it. */
39
- centerLabel?: string
40
- centerSubLabel?: string
39
+ centerLabel?: string | undefined
40
+ centerSubLabel?: string | undefined
41
41
  /** Default 240. */
42
- height?: number | ResponsiveChartHeight
43
- state?: ChartState
42
+ height?: number | ResponsiveChartHeight | undefined
43
+ state?: ChartState | undefined
44
44
  }
45
45
 
46
46
  /** Slices kept before the rest fold into "Other". */
@@ -64,12 +64,12 @@ export type DualPanelBar<T> = {
64
64
  /** Tick and tooltip formatter. */
65
65
  format: (v: number) => string
66
66
  /** The tooltip row's value, for a unit or a decimal `format` cannot carry. */
67
- formatValue?: (v: number, d: T) => string
67
+ formatValue?: ((v: number, d: T) => string) | undefined
68
68
  /** Default `'auto'`: a symmetric `[-maxAbs, maxAbs]` from the data, rounded to nice ticks. A tuple
69
69
  * is used verbatim. */
70
- domain?: readonly [number, number] | 'auto'
70
+ domain?: readonly [number, number] | 'auto' | undefined
71
71
  /** When `'auto'`: floors `maxAbs`, so a plateau does not amplify to fill the pane. */
72
- maxAbsFloor?: number
72
+ maxAbsFloor?: number | undefined
73
73
  }
74
74
 
75
75
  export type DualPanelProps<T, X extends Date | number = Date | number> = Pick<
@@ -91,15 +91,22 @@ export type DualPanelProps<T, X extends Date | number = Date | number> = Pick<
91
91
  series: readonly DualPanelLine<T>[]
92
92
  /** Top pane y axis. Default domain `'auto'`: the visible lines' span plus 8%, rounded to nice
93
93
  * ticks (not zero-based: the interesting range is often a narrow band far from zero). */
94
- top?: { domain?: readonly [number, number] | 'auto'; format?: (v: number) => string }
94
+ top?:
95
+ | {
96
+ domain?: readonly [number, number] | 'auto' | undefined
97
+ format?: ((v: number) => string) | undefined
98
+ }
99
+ | undefined
95
100
  /**
96
101
  * Shades the band between two top lines (by key), where both are visible. `aboveFill` colours the
97
102
  * side where `to` runs above `from`; omitted, both sides take `fill`.
98
103
  */
99
- fillBetween?: { from: string; to: string; fill: string; aboveFill?: string }
104
+ fillBetween?:
105
+ | { from: string; to: string; fill: string; aboveFill?: string | undefined }
106
+ | undefined
100
107
  bar: DualPanelBar<T>
101
108
  /** The top pane's share of the chart height. Default 0.62. */
102
- topFraction?: number
109
+ topFraction?: number | undefined
103
110
  }
104
111
 
105
112
  type FillRow<T, X> = { x: X; from: number; to: number; series: string; datum: T }
@@ -18,6 +18,7 @@ import type { CSSProperties, ReactElement } from 'react'
18
18
  import type { BasaltProps } from '../common/props'
19
19
  import { alpha, VX } from '../tokens'
20
20
  import { resolveContainerClass } from '../tokens/size-classes'
21
+ import { strideTicks } from './compile'
21
22
  import type { CartesianDefinition } from './compile'
22
23
  import { ChartEngine, idPrefixOf } from './host'
23
24
  import { useCoarsePointer } from '../common/use-media-query'
@@ -37,26 +38,28 @@ export type HeatmapProps<T> = BasaltProps & {
37
38
  /** The intensity; `null` is an absent cell (flat neutral), never a zero. */
38
39
  getValue: (d: T) => number | null
39
40
  /** Explicit band order. Default: first seen in `data`. */
40
- rows?: readonly string[]
41
- cols?: readonly string[]
41
+ rows?: readonly string[] | undefined
42
+ cols?: readonly string[] | undefined
42
43
  /** The ink at full intensity, a `VX.*` ref or a series token. Default `VX.line`. */
43
- color?: string
44
+ color?: string | undefined
44
45
  /** The value range the color scale spans. Default `[0, max]` of the data. */
45
- domain?: readonly [number, number]
46
+ domain?: readonly [number, number] | undefined
46
47
  /** Tooltip, legend-end and `aria` value format. Default: the number as-is. */
47
- formatValue?: (v: number) => string
48
+ formatValue?: ((v: number) => string) | undefined
48
49
  /** Band tick and tooltip header labels. Default: the band key. */
49
- formatRow?: (row: string) => string
50
- formatCol?: (col: string) => string
50
+ formatRow?: ((row: string) => string) | undefined
51
+ formatCol?: ((col: string) => string) | undefined
51
52
  /** `false` hides the gradient legend; `label` titles it. */
52
- legend?: false | { label?: string }
53
+ legend?: false | { label?: string | undefined } | undefined
53
54
  /** Rows after the value row, per cell (a sample count, a failure reason). Called with the datum. */
54
- tooltip?: false | { rows?: (d: T) => readonly ChartTooltipRow[] }
55
+ tooltip?: (false | { rows?: ((d: T) => readonly ChartTooltipRow[]) | undefined }) | undefined
55
56
  /** A fixed height, or one per container class. Default: by row count (160–420px). */
56
- height?: number | ResponsiveChartHeight
57
- /** Fill the parent's height instead (the parent needs a definite height). Overrides `height`. */
58
- matchHeight?: boolean
59
- state?: ChartState
57
+ height?: number | ResponsiveChartHeight | undefined
58
+ /** Fill the parent's height instead. Overrides `height`. CSS-owned like every other kind's box
59
+ * (`ChartBox`, no measuring, SSR-stable), so the parent needs a definite CSS height — a grid or
60
+ * flex stretch, `h-full`, an explicit height — which no kind can read off an auto-height parent. */
61
+ matchHeight?: boolean | undefined
62
+ state?: ChartState | undefined
60
63
  }
61
64
 
62
65
  /** Intensity steps: quantized, so the gradient legend and the cells are the same ramp. */
@@ -149,8 +152,9 @@ export function Heatmap<T>(props: HeatmapProps<T>): ReactElement {
149
152
  return {
150
153
  chart: ({ width }: ChartBuildContext): StoredChartSpec => {
151
154
  const ticks = { line: false as const, size: 0 }
155
+ const cls = resolveContainerClass(width)
152
156
  const tickLabels = {
153
- fontSize: resolveContainerClass(width) === 'wide' ? VX.text.micro : VX.text.nano,
157
+ fontSize: cls === 'wide' ? VX.text.micro : VX.text.nano,
154
158
  opacity: 1,
155
159
  thin: true,
156
160
  }
@@ -190,7 +194,11 @@ export function Heatmap<T>(props: HeatmapProps<T>): ReactElement {
190
194
  scale: scaleBand<string>().domain(cols).paddingInner(0).paddingOuter(0),
191
195
  axis: {
192
196
  line: AXIS_LINE,
193
- ticks: { values: cols, format: (v) => formatCol(String(v)), ...ticks },
197
+ ticks: {
198
+ values: strideTicks(cols, cls),
199
+ format: (v) => formatCol(String(v)),
200
+ ...ticks,
201
+ },
194
202
  tickLabels,
195
203
  },
196
204
  },
@@ -35,11 +35,11 @@ export type MirroredBarPane = {
35
35
  * entry throws: the pane would vanish, which reads as a measured zero. */
36
36
  key: string
37
37
  /** The pane's upper bound. `'auto'` (default) is its own visible maximum; a number pins it. */
38
- max?: number | 'auto'
38
+ max?: number | 'auto' | undefined
39
39
  /** When `'auto'`: floors the upper bound, so a quiet window does not amplify noise to full height. */
40
- autoMaxFloor?: number
40
+ autoMaxFloor?: number | undefined
41
41
  /** Tick count for this pane's own axis. Default 3. */
42
- ticks?: number
42
+ ticks?: number | undefined
43
43
  /** Tick and tooltip formatter, in the pane's OWN units. */
44
44
  format: (v: number) => string
45
45
  }
@@ -71,18 +71,18 @@ export type MirroredBarsProps<T, X extends ChartX = ChartX> = Pick<
71
71
  /** Drawn below the baseline. */
72
72
  down: MirroredBarPane
73
73
  /** The up pane's share of the height. Default 0.35: the pair is usually asymmetric. */
74
- upFraction?: number
74
+ upFraction?: number | undefined
75
75
  /** 0..1 of a bar's width that no source sample covers, drawn neutral across both panes. */
76
- getAbsentFraction?: (d: T) => number
76
+ getAbsentFraction?: ((d: T) => number) | undefined
77
77
  /** Per-datum bar opacity: a real but qualified measurement (a short bucket) drawn dimmer. */
78
- getBarOpacity?: (d: T) => number
78
+ getBarOpacity?: ((d: T) => number) | undefined
79
79
  /**
80
80
  * Collapse adjacent data where one band per datum would be sub-pixel. `merge` is the consumer's
81
81
  * (max, sum, or a rate from summed parts) and must keep `group[0]`'s x, the slot start.
82
82
  */
83
- fold?: { minBandPx?: number; merge: (group: T[]) => T }
83
+ fold?: { minBandPx?: number | undefined; merge: (group: T[]) => T } | undefined
84
84
  /** Default `'leading'`: x is a slot's start. */
85
- cursorResolution?: CartesianChartProps<T, X>['cursorResolution']
85
+ cursorResolution?: CartesianChartProps<T, X>['cursorResolution'] | undefined
86
86
  }
87
87
 
88
88
  const DEFAULT_UP_FRACTION = 0.35
@@ -18,7 +18,9 @@ import type { CartesianDefinition } from './compile'
18
18
  import { chartCurve } from './curves'
19
19
  import { barMark } from './marks'
20
20
  import { ChartEngine, idPrefixOf } from './host'
21
+ import { ChartPlaceholder, resolveChartState } from './state'
21
22
  import { CHART_THEME } from './theme'
23
+ import type { ChartState } from './types'
22
24
 
23
25
  export type SparklineProps = BasaltProps & {
24
26
  /** The accessible name. Required: a chart without one is an unlabeled image. */
@@ -27,15 +29,18 @@ export type SparklineProps = BasaltProps & {
27
29
  /** Height in px. */
28
30
  height: number
29
31
  /** Width in px. Default: the container's, measured by the host. */
30
- width?: number
32
+ width?: number | undefined
31
33
  /** Default `'line'`. */
32
- mark?: 'line' | 'bar'
34
+ mark?: 'line' | 'bar' | undefined
33
35
  /** A `VX.*` ref or a series token. Default `VX.faint`. */
34
- color?: string
36
+ color?: string | undefined
35
37
  /** Paints the last point in `emphasisColor` — the one reading a trend beside a KPI is for. */
36
- emphasizeLast?: boolean
38
+ emphasizeLast?: boolean | undefined
37
39
  /** Default `VX.accent`. */
38
- emphasisColor?: string
40
+ emphasisColor?: string | undefined
41
+ /** Pending / error / empty, the same contract as every other kind. The placeholder fills the
42
+ * sparkline's own `width` × `height`, so a loading tile does not shift when the trend lands. */
43
+ state?: ChartState | undefined
39
44
  }
40
45
 
41
46
  type Point = { i: number; v: number | null }
@@ -145,6 +150,7 @@ export function Sparkline(props: SparklineProps): ReactElement {
145
150
  color = VX.faint,
146
151
  emphasizeLast = false,
147
152
  emphasisColor = VX.accent,
153
+ state,
148
154
  className,
149
155
  style,
150
156
  } = props
@@ -156,13 +162,15 @@ export function Sparkline(props: SparklineProps): ReactElement {
156
162
  if (range === null) return null
157
163
  return compileSparkline(rows, range, mark, color, emphasizeLast ? emphasisColor : null)
158
164
  }, [data, mark, color, emphasizeLast, emphasisColor])
165
+ const resolved = resolveChartState(state)
159
166
  return (
160
167
  <div
161
168
  data-basalt-sparkline=""
162
169
  {...(className !== undefined && { className })}
163
170
  style={{ width: width ?? '100%', height, minWidth: 0, ...style }}
164
171
  >
165
- {definition !== null && (
172
+ {resolved !== null && <ChartPlaceholder state={state} height={height} compact />}
173
+ {resolved === null && definition !== null && (
166
174
  <ChartEngine
167
175
  definition={definition}
168
176
  ariaLabel={ariaLabel}
@@ -25,7 +25,7 @@ export type StackedAreaBand<T> = Pick<
25
25
  'key' | 'label' | 'color' | 'getValue' | 'formatValue' | 'tooltip' | 'legend' | 'role' | 'note'
26
26
  > & {
27
27
  /** Default 0.85. */
28
- fillOpacity?: number
28
+ fillOpacity?: number | undefined
29
29
  }
30
30
 
31
31
  export type StackedAreaProps<T, X extends ChartX = ChartX> = Omit<
@@ -35,7 +35,7 @@ export type StackedAreaProps<T, X extends ChartX = ChartX> = Omit<
35
35
  /** The bands, bottom to top. */
36
36
  series: readonly StackedAreaBand<T>[]
37
37
  /** No `scale`: a log axis has no additive zero to stack from. */
38
- y?: Omit<NonNullable<CartesianChartProps<T, X>['y']>, 'scale'>
38
+ y?: Omit<NonNullable<CartesianChartProps<T, X>['y']>, 'scale'> | undefined
39
39
  }
40
40
 
41
41
  export function StackedArea<T, X extends ChartX = ChartX>(
@@ -39,6 +39,7 @@ import type {
39
39
  ChartTooltipRow,
40
40
  ChartX,
41
41
  PlotRow,
42
+ ZoneSpec,
42
43
  } from './types'
43
44
 
44
45
  /**
@@ -196,6 +197,16 @@ function xTickPolicy<X extends ChartX>(
196
197
  return { count: xTicks }
197
198
  }
198
199
 
200
+ /**
201
+ * Every n-th value, n the smallest stride that leaves at most the class's x tick count (the same
202
+ * ladder a continuous x axis counts to). A band axis labels every value it is given and only drops
203
+ * the ones that collide, so a short-label grid (18 hour columns) would otherwise draw all of them.
204
+ */
205
+ export function strideTicks<V>(values: readonly V[], cls: ContainerClass): V[] {
206
+ const stride = Math.max(1, Math.ceil(values.length / LADDER[cls].xTicks))
207
+ return values.filter((_, i) => i % stride === 0)
208
+ }
209
+
199
210
  export function xGuide<X extends ChartX>(opts: {
200
211
  model: XScaleModel<X>
201
212
  xTicks: number | readonly X[] | undefined
@@ -239,6 +250,86 @@ export function areaGradient(index: number, color: string): ChartGradient {
239
250
  }
240
251
  }
241
252
 
253
+ const areaTint = (color: string): string =>
254
+ `color-mix(in srgb, ${color} var(--vx-area-top), transparent)`
255
+
256
+ /**
257
+ * A series area filled by value zone: a vertical gradient whose stops sit at the zone boundaries.
258
+ * The engine writes `<linearGradient>` in the default `objectBoundingBox` units, so offsets are
259
+ * fractions of the AREA's own box, not of the plot: from the larger of the series' maximum and 0 down
260
+ * to the smaller of its minimum and 0, the baseline the area closes to even when the y domain
261
+ * excludes 0. A boundary at value `v` sits at `(ceiling - v) / (ceiling - floor)`. Hard edges:
262
+ * neighbouring zones meet at one offset; overlapping zones never run backwards. `null` when no zone
263
+ * reaches the area (the caller keeps the plain series-colour fade). Linear axes only.
264
+ */
265
+ export function zoneAreaGradient(
266
+ index: number,
267
+ zones: readonly ZoneSpec[],
268
+ values: readonly (number | null)[],
269
+ ): ChartGradient | null {
270
+ let floor = 0
271
+ let ceiling = 0
272
+ for (const v of values) {
273
+ if (v === null || !Number.isFinite(v)) continue
274
+ if (v < floor) floor = v
275
+ if (v > ceiling) ceiling = v
276
+ }
277
+ const span = ceiling - floor
278
+ if (span <= 0) return null
279
+ const offsetOf = (v: number): number => (ceiling - Math.min(ceiling, Math.max(floor, v))) / span
280
+ const reaching = zones
281
+ .filter((z) => !Number.isNaN(z.from) && !Number.isNaN(z.to) && z.to > floor && z.from < ceiling)
282
+ .toSorted((p, q) => q.to - p.to)
283
+ let previous = 0
284
+ const stops = reaching.flatMap((z) =>
285
+ [offsetOf(z.to), offsetOf(z.from)].map((raw) => {
286
+ previous = Math.max(previous, raw)
287
+ return { offset: previous, color: areaTint(z.fill) }
288
+ }),
289
+ )
290
+ if (stops.length === 0) return null
291
+ return { id: areaGradientId(index), x1: 0, y1: 0, x2: 0, y2: 1, stops }
292
+ }
293
+
294
+ let warnedLogZones = false
295
+
296
+ /** Zone stops are placed linearly in value; a log axis would put them in the wrong place (V2-SPEC
297
+ * § 9.9 precedent): dev throws, production logs once and keeps the series-colour fade. */
298
+ function guardLogZones(onLog: boolean): boolean {
299
+ if (!onLog) return false
300
+ const message = "area: 'zones' cannot use a log axis; use area: true"
301
+ if (process.env.NODE_ENV !== 'production') throw new Error(message)
302
+ // oxlint-disable-next-line no-console -- the production leg of a dev-only throw
303
+ if (!warnedLogZones) console.error(message)
304
+ warnedLogZones = true
305
+ return true
306
+ }
307
+
308
+ /** The gradient resource for a series' `area` fill: zones, a colour ref, or the series colour. */
309
+ function seriesAreaGradient<T, X extends ChartX>(
310
+ s: ChartSeries<T>,
311
+ index: number,
312
+ input: CartesianInput<T, X>,
313
+ axes: Axes<T>,
314
+ ): ChartGradient {
315
+ const plain = areaGradient(
316
+ index,
317
+ typeof s.area === 'string' && s.area !== 'zones' ? s.area : s.color,
318
+ )
319
+ if (s.area !== 'zones') return plain
320
+ const side = axes.onRight(s) ? 'right' : 'left'
321
+ const axisConfig = side === 'right' ? input.y2 : input.y
322
+ if (guardLogZones(axisConfig?.scale === 'log')) return plain
323
+ const zones = (input.zones ?? []).filter((z) => axes.scaleId(z.axisSide) === axes.scaleId(side))
324
+ return (
325
+ zoneAreaGradient(
326
+ index,
327
+ zones,
328
+ input.data.map((d) => s.getValue(d)),
329
+ ) ?? plain
330
+ )
331
+ }
332
+
242
333
  const isBar = <T>(s: ChartSeries<T>): boolean => s.mark === 'bar'
243
334
 
244
335
  /** The two synthetic totals standing in for a stack's bars in its AUTO domain only: the positive
@@ -538,7 +629,7 @@ export function compileCartesian<T, X extends ChartX>(
538
629
  const formatX = (v: ChartValue): string =>
539
630
  input.formatX ? input.formatX(v as X) : defaultFormatX(v)
540
631
  const gradients = input.series.flatMap((s, i) =>
541
- s.area ? [areaGradient(i, typeof s.area === 'string' ? s.area : s.color)] : [],
632
+ s.area ? [seriesAreaGradient(s, i, input, axes)] : [],
542
633
  )
543
634
 
544
635
  return {
@@ -28,6 +28,7 @@ export {
28
28
  } from './MirroredBars'
29
29
  export { ChartCard, type ChartCardProps, type ChartCardSlot } from './ChartCard'
30
30
  export { chartCursor } from './cursor'
31
+ export { chartCurve, type CurveName } from './curves'
31
32
  export type {
32
33
  AxisConfig,
33
34
  CartesianChartProps,