basalt-ui 1.30.1 → 1.30.2

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 CHANGED
@@ -1,3 +1,10 @@
1
+ ## [1.30.2](https://github.com/jkrumm/basalt-ui/compare/v1.30.1...v1.30.2) (2026-09-09)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * stop legendWins collapsing a self-measured fill frame to no plot ([07f0fcd](https://github.com/jkrumm/basalt-ui/commit/07f0fcd65df5047caec1ce09aaaf7f05958e58ad))
7
+
1
8
  ## [1.30.1](https://github.com/jkrumm/basalt-ui/compare/v1.30.0...v1.30.1) (2026-09-09)
2
9
 
3
10
 
package/MIGRATING.md CHANGED
@@ -66,18 +66,18 @@ There is one row per SURFACE, not per export, and a surface earns a row once any
66
66
  shipped lefthook preset both changed in 1.30.0 with no row that owned them — which is how a
67
67
  consumer read this index, found nothing, and diffed a KPI row that had silently gone 4-up → 2-up.
68
68
 
69
- | Surface | 1.27.0 | 1.28.0 | 1.29.0 | 1.30.0 | 1.30.1 | Unreleased |
70
- | ----------------------------- | ------------------------------------------------------------ | -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | --------------------------------------------------------------------- | ---------- |
71
- | **tokens** (`./tokens`) | — | `--vx-divider` → `rgba()` on both schemes (§ Shell) | `--vx-space-touch-target` added (§ Additions) | eight spacing numbers (§ Chrome) | — | — |
72
- | **charts** (`./charts`) | — | phone tier, `curve`, formatters, `state`, log axis (§ preamble) | `ChartState.empty: string`, `AxisBottomNumeric` (§ Additions) | responsive `height`, `legend.maxRows` (§ Charts) | `height` reaches the kinds; `maxRows` beats the `fill` fit (§ Charts) | — |
73
- | **shell** (`.`) | — | scrollport, region seams, **the brand left the sidebar** (§§ scrollport, brand, Shell) | `PageTitle`, `BasaltDevDock`, `useBreakpoint` (§ Additions) | More popover, aside pill, the responsive gutter, `useBasaltSpacing` (§ Chrome) | — | — |
74
- | **controls** (`./controls`) | `NumberFilter`, `field.number` (§ Controls) | sheet renders panel rows; `PanelRow`, `SliderControl` (§ `basalt-ui/controls`) | — | — | — | — |
75
- | **forms** (`./forms`) | — | `field` → `inputProps`, **`inputProps` drops `key`**, the layout tier (§ `inputProps`) | `field` DELETED — and the `--fix` recipe no longer works (§ Consolidation) | — | — | — |
76
- | **data** (`./data/table`) | — | root is a `<div>`, row selection, `getItemKey` required (§ `basalt-ui/data`) | `./data` + `./query` + `./connectivity` dropped (§ Consolidation) | — | — | — |
77
- | **dashboard** (`.`) | `StatCard`: `unit`, `breakdown`, delta format (§ `StatCard`) | `StatGroup`, `WidgetGrid`, query-aware `StatCard` — additive | — | **the column law keys on the CONTAINER; `cols={4}` is 2-up at `sm`** (§ Dashboard) | — | — |
78
- | **toolchain** (`./configs/*`) | — | — | the shipped lefthook preset's `check-theme` BROKE — the CLI resolver stopped ascending (§ CLI) | the preset's default bin, and the `root:` recipe (§ Toolchain) | — | — |
79
- | **CLI** (`basalt-ui`) | — | — | **resolver stops relocating; `doctor` loses 6 of 10 checks** (§ CLI) | — | — | — |
80
- | **guards** | five rules → `error` (§ Guards) | six new ids at `warn` (§ preamble) | `query-dual-import` retired in 1.29.1 (§ Guards) | four rules → `error` (§ Guards) | — | — |
69
+ | Surface | 1.27.0 | 1.28.0 | 1.29.0 | 1.30.0 | 1.30.1 | 1.30.2 | Unreleased |
70
+ | ----------------------------- | ------------------------------------------------------------ | -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | --------------------------------------------------------------------- | ------ | ---------- |
71
+ | **tokens** (`./tokens`) | — | `--vx-divider` → `rgba()` on both schemes (§ Shell) | `--vx-space-touch-target` added (§ Additions) | eight spacing numbers (§ Chrome) | — | — | — |
72
+ | **charts** (`./charts`) | — | phone tier, `curve`, formatters, `state`, log axis (§ preamble) | `ChartState.empty: string`, `AxisBottomNumeric` (§ Additions) | responsive `height`, `legend.maxRows` (§ Charts) | `height` reaches the kinds; `maxRows` beats the `fill` fit (§ Charts) | — | — |
73
+ | **shell** (`.`) | — | scrollport, region seams, **the brand left the sidebar** (§§ scrollport, brand, Shell) | `PageTitle`, `BasaltDevDock`, `useBreakpoint` (§ Additions) | More popover, aside pill, the responsive gutter, `useBasaltSpacing` (§ Chrome) | — | — | — |
74
+ | **controls** (`./controls`) | `NumberFilter`, `field.number` (§ Controls) | sheet renders panel rows; `PanelRow`, `SliderControl` (§ `basalt-ui/controls`) | — | — | — | — | — |
75
+ | **forms** (`./forms`) | — | `field` → `inputProps`, **`inputProps` drops `key`**, the layout tier (§ `inputProps`) | `field` DELETED — and the `--fix` recipe no longer works (§ Consolidation) | — | — | — | — |
76
+ | **data** (`./data/table`) | — | root is a `<div>`, row selection, `getItemKey` required (§ `basalt-ui/data`) | `./data` + `./query` + `./connectivity` dropped (§ Consolidation) | — | — | — | — |
77
+ | **dashboard** (`.`) | `StatCard`: `unit`, `breakdown`, delta format (§ `StatCard`) | `StatGroup`, `WidgetGrid`, query-aware `StatCard` — additive | — | **the column law keys on the CONTAINER; `cols={4}` is 2-up at `sm`** (§ Dashboard) | — | — | — |
78
+ | **toolchain** (`./configs/*`) | — | — | the shipped lefthook preset's `check-theme` BROKE — the CLI resolver stopped ascending (§ CLI) | the preset's default bin, and the `root:` recipe (§ Toolchain) | — | — | — |
79
+ | **CLI** (`basalt-ui`) | — | — | **resolver stops relocating; `doctor` loses 6 of 10 checks** (§ CLI) | — | — | — | — |
80
+ | **guards** | five rules → `error` (§ Guards) | six new ids at `warn` (§ preamble) | `query-dual-import` retired in 1.29.1 (§ Guards) | four rules → `error` (§ Guards) | — | — | — |
81
81
 
82
82
  ---
83
83
 
@@ -86,6 +86,50 @@ consumer read this index, found nothing, and diffed a KPI row that had silently
86
86
  _Nothing yet. Author the next section under this heading as `## Unreleased — <title>`; the
87
87
  release renames it and opens a new one here._
88
88
 
89
+ ## 1.30.2 — the `legendWins` collapse, and two things `ChartFrame` never said out loud
90
+
91
+ **A patch. Nothing is added, renamed or removed.** One rendering regression 1.30.1 shipped is
92
+ fixed; the other two entries correct documentation, not behaviour.
93
+
94
+ ### Charts — a stated `legend.maxRows` no longer costs a `fill` chart its whole plot
95
+
96
+ 1.30.1's `legendWins` drops `resolvePlotRect`'s `VX.minPlotHeight` floor so the caller's cap is
97
+ paid for out of the frame's own cell. `ChartFrame` measures its OWN node, and under `fill` its
98
+ `height: 100%` resolves to `auto` in a parent that states no height — so with the floor gone the
99
+ sequence has a fixpoint at zero: plot 0 → the legend is the only content → the box IS the legend
100
+ band → 0 again, forever. MEASURED in headless Chrome: a 7-entry `fill` chart at `maxRows: 3` in an
101
+ unsized parent rendered a 29.375px frame and **no plot svg at all**; with nothing stated the same
102
+ chart rendered a 120px plot in a 149.375px frame.
103
+
104
+ The floor now stands while the frame measures exactly its own legend band and nothing else, which
105
+ is the signature of a box nothing else sized. From there the frame is 120px + the band, the box has
106
+ room, and `legendWins` applies normally.
107
+
108
+ - **In the documented `fill` shape — a parent that states a height — nothing changes.** The plot
109
+ still yields the cell to a stated cap, below `VX.minPlotHeight` and down to 0 when the legend
110
+ band is taller than the cell. That is still the caller's arithmetic, made visible.
111
+ - **`fill` in a parent that states no height is not a supported shape** and never was: the frame
112
+ has no external height to fill, so it settles on the plot floor. State a height on the parent
113
+ cell, or use `height` / `aspectRatio` instead of `fill`.
114
+ - Pinned by `tests/layout/charts.layout.test.ts` INVARIANT 7 (both halves) — happy-dom cannot
115
+ observe a `ResizeObserver` fixpoint at all.
116
+
117
+ ### `ChartFrame` warns in dev when `height` or `aspectRatio` is passed with `fill`
118
+
119
+ `fill` always won and the other two were never read; the JSDoc said so and nothing else did. Both
120
+ now `console.error` once in dev through `common/errors.ts`' new `ignoredProp` builder. **No
121
+ rendering moves** — the props were already ignored. A consumer carrying `height` alongside `fill`
122
+ will see one message per prop and can delete the dead constants behind it. A mutually-exclusive
123
+ union prop is the stronger fix and is deliberately not in a patch.
124
+
125
+ ### `legend.maxRows` counts ENTRIES, not rows — a documentation correction
126
+
127
+ `ChartLegend` slices `entries.slice(0, cap)`. `maxRows: 3` on a 7-entry legend renders three
128
+ entries and a `+4 more` chip, on ONE row at any width they fit. This is unchanged behaviour and
129
+ matches `ChartLegend`'s own JSDoc; § 1.30.1's "takes the rows it asked for" and the name itself
130
+ (plus `ChartTierMetrics.legendMaxRows`) said otherwise. **Nothing is renamed** — a public prop
131
+ rename is not a patch, and the value you pass keeps meaning exactly what it always meant.
132
+
89
133
  ## 1.30.1 — finishing 1.30.0
90
134
 
91
135
  **A patch. Nothing is added, renamed or removed — three things 1.30.0 shipped are made to work, and
@@ -118,8 +162,8 @@ exactly the case it was restored for. Measured: a 7-entry legend in a 92–150px
118
162
  entries at `maxRows` 3, 6 **and** 99, with five series drawn in colours nothing named.
119
163
 
120
164
  The fit now resolves an ABSENT cap only. When a caller states a number on a `fill` band, the legend
121
- takes the rows it asked for and **`VX.minPlotHeight` stops being a floor** — the plot yields the
122
- height instead. Both things that used to trim the number are framework DEFAULTS (the tier's cap,
165
+ takes the ENTRIES it asked for (`maxRows` counts entries, not rows — see § Unreleased) and
166
+ **`VX.minPlotHeight` stops being a floor** — the plot yields the height instead. Both things that used to trim the number are framework DEFAULTS (the tier's cap,
123
167
  and a floor about the plot); the stated number is the only intent in the room.
124
168
 
125
169
  - **Expect a taller legend and a shorter plot** anywhere you set `maxRows` on a docked/`fill` chart.
@@ -13,7 +13,13 @@ export type ChartFrameLegend = {
13
13
  /** Default 'bottom'. */
14
14
  placement?: LegendPlacement;
15
15
  /**
16
- * Wrap cap → "+N more" rollup at high cardinality.
16
+ * Rollup cap at high cardinality: the legend renders this many ENTRIES and folds the remainder
17
+ * into a `+N more` disclosure chip.
18
+ *
19
+ * **It counts entries, not rows** — the name is inherited and now load-bearing across
20
+ * `ChartTierMetrics.legendMaxRows`, so it is not renamed in a patch. `maxRows: 3` on a 7-entry
21
+ * legend renders three entries, which is ONE row at any width they fit on. `ChartLegend` slices
22
+ * (`entries.slice(0, cap)`); nothing here measures rows.
17
23
  *
18
24
  * An explicit value WINS OUTRIGHT — over the phone tier's two-row default (`chartTierMetrics`)
19
25
  * AND over a `fill` frame's measured fit (`legendEntryCap`). 1.30.0 fixed only the first half:
@@ -43,7 +49,8 @@ export type ChartFrameProps = BasaltProps & {
43
49
  series: readonly SeriesStyle[];
44
50
  /**
45
51
  * Height in pixels, or one per size step. Used when neither `aspectRatio` nor `fill` is set.
46
- * Default 240.
52
+ * Default 240. Passing it WITH `fill` is a wiring mistake, not a fallback — `fill` wins and
53
+ * this is never read, which now warns once in dev (`common/errors.ts`' `ignoredProp`).
47
54
  *
48
55
  * `{ base: 180, md: 260 }` is the declarative escape from a page of fixed-height charts: the
49
56
  * steps are compared against the frame's own MEASURED width, not the viewport, so a chart squeezed
@@ -51,7 +58,8 @@ export type ChartFrameProps = BasaltProps & {
51
58
  * supported ({@link resolveFrameHeight}).
52
59
  */
53
60
  height?: number | ResponsiveChartHeight;
54
- /** height = Math.round(containerWidth / aspectRatio). Ignored when `fill` is set. */
61
+ /** height = Math.round(containerWidth / aspectRatio). Ignored when `fill` is set — which warns
62
+ * once in dev, for the same reason `height` does. */
55
63
  aspectRatio?: number;
56
64
  /** Fill the parent flex/grid cell's measured height instead of a fixed/derived one. */
57
65
  fill?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"ChartFrame.d.ts","sourceRoot":"","sources":["../../../src/charts/primitives/ChartFrame.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,SAAS,EAAE,MAAM,OAAO,CAAA;AAErD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAIrD,OAAO,KAAK,EAAE,iBAAiB,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAOhF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAA;AAIjE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAEhD;qGACqG;AACrG,OAAO,EACL,cAAc,EACd,mBAAmB,EACnB,eAAe,EACf,gBAAgB,GACjB,MAAM,sBAAsB,CAAA;AAC7B,YAAY,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAA;AAKjE;0EAC0E;AAC1E,MAAM,MAAM,gBAAgB,GAAG;IAC7B,wBAAwB;IACxB,SAAS,CAAC,EAAE,eAAe,CAAA;IAC3B;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,4DAA4D;IAC5D,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,wDAAwD;IACxD,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IAC1C;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,eAAe,GAAG,WAAW,GAAG;IAC1C,uGAAuG;IACvG,MAAM,EAAE,SAAS,WAAW,EAAE,CAAA;IAC9B;;;;;;;;OAQG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,qBAAqB,CAAA;IACvC,qFAAqF;IACrF,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,uFAAuF;IACvF,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,iGAAiG;IACjG,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,6FAA6F;IAC7F,MAAM,CAAC,EAAE,gBAAgB,GAAG,KAAK,CAAA;IACjC;;;;;;;;OAQG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,UAAU,CAAA;IAClB;;;;;;;;;;;OAWG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;oDACgD;IAChD,QAAQ,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,SAAS,CAAA;CACxC,CAAA;AAED,0FAA0F;AAC1F,MAAM,MAAM,QAAQ,GAAG;IACrB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;CAC5B,CAAA;AAcD;oFACoF;AACpF,MAAM,MAAM,qBAAqB,GAAG;IAClC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,WAAW,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;CAC1C,CAAA;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,iBAAiB,GAAG,KAAK,GAAG,SAAS,EAC7C,KAAK,CAAC,EAAE,qBAAqB,EAC7B,WAAW,CAAC,EAAE,MAAM,GACnB,gBAAgB,GAAG,KAAK,CAU1B;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,UAAU,CAAC,EACzB,MAAM,EACN,MAAM,EACN,WAAW,EACX,IAAY,EACZ,QAA4B,EAC5B,OAAO,EACP,MAAW,EACX,SAAS,EACT,SAAiB,EACjB,KAAK,EACL,SAAS,EACT,KAAK,EACL,QAAQ,GACT,EAAE,eAAe,GAAG,SAAS,CAuG7B"}
1
+ {"version":3,"file":"ChartFrame.d.ts","sourceRoot":"","sources":["../../../src/charts/primitives/ChartFrame.tsx"],"names":[],"mappings":"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"}
@@ -1,5 +1,7 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { useCallback, useState } from "react";
3
+ import { ignoredProp } from "../../common/errors.js";
4
+ import { useValidateProps } from "../../common/validate.js";
3
5
  import { VX } from "../../tokens/index.js";
4
6
  import { useChartSize } from "../hooks/useChartSize.js";
5
7
  import { deriveLegend } from "../series.js";
@@ -67,6 +69,22 @@ function ChartFrame({
67
69
  return next;
68
70
  });
69
71
  }, []);
72
+ useValidateProps(
73
+ "ChartFrame",
74
+ () => [
75
+ fill && height !== void 0 ? ignoredProp(
76
+ "ChartFrame",
77
+ "height",
78
+ "`fill` is set \u2014 a fill frame takes its parent cell's measured height. Pass one or the other."
79
+ ) : null,
80
+ fill && aspectRatio !== void 0 ? ignoredProp(
81
+ "ChartFrame",
82
+ "aspectRatio",
83
+ "`fill` is set \u2014 a fill frame takes its parent cell's measured height. Pass one or the other."
84
+ ) : null
85
+ ],
86
+ [fill, height, aspectRatio]
87
+ );
70
88
  const placement = legend === false ? "bottom" : legend.placement ?? "bottom";
71
89
  const vertical = placement === "left" || placement === "right";
72
90
  const resolvedState = resolveChartState({ ...state !== void 0 && { state }, isPending });
@@ -27,9 +27,11 @@ export type LegendEntry = {
27
27
  * Shared legend for all non-sparkline charts. `highlighted`/`onHighlight` are optional — omit for
28
28
  * static legends without hover interactivity. `chartId` namespaces `split` clipPath ids so two
29
29
  * legends on the same page never collide. `groups` renders series → hairline divider → overlay →
30
- * reference bands instead of one flat smear. `maxRows` caps the number of rendered entries and
31
- * rolls the remainder into a `+N more` chip (row-accurate wrap measurement is `ChartFrame`'s job —
32
- * this is an entry-count cap, a deliberate simplification until that primitive lands).
30
+ * reference bands instead of one flat smear. `maxRows` caps the number of rendered ENTRIES and
31
+ * rolls the remainder into a `+N more` chip — it is an entry count, not a row count, in spite of
32
+ * the name (`entries.slice(0, cap)` below is the whole of it, and `ChartFrame` measures rows for a
33
+ * different purpose in `legendEntryCap`). The name is inherited and reaches the public
34
+ * `ChartTierMetrics.legendMaxRows`, so it stays until a minor can rename both.
33
35
  *
34
36
  * `onToggle` makes an entry a real toggle: clicking it hides the series in the plot, the tooltip,
35
37
  * and the auto y-domain together (`ChartFrame` owns the `hidden` set — see `docs/CHARTS-SPEC.md`
@@ -1 +1 @@
1
- {"version":3,"file":"ChartLegend.d.ts","sourceRoot":"","sources":["../../../src/charts/primitives/ChartLegend.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAErD,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAG5D,MAAM,MAAM,WAAW,GAAG;IACxB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,KAAK,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,WAAW,CAAA;IAC9C,mFAAmF;IACnF,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,kGAAkG;IAClG,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;0EACsE;IACtE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,mFAAmF;IACnF,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB;iFAC6E;IAC7E,QAAQ,CAAC,EAAE,WAAW,EAAE,CAAA;IACxB;gCAC4B;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAA;CACd,CAAA;AAgOD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,WAAW,CAAC,EAC1B,KAAK,EACL,WAAkB,EAClB,WAAW,EACX,MAAkB,EAClB,QAAQ,EACR,OAAY,EACZ,SAAoB,EACpB,MAAc,EACd,OAAO,EACP,SAAS,EACT,KAAK,GACN,EAAE,WAAW,GAAG;IACf,KAAK,EAAE,WAAW,EAAE,CAAA;IACpB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IAC1C,kCAAkC;IAClC,MAAM,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;IAC5B,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;IAChC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,eAAe,CAAA;IAC3B,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB,+BAkFA"}
1
+ {"version":3,"file":"ChartLegend.d.ts","sourceRoot":"","sources":["../../../src/charts/primitives/ChartLegend.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAErD,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAG5D,MAAM,MAAM,WAAW,GAAG;IACxB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,KAAK,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,WAAW,CAAA;IAC9C,mFAAmF;IACnF,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,kGAAkG;IAClG,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;0EACsE;IACtE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,mFAAmF;IACnF,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB;iFAC6E;IAC7E,QAAQ,CAAC,EAAE,WAAW,EAAE,CAAA;IACxB;gCAC4B;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAA;CACd,CAAA;AAgOD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,WAAW,CAAC,EAC1B,KAAK,EACL,WAAkB,EAClB,WAAW,EACX,MAAkB,EAClB,QAAQ,EACR,OAAY,EACZ,SAAoB,EACpB,MAAc,EACd,OAAO,EACP,SAAS,EACT,KAAK,GACN,EAAE,WAAW,GAAG;IACf,KAAK,EAAE,WAAW,EAAE,CAAA;IACpB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IAC1C,kCAAkC;IAClC,MAAM,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;IAC5B,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;IAChC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,eAAe,CAAA;IAC3B,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB,+BAkFA"}
@@ -152,9 +152,10 @@ export declare function resolveLegendRollup(input: {
152
152
  * column with `height: auto`, so that growth is automatic. Under `fill` the box CANNOT grow, so
153
153
  * the legend is capped instead ({@link legendEntryCap}) — UNLESS the caller stated
154
154
  * `legend.maxRows`, which is `legendWins`: there the floor itself yields, because a floor is a
155
- * default and the caller's number is not. An unmeasured box (`resolvedHeight <= 0`, i.e. a
156
- * `fill` frame before its first measurement) stays at 0 and renders nothing, exactly as before —
157
- * the floor must never invent a height for a box nobody has measured.
155
+ * default and the caller's number is not, but only as far as the box HAS room
156
+ * ({@link SELF_MEASURED_SLACK}). An unmeasured box (`resolvedHeight <= 0`, i.e. a `fill` frame
157
+ * before its first measurement) stays at 0 and renders nothing, exactly as before — the floor
158
+ * must never invent a height for a box nobody has measured.
158
159
  */
159
160
  export declare function resolvePlotRect(input: {
160
161
  containerW: number;
@@ -1 +1 @@
1
- {"version":3,"file":"chart-frame-layout.d.ts","sourceRoot":"","sources":["../../../src/charts/primitives/chart-frame-layout.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"chart-frame-layout.d.ts","sourceRoot":"","sources":["../../../src/charts/primitives/chart-frame-layout.ts"],"names":[],"mappings":"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"}
@@ -76,12 +76,17 @@ function resolvePlotRect(input) {
76
76
  topBottomLegendHeight,
77
77
  legendWins = false
78
78
  } = input;
79
- const plotFloor = legendWins ? 0 : VX.minPlotHeight;
79
+ const room = resolvedHeight - topBottomLegendHeight;
80
+ const plotFloor = legendWins && !isSelfMeasured(room) ? 0 : VX.minPlotHeight;
80
81
  return {
81
82
  width: containerW === 0 ? minWidth : Math.max(containerW - sideLegendWidth, 1),
82
- height: resolvedHeight <= 0 ? 0 : Math.max(resolvedHeight - topBottomLegendHeight, plotFloor)
83
+ height: resolvedHeight <= 0 ? 0 : Math.max(room, plotFloor)
83
84
  };
84
85
  }
86
+ const SELF_MEASURED_SLACK = 0.5;
87
+ function isSelfMeasured(room) {
88
+ return room >= 0 && room < SELF_MEASURED_SLACK;
89
+ }
85
90
  const legendBandHeight = (rows) => LEGEND_PAD_Y + rows * LEGEND_LINE_H + Math.max(rows - 1, 0) * VX.legendGap;
86
91
  const legendEntryWidth = (item) => LEGEND_SWATCH_W + measureText(
87
92
  item.note === void 0 ? item.label : `${item.label} ${item.note}`,
@@ -23,6 +23,17 @@ export declare function oneOf(component: string, prop: string, allowed: readonly
23
23
  * Majors are banned here, so the deprecation notice IS the migration channel.
24
24
  */
25
25
  export declare function deprecatedProp(component: string, oldProp: string, newProp: string, removeIn: string): string;
26
+ /**
27
+ * A prop the component ACCEPTS, reads nothing from, and cannot report as wrong — because a SECOND
28
+ * prop it was passed alongside decides the same thing and won.
29
+ *
30
+ * Distinct from {@link deprecatedProp} (still read, going away) and {@link oneOf} (read, and the
31
+ * value is out of range): this one renders something the caller did not ask for and nothing in the
32
+ * type system, the linter or the console says so. `ChartFrame`'s `height` under `fill` is the
33
+ * reference case — meteo passed both for the life of the file and never learnt that eight height
34
+ * constants and a whole `compact` branch changed no pixel.
35
+ */
36
+ export declare function ignoredProp(component: string, prop: string, because: string): string;
26
37
  /**
27
38
  * An imperative call needs a layer that is not there — the optional peer is not installed, or the
28
39
  * mount that subscribes to its event bus is disabled. Both halves are named because the remedy
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/common/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,yDAAyD;AACzD,eAAO,MAAM,aAAa,aAAa,CAAA;AAMvC;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAEnF;AAED,oEAAoE;AACpE,wBAAgB,KAAK,CACnB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,GAAG,EAAE,OAAO,GACX,MAAM,CAMR;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GACf,MAAM,CAKR;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAErF;AAED,kGAAkG;AAClG,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAKxD;AAOD;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAI5D;AAqBD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,QAAQ,SAAwB,GAAG,MAAM,CAKrF"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/common/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,yDAAyD;AACzD,eAAO,MAAM,aAAa,aAAa,CAAA;AAMvC;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAEnF;AAED,oEAAoE;AACpE,wBAAgB,KAAK,CACnB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,GAAG,EAAE,OAAO,GACX,MAAM,CAMR;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GACf,MAAM,CAKR;AAED;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAEpF;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAErF;AAED,kGAAkG;AAClG,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAKxD;AAOD;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAI5D;AAqBD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,QAAQ,SAAwB,GAAG,MAAM,CAKrF"}
@@ -17,6 +17,9 @@ function deprecatedProp(component, oldProp, newProp, removeIn) {
17
17
  `prop "${oldProp}" is deprecated \u2014 use "${newProp}" instead. "${oldProp}" is removed in ${removeIn}.`
18
18
  );
19
19
  }
20
+ function ignoredProp(component, prop, because) {
21
+ return message(component, `prop "${prop}" is ignored because ${because}`);
22
+ }
20
23
  function missingLayer(component, layer, remedy) {
21
24
  return message(component, `needs ${layer}, which is not available \u2014 ${remedy}.`);
22
25
  }
@@ -57,6 +60,7 @@ export {
57
60
  deprecatedProp,
58
61
  duplicateMount,
59
62
  errorStatus,
63
+ ignoredProp,
60
64
  missingLayer,
61
65
  oneOf,
62
66
  requiredProp,
package/llms.txt CHANGED
@@ -1,4 +1,4 @@
1
- # basalt-ui v1.30.1
1
+ # basalt-ui v1.30.2
2
2
  # Opinionated framework for Mantine-based React apps — theme, app shell, visx charts, toolchain presets
3
3
  #
4
4
  # basalt-ui is an opinionated framework for Mantine-based React apps.
@@ -19,7 +19,7 @@
19
19
  # ./tokens.css subpath. Guide: docs/FRAMEWORK-FREE.md in the repo.
20
20
 
21
21
  Package: basalt-ui
22
- Version: 1.30.1
22
+ Version: 1.30.2
23
23
 
24
24
  ## basalt-ui
25
25
  Import: import { ... } from 'basalt-ui'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "basalt-ui",
3
- "version": "1.30.1",
3
+ "version": "1.30.2",
4
4
  "description": "Opinionated framework for Mantine-based React apps — theme, app shell, visx charts, toolchain presets",
5
5
  "keywords": [
6
6
  "charts",
@@ -1,6 +1,8 @@
1
1
  import type { CSSProperties, ReactNode } from 'react'
2
2
  import { useCallback, useState } from 'react'
3
3
  import type { BasaltProps } from '../../common/props'
4
+ import { ignoredProp } from '../../common/errors'
5
+ import { useValidateProps } from '../../common/validate'
4
6
  import { VX } from '../../tokens'
5
7
  import { useChartSize } from '../hooks/useChartSize'
6
8
  import { deriveLegend } from '../series'
@@ -36,7 +38,13 @@ export type ChartFrameLegend = {
36
38
  /** Default 'bottom'. */
37
39
  placement?: LegendPlacement
38
40
  /**
39
- * Wrap cap → "+N more" rollup at high cardinality.
41
+ * Rollup cap at high cardinality: the legend renders this many ENTRIES and folds the remainder
42
+ * into a `+N more` disclosure chip.
43
+ *
44
+ * **It counts entries, not rows** — the name is inherited and now load-bearing across
45
+ * `ChartTierMetrics.legendMaxRows`, so it is not renamed in a patch. `maxRows: 3` on a 7-entry
46
+ * legend renders three entries, which is ONE row at any width they fit on. `ChartLegend` slices
47
+ * (`entries.slice(0, cap)`); nothing here measures rows.
40
48
  *
41
49
  * An explicit value WINS OUTRIGHT — over the phone tier's two-row default (`chartTierMetrics`)
42
50
  * AND over a `fill` frame's measured fit (`legendEntryCap`). 1.30.0 fixed only the first half:
@@ -67,7 +75,8 @@ export type ChartFrameProps = BasaltProps & {
67
75
  series: readonly SeriesStyle[]
68
76
  /**
69
77
  * Height in pixels, or one per size step. Used when neither `aspectRatio` nor `fill` is set.
70
- * Default 240.
78
+ * Default 240. Passing it WITH `fill` is a wiring mistake, not a fallback — `fill` wins and
79
+ * this is never read, which now warns once in dev (`common/errors.ts`' `ignoredProp`).
71
80
  *
72
81
  * `{ base: 180, md: 260 }` is the declarative escape from a page of fixed-height charts: the
73
82
  * steps are compared against the frame's own MEASURED width, not the viewport, so a chart squeezed
@@ -75,7 +84,8 @@ export type ChartFrameProps = BasaltProps & {
75
84
  * supported ({@link resolveFrameHeight}).
76
85
  */
77
86
  height?: number | ResponsiveChartHeight
78
- /** height = Math.round(containerWidth / aspectRatio). Ignored when `fill` is set. */
87
+ /** height = Math.round(containerWidth / aspectRatio). Ignored when `fill` is set — which warns
88
+ * once in dev, for the same reason `height` does. */
79
89
  aspectRatio?: number
80
90
  /** Fill the parent flex/grid cell's measured height instead of a fixed/derived one. */
81
91
  fill?: boolean
@@ -220,6 +230,32 @@ export function ChartFrame({
220
230
  })
221
231
  }, [])
222
232
 
233
+ // The three sizing props are mutually exclusive and `fill` wins, silently — the type cannot say
234
+ // so without a union prop, which is a bigger change than a patch carries. Warn instead: a
235
+ // consumer passed `height` alongside `fill` for the life of a file and kept eight height
236
+ // constants (and a whole `compact` branch) that never moved a pixel, with nothing from tsc,
237
+ // oxlint or check-theme saying a word.
238
+ useValidateProps(
239
+ 'ChartFrame',
240
+ () => [
241
+ fill && height !== undefined
242
+ ? ignoredProp(
243
+ 'ChartFrame',
244
+ 'height',
245
+ "`fill` is set — a fill frame takes its parent cell's measured height. Pass one or the other.",
246
+ )
247
+ : null,
248
+ fill && aspectRatio !== undefined
249
+ ? ignoredProp(
250
+ 'ChartFrame',
251
+ 'aspectRatio',
252
+ "`fill` is set — a fill frame takes its parent cell's measured height. Pass one or the other.",
253
+ )
254
+ : null,
255
+ ],
256
+ [fill, height, aspectRatio],
257
+ )
258
+
223
259
  const placement = legend === false ? 'bottom' : (legend.placement ?? 'bottom')
224
260
  const vertical = placement === 'left' || placement === 'right'
225
261
  // Every non-null state replaces the plot, and all three suppress the legend for one reason: a
@@ -255,9 +255,11 @@ function LegendChild({ item }: { item: LegendEntry }) {
255
255
  * Shared legend for all non-sparkline charts. `highlighted`/`onHighlight` are optional — omit for
256
256
  * static legends without hover interactivity. `chartId` namespaces `split` clipPath ids so two
257
257
  * legends on the same page never collide. `groups` renders series → hairline divider → overlay →
258
- * reference bands instead of one flat smear. `maxRows` caps the number of rendered entries and
259
- * rolls the remainder into a `+N more` chip (row-accurate wrap measurement is `ChartFrame`'s job —
260
- * this is an entry-count cap, a deliberate simplification until that primitive lands).
258
+ * reference bands instead of one flat smear. `maxRows` caps the number of rendered ENTRIES and
259
+ * rolls the remainder into a `+N more` chip — it is an entry count, not a row count, in spite of
260
+ * the name (`entries.slice(0, cap)` below is the whole of it, and `ChartFrame` measures rows for a
261
+ * different purpose in `legendEntryCap`). The name is inherited and reaches the public
262
+ * `ChartTierMetrics.legendMaxRows`, so it stays until a minor can rename both.
261
263
  *
262
264
  * `onToggle` makes an entry a real toggle: clicking it hides the series in the plot, the tooltip,
263
265
  * and the auto y-domain together (`ChartFrame` owns the `hidden` set — see `docs/CHARTS-SPEC.md`
@@ -260,9 +260,10 @@ export function resolveLegendRollup(input: {
260
260
  * column with `height: auto`, so that growth is automatic. Under `fill` the box CANNOT grow, so
261
261
  * the legend is capped instead ({@link legendEntryCap}) — UNLESS the caller stated
262
262
  * `legend.maxRows`, which is `legendWins`: there the floor itself yields, because a floor is a
263
- * default and the caller's number is not. An unmeasured box (`resolvedHeight <= 0`, i.e. a
264
- * `fill` frame before its first measurement) stays at 0 and renders nothing, exactly as before —
265
- * the floor must never invent a height for a box nobody has measured.
263
+ * default and the caller's number is not, but only as far as the box HAS room
264
+ * ({@link SELF_MEASURED_SLACK}). An unmeasured box (`resolvedHeight <= 0`, i.e. a `fill` frame
265
+ * before its first measurement) stays at 0 and renders nothing, exactly as before — the floor
266
+ * must never invent a height for a box nobody has measured.
266
267
  */
267
268
  export function resolvePlotRect(input: {
268
269
  containerW: number
@@ -288,13 +289,46 @@ export function resolvePlotRect(input: {
288
289
  topBottomLegendHeight,
289
290
  legendWins = false,
290
291
  } = input
291
- const plotFloor = legendWins ? 0 : VX.minPlotHeight
292
+ const room = resolvedHeight - topBottomLegendHeight
293
+ const plotFloor = legendWins && !isSelfMeasured(room) ? 0 : VX.minPlotHeight
292
294
  return {
293
295
  width: containerW === 0 ? minWidth : Math.max(containerW - sideLegendWidth, 1),
294
- height: resolvedHeight <= 0 ? 0 : Math.max(resolvedHeight - topBottomLegendHeight, plotFloor),
296
+ height: resolvedHeight <= 0 ? 0 : Math.max(room, plotFloor),
295
297
  }
296
298
  }
297
299
 
300
+ /**
301
+ * Sub-pixel slack on "the frame measures its own legend band and nothing else".
302
+ *
303
+ * MEASURED exact in the collapse this guards (both `ResizeObserver` contentRects read 29.375 in
304
+ * headless Chrome, `tests/layout/charts.layout.test.ts` INVARIANT 7) — the slack exists only so a
305
+ * future rounding or border difference between the two observers cannot re-open a fixpoint that
306
+ * has no other way out. A plot this thin has nothing to draw either way.
307
+ */
308
+ const SELF_MEASURED_SLACK = 0.5
309
+
310
+ /**
311
+ * Is the frame's height its OWN content rather than a box something else stated?
312
+ *
313
+ * `ChartFrame` measures its own node (`useChartSize` → visx `useParentSize` observes the element
314
+ * the ref is attached to, its name notwithstanding), and under `fill` its `height: 100%` resolves
315
+ * to `auto` in any parent that states no height. Its only children are the plot and the legend, so
316
+ * `room === 0` means the box IS the legend band — which happens exactly when no plot was drawn.
317
+ *
318
+ * That is why `legendWins` cannot yield the floor there: with the floor gone the sequence has a
319
+ * FIXPOINT AT ZERO — plot 0 → content is the legend → box = band → room 0 → plot 0, forever, and
320
+ * the chart never appears. Keeping the floor while `room` is 0 converges on the same box a `fill`
321
+ * frame in an unsized parent has always had (plot `VX.minPlotHeight`, box = plot + band), from
322
+ * which `room` is positive and `legendWins` applies normally.
323
+ *
324
+ * A NEGATIVE `room` is not this case and must keep yielding: a self-measured box can never be
325
+ * shorter than its own content, so a band taller than the box means the box was stated by
326
+ * something else, and spending all of it on legend rows is the caller's own arithmetic.
327
+ */
328
+ function isSelfMeasured(room: number): boolean {
329
+ return room >= 0 && room < SELF_MEASURED_SLACK
330
+ }
331
+
298
332
  /** Height of a legend band `rows` rows tall, wrapper padding included. */
299
333
  const legendBandHeight = (rows: number): number =>
300
334
  LEGEND_PAD_Y + rows * LEGEND_LINE_H + Math.max(rows - 1, 0) * VX.legendGap
@@ -55,6 +55,20 @@ export function deprecatedProp(
55
55
  )
56
56
  }
57
57
 
58
+ /**
59
+ * A prop the component ACCEPTS, reads nothing from, and cannot report as wrong — because a SECOND
60
+ * prop it was passed alongside decides the same thing and won.
61
+ *
62
+ * Distinct from {@link deprecatedProp} (still read, going away) and {@link oneOf} (read, and the
63
+ * value is out of range): this one renders something the caller did not ask for and nothing in the
64
+ * type system, the linter or the console says so. `ChartFrame`'s `height` under `fill` is the
65
+ * reference case — meteo passed both for the life of the file and never learnt that eight height
66
+ * constants and a whole `compact` branch changed no pixel.
67
+ */
68
+ export function ignoredProp(component: string, prop: string, because: string): string {
69
+ return message(component, `prop "${prop}" is ignored because ${because}`)
70
+ }
71
+
58
72
  /**
59
73
  * An imperative call needs a layer that is not there — the optional peer is not installed, or the
60
74
  * mount that subscribes to its event bus is disabled. Both halves are named because the remedy