basalt-ui 1.29.2 → 1.30.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/MIGRATING.md +901 -366
- package/README.md +23 -2
- package/agent/rules/basalt-controls.md +34 -34
- package/agent/skills/basalt-app/SKILL.md +4 -2
- package/agent/templates/query-client.ts.tpl +18 -4
- package/configs/lefthook.yml +51 -19
- package/configs/oxlint-plugin.js +210 -84
- package/configs/oxlint-plugin.test.ts +182 -5
- package/configs/oxlint.json +4 -4
- package/dist/charts/index.d.ts +1 -1
- package/dist/charts/index.d.ts.map +1 -1
- package/dist/charts/kinds/BandStrip.d.ts +5 -3
- package/dist/charts/kinds/BandStrip.d.ts.map +1 -1
- package/dist/charts/kinds/Bars.d.ts +4 -2
- package/dist/charts/kinds/Bars.d.ts.map +1 -1
- package/dist/charts/kinds/Donut.d.ts +4 -2
- package/dist/charts/kinds/Donut.d.ts.map +1 -1
- package/dist/charts/kinds/DualPanel.d.ts +4 -2
- package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
- package/dist/charts/kinds/Heatmap.d.ts +5 -2
- package/dist/charts/kinds/Heatmap.d.ts.map +1 -1
- package/dist/charts/kinds/MirroredBars.d.ts +4 -2
- package/dist/charts/kinds/MirroredBars.d.ts.map +1 -1
- package/dist/charts/kinds/MultiLine.d.ts +4 -2
- package/dist/charts/kinds/MultiLine.d.ts.map +1 -1
- package/dist/charts/kinds/StackedArea.d.ts +4 -2
- package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
- package/dist/charts/kinds/ZonedLine.d.ts +4 -2
- package/dist/charts/kinds/ZonedLine.d.ts.map +1 -1
- package/dist/charts/primitives/CartesianChart.d.ts +4 -1
- package/dist/charts/primitives/CartesianChart.d.ts.map +1 -1
- package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
- package/dist/charts/primitives/ChartCard.js +2 -2
- package/dist/charts/primitives/ChartFrame.d.ts +25 -4
- package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
- package/dist/charts/primitives/ChartFrame.js +23 -16
- package/dist/charts/primitives/chart-frame-layout.d.ts +115 -6
- package/dist/charts/primitives/chart-frame-layout.d.ts.map +1 -1
- package/dist/charts/primitives/chart-frame-layout.js +46 -6
- package/dist/cli/check-theme.d.ts.map +1 -1
- package/dist/cli/check-theme.js +6 -0
- package/dist/cli/doctor.d.ts +10 -4
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +28 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +2 -1
- package/dist/cli/index.d.ts +12 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +29 -0
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +4 -15
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +8 -14
- package/dist/controls/actions.d.ts.map +1 -1
- package/dist/controls/actions.js +12 -2
- package/dist/dashboard/section.module.css +15 -7
- package/dist/dashboard/stat-card.d.ts.map +1 -1
- package/dist/dashboard/stat-card.js +7 -2
- package/dist/dashboard/stat-group.d.ts +8 -2
- package/dist/dashboard/stat-group.d.ts.map +1 -1
- package/dist/dashboard/stat-group.js +23 -15
- package/dist/dashboard/stat-group.module.css +85 -9
- package/dist/dashboard/stat-group.module.css.d.ts +1 -0
- package/dist/dashboard/widget-grid.d.ts.map +1 -1
- package/dist/dashboard/widget-grid.js +4 -5
- package/dist/dashboard/widget-grid.module.css +64 -21
- package/dist/dashboard/widget-grid.module.css.d.ts +1 -0
- package/dist/guard/index.d.ts +19 -2
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +27 -4
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/shell/app-brand.module.css +12 -3
- package/dist/shell/app-header.module.css +49 -9
- package/dist/shell/app-main.module.css +24 -4
- package/dist/shell/app-mobile-nav.d.ts.map +1 -1
- package/dist/shell/app-mobile-nav.js +98 -51
- package/dist/shell/app-mobile-nav.module.css +99 -11
- package/dist/shell/app-mobile-nav.module.css.d.ts +1 -0
- package/dist/shell/app-sidebar.module.css +55 -2
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +3 -3
- package/dist/shell/mobile-nav-model.d.ts +26 -7
- package/dist/shell/mobile-nav-model.d.ts.map +1 -1
- package/dist/shell/mobile-nav-model.js +12 -3
- package/dist/shell/page-aside.module.css +8 -6
- package/dist/shell/page-bar.d.ts +13 -0
- package/dist/shell/page-bar.d.ts.map +1 -1
- package/dist/shell/page-bar.js +13 -3
- package/dist/shell/page-bar.module.css +20 -12
- package/dist/shell/page-title.module.css +3 -1
- package/dist/theme/use-basalt-spacing.d.ts.map +1 -1
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +51 -7
- package/dist/tokens/palette.d.ts +98 -17
- package/dist/tokens/palette.d.ts.map +1 -1
- package/dist/tokens/palette.js +110 -18
- package/dist/tokens.css +17 -5
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/src/charts/index.ts +1 -0
- package/src/charts/kinds/BandStrip.tsx +5 -3
- package/src/charts/kinds/Bars.tsx +4 -2
- package/src/charts/kinds/Donut.tsx +4 -2
- package/src/charts/kinds/DualPanel.tsx +4 -2
- package/src/charts/kinds/Heatmap.tsx +5 -2
- package/src/charts/kinds/MirroredBars.tsx +4 -2
- package/src/charts/kinds/MultiLine.tsx +4 -2
- package/src/charts/kinds/StackedArea.tsx +4 -2
- package/src/charts/kinds/ZonedLine.tsx +4 -2
- package/src/charts/primitives/CartesianChart.tsx +4 -1
- package/src/charts/primitives/ChartCard.tsx +12 -7
- package/src/charts/primitives/ChartFrame.tsx +51 -37
- package/src/charts/primitives/chart-frame-layout.ts +172 -13
- package/src/cli/check-theme.ts +13 -0
- package/src/cli/doctor.ts +49 -8
- package/src/cli/help.ts +5 -3
- package/src/cli/index.ts +49 -0
- package/src/cli/init.ts +6 -27
- package/src/cli/sync.ts +11 -22
- package/src/controls/actions.tsx +51 -0
- package/src/dashboard/section.module.css +15 -7
- package/src/dashboard/stat-card.tsx +7 -2
- package/src/dashboard/stat-group.module.css +85 -9
- package/src/dashboard/stat-group.module.css.d.ts +1 -0
- package/src/dashboard/stat-group.tsx +40 -20
- package/src/dashboard/widget-grid.module.css +64 -21
- package/src/dashboard/widget-grid.module.css.d.ts +1 -0
- package/src/dashboard/widget-grid.tsx +30 -12
- package/src/guard/index.ts +79 -11
- package/src/index.ts +9 -0
- package/src/shell/app-brand.module.css +12 -3
- package/src/shell/app-header.module.css +49 -9
- package/src/shell/app-main.module.css +24 -4
- package/src/shell/app-mobile-nav.module.css +99 -11
- package/src/shell/app-mobile-nav.module.css.d.ts +1 -0
- package/src/shell/app-mobile-nav.tsx +225 -83
- package/src/shell/app-sidebar.module.css +55 -2
- package/src/shell/index.tsx +26 -6
- package/src/shell/mobile-nav-model.ts +37 -9
- package/src/shell/page-aside.module.css +8 -6
- package/src/shell/page-bar.module.css +20 -12
- package/src/shell/page-bar.tsx +39 -5
- package/src/shell/page-title.module.css +3 -1
- package/src/theme/use-basalt-spacing.ts +8 -6
- package/src/tokens/index.ts +90 -6
- package/src/tokens/palette.ts +111 -18
package/MIGRATING.md
CHANGED
|
@@ -13,9 +13,32 @@ on was written against source, not against its commit messages — which is not
|
|
|
13
13
|
1.23.0 commit message describes a change it did not make (see `1.23.1` § Corrections). Check
|
|
14
14
|
the types, not this table, if the two disagree.
|
|
15
15
|
|
|
16
|
-
**The newest section is headed `## Unreleased`, and
|
|
17
|
-
is written before `semantic-release` picks the number, so a number written here is a guess
|
|
18
|
-
was wrong
|
|
16
|
+
**The newest section is headed `## Unreleased`, and the RELEASE renames it to its own number.**
|
|
17
|
+
This file is written before `semantic-release` picks the number, so a number written here is a guess
|
|
18
|
+
— and it was wrong four rounds running. The rename is therefore a release-time step, not an
|
|
19
|
+
authoring-time one: the section is authored as `## Unreleased — <title>`, and the release swaps the
|
|
20
|
+
word `Unreleased` for the computed version, leaving the title alone; a fresh `## Unreleased` opens
|
|
21
|
+
above it, with a fresh column in the index below.
|
|
22
|
+
|
|
23
|
+
**Since 1.30.1 a script does it, and that is the fix the previous revision only claimed.** This
|
|
24
|
+
paragraph used to end "so that is where the swap belongs" — a statement of intent that read as a
|
|
25
|
+
statement of fact. `prepareCmd` was never given the step and MIGRATING.md was never added to
|
|
26
|
+
`@semantic-release/git`'s assets, so even a correct local edit could not have reached the tarball,
|
|
27
|
+
and 1.30.0 duly shipped `## Unreleased — the chrome wave` to npm; three separate consumer agents
|
|
28
|
+
found it there. `packages/basalt-ui/scripts/release-migrating.ts` now performs the rename in
|
|
29
|
+
`prepareCmd` beside `gen-llms.ts` and commits through the same assets list, `--check` runs it in
|
|
30
|
+
`scripts/release.sh`'s preflight (a dry run SKIPS `prepare`, so nothing else in the pipeline can
|
|
31
|
+
catch a shape it cannot rewrite), and it FAILS rather than doing nothing when the shape surprises
|
|
32
|
+
it. If you are reading a heading that still says `Unreleased` in an installed tarball, that is a
|
|
33
|
+
bug, not an authoring slip.
|
|
34
|
+
|
|
35
|
+
**Skipping that step is what this file's worst defect was.** 1.27.0, 1.28.0 and 1.29.0 all shipped
|
|
36
|
+
under one `## Unreleased` heading — ~1000 lines, two of its subsections still titled "targeting
|
|
37
|
+
1.29.0" weeks after 1.29.0 was on npm. Four of five consumer migrations off 1.25.0/1.27.0 named it
|
|
38
|
+
their top finding independently: a reader cannot tell which minor introduced which change, and
|
|
39
|
+
"read every section between your version and the target" is unusable when the last three sections
|
|
40
|
+
claim not to be between them. One of them shipped a real defect through the gap. Restored to
|
|
41
|
+
per-minor sections below; keep it that way.
|
|
19
42
|
|
|
20
43
|
**No majors, by policy.** A rename or a removal ships as a plain `feat:` on the 1.x line, so a minor
|
|
21
44
|
bump can require code changes. Skipping several at once is the expensive case — read every section
|
|
@@ -28,43 +51,660 @@ between your version and the target.
|
|
|
28
51
|
1.10.0, 1.13.0, 1.14.0, 1.16.0, 1.18.0 — and every patch. Additive-only subpaths: `./tokens.css`
|
|
29
52
|
at 1.3.0, `./agent-chat` at 1.10.0.
|
|
30
53
|
|
|
54
|
+
## What changed, by surface
|
|
55
|
+
|
|
56
|
+
**Read your row, then the sections it names — not the whole file.** A `tokens-only` consumer read
|
|
57
|
+
all 2294 lines of the previous revision to learn that four minors moved two things in the token
|
|
58
|
+
layer; this index is that answer in one line. It is a POINTER, not a second copy: every cell names
|
|
59
|
+
the section that carries the detail, and a surface with no row in a minor had no delta in it. The
|
|
60
|
+
`Unreleased` column takes the version number at the same moment the section below it does, and a new
|
|
61
|
+
column opens at the same time — both performed by `scripts/release-migrating.ts` in the release
|
|
62
|
+
commit, so the column head and the section it points at can never disagree.
|
|
63
|
+
|
|
64
|
+
There is one row per SURFACE, not per export, and a surface earns a row once anything in it moves:
|
|
65
|
+
**dashboard** and **toolchain** were added at 1.30.1 because `StatGroup`/`WidgetGrid` and the
|
|
66
|
+
shipped lefthook preset both changed in 1.30.0 with no row that owned them — which is how a
|
|
67
|
+
consumer read this index, found nothing, and diffed a KPI row that had silently gone 4-up → 2-up.
|
|
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) | — | — |
|
|
81
|
+
|
|
31
82
|
---
|
|
32
83
|
|
|
33
84
|
## Unreleased
|
|
34
85
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
86
|
+
_Nothing yet. Author the next section under this heading as `## Unreleased — <title>`; the
|
|
87
|
+
release renames it and opens a new one here._
|
|
88
|
+
|
|
89
|
+
## 1.30.1 — finishing 1.30.0
|
|
90
|
+
|
|
91
|
+
**A patch. Nothing is added, renamed or removed — three things 1.30.0 shipped are made to work, and
|
|
92
|
+
one number it published is corrected.** If you are on 1.29.x, read § 1.30.0 first and this second;
|
|
93
|
+
if you are already on 1.30.0, this section is the whole delta.
|
|
94
|
+
|
|
95
|
+
### Charts — the responsive `height` reaches the kinds it was written for
|
|
96
|
+
|
|
97
|
+
`ResponsiveChartHeight` (`{ base, sm?, md?, lg? }`) landed at 1.30.0 on `ChartFrame.height` alone.
|
|
98
|
+
`CartesianChart` and all nine kinds still declared `height?: number` and merely spread the value
|
|
99
|
+
through — and `basalt/hand-rolled-plot` forbids composing `ChartFrame` directly for a single-plot
|
|
100
|
+
cartesian chart, so the feature could not be reached from any compliant call site.
|
|
101
|
+
`MultiLine height={{ base: 200, sm: 260 }}` was a `tsc` error over a value that would have resolved
|
|
102
|
+
correctly at runtime.
|
|
103
|
+
|
|
104
|
+
Ten declarations are widened to `number | ResponsiveChartHeight` — `CartesianChart`, `MultiLine`,
|
|
105
|
+
`Bars`, `StackedArea`, `ZonedLine`, `BandStrip`, `MirroredBars`, `Heatmap`, `DualPanel`, `Donut` —
|
|
106
|
+
and `ResponsiveChartHeight` is exported from `./charts` so a consumer can name the type.
|
|
107
|
+
**Type-only, additive, no rendering moves**: a plain `number` behaves exactly as before, and the steps resolve
|
|
108
|
+
against the MEASURED container width (the same width the chart tier reads), never the viewport.
|
|
109
|
+
|
|
110
|
+
### Charts — `legend.maxRows` wins under `fill` too, and the plot yields
|
|
111
|
+
|
|
112
|
+
**This one MOVES RENDERING for a chart that already sets `legend.maxRows` inside a `fill` frame.**
|
|
113
|
+
1.30.0 promised an explicit `maxRows` "WINS OUTRIGHT" and delivered half of it: the tier default
|
|
114
|
+
stopped trimming the number, but `legendEntryCap`'s measured fit then ran `Math.min(fitted, caller)`
|
|
115
|
+
on top. In a docked panel — where `fill` is the norm and the row height sits near
|
|
116
|
+
`VX.minPlotHeight` — the fit is the binding constraint, so the documented escape was inert in
|
|
117
|
+
exactly the case it was restored for. Measured: a 7-entry legend in a 92–150px row rendered 2
|
|
118
|
+
entries at `maxRows` 3, 6 **and** 99, with five series drawn in colours nothing named.
|
|
119
|
+
|
|
120
|
+
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,
|
|
123
|
+
and a floor about the plot); the stated number is the only intent in the room.
|
|
124
|
+
|
|
125
|
+
- **Expect a taller legend and a shorter plot** anywhere you set `maxRows` on a docked/`fill` chart.
|
|
126
|
+
A number too large for the box now costs the plot visibly, in the frame you sized, rather than
|
|
127
|
+
being silently rolled back up.
|
|
128
|
+
- **Opt-out: drop `maxRows`.** With nothing stated the measured fit applies exactly as it did at
|
|
129
|
+
1.30.0, bounded by the tier's default. That is the same setting as before this change.
|
|
130
|
+
- **Fixed-height frames are unaffected** — they grow around their legend and never entered the fit.
|
|
131
|
+
|
|
132
|
+
### This file — the release-time rename is a script now
|
|
133
|
+
|
|
134
|
+
The `## Unreleased` → `## <version>` swap and its index column are performed by
|
|
135
|
+
`packages/basalt-ui/scripts/release-migrating.ts` in `prepareCmd`, committed through the same
|
|
136
|
+
`@semantic-release/git` assets list that keeps `llms.txt` current, gated by `--check` in the release
|
|
137
|
+
wrapper's preflight, and covered by tests. See this file's preamble for why the previous revision's
|
|
138
|
+
claim to have already done this was false. **Consumer-visible consequence:** the newest section in
|
|
139
|
+
an installed tarball carries its own version number. If it says `Unreleased`, file it.
|
|
140
|
+
|
|
141
|
+
**§ 1.30.0 § Guards carries a corrected number**, restated here because a published measurement
|
|
142
|
+
moved: meteo is 5 `control-outside-home` incumbents, not 0, and the fleet total is 34, not 29 — the
|
|
143
|
+
zero was a failed `npx --no-install oxlint` invocation read as "no findings". The four promotions to
|
|
144
|
+
`error` are unaffected and stand as shipped; only the extended C1 pair's count changed.
|
|
145
|
+
|
|
146
|
+
## 1.30.0 — the chrome wave
|
|
147
|
+
|
|
148
|
+
**Mostly CHROME, which removes and renames nothing: eight spacing numbers move, the mobile More
|
|
149
|
+
surface becomes a compact popover instead of a bottom sheet, and the aside's phone pill takes the
|
|
150
|
+
aside's own title.** Read § Chrome for the table and the opt-outs before you diff a screenshot.
|
|
151
|
+
|
|
152
|
+
**The one thing that can fail your build: four guard rules promote from `warn` to `error`** — and
|
|
153
|
+
all four were measured at ZERO incumbents across every consumer in the fleet before the promotion,
|
|
154
|
+
so the expected cost is nothing. § Guards has the measurement and the one rule that did NOT
|
|
155
|
+
promote.
|
|
156
|
+
|
|
157
|
+
### Chrome — the spacing pass, the More popover, and the aside's pill
|
|
158
|
+
|
|
159
|
+
**A VISUAL change, shipping as a plain `feat:` on the 1.x line.** Nothing is removed, nothing is
|
|
160
|
+
renamed and no call site has to change — but eight spacing numbers, the mobile More surface and one
|
|
161
|
+
pill label all move, so an app pinned to a screenshot will see a diff. Majors are banned here
|
|
162
|
+
(`feat!:` and `BREAKING CHANGE:` footers are forbidden), so the version number cannot carry that
|
|
163
|
+
warning and this section is it. Each behaviour change below names its opt-out.
|
|
164
|
+
|
|
165
|
+
**Eight spacing numbers** — six moved, two new, all level-0 values and all density-tracked
|
|
166
|
+
(`tokens/palette.ts`; every entry carries its own reasoning, and `theme/spacing.test.ts`'s file
|
|
167
|
+
header carries the wave's). One report drove all eight: the sidebar and header band read as small
|
|
168
|
+
controls floating in dead air while the page content sat too close to the seam.
|
|
169
|
+
|
|
170
|
+
| Token | Was | Now | What it sizes |
|
|
171
|
+
| ------------------------ | ------- | --- | ------------------------------------------------------------------------------------------------------------------ |
|
|
172
|
+
| `controlHeightCtl` | 30 | 32 | the `ctl` tier — the whole chrome tier lands on ONE number now |
|
|
173
|
+
| `appShellHeaderHeight` | 48 | 44 | the app-header band, one token tall at every viewport |
|
|
174
|
+
| `pageBarRowHeight` | 40 | 36 | `PageBar` row 2's band — the secondary band stays a clear step under the primary |
|
|
175
|
+
| `sidebarSectionGap` | 12 | 16 | between nav sections |
|
|
176
|
+
| `sidebarSectionLabelGap` | 2 | 6 | a section label to its own list. The proximity pair was INVERTED, so every label read as heading the list ABOVE it |
|
|
177
|
+
| `sidebarRegionGap` | 10 | 12 | between the sidebar's regions — now visibly bigger than the gaps inside one |
|
|
178
|
+
| `appShellInset` | _(new)_ | 20 | the page gutter from `sm` up, replacing the `SPACE_SCALE.sm` (13) the main column used to borrow |
|
|
179
|
+
| `appShellInsetMobile` | _(new)_ | 8 | the page gutter below `sm` — at 13 a phone spent 6.7% of a 390px viewport on margin before a card was drawn |
|
|
180
|
+
|
|
181
|
+
Both new entries ARE emitted as CSS variables — `--vx-space-app-shell-inset` /
|
|
182
|
+
`--vx-space-app-shell-inset-mobile`, unlike their JS-only `appShell*` neighbours. **The page gutter
|
|
183
|
+
is now RESPONSIVE**: `BasaltShell` hands Mantine `padding={{ base: 8, sm: 20 }}` and gives
|
|
184
|
+
`AppShell.Header` the same pair, both stepping at `theme.breakpoints.sm`, so the breadcrumb's left
|
|
185
|
+
edge, the global actions' right edge and the page's first column share one gutter at every width —
|
|
186
|
+
and above `sm` they land on the vertical line the sidebar's nav icons already sat on.
|
|
187
|
+
|
|
188
|
+
**The opt-out, and its one sharp edge.** These are structural theme tokens, so the lever is
|
|
189
|
+
`createBasaltTheme(overrides, { density })` — an integer level that moves the whole scale by the
|
|
190
|
+
law — never a per-token override. Re-declaring `--vx-space-app-shell-inset` in your own CSS restyles
|
|
191
|
+
whatever READS that var, but NOT the shell's own gutter: that value reaches Mantine as a JS number
|
|
192
|
+
through `useBasaltSpacing()`, deliberately — a Mantine spacing KEY would resolve back to
|
|
193
|
+
`var(--mantine-spacing-sm)` and hand the gutter straight back to the 13 this replaces.
|
|
194
|
+
|
|
195
|
+
- **`useBasaltSpacing` is on the root barrel** (`basalt-ui`) — the derived spacing objects behind
|
|
196
|
+
those numbers, for a consumer computing geometry that has to track `density` rather than pinning a
|
|
197
|
+
literal. Additive; it was already the shell's own reader.
|
|
198
|
+
|
|
199
|
+
**READING the gutter is the other direction, and it is a different variable.** The paragraph above
|
|
200
|
+
states the WRITE consequence; the read one caught a consumer. `--vx-space-app-shell-inset` is a
|
|
201
|
+
single value (20) with no phone twin in the same var, while the shell's real gutter reaches Mantine
|
|
202
|
+
as the JS pair and lands in **`--app-shell-padding`**, which Mantine emits per breakpoint. So
|
|
203
|
+
full-bleed math (`margin-inline: calc(var(--…) * -1)`, a height budget subtracting the page inset)
|
|
204
|
+
must read `--app-shell-padding` — grepping for the basalt-prefixed token gets a number that is
|
|
205
|
+
correct by coincidence from `sm` up and wrong on a phone (20 against the real 8). Inside a
|
|
206
|
+
`BasaltShell`, read Mantine's var.
|
|
207
|
+
|
|
208
|
+
**Shell-less, the two `--vx-*` gutter vars ARE your lever**, and that is the case they were emitted
|
|
209
|
+
for — nothing else in the framework reads them, which is why they are the exception among the
|
|
210
|
+
JS-only `appShell*` entries. A provider-only app that had a hand-maintained `Container px` pair and a
|
|
211
|
+
matching negative-margin `.bleed` pair, restating the same two numbers in two files behind two media
|
|
212
|
+
queries, deletes both and reads `--vx-space-app-shell-inset{,-mobile}` instead. Only `BasaltShell`'s
|
|
213
|
+
own gutter is unreachable that way.
|
|
214
|
+
|
|
215
|
+
**The mobile More surface is a compact POPOVER, not a bottom sheet**, and `MOBILE_MENU_MAX_DEFAULT`
|
|
216
|
+
rises 6 → 12. `projectMobileNav` is unchanged in shape — the surface is still INFERRED from row
|
|
217
|
+
count (0 drops the slot, 1 is a plain link, ≤ `menuMax` is a `menu`, past it a `sheet`) — but a
|
|
218
|
+
7-to-12-row slot now lands on `menu` where it used to raise a full-width Drawer. **The
|
|
219
|
+
never-below-the-fold guarantee moved off the constant and onto CSS**: the dropdown declares
|
|
220
|
+
`max-height: min(72dvh, 560px)` with `overflow-y: auto`, which binds at ANY row count, so the
|
|
221
|
+
constant is a taste bound on how tall one popover may get rather than the arithmetic it claimed to
|
|
222
|
+
be (that arithmetic — "6 × 44px against 415px of headroom" — was stale in nearly every term; the
|
|
223
|
+
rows are 40px). `menuMax` survives for the nav that has genuinely outgrown a popover. **Opt-out:
|
|
224
|
+
`mobileNav.menuMax`**, the unchanged prop — set it to `6` to restore the old cut-off, and it still
|
|
225
|
+
moves the line in both directions.
|
|
226
|
+
|
|
227
|
+
Riding along on the bar, all visible with no code change:
|
|
228
|
+
|
|
229
|
+
- **The bar paints `--vx-surface-panel`.** It used to resolve to `--vx-surface-bg` — the exact fill
|
|
230
|
+
the scrollport above it paints — so the only thing separating the bar from the page was the region
|
|
231
|
+
seam's 9%-alpha hairline, which is invisible on a phone. No new token, no shadow, no `withBorder`.
|
|
232
|
+
- **The dead taps.** Re-tapping the active tab scrolls to top and now REPORTS whether anything
|
|
233
|
+
actually scrolled, so an already-at-top retap navigates instead of doing nothing; `menuRow` and
|
|
234
|
+
`sheetRow` adopt the link row's activation rule (`aria-current`, the disabled guard) instead of
|
|
235
|
+
each answering it differently; a disabled row no longer sets `pointer-events: none`, which
|
|
236
|
+
swallowed the tap with no press state; the More tab is a real toggle, so a second tap closes it;
|
|
237
|
+
and a sheet's overlay stops being hit-testable over the other tabs after `onClose`.
|
|
238
|
+
|
|
239
|
+
**`PageBar`'s phone aside pill carries the aside's own `title`**, not the literal word `Panel`
|
|
240
|
+
(`docs/ASIDE-SPEC.md` §0 — the title names the CONTENT, never the region, which is the rule the
|
|
241
|
+
desktop header already obeyed). The separate `ariaLabel` went with it: it existed only to put the
|
|
242
|
+
real title back for a screen reader, and once the visible label IS the title the two said the same
|
|
243
|
+
thing twice. No opt-out and none is needed — the pill takes no label prop — but a test asserting the
|
|
244
|
+
string `Panel` now reads whatever your `PageAside` names itself. Still no count: an aside's children
|
|
245
|
+
are not a `FilterSet`, so unlike `Filters (n)` there is no census to derive one from.
|
|
246
|
+
|
|
247
|
+
- **One new dev warning, no behaviour change.** Below `sm` a `PageAside` can only project into
|
|
248
|
+
`PageBar` row 2, and that row exists ONLY when the bar has `tabs`, `filters` or a non-empty
|
|
249
|
+
`filtersEnd`. A page whose bar carries `actions` alone therefore renders the aside in flow at the
|
|
250
|
+
bottom of the page — deliberate, pinned by tests, and invisible on the desktop it was authored
|
|
251
|
+
on, so `PageAside` now says it once in dev. The remedy is to give that bar a row 2.
|
|
252
|
+
|
|
253
|
+
### Dashboard — `StatGroup` and `WidgetGrid` key on the CONTAINER, and `cols={4}` goes 2-up at `sm`
|
|
254
|
+
|
|
255
|
+
**This is the item in 1.30.0 that can silently change a production layout, and the previous revision
|
|
256
|
+
of this section did not mention it at all.** Neither export changed shape; both changed where their
|
|
257
|
+
column law reads its width from.
|
|
258
|
+
|
|
259
|
+
- **The law now keys on the CONTAINER, not the viewport.** Each primitive renders its own
|
|
260
|
+
`container-type: inline-size` box around the grid, so the counts follow the width the row
|
|
261
|
+
ACTUALLY has. The shell moves that width with the viewport held still — a `PageAside` claiming its
|
|
262
|
+
300px, the sidebar collapsing 256px → its 48px rail, your own split pane — and `@media` sees none
|
|
263
|
+
of it. Measured: a `PageAside` on a 1512px desktop left a KPI row ~956px wide, `@media` still
|
|
264
|
+
resolved `lg`, and four cells of 239px truncated their own values mid-word. **If you mount either
|
|
265
|
+
primitive inside an aside, a split pane or beside a collapsible sidebar, expect a column count to
|
|
266
|
+
drop where it used to overflow.** The old viewport law survives verbatim under
|
|
267
|
+
`@supports not (container-type: inline-size)` for pre-2023 browsers, so there are two laws in the
|
|
268
|
+
file and exactly one is ever live.
|
|
269
|
+
- **`StatGroup cols={4}` now takes TWO columns at `sm`, not three.** The general rule is still
|
|
270
|
+
`min(cols, 3)`; `4` is the exception, because three columns leave a four-KPI row as 3 + 1 — one
|
|
271
|
+
orphan cell against two thirds of empty track. `2`, `3` and `5` are unchanged. The full law is
|
|
272
|
+
`base 2 → sm min(cols, 3), except 4 → 2 → lg cols`.
|
|
273
|
+
- **No opt-out, and none is offered**: `cols` is the DESKTOP count and a responsive object is
|
|
274
|
+
deliberately not forwarded (that seam is what let five call sites in one app disagree). If you
|
|
275
|
+
need a different law, you are outside these primitives.
|
|
276
|
+
|
|
277
|
+
Worth checking while you are here: `WidgetGrid` and `StatGroup` exist to own exactly the
|
|
278
|
+
`SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }}` a dashboard hand-writes per page, and the container
|
|
279
|
+
fix only reaches the rows that adopted them. In the reference consumer, five KPI rows still
|
|
280
|
+
hand-roll the law over raw Mantine `Card` and no guard names it — a `SimpleGrid` of card-shaped
|
|
281
|
+
children is invisible to every rule. Grep for `SimpleGrid` before concluding the wave reached you.
|
|
282
|
+
|
|
283
|
+
### Charts — a responsive `height`, and the legend escape 1.30.0 promised
|
|
284
|
+
|
|
285
|
+
Both of these landed in 1.30.0 with no row here, and both were only partly reachable. **Read this
|
|
286
|
+
section against 1.30.1, not 1.30.0** — the § Unreleased entries above are what makes them usable.
|
|
287
|
+
|
|
288
|
+
- **`height` accepts `{ base, sm?, md?, lg? }`** as well as a plain number, resolved against the
|
|
289
|
+
chart's MEASURED container width (the same width the tier reads), not the viewport. In 1.30.0 the
|
|
290
|
+
widened type reached `ChartFrame` alone, while `CartesianChart` and all nine kinds still declared
|
|
291
|
+
`height?: number` and merely spread it through — and `basalt/hand-rolled-plot` forbids composing
|
|
292
|
+
`ChartFrame` directly for a single-plot cartesian chart, so the feature was unreachable from any
|
|
293
|
+
compliant call site. It was type-only: the object always resolved correctly at runtime.
|
|
294
|
+
- **An explicit `legend={{ maxRows: n }}` beats the phone tier's default cap.** The tier keys on the
|
|
295
|
+
measured box, so a 380px inspector panel on a 1440px desktop IS "a phone" and its two-row default
|
|
296
|
+
rolled a six-series legend up to `+4 more` with no way to say no. `margin` and `xLabelRotate`
|
|
297
|
+
always had that escape; the legend did not. Under `fill` a measured cap still applies on top — see
|
|
298
|
+
§ Unreleased for what that actually costs, because "can still roll up FURTHER" understated it.
|
|
299
|
+
|
|
300
|
+
### Toolchain — the shipped lefthook preset's `check-theme`, and what it does NOT fix
|
|
301
|
+
|
|
302
|
+
**The one item in this release with a known-broken incumbent state, and it had no row at all.** The
|
|
303
|
+
preset is `extends:`-ed, which means it auto-updates unread — so if you hand-patched around the
|
|
304
|
+
1.29.0 breakage, nothing else will tell you to re-examine that patch.
|
|
305
|
+
|
|
306
|
+
**What broke in 1.29.0** (two things, in one release): the CLI resolver stopped ascending to a
|
|
307
|
+
config-carrying ancestor — since then it is `BASALT_CWD`, else the invocation cwd, and it infers
|
|
308
|
+
nothing — while the preset's default bin was `bunx --no-install basalt-ui`, which resolves to
|
|
309
|
+
nothing at a repo root under bun's isolated linker. Lefthook runs commands at the REPO ROOT, so for
|
|
310
|
+
every consumer whose `basalt` config is not at the repo root the hook was a hard `0 files scanned`
|
|
311
|
+
failure. (It keys on where the **config** lives, not where the app lives: an app at `apps/web` with
|
|
312
|
+
its `basalt` block, manifest and `lefthook.yml` at the root was never affected.)
|
|
313
|
+
|
|
314
|
+
**What 1.30.0 fixes is ONE of the two.** The default bin is now
|
|
315
|
+
`${BASALT_BIN:-${BASALT_CWD:+$BASALT_CWD/}node_modules/.bin/basalt-ui}` — the local bin of the
|
|
316
|
+
package the guard runs FOR, failing with the path it looked for instead of a resolver error — and
|
|
317
|
+
the preset gained `root:` in its list of merging seams plus a monorepo recipe at the bottom of the
|
|
318
|
+
file. **The resolver is untouched.** So:
|
|
319
|
+
|
|
320
|
+
- **If you set `BASALT_CWD`, keep it.** Deleting the `env:` block is the natural reading of "the
|
|
321
|
+
preset's `check-theme` was fixed" and it silently un-gates the hook again: with no `BASALT_CWD`
|
|
322
|
+
the guard is back to scanning a config-less root. Only the `BASALT_BIN` half may now be redundant.
|
|
323
|
+
- **If you set `BASALT_BIN` alone**, it still wins outright and nothing changes.
|
|
324
|
+
- **If you spell the commands out with `root: '<pkg>/'`** rather than extending the preset — the
|
|
325
|
+
sanctioned monorepo recipe — the new default never runs and this is a no-op for you.
|
|
326
|
+
- **`basalt-ui doctor` reads the MERGED config** (`lefthook dump`), so it is what tells you whether
|
|
327
|
+
the result actually gates. The file's own comments are accurate; check against them, not against
|
|
328
|
+
a summary.
|
|
329
|
+
|
|
330
|
+
### Guards — four rules promote to `error`, and the C1 pair does not
|
|
331
|
+
|
|
332
|
+
Every one of these six entries said in its own `why` that this release "is when the incumbent count
|
|
333
|
+
across argo, linewatch, image-share, rb, image-gen and the playground is re-measured" (law C16).
|
|
334
|
+
That measurement was taken 2026-09-09, with all seven consumers freshly migrated to 1.29.2 and
|
|
335
|
+
basalt built from `master`, by running `npx oxlint .` in each repo and counting by rule id — a
|
|
336
|
+
method that failed open in one of them, see the correction under the table:
|
|
337
|
+
|
|
338
|
+
| Rule | argo | image-gen | image-share | rb | linewatch | meteo | obsidian | playground | Verdict |
|
|
339
|
+
| ----------------------------------------------------------------- | ---- | --------- | ----------- | --- | --------- | ----- | -------- | ---------- | ------------ |
|
|
340
|
+
| `basalt/provider-above-router` | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | → `error` |
|
|
341
|
+
| `basalt/forms-field-key` | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | → `error` |
|
|
342
|
+
| `basalt/query-fn-unwrap` | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | → `error` |
|
|
343
|
+
| `basalt/bound-control-outside-home` | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | → `error` |
|
|
344
|
+
| `basalt/control-outside-home` + `raw-selection-control` (one law) | 2 | 10 | 6 | 8 | 0 | 5 | 0 | 3 | stays `warn` |
|
|
345
|
+
|
|
346
|
+
**Corrected at 1.30.1: meteo is 5, not 0 — the fleet total is 34, not 29.** The zero published at
|
|
347
|
+
1.30.0 was an artifact: meteo has no local oxlint binary, so `npx --no-install oxlint` failed there
|
|
348
|
+
with a package error and the counting pipeline read that failure as "no findings" — the same
|
|
349
|
+
silent-zero shape this file already warns about for `check-theme`'s fabricated roots, arrived at from the other side. The
|
|
350
|
+
five are real (`display-controls.tsx:258`, `stack-editor.tsx:301/323/337`, `forecast-tab.tsx:151`),
|
|
351
|
+
and re-measuring with a guaranteed-present binary is what found them. **The DECISION is unchanged**:
|
|
352
|
+
all four promoted rules re-measured at zero in every repo including meteo, so those four promotions
|
|
353
|
+
stand exactly as shipped — only the extended pair's count moved. A count taken through a repo's own
|
|
354
|
+
`lint` script rather than a bare oxlint invocation is not a measurement; meteo's `lint` is
|
|
355
|
+
`basalt-ui check-theme` alone and never runs oxlint at all.
|
|
356
|
+
|
|
357
|
+
**The four promotions cost nothing to take.** They were measured at zero, in every consumer, before
|
|
358
|
+
being flipped — which is the entire point of a grace window: it is a period in which the count is
|
|
359
|
+
allowed to fall to zero on its own, not a timer that fires regardless. If one of them fires in your
|
|
360
|
+
app, it is finding something none of the fleet had, and the message names the remedy.
|
|
361
|
+
|
|
362
|
+
**The C1 pair is extended to 1.31.0, and the reason is not the count.** The argument is that
|
|
363
|
+
**until this release, one class of consumer had no reachable home at all.** Every home both messages
|
|
364
|
+
listed was a shell slot — `PageBar` / `Section` / `WidgetHeader` — so an app mounting
|
|
365
|
+
`BasaltProvider` and no `BasaltShell` was being told to move a control somewhere it could not
|
|
366
|
+
render one. This release adds the missing sentence to both messages: a `PageBar` outside a shell
|
|
367
|
+
renders in flow, sticky, with its own `title`, which is what `PageBar` has always actually done.
|
|
368
|
+
Promoting a rule to `error` in the same minor that first makes it satisfiable hands you zero minors
|
|
369
|
+
to act on the answer.
|
|
370
|
+
|
|
371
|
+
**What that sentence does and does not buy, measured rather than assumed** — the 1.30.0 revision of
|
|
372
|
+
this paragraph rested the extension on "image-gen is a Tauri window with a hand-rolled header and
|
|
373
|
+
carries 10 of the 29 by itself" (the count as
|
|
374
|
+
published), implying a shell-less `PageBar` addresses those 10. It addresses
|
|
375
|
+
**zero** of them. image-gen's header `SegmentedControl` was already waived and is not in the count;
|
|
376
|
+
the real 10 are labelled `Select`/`SegmentedControl` form fields, a `Chip.Group` role tagger and a
|
|
377
|
+
canvas preview-mode switch. The remedy that moves that number is **`basalt-ui/forms`** — `FormRow`
|
|
378
|
+
and `FormGroup` both silence the rule, verified against a bare labelled `Select` — and adopting it
|
|
379
|
+
is a nine-site change that moves each label off the input onto the row. The extension still stands;
|
|
380
|
+
the reason is this:
|
|
381
|
+
|
|
382
|
+
- **A shell-less SINGLE-page app is closed by `PageBar`** — one page, so every control is a page
|
|
383
|
+
control and row 2 is the honest home. That is the whole of linewatch.
|
|
384
|
+
- **A shell-less app whose switcher is primary NAVIGATION is not.** `PageBar`'s `tabs` models a
|
|
385
|
+
page's sub-views; basalt models top-level nav only through `BasaltShell`, and `BasaltShell` is
|
|
386
|
+
all-or-nothing — sidebar plus header plus scrollport — which a 100vh Tauri window with per-view
|
|
387
|
+
keep-alive scrollports cannot take. The missing primitive there is a shell-less top-level nav,
|
|
388
|
+
not a page bar.
|
|
389
|
+
- **A shell-FUL app was never blocked on reachability at all**, and 14 of the 34 are in one (rb and
|
|
390
|
+
image-share both mount `BasaltShell`). Their blocker is law C1's other half: `PageBar.filters`
|
|
391
|
+
accepts `FilterSet` descendants only, so the honest fix is porting hand-rolled `value`/`onChange`
|
|
392
|
+
clusters onto bound `FieldHandle`s plus a `createSearchStore` — a page-shape change per page, and
|
|
393
|
+
the 1.30.0 sentence made it no cheaper.
|
|
394
|
+
|
|
395
|
+
**1.31.0 is when the 34 are re-measured — with a bare oxlint invocation in every repo, and split by
|
|
396
|
+
whether the app mounts a shell.** The raw count is the wrong number to promote on; "how many are in
|
|
397
|
+
an app that already had a home" is the one that says what an `error` would cost.
|
|
398
|
+
|
|
399
|
+
Riding along, and the reason a single waiver now works: `control-outside-home` (the AST lane) and
|
|
400
|
+
`raw-selection-control` (the text lane) are one law with two rule ids, and until now
|
|
401
|
+
`theme-allow control-outside-home` parsed in the text lane and waived nothing there. A waiver naming
|
|
402
|
+
either id now covers both, at line scope and at `theme-allow-file` scope, in both directions.
|
|
403
|
+
|
|
404
|
+
## 1.29.2 — `unwrap` over a union-typed Eden response
|
|
405
|
+
|
|
406
|
+
A patch, one export's TYPE only. Nothing renamed, nothing removed.
|
|
43
407
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
408
|
+
- **`unwrap` strips `null`/`undefined` from a union-typed Eden response again** — the single
|
|
409
|
+
conditional signature 1.29.1 landed still inferred `TData` off `Envelope<infer TData>`, and an Eden
|
|
410
|
+
Treaty response is a UNION of envelopes (`{ data: T; error: null } | { data: null; error:
|
|
411
|
+
EdenFetchError }`), not one envelope with a nullable `data`; inferring through the wrapper type
|
|
412
|
+
read `T | null` at every Eden call site. `UnwrapResult` now infers the raw `data` field and wraps
|
|
413
|
+
it in `NonNullable`, which distributes over a union `R` — `unwrap(await api.x.get())` resolves to
|
|
414
|
+
`T` again, with no `| null`. Runtime unchanged.
|
|
48
415
|
|
|
49
|
-
|
|
416
|
+
## 1.29.1 — the DevDock router prop, `unwrap` as a callback, one retired guard
|
|
417
|
+
|
|
418
|
+
A patch. Two type corrections to exports 1.29.0 introduced, and one lint rule retired
|
|
419
|
+
because 1.29.0 made its premise unreachable — see § Guards below.
|
|
50
420
|
|
|
51
421
|
- **`BasaltDevDock`'s `router` prop now accepts a real TanStack `Router` instance with no cast** —
|
|
52
422
|
the prior structural stand-in rejected a class instance under TS weak-type detection (no property
|
|
53
423
|
overlap, no index signature), forcing `router={router as never}`. It is now `router?: object`:
|
|
54
424
|
any non-null object is assignable, and the devtools panel remains the only reader.
|
|
55
425
|
- **`unwrap` collapses to ONE generic signature with a conditional return**, replacing the two
|
|
56
|
-
overloads
|
|
426
|
+
overloads 1.29.0 introduced. The overload pair resolved `unwrap` to `unknown` whenever it
|
|
57
427
|
was passed as a bare callback (`api.threads.get().then(unwrap)`) — `.then()` reads the declared
|
|
58
428
|
type of the reference rather than applying an overload, and TS cannot propagate generic inference
|
|
59
429
|
across an overload set the way it can for a single signature. `.then(unwrap)` now infers `TData`
|
|
60
430
|
again, alongside `unwrap(await p)` and `unwrap(p)` — nothing about the runtime behaviour changed.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
431
|
+
|
|
432
|
+
### Guards — `basalt/query-dual-import` removed
|
|
433
|
+
|
|
434
|
+
**Retired, not merely relaxed** — its `RETIRED_RULE_IDS` entry (kept recognized, so an existing
|
|
435
|
+
`theme-allow query-dual-import` still reads as a dead waiver rather than a typo) and its
|
|
436
|
+
`PLUGIN_RULE_GRACE` ledger row are both deleted. Its whole premise — a raw `@tanstack/react-query`
|
|
437
|
+
import beside `basalt-ui/query` — went unreachable the moment `./query` dropped (see § Consolidation
|
|
438
|
+
in 1.29.0 below): a consumer now MUST import `@tanstack/react-query` directly, and the rule warned on every
|
|
439
|
+
one of those imports instead of the drift it was written to catch (90+ false positives on a single
|
|
440
|
+
consumer). `query-fn-unwrap` is unaffected — it is a different rule, still `warn` (grace → 1.30.0).
|
|
441
|
+
|
|
442
|
+
## 1.29.0 — the consolidation: three subpaths dropped, every shim deleted, the CLI narrowed
|
|
443
|
+
|
|
444
|
+
**The removal minor.** Three published subpaths dropped, every `@deprecated` shim in the package
|
|
445
|
+
deleted at once, and the CLI narrowed to what a consumer actually runs — plus one additive wave (C5)
|
|
446
|
+
whose whole point is code you get to delete. Two of the three most expensive findings in the 1.25.0
|
|
447
|
+
→ 1.29.2 fleet migration came out of this minor and neither had a row of its own: the CLI resolver
|
|
448
|
+
change (§ CLI below) and the `field` deletion breaking this file's own `--fix` recipe
|
|
449
|
+
(§ Consolidation below). Both have one now.
|
|
450
|
+
|
|
451
|
+
### Consolidation — three subpaths dropped, every `@deprecated` shim deleted
|
|
452
|
+
|
|
453
|
+
**Dropped subpaths — three, all folded into `.`/`./provider`, none deleted outright.**
|
|
454
|
+
|
|
455
|
+
<!-- C1 symbol list -->
|
|
456
|
+
|
|
457
|
+
- `./query` — dropped. `createBasaltQueryClient`, `unwrap`, `BasaltQueryDevtools`,
|
|
458
|
+
`toErrorMessage`, `errorStatus` now on `.` (root barrel) — import from `basalt-ui` instead of
|
|
459
|
+
`basalt-ui/query`. The nine raw TanStack re-exports it carried (`QueryClientProvider`,
|
|
460
|
+
`QueryErrorResetBoundary`, `useQueryErrorResetBoundary`, `useQuery`, `useSuspenseQuery`,
|
|
461
|
+
`useMutation`, `useQueryClient`, `useInfiniteQuery`, `queryOptions`) are gone from basalt entirely —
|
|
462
|
+
import them from `@tanstack/react-query`, which the root entry already requires.
|
|
463
|
+
- `./data/table` — **narrowed, not dropped**: `BasaltDataTable`, its types and `createColumnHelper`
|
|
464
|
+
stay; the TanStack pass-throughs (`useReactTable`, `flexRender`, `getCoreRowModel`,
|
|
465
|
+
`getSortedRowModel`, `getFilteredRowModel`, `getPaginationRowModel`, `ColumnDef`, `SortingState`,
|
|
466
|
+
`ColumnHelper`, `PaginationState`, `ColumnPinningState`, `RowSelectionState`, `ColumnFiltersState`)
|
|
467
|
+
are imported from `@tanstack/react-table` directly.
|
|
468
|
+
- `./connectivity` — dropped. `ConnectivityProvider` (auto-mounted by `BasaltProvider`, no
|
|
469
|
+
consumer-side import needed), `useConnectivity`, `ConnectivityIndicator` now on `.`.
|
|
470
|
+
- `./data` (bare) — dropped. It never carried its own exports beyond re-exporting `./data/table`
|
|
471
|
+
and `./data/virtual` — import the narrow subpath you need directly; nothing renamed.
|
|
472
|
+
- `./controls-dates` — **unaffected**, stays a separate subpath (inlining it into `./controls`
|
|
473
|
+
would pull `@mantine/dates` into a subpath that must resolve without it).
|
|
474
|
+
|
|
475
|
+
**Deleted `@deprecated` shims** (every one slated for 1.29.0 removal in an earlier minor's
|
|
476
|
+
deprecation row — see that minor's own `MIGRATING.md` section for the original `@deprecated`
|
|
477
|
+
annotation and rationale):
|
|
478
|
+
|
|
479
|
+
- `state.ts`'s legacy connectivity export — use `./provider`'s `useConnectivity` instead.
|
|
480
|
+
- `BasaltProvider`'s flat `sseUrl` / `healthUrl` / `healthIntervalMs` props — use
|
|
481
|
+
`connectivity={{ sseUrl, healthUrl, healthIntervalMs }}`.
|
|
482
|
+
- `QueryStateVariant` and the `variant` prop on `QueryState`/`LoadingState`/`ErrorState`/`EmptyState`
|
|
483
|
+
— use `QueryStateTier` and `tier`. **Identical values, identical branch behaviour**, verified by
|
|
484
|
+
diffing `src/dashboard/query-state.tsx` at `v1.25.0` against 1.29.2: the union is `'page' |
|
|
485
|
+
'section'` in both, and `'section'` takes the same bare/compact branch in both. A pure rename, so a
|
|
486
|
+
`variant="section"` becomes `tier="section"` with nothing to eyeball afterwards.
|
|
487
|
+
`QueryStateTier`'s own JSDoc says "`'section'` meant two different things across the two
|
|
488
|
+
spellings" — that is the NAMING rationale for picking `tier` (audit B #19), not a behaviour
|
|
489
|
+
warning, and it has been read as one twice: once by a fleet audit that told consumers to "eyeball
|
|
490
|
+
the rendered tier once after the change", once by a migration agent that stopped to diff the
|
|
491
|
+
sources. It does not describe a value-semantics change.
|
|
492
|
+
- `createSearchParamStore` / `createMultiSearchParamStore` (`basalt-ui/router-tanstack`) and the
|
|
493
|
+
legacy branches they kept alive in the field vocabulary — use `createSearchStore` with
|
|
494
|
+
`field.enum` / `field.multi`.
|
|
495
|
+
- `field` on `basalt-ui/forms` — use `inputProps(form, path)` + `fieldKey(form, path)`.
|
|
496
|
+
**The two-pass `oxlint --fix` recipe in 1.28.0's § `inputProps` DOES NOT WORK if you land here.**
|
|
497
|
+
Pass 1 (`basalt/deprecated-export` autofixing `field` → `inputProps as field`) never fires: the
|
|
498
|
+
row leaves `DEPRECATED_EXPORTS` in the same minor the export does, so at 1.29.x there is nothing
|
|
499
|
+
deprecated to nudge. Pass 2 (`basalt/forms-field-key`) reports the `field(` spread but does not
|
|
500
|
+
autofix it — its fixer only handles an `inputProps(` spread missing a `key`. So: **coming from
|
|
501
|
+
1.28.x the recipe holds; coming from 1.27.x or earlier, every call site is a hand edit.** It is
|
|
502
|
+
two edits per field, and `field` at least fails to compile, unlike the `key` half:
|
|
503
|
+
|
|
504
|
+
```tsx
|
|
505
|
+
/* 1.27 */ <TextInput {...field(form, 'email')} />
|
|
506
|
+
/* 1.29 */ <TextInput key={fieldKey(form, 'email')} {...inputProps(form, 'email')} />
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
Find them all: `rg -n '\bfield\(\s*\w*[Ff]orm\b' --type ts` (`--type ts` covers `.tsx`).
|
|
510
|
+
|
|
511
|
+
- `BasaltNotifications` / `BasaltNotificationsProps` (`basalt-ui/notifications`) — mount
|
|
512
|
+
`<BasaltOverlays notifications />` from `basalt-ui/commands` instead. **Doctrine exception,
|
|
513
|
+
recorded**: this one never carried a `deprecated-export` row; it was superseded in 1.2x and its own
|
|
514
|
+
JSDoc warned against mounting it next to `BasaltOverlays`, so it leaves in the same minor as the
|
|
515
|
+
other shims. The `duplicate-notifications-mount` lint rule that only existed to catch the
|
|
516
|
+
double-mount is retired with it.
|
|
517
|
+
- `ConnectivityIndicatorProps` stays on the root barrel (it is `BasaltProps`).
|
|
518
|
+
|
|
519
|
+
- `ZonedLine`/`Bars`' `ZoneSpec` aliases — use the canonical `AxisConfig`-scoped zone type each
|
|
520
|
+
kind now exports directly.
|
|
521
|
+
- `inputProps`'s `key` return field (already shape-changed in 1.28.0 — see § `inputProps` no longer
|
|
522
|
+
returns `key` there) — fully removed this minor; use `fieldKey(form, name)` alongside
|
|
523
|
+
`inputProps(form, name)`.
|
|
524
|
+
|
|
525
|
+
**On notice for 1.30.0** — a public export with exactly one consumer (the playground) as of this
|
|
526
|
+
audit. Adopt-or-delete: gains a real consumer by 1.30.0 or is removed. Not a promise either way;
|
|
527
|
+
record your adoption in `docs/ARGO-MIGRATION-LEARNINGS.md` if you pick one up.
|
|
528
|
+
|
|
529
|
+
- **Threads/agent-chat composite tier**: `ThreadWorkspace`, `ThreadFeed`, `ThreadFeedRow`,
|
|
530
|
+
`ThreadDetailPanel`, `ThreadOutcomeCard`, `ThreadsStoreAdapter`, `createThreadsStore`,
|
|
531
|
+
`createAdapterThreadsStore`, `threadsStoreAdapterContract`, `threadPartRenderers`,
|
|
532
|
+
`definePartRenderers`.
|
|
533
|
+
- **Chart kind**: `Heatmap` only — `DualPanel` has an argo consumer and `MirroredBars`/`BandStrip`
|
|
534
|
+
are consumed by linewatch, so those three are NOT on notice.
|
|
535
|
+
- **Nav/shell**: `PageAside`, `PanelRow`, `SettingsRow` (playground-only as of this audit).
|
|
536
|
+
- **Controls**: `SliderControl`, `SearchFilter`, `ToggleFilter`, `CompareFilter`, `OverflowMenu`,
|
|
537
|
+
`ControlGroup`.
|
|
538
|
+
|
|
539
|
+
**The common primitives, as a unit.** `src/common` (shipped from the maturation round) is now the
|
|
540
|
+
one place the base prop / error / ref vocabulary lives: `BasaltProps`, `SlotStylesProps`, `cx`,
|
|
541
|
+
`mergeRefs`/`assignRef`, the prefixed `errors.ts` table (`toErrorMessage`/`errorStatus`),
|
|
542
|
+
`useValidateProps`, `assertRequiredProps`. Every basalt component extends `BasaltProps` for its own
|
|
543
|
+
prop type rather than redeclaring `className`/`style`; a consumer composing over a basalt component
|
|
544
|
+
should do the same.
|
|
545
|
+
|
|
546
|
+
### Additions — what C5 lets a consumer delete
|
|
547
|
+
|
|
548
|
+
C5 consolidation — every item below names the argo lines it lets a consumer delete
|
|
549
|
+
(`.claude/maturation/consolidation-plan.md` §C5, `audit-f-argo-consumer.md`). All additive; nothing
|
|
550
|
+
in this block removes or renames an existing export. **Additive is not the same as ignorable**: two
|
|
551
|
+
of these REPLACE code a consumer is already hand-rolling — see § `basalt-ui/format` below, and
|
|
552
|
+
`PageTitle`, which is the answer to the `in-body-page-title` waiver a shell-less page needed.
|
|
553
|
+
|
|
554
|
+
- **`basalt-ui/format`** — new subpath, Mantine-free and React-free: `money`, `percent`, `integer`,
|
|
555
|
+
`compact`, `deltaPct`, `duration` (seconds → `"1h 02m"`; `{ unit: 'minutes' }` for a
|
|
556
|
+
minutes-based input), `durationClock` (`"1:02:03"`), `clock`, `relativeTime`, `weekday`, `km`,
|
|
557
|
+
`kcal` — plus the original chart-formatter names (`fmtCompact`/`fmtPercent`/`fmtCurrency`/
|
|
558
|
+
`fmtInt`/`fmtAxisDate`/`fmtTooltipDate`/`NON_FINITE`/`formatters`), now re-exported from here;
|
|
559
|
+
`./charts`' `utils/format.ts` is an internal re-export shim, not a second implementation.
|
|
560
|
+
- **`ChartState.empty`** widens to `boolean | string` — a string is both truthy and the label
|
|
561
|
+
`ChartEmpty` paints. `ChartCard` gains `state?: ChartState`, `placeholderHeight?: number`
|
|
562
|
+
(default 240) and `stateAction?: ReactNode`, and `children` is now optional so a title-less
|
|
563
|
+
pending card can be a route's own `<Suspense fallback>`. Resolution order is the same
|
|
564
|
+
`resolveChartState` precedence every kind already has — pending → error → empty — and
|
|
565
|
+
`state={{}}`/omitting `state` renders `children` unchanged. `ChartCenter`'s `width` widens to
|
|
566
|
+
`number | string` (`'100%'` for a non-measured block body).
|
|
567
|
+
- **`DualPanel`'s `fillBetween` gains `aboveFill?: string`** — the above-side fill color; omitted,
|
|
568
|
+
both sides keep sharing `fill` (unchanged behaviour). **`AxisBottomNumeric`** exported from
|
|
569
|
+
`./charts` — the numeric twin of `AxisLeftNumeric`, for a bespoke continuous-x plot.
|
|
570
|
+
- **`useBreakpoint(name, edge?, options?)`** on the root barrel — SSR/hydration-safe media query
|
|
571
|
+
over `theme.breakpoints`, built on `useSyncExternalStore` (not `@mantine/hooks`' `useMediaQuery`,
|
|
572
|
+
which mismatches during hydration). `edge` is `'min'` (default, "at least this wide") or `'max'`.
|
|
573
|
+
`page-aside.tsx` now composes this hook's own internals instead of a private duplicate.
|
|
574
|
+
- **`--vx-space-touch-target` / `VX.spaceTouchTarget`** — the WCAG touch-target floor (44px),
|
|
575
|
+
density-exempt. The one `SPACE_FIXED` member that IS emitted as a CSS var (every other one stays
|
|
576
|
+
JS-only) — a consumer's own `@media (pointer: coarse)` CSS-module rule reads it directly.
|
|
577
|
+
- **`SettingsMenuItem.active?: boolean`** — renders a trailing check glyph and `aria-current` in
|
|
578
|
+
every projection (the sidebar's flat rows and gear menu, the mobile More sheet).
|
|
579
|
+
- **`basalt-ui/commands` gains imperative shell handles**: `setColorScheme(scheme)` and
|
|
580
|
+
`toggleSidebar()`, wired automatically — `BasaltProvider` registers the Mantine color-scheme
|
|
581
|
+
setter, `BasaltShell` registers the collapse toggle, so a command's `run` calls either with zero
|
|
582
|
+
`__root.tsx` bridge wiring (unlike the hand-rolled `color-scheme-bridge.ts`/`sidebar-bridge.ts`
|
|
583
|
+
shape this seeds from). `Slot<K, Constraint>` (`register.ts`) changed its internal resolution
|
|
584
|
+
from a whole-interface mapped-type match to an indexed-access form — same public behaviour,
|
|
585
|
+
fixes the commands↔overlays same-file type cycle a command calling `overlays.open(...)` used to
|
|
586
|
+
hit while `defineCommands`'s own type was still being inferred.
|
|
587
|
+
- **`BasaltDevDock`** on the root barrel — a fixed, dev-only bottom drawer hosting TanStack Router
|
|
588
|
+
devtools, TanStack Query devtools and the theme lab, each peer lazy-loaded. New optional peer:
|
|
589
|
+
`@tanstack/react-router-devtools`.
|
|
590
|
+
- **`BasaltErrorBoundaryProps`** is now exported; `onError` is optional (defaults to the same
|
|
591
|
+
console-warn-in-dev fallback `BasaltProvider`'s own top-level `onError` uses); an omitted
|
|
592
|
+
`fallback` now renders a minimal built-in default (a `PageTitle` + reload button) instead of
|
|
593
|
+
`null` — pass `fallback={null}` explicitly for the old swallow-it behaviour. **`PageTitle`** is a
|
|
594
|
+
new shell-less page-title primitive on the root barrel (a plain `<h1>`, not Mantine's `<Title>`,
|
|
595
|
+
so it needs no `in-body-page-title` waiver) — the remedy for the `in-body-page-title` waiver a
|
|
596
|
+
shell-less error/auth page used to need.
|
|
597
|
+
- **`unwrap` gains a second overload** — the original took only a `Promise` of the `{ data, error }`
|
|
598
|
+
envelope; it now also accepts the ALREADY-RESOLVED envelope directly (`unwrap(await api.x.get())`,
|
|
599
|
+
or as a bare `.then(unwrap)` callback), inferring which overload from whether the argument IS a
|
|
600
|
+
`Promise`. The absence guard also now throws on `undefined` data, not just `null`.
|
|
601
|
+
|
|
602
|
+
### CLI — one resolver, and six checks `doctor` stopped running
|
|
603
|
+
|
|
604
|
+
**Read this one if your app is not at the repo root.** A consumer migration reported it costing
|
|
605
|
+
more debugging than any of the five export removals in this minor, and until now it was one clause
|
|
606
|
+
inside a bullet.
|
|
607
|
+
|
|
608
|
+
**The resolver stopped relocating.** Every project-scoped command (`check-theme`, `doctor`, `sync`)
|
|
609
|
+
used to descend two levels and ascend to the nearest ancestor looking for a `basalt` config. It now
|
|
610
|
+
resolves ONE way — `BASALT_CWD` when set, else the invocation cwd — plus a declared `basalt.roots`,
|
|
611
|
+
and infers nothing from siblings or parents (`resolveProjectDir`, `src/cli/index.ts`). That is the
|
|
612
|
+
right call (the ascend existed to paper over `check-theme` FABRICATING `roots: ["src"]`), and it is
|
|
613
|
+
a silent break for every workspace consumer:
|
|
614
|
+
|
|
615
|
+
| Where you run it | Before 1.29.0 | 1.29.0+ |
|
|
616
|
+
| -------------------------------- | ----------------------------------- | ---------------------------------------------------------------- |
|
|
617
|
+
| repo root, config in `apps/web` | relocated to `apps/web` and said so | reads the root, finds no config, **`✖ 0 files scanned`, exit 1** |
|
|
618
|
+
| `apps/web` | unchanged | unchanged |
|
|
619
|
+
| repo root, `BASALT_CWD=apps/web` | unchanged | unchanged — this is now the only way |
|
|
620
|
+
|
|
621
|
+
**The consequence nobody wrote down: the SHIPPED lefthook preset broke itself.** Lefthook runs every
|
|
622
|
+
command at the repo root, so a monorepo consumer who did nothing but bump the version got a
|
|
623
|
+
pre-commit hook that scanned zero files and failed — from a preset they cannot override the `run:`
|
|
624
|
+
of. Its default `BASALT_BIN` fallback (`bunx --no-install basalt-ui`) also resolves to nothing at
|
|
625
|
+
the root under bun's isolated linker, which puts basalt beside the package that depends on it.
|
|
626
|
+
Two fixes, either one enough:
|
|
627
|
+
|
|
628
|
+
```yaml
|
|
629
|
+
# <repo>/lefthook.yml — the one line that fixes the guard
|
|
630
|
+
pre-commit:
|
|
631
|
+
commands:
|
|
632
|
+
check-theme:
|
|
633
|
+
env: { BASALT_CWD: apps/web }
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
or run the whole hook inside the package (`root: 'apps/web/'` per command — it merges, the preset
|
|
637
|
+
does not define it). `configs/lefthook.yml` now carries both as a MONOREPO RECIPE block and derives
|
|
638
|
+
its default bin from `BASALT_CWD`; a consumer on 1.29.x writes the `env:` block by hand.
|
|
639
|
+
|
|
640
|
+
**`doctor` lost six of its ten checks.** At 1.25.0 it printed ten lines; it prints four. Verified
|
|
641
|
+
against the shipped `dist/cli/doctor.js` — the code is deleted, not skipped, so nothing reports a
|
|
642
|
+
skip either:
|
|
643
|
+
|
|
644
|
+
| Check | 1.25.0 | 1.29.x | What you lose |
|
|
645
|
+
| ------------------------------------------------------------ | -------------- | ----------------- | -------------------------------------------------------------------------------- |
|
|
646
|
+
| `.basalt/manifest.json` exists | ✓ | ✓ | — |
|
|
647
|
+
| `basalt-ui` resolves from here, and at which version | ✓ | **gone** | the "you are silently running a `bunx`-downloaded copy" answer |
|
|
648
|
+
| installed `basalt-ui` matches the manifest's `basaltVersion` | ✓ | **gone** | the install/manifest skew a CI `doctor` step existed to catch — now a no-op |
|
|
649
|
+
| spacing scale matches the last `sync` | ✓ | **gone** | the token layer moving under a consumer between minors |
|
|
650
|
+
| running CLI version matches the installed package | ✓ | **gone** | the 1.20.0-cache-against-a-1.22.0-pin failure `basaltBinCommand` was written for |
|
|
651
|
+
| `guard-scan`: `basalt.roots` resolves to > 0 files | ✓ | **gone** | **the check that made a silently-scanning-nothing `check-theme` impossible** |
|
|
652
|
+
| `basaltAppPlugin({ icons })` files exist | ✓ | **gone** | a manifest referencing icons that were never added |
|
|
653
|
+
| oxlint preset extended | ✓ | ✓ | — |
|
|
654
|
+
| lefthook preset wired | ✓ | ✓ | — |
|
|
655
|
+
| `ai` major parity | workspace-wide | this package only | skew between two workspace packages |
|
|
656
|
+
|
|
657
|
+
The `guard-scan` row and the resolver change are the same failure from two directions: 1.29.0
|
|
658
|
+
deleted the ascend that used to find your config AND the check that would have told you it hadn't.
|
|
659
|
+
**If your CI runs `basalt-ui doctor` as a version-skew gate, that step no longer gates** — assert
|
|
660
|
+
the version yourself, or keep `check-theme` (which still fails loudly on zero roots) as the gate.
|
|
661
|
+
|
|
662
|
+
**Two commands are gone**: `info` (it graded a consumer's `vite.config.ts` by parsing it and nobody
|
|
663
|
+
read the output — no replacement) and `check-coverage` (repo-internal, now
|
|
664
|
+
`bun scripts/check-coverage.ts` inside this repo; it never had a consumer meaning). A consumer's own
|
|
665
|
+
docs or CI naming either will fail on an unknown command, which the CLI reports by name.
|
|
666
|
+
|
|
667
|
+
### `basalt-ui/format` — the subpath that deletes your hand-rolled formatters
|
|
668
|
+
|
|
669
|
+
**An addition, but a MIGRATION**, and the reason it gets a row: it does not add a capability, it
|
|
670
|
+
replaces one every consumer already hand-rolled. Nobody greps a migration file for a thing that was
|
|
671
|
+
not taken away, so it is written down here as well as in § Additions.
|
|
672
|
+
|
|
673
|
+
```tsx
|
|
674
|
+
/* before */ <Text>${(cents / 100).toFixed(2)}</Text>
|
|
675
|
+
/* after */ <Text>{money(cents / 100)}</Text> // import { money } from 'basalt-ui/format'
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
`money`, `percent`, `integer`, `compact`, `deltaPct`, `duration`, `durationClock`, `clock`,
|
|
679
|
+
`relativeTime`, `weekday`, `km`, `kcal` — all `Intl`-backed with an explicit `locale` defaulting to
|
|
680
|
+
the runtime's, all returning an em dash for non-finite input instead of `NaN`/`∞`. Mantine-free and
|
|
681
|
+
React-free, so a chart axis, a table cell and a server-rendered string share one implementation. The
|
|
682
|
+
chart formatters (`fmtCompact`/`fmtPercent`/`fmtCurrency`/`fmtInt`/`fmtAxisDate`/`fmtTooltipDate`)
|
|
683
|
+
are re-exported from here and `./charts`' `utils/format.ts` is a shim over them, not a second copy —
|
|
684
|
+
so there is no "which one do I import" question to get wrong.
|
|
685
|
+
|
|
686
|
+
Find your call sites: `rg -n 'toFixed\(|toLocaleString\(|Intl\.NumberFormat' --type ts`
|
|
687
|
+
|
|
688
|
+
## 1.28.0 — the aside, the shell scrollport, the form layer, and `inputProps` without `key`
|
|
689
|
+
|
|
690
|
+
**Nothing removed, and ONE thing changed shape: `inputProps` no longer returns `key`** — a silent
|
|
691
|
+
behaviour change at every field call site, caught by the new `basalt/forms-field-key` and described
|
|
692
|
+
in its own § below. Read that one before upgrading; the rest of the export delta is additive. Six
|
|
693
|
+
new oxlint rule ids ship at `warn`; **nothing is promoted to `error` in this minor** — the five
|
|
694
|
+
oxlint rules and one guard kind that went `error` were 1.27.0's, see § Guards there. Every level
|
|
695
|
+
honours the `theme-allow` grammar unchanged.
|
|
696
|
+
|
|
697
|
+
**And one silent DELETION that no compiler, linter or test reports: the brand left the sidebar.**
|
|
698
|
+
If your app suppresses the header region, it also lost its brand and its sidebar collapse toggle the
|
|
699
|
+
moment it bumped — see § Shell — the brand moved into the header (below).
|
|
700
|
+
|
|
701
|
+
**One behaviour change with a blast radius past this package: `AppShell.Main` is now the
|
|
702
|
+
scrollport, and the WINDOW no longer scrolls inside a `BasaltShell`.** Nothing was removed or
|
|
703
|
+
renamed, but four things a consumer may be doing stop working, and all four have a one-line remedy —
|
|
704
|
+
see § The shell scrollport below.
|
|
705
|
+
|
|
706
|
+
What is new, one line each — nothing here renames or removes anything:
|
|
707
|
+
|
|
68
708
|
- **`scrollParentOf` / `SCROLLPORT_ATTRIBUTE`** (`basalt-ui`) — resolve which box actually scrolls an
|
|
69
709
|
element (`null` = the document). The seam behind the shell scrollport change below.
|
|
70
710
|
- **`PageAside`** (`basalt-ui`) — the right-hand aside REGION a route claims, with the panel filter
|
|
@@ -169,7 +809,9 @@ EdenFetchError }`), not one envelope with a nullable `data`; inferring through t
|
|
|
169
809
|
flattened `BasaltProvider` connectivity props. Permanently `warn` — a deprecation is a schedule,
|
|
170
810
|
not a defect. **On the `field` row, take the two fixes in order**: that rename lands the new
|
|
171
811
|
`inputProps` shape, which has no `key`, and `basalt/forms-field-key` then reports the same line
|
|
172
|
-
and inserts the `key`. Two `--fix` passes, or one manual edit writing both.
|
|
812
|
+
and inserts the `key`. Two `--fix` passes, or one manual edit writing both. **This works only
|
|
813
|
+
while `field` is still deprecated-but-shipped, i.e. on 1.28.x** — 1.29.0 deletes both the export
|
|
814
|
+
and its ledger row, and with them pass 1.
|
|
173
815
|
- **Dev-only duplicate-mount warnings** — `BasaltProvider` now warns when a second instance mounts
|
|
174
816
|
while the first is still mounted; `<BasaltOverlays notifications />` and `<BasaltNotifications />`
|
|
175
817
|
now warn when BOTH are mounted at once (previously prose-only in both cases). Both are
|
|
@@ -371,7 +1013,14 @@ mode: a form that stops resetting, with no signal anywhere.
|
|
|
371
1013
|
reports every `{...inputProps(…)}` spread whose element carries no sibling `key`, and it
|
|
372
1014
|
**autofixes** — inserting `key={fieldKey(<same args>)}` and adding `fieldKey` to the existing
|
|
373
1015
|
`basalt-ui/forms` import. Run `oxlint --fix` over the app and read the diff; the escape hatch for an
|
|
374
|
-
element genuinely remounted by its parent is `theme-allow forms-field-key — <why>`.
|
|
1016
|
+
element genuinely remounted by its parent is `theme-allow forms-field-key — <why>`. **Scope the
|
|
1017
|
+
`--fix` to the paths you are migrating** (`oxlint --fix src/features/settings`): the shipped preset
|
|
1018
|
+
enables autofixable stylistic rules, so a bare `oxlint --fix .` rewrites untouched files in the same
|
|
1019
|
+
commit.
|
|
1020
|
+
|
|
1021
|
+
> **If you are landing on 1.29.x, not 1.28.x, the two-pass recipe below does not apply** — `field`
|
|
1022
|
+
> is deleted there, so pass 1 has nothing to fix and the edit is manual. See 1.29.0 § Consolidation,
|
|
1023
|
+
> the `field` row.
|
|
375
1024
|
|
|
376
1025
|
**The deprecated `field` alias did NOT follow `inputProps` here.** It is no longer
|
|
377
1026
|
`export const field = inputProps`: it keeps the 1.27 return shape, `key` included, so
|
|
@@ -452,9 +1101,9 @@ Two unrelated exports shared the name `field`: the forms adapter's `getInputProp
|
|
|
452
1101
|
the `field.enum/multi/range/number/boolean/string` store-field builder in `basalt-ui/state` and
|
|
453
1102
|
`basalt-ui/router-tanstack`. A page combining a form and a filter store had to alias one on import.
|
|
454
1103
|
|
|
455
|
-
| Removed / renamed | Replacement | Note
|
|
456
|
-
| --------------------------- | ------------ |
|
|
457
|
-
| `field` (`basalt-ui/forms`) | `inputProps` | Same signature — `inputProps(form, path)`. `field` still resolves from `basalt-ui/forms` as a `@deprecated` alias, so nothing breaks;
|
|
1104
|
+
| Removed / renamed | Replacement | Note |
|
|
1105
|
+
| --------------------------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1106
|
+
| `field` (`basalt-ui/forms`) | `inputProps` | Same signature — `inputProps(form, path)`. `field` still resolves from `basalt-ui/forms` as a `@deprecated` alias in 1.28.x, so nothing breaks here; **it is deleted in 1.29.0** (see that minor's § Consolidation, which also explains why the `--fix` recipe stops working there). **In 1.28.0 the two are no longer the same function**: `inputProps` dropped `key` from its return and `field` did not — see § `inputProps` no longer returns `key`. |
|
|
458
1107
|
|
|
459
1108
|
The `field.*` store builder (`basalt-ui/state`, `basalt-ui/router-tanstack`) is untouched.
|
|
460
1109
|
|
|
@@ -500,58 +1149,235 @@ redundant (Mantine's own default).
|
|
|
500
1149
|
hard `0` floor and their marks anchor at the baseline, so a log scale on either yields `NaN`
|
|
501
1150
|
geometry — don't opt in there. Additive; omitted stays linear.
|
|
502
1151
|
|
|
503
|
-
###
|
|
1152
|
+
### `basalt-ui/data` — a typed facet id, and a required `getItemKey`
|
|
504
1153
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
`
|
|
509
|
-
|
|
510
|
-
`
|
|
1154
|
+
**`BasaltDataTable`: a mistyped `facets[].columnId` used to render no pill, silently — `if
|
|
1155
|
+
(!column) return null`.** It now throws in dev, naming the id and every known column id
|
|
1156
|
+
(`facet columnId "…" matches no column`); production still degrades to no pill for that one facet.
|
|
1157
|
+
`DataTableFacet` also gained a type parameter — `DataTableFacet<T = unknown>` — so `columnId` is
|
|
1158
|
+
typed off the row shape wherever TanStack's `accessor`/manual `id` inference allows it
|
|
1159
|
+
(`Extract<keyof T, string> | (string & {})`); the default keeps a bare `DataTableFacet[]`
|
|
1160
|
+
compiling unchanged. `BasaltDataTableProps.facets` is now `DataTableFacet<T>[]`, and
|
|
1161
|
+
`initialColumnPinning` moved from TanStack's own `ColumnPinningState` to a new, `T`-derived
|
|
1162
|
+
`DataTableColumnPinning<T>` (structurally assignable to `ColumnPinningState`, so no cast is
|
|
1163
|
+
needed at either end). Both are additive for existing call sites — an id that was already a real
|
|
1164
|
+
column keeps compiling and rendering exactly as before.
|
|
511
1165
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
annotation over one. That branch was always dead at runtime; the type just stopped pretending.
|
|
518
|
-
- **…and one more, in a consumer's own WRAPPER.** A prop typed `FieldHandle<RangeField<P>>` — `C`
|
|
519
|
-
defaulting to `boolean` — no longer accepts a handle from a `field.range` without `custom: true`,
|
|
520
|
-
because that handle's setter is now `RangeValue<P>`-typed and a setter is contravariant. Pin
|
|
521
|
-
`FieldHandle<RangeField<P, false>>` for the preset-only shape, `RangeField<P, true>` for a
|
|
522
|
-
picker-backed one, or make the wrapper generic over the flag
|
|
523
|
-
(`<P extends string, C extends boolean>` + `FieldHandle<RangeField<P, C>>`), which is what
|
|
524
|
-
`RangeFilter` does. An existing `as FieldHandle<RangeField<P>>` cast keeps compiling.
|
|
1166
|
+
**`BasaltVirtualList.getItemKey` is now required.** The index fallback hid stale rows across a
|
|
1167
|
+
mutation: an insert/delete shifts every index below it, so the OLD item kept rendering at each
|
|
1168
|
+
shifted index until an unrelated re-render happened to catch it up — in the one component whose
|
|
1169
|
+
job is a long mutable list. Pass a real per-item key (`(item) => item.id`); there is no
|
|
1170
|
+
replacement for the index fallback.
|
|
525
1171
|
|
|
526
|
-
**`
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
`
|
|
530
|
-
|
|
1172
|
+
**`BasaltVirtualList` gained a ref.** `ref?: Ref<BasaltVirtualListHandle>` exposes
|
|
1173
|
+
`scrollToIndex`/`scrollToOffset`/`scrollToEnd` (delegating to the internal
|
|
1174
|
+
`@tanstack/react-virtual` instance) plus `getVirtualizer()` as the full escape hatch — mirrors
|
|
1175
|
+
`./data/table`'s `useReactTable` re-export as the table's own. Additive; omitting `ref` changes
|
|
1176
|
+
nothing.
|
|
531
1177
|
|
|
532
|
-
|
|
533
|
-
the switch is deleted or the cast just moves: the field now carries its resolver keys (`RangeField`'s
|
|
534
|
-
fifth type parameter, inferred from `window`), and `toWindow` returns
|
|
535
|
-
`{ window: Exclude<P, W> } | { from; to }`. A `resolveWindow` whose target is
|
|
536
|
-
`{ window: '7d' | '30d' | '90d' | 'all' } | { from; to }` becomes a one-line delegation with no
|
|
537
|
-
`as`. One guard survives on a `custom: true` field: a `'custom'` preset that arrives WITHOUT dates
|
|
538
|
-
resolves to `{ window: 'custom' }`, so `'custom'` stays in the union by construction.
|
|
1178
|
+
### `basalt-ui/commands` — `BasaltOverlays` no longer WRAPS your app in `ModalsProvider`
|
|
539
1179
|
|
|
540
|
-
**
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
1180
|
+
**Same props, same imperative API, one structural change: every layer — `ModalsProvider`
|
|
1181
|
+
included — now renders as a SIBLING of `children` instead of an ancestor.** The old shape put the
|
|
1182
|
+
whole app under a `React.lazy` Suspense boundary (`<Suspense fallback={<>{app}</>}>
|
|
1183
|
+
<LazyModalsProvider>{app}</LazyModalsProvider></Suspense>`), and `React.lazy` suspends on its first
|
|
1184
|
+
render even when the module is already warm. Two consequences, both real: the app's FIRST COMMIT was
|
|
1185
|
+
deferred past a microtask — long enough for TanStack Router's async `loadMatches` to `setState` onto
|
|
1186
|
+
a not-yet-mounted fiber ("Can't perform a React state update on a component that hasn't mounted
|
|
1187
|
+
yet", once per full page load) — and the app tree mounted TWICE, once inside the fallback and once
|
|
1188
|
+
resolved, so every effect in the app ran twice on boot.
|
|
545
1189
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
setter instead. And a patched write **from a route that does not validate the field** persists (A1)
|
|
551
|
-
but drops the patch with it — there is no navigate to merge into — which now warns once per field in
|
|
552
|
-
dev instead of doing nothing.
|
|
1190
|
+
Nothing to change for the overlay APIs. `modals.open` / `modals.openConfirmModal` (and so basalt's
|
|
1191
|
+
own `overlays.open` / `overlays.close`) go through `@mantine/modals`' window CustomEvent bus, which
|
|
1192
|
+
`ModalsProvider` subscribes to — being inside it was never a requirement, and the provider renders
|
|
1193
|
+
its own managed `Modal` regardless of where it sits.
|
|
553
1194
|
|
|
554
|
-
**
|
|
1195
|
+
**The one thing a sibling cannot serve is React CONTEXT**: `useModals()` from `@mantine/modals`
|
|
1196
|
+
called inside your app now throws its "was called outside of context" error. If you use it (or
|
|
1197
|
+
`openContextModal`, which needs the provider's `modals` map that `BasaltOverlays` never passed
|
|
1198
|
+
anyway), mount your own provider and turn basalt's off — two providers would both answer the event
|
|
1199
|
+
bus and open every modal twice:
|
|
1200
|
+
|
|
1201
|
+
```tsx
|
|
1202
|
+
<BasaltOverlays modals={false}>
|
|
1203
|
+
<ModalsProvider modals={CONTEXT_MODALS}>
|
|
1204
|
+
<App />
|
|
1205
|
+
</ModalsProvider>
|
|
1206
|
+
</BasaltOverlays>
|
|
1207
|
+
```
|
|
1208
|
+
|
|
1209
|
+
### `basalt-ui/controls` — the mobile `Filters (n)` sheet renders panel rows, not its own list
|
|
1210
|
+
|
|
1211
|
+
**The sheet's `SheetOptionList` (44px rows, a hairline between them, one per option) is gone.**
|
|
1212
|
+
`useFilterSurface() === 'sheet'` now resolves to the SAME `PanelRow`/`PanelChoice`/facet-list body
|
|
1213
|
+
`'panel'` already renders (`docs/CONTROLS-SPEC.md` §3: "sheet = panel rows inside a Drawer") — label
|
|
1214
|
+
above, full-width control below, folding past `PANEL_TRACK_MAX` (3, a `Select`) or `max` (a
|
|
1215
|
+
`MultiSelectFilter`'s `Show N more`) the same way the aside already did. A set with many options no
|
|
1216
|
+
longer grows the sheet to a full column of option rows. `RangeFilter`'s sheet form is the panel's one
|
|
1217
|
+
`Select` (presets plus a `Custom range…` row revealing the injected picker) instead of a
|
|
1218
|
+
`SheetOptionList` with the picker behind a separate disclosure row.
|
|
1219
|
+
|
|
1220
|
+
Also fixed in the same pass: a `fullWidth` `SegmentedControl` (`PanelChoice`'s ≤3-option form, and
|
|
1221
|
+
`ViewTabs`) now splits its options EVENLY — Mantine's own `.control` carries no `min-width`
|
|
1222
|
+
override, so the initial `auto` floored each option at its content width and a `fullWidth` track
|
|
1223
|
+
split unevenly; a long label now ellipsizes inside its equal share instead of forcing the column
|
|
1224
|
+
wide. The active segment's fill is a second ink-mix step
|
|
1225
|
+
(`color-mix(in srgb, var(--vx-ink) 12%, var(--vx-surface-panel))`), not a flat `--vx-surface-panel`,
|
|
1226
|
+
so it stays visible against its own track when the whole control sits on a panel-coloured surface (a
|
|
1227
|
+
`PanelRow`), where a flat panel fill read as barely-there next to the 6%-tinted track around it.
|
|
1228
|
+
|
|
1229
|
+
| Removed | Replacement | Note |
|
|
1230
|
+
| ---------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1231
|
+
| `SheetOptionList`, `SheetField`, `SheetDisclosure` (`basalt-ui/controls`) | `PanelRow` / `PanelChoice` (already exported) | Were never part of the public `./controls` barrel — internal to `filter-sheet.tsx` — so this only breaks a consumer test that imported them directly out of `basalt-ui/controls/filter-sheet` or hooked a `role="radio"` `<fieldset>` DOM shape inside the sheet (now a `PanelChoice` `SegmentedControl`, `role="radiogroup"`, or a `Select`). |
|
|
1232
|
+
| `SheetRow`, `sheetRowClassNames` (`basalt-ui/controls`) | none | Also internal-only. `ToggleFilter`'s sheet form is now a `PanelRow` whose `Switch` rides the label line, the same shape the panel surface always used. |
|
|
1233
|
+
| `.sheetOption` / `.sheetList` / `.sheetField` / `.sheetRow` / `.sheetDisclosureBody` / `.sheetLabel` (`controls.module.css`) | `panel-row.module.css`'s `.row`/`.head`/`.control`/`.label` | CSS-module classes were never a public contract; named for a consumer that vendored a copy of the sheet's old CSS. |
|
|
1234
|
+
|
|
1235
|
+
### The shell scrollport — Main scrolls, not the window
|
|
1236
|
+
|
|
1237
|
+
`BasaltShell` dropped Mantine's `layout="alt"` (the header now spans the full viewport width, above
|
|
1238
|
+
the sidebar and the aside, instead of being inset beside the sidebar) and gave `AppShell.Main` a
|
|
1239
|
+
bounded height with `overflow: auto`. Main is therefore THE scrolling element: the document's
|
|
1240
|
+
`scrollHeight` equals the viewport height, the browser scrollbar sits on Main's own right edge
|
|
1241
|
+
(inside the aside, where the content is) instead of at the far edge of the window, and
|
|
1242
|
+
`window.scrollY` is pinned at 0.
|
|
1243
|
+
|
|
1244
|
+
Main carries two handles: `data-basalt-scrollport` (basalt's own, and what `scrollParentOf` looks
|
|
1245
|
+
for) and `data-scroll-restoration-id="basalt-main"` (the attribute `@tanstack/router-core`'s scroll
|
|
1246
|
+
restoration reads, so a router-driven app restores this element rather than the window).
|
|
1247
|
+
|
|
1248
|
+
What to change in a consumer:
|
|
1249
|
+
|
|
1250
|
+
| If your app… | Do this |
|
|
1251
|
+
| ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1252
|
+
| reads `window.scrollY` / listens for `scroll` on `window` | `const port = scrollParentOf(el)` and read `port.scrollTop` / listen on `port` — `null` still means the window, so shell-less code is unchanged |
|
|
1253
|
+
| scrolls with `window.scrollTo` / `document.scrollingElement` | target `document.querySelector('[data-basalt-scrollport]')` first, with the document as the fallback |
|
|
1254
|
+
| uses window-based scroll restoration | point it at `[data-scroll-restoration-id="basalt-main"]`; TanStack Router picks it up with no config |
|
|
1255
|
+
| overrides `--vx-space-sticky-header-clearance` | the token NO LONGER includes the app-shell header height — it is `anchors.stackMd` (12px at level 0), pure breathing room, because BOTH the header and the page-bar band are regions outside the scrollport |
|
|
1256
|
+
| passes `BasaltDataTable stickyHeaderOffset` inside a shell | drop it entirely. Nothing overhangs the scrollport any more, so a sticky table head wants `top: 0`, which is the default |
|
|
1257
|
+
| computes a viewport-filling body from `--basalt-page-bar-h` | inside a shell that is just `100%` now — the band is outside Main rather than scrolling with it. The var is still published |
|
|
1258
|
+
| passes `mobileNav.getScrollElement` only to reach the page scroller | you can drop it — the default resolves the scrollport now |
|
|
1259
|
+
|
|
1260
|
+
Two more rendering changes ride along, both visible without any code change (the third,
|
|
1261
|
+
the brand relocation, is NOT always visible — it has its own § below):
|
|
1262
|
+
|
|
1263
|
+
- **`PageBar` row 2 is a shell-owned BAND**, not a row in the page: it portals into an outlet
|
|
1264
|
+
`BasaltShell` renders between the header and the scrollport, exactly as row 1 portals into the
|
|
1265
|
+
header. It spans Main's width, carries `--vx-surface-bg` and one `--vx-divider` hairline, and is a
|
|
1266
|
+
zero-height, seam-less box on a route with no `PageBar`. It is no longer sticky and no longer
|
|
1267
|
+
depends on where you wrote `<PageBar>` — nested in a `Stack` or written as Main's direct child now
|
|
1268
|
+
produce identical geometry. Its padding-block tightened from `--vx-space-stack-sm` to
|
|
1269
|
+
`--vx-space-stack-xs`, so the bar is 8px shorter.
|
|
1270
|
+
- **`BasaltDataTable` contains itself horizontally by default.** A table with neither `maxHeight`
|
|
1271
|
+
nor `minWidth` used to render a bare `<table>`, which sizes to its own min-content — a five-column
|
|
1272
|
+
table measured 448px inside a 390px viewport and dragged the whole page sideways. Every table now
|
|
1273
|
+
renders inside `Table.ScrollContainer type="native"` (`minWidth: 0` when you set none), so a wide
|
|
1274
|
+
table scrolls inside its own card. `stickyHeader` with neither prop cannot take that container —
|
|
1275
|
+
an `overflow-x` box computes `overflow-y` to `auto` and becomes the header's scrollport, and with
|
|
1276
|
+
no height cap it has no scroll range, so the header would go inert — so it takes a wrapper whose
|
|
1277
|
+
overflow is **measured** instead: while the table fits its container the wrapper declares no
|
|
1278
|
+
overflow at all and the page-sticky header sticks exactly as before; once the table is wider the
|
|
1279
|
+
wrapper flips to `overflow-x: auto`, the columns stay reachable, and the header is inert at that
|
|
1280
|
+
width. The flip is a live `ResizeObserver` reading in both directions, so the wrapper goes back to
|
|
1281
|
+
bare when the space returns (the window widened, the sidebar collapsed, the aside closed), and it
|
|
1282
|
+
publishes `data-contained="true|false"`. The dev warning on that shape now states the trade rather
|
|
1283
|
+
than a defect and points at `maxHeight` for a table that must both scroll and stick. Pinning takes
|
|
1284
|
+
the same wrapper: the `overflow-x: auto` `Box` a pinned, uncapped, sticky table used to get is
|
|
1285
|
+
gone, because it was the inert-header shape it was meant to avoid. **The TOOLBAR was the other
|
|
1286
|
+
half of the same defect and is fixed with it**: the toolbar `Group` is the header row's flex item
|
|
1287
|
+
(`CtlSlot` between them is `display: contents`) and carried `flex: 0 0 auto`, so a 220px search
|
|
1288
|
+
beside a 230px facet pill row was an unshrinkable 461px box in a 302px column — `AppShell.Main`
|
|
1289
|
+
measured `scrollWidth` 505 against `clientWidth` 390 at 390x844. It is now `flex: 0 1 auto` with
|
|
1290
|
+
`min-width: 0`, and the search's `w={220}` became a flex BASIS (`flex: 0 1 220px`, floored at
|
|
1291
|
+
`12ch`, capped at `100%`). The field still resolves to 220px wherever 220 fits and still never
|
|
1292
|
+
grows with the table; below that the search and the pills wrap onto their own lines.
|
|
1293
|
+
|
|
1294
|
+
### Shell — the brand moved into the header, and that can DELETE it
|
|
1295
|
+
|
|
1296
|
+
**This one is filed separately because "visible without any code change" is exactly what it is
|
|
1297
|
+
not.** The header now spans the full viewport width, so the sidebar's `appShellHeaderHeight` brand
|
|
1298
|
+
row was painting a SECOND band under the header seam. The brand is the header's leading zone
|
|
1299
|
+
instead — `--app-shell-navbar-offset` wide, so it still tracks the rail collapse and still ends on
|
|
1300
|
+
the sidebar\|main seam — and the sidebar column starts at `SidebarSearch`. `brand`, `brand.menu` and
|
|
1301
|
+
the collapse toggle are unchanged; only the markup's home moved.
|
|
1302
|
+
|
|
1303
|
+
**If your app suppresses the header, the brand and the sidebar collapse toggle are now GONE.**
|
|
1304
|
+
Nothing type-checks differently, nothing lints, no test fails: the app simply boots without them.
|
|
1305
|
+
A phone-first consumer that zeroes the header (`--app-shell-header-height: 0`,
|
|
1306
|
+
`--app-shell-header-offset: 0`, or `display: none` on the region) hit exactly this, and found it by
|
|
1307
|
+
looking at the app. There is currently no supported way to suppress a shell region and no signal
|
|
1308
|
+
when a region's contents move underneath you — **so if you have ever styled a `mantine-AppShell-*`
|
|
1309
|
+
region away, re-check it against every minor that touches the shell.** The remedy is to stop
|
|
1310
|
+
suppressing the header (it is one token tall) or to render your own brand where the header used to
|
|
1311
|
+
be.
|
|
1312
|
+
|
|
1313
|
+
**Two more consequences, for a consumer mounting `AppSidebar` DIRECTLY** rather than through
|
|
1314
|
+
`BasaltShell` (which is the supported path): it no longer paints a brand row, and its
|
|
1315
|
+
`onToggleCollapse` prop is no longer read there — `BasaltShell` feeds it to the header instead. Both
|
|
1316
|
+
are silent in the same way.
|
|
1317
|
+
|
|
1318
|
+
Find out whether this reaches you: `rg -n "app-shell-(header|navbar)-(height|offset)|AppShell-header"`
|
|
1319
|
+
|
|
1320
|
+
## 1.27.0 — the number lane, `StatCard`'s breakdown, and five promotions
|
|
1321
|
+
|
|
1322
|
+
**Nothing removed. Two exports added (`NumberFilter`, and three members on the number handle),
|
|
1323
|
+
one member added to every `FieldHandle` (`clear()`), four `StatCard` props added, and enforcement
|
|
1324
|
+
tightened: five oxlint rules plus one guard kind became `error`.** The store half of this minor also
|
|
1325
|
+
fixed four inference and notification defects consumers hit while porting, plus two ways a fallback
|
|
1326
|
+
got pinned into localStorage — see § Stores below. Every level change honours the `theme-allow`
|
|
1327
|
+
grammar unchanged; only the severity of an unwaived finding moves.
|
|
1328
|
+
|
|
1329
|
+
### Stores — the `custom` flag, patched writes, lazy fallbacks, derived windows
|
|
1330
|
+
|
|
1331
|
+
**Additive, except one type that got narrower on purpose.** `field.range({ presets, fallback })`
|
|
1332
|
+
written INLINE inside `createSearchStore({ fields })` used to infer `custom` as `boolean` — the
|
|
1333
|
+
contextual `AnyField` return type won over the argument — so every value and search type carried a
|
|
1334
|
+
`'custom'` preset the field itself rejects at runtime, and `RangeFilter field={…}` needed a cast or
|
|
1335
|
+
an explicit `custom: false`. `field.range` is now three overloads: an omitted or `false` flag is
|
|
1336
|
+
`false`, a literal `true` is `true`, a value typed `boolean` stays widened.
|
|
1337
|
+
|
|
1338
|
+
- **Delete the workaround, not the field.** An explicit `custom: false` still works (same
|
|
1339
|
+
overload); a cast on `<RangeFilter field={…} />` can go, and `RangeFilterProps<P, C>` is generic
|
|
1340
|
+
over the flag so a preset-only handle binds directly.
|
|
1341
|
+
- **What can newly type-error:** code that read `'custom'` out of a range whose field never declared
|
|
1342
|
+
it — `if (search.range === 'custom')` on a preset-only field, or a `RangeValue<P | 'custom'>`
|
|
1343
|
+
annotation over one. That branch was always dead at runtime; the type just stopped pretending.
|
|
1344
|
+
- **…and one more, in a consumer's own WRAPPER.** A prop typed `FieldHandle<RangeField<P>>` — `C`
|
|
1345
|
+
defaulting to `boolean` — no longer accepts a handle from a `field.range` without `custom: true`,
|
|
1346
|
+
because that handle's setter is now `RangeValue<P>`-typed and a setter is contravariant. Pin
|
|
1347
|
+
`FieldHandle<RangeField<P, false>>` for the preset-only shape, `RangeField<P, true>` for a
|
|
1348
|
+
picker-backed one, or make the wrapper generic over the flag
|
|
1349
|
+
(`<P extends string, C extends boolean>` + `FieldHandle<RangeField<P, C>>`), which is what
|
|
1350
|
+
`RangeFilter` does. An existing `as FieldHandle<RangeField<P>>` cast keeps compiling.
|
|
1351
|
+
|
|
1352
|
+
**`toWindow()` resolves derived presets.** `field.range({ window: { '3m': (now) => ({ from, to }) } })`
|
|
1353
|
+
makes `toWindow({ preset: '3m' })` return `{ from, to }` (ISO dates, resolved at call time with the
|
|
1354
|
+
current `Date`) while `3m` stays one preset in the URL. A preset with no resolver keeps
|
|
1355
|
+
`{ window: preset }`, in the same field — which is what retires the last hand-rolled
|
|
1356
|
+
`presetToParams` a consumer kept beside the store for `3m` / `6m` / `1y` / `ytd`.
|
|
1357
|
+
|
|
1358
|
+
**The resolved presets are gone from the return TYPE too**, which is the half that decides whether
|
|
1359
|
+
the switch is deleted or the cast just moves: the field now carries its resolver keys (`RangeField`'s
|
|
1360
|
+
fifth type parameter, inferred from `window`), and `toWindow` returns
|
|
1361
|
+
`{ window: Exclude<P, W> } | { from; to }`. A `resolveWindow` whose target is
|
|
1362
|
+
`{ window: '7d' | '30d' | '90d' | 'all' } | { from; to }` becomes a one-line delegation with no
|
|
1363
|
+
`as`. One guard survives on a `custom: true` field: a `'custom'` preset that arrives WITHOUT dates
|
|
1364
|
+
resolves to `{ window: 'custom' }`, so `'custom'` stays in the union by construction.
|
|
1365
|
+
|
|
1366
|
+
**A field setter takes an optional second argument.** `set(next, { patch })` merges extra search
|
|
1367
|
+
params into the SAME navigate — URL lane only, for keys the store does not own (`{ patch: {
|
|
1368
|
+
detailDate: undefined } }` clears a sibling param the page put there). The field's own params always
|
|
1369
|
+
win over the patch, so it cannot corrupt the value being set. A local/memory-lane write has no
|
|
1370
|
+
navigate to merge into and ignores it.
|
|
1371
|
+
|
|
1372
|
+
Two limits are now enforced rather than documented. A patch key **another field of the same store
|
|
1373
|
+
owns** is refused — it would reach the URL while the store's own write path never touched the mirror,
|
|
1374
|
+
so the next paramless visit and every `linkSearch` link resolve the OLD value: `createSearchStore`
|
|
1375
|
+
throws in dev, and in production keeps the write and logs it once. Write that field through its own
|
|
1376
|
+
setter instead. And a patched write **from a route that does not validate the field** persists (A1)
|
|
1377
|
+
but drops the patch with it — there is no navigate to merge into — which now warns once per field in
|
|
1378
|
+
dev instead of doing nothing.
|
|
1379
|
+
|
|
1380
|
+
**A fallback may be a thunk.** `field.*({ fallback: () => T })` (every kind — `field.number`'s codec
|
|
555
1381
|
froze its thunk at store definition in the first cut of this minor and does not now) is resolved at
|
|
556
1382
|
READ time and re-resolved while nothing is written, and is never persisted on its own — a write
|
|
557
1383
|
stores what the control produced, as before. **Local and memory lanes only:** `createSearchStore` now
|
|
@@ -747,297 +1573,6 @@ half is deliberately NOT exempted the same way — a local `StatCard` beside an
|
|
|
747
1573
|
`import { StatCard as Base }` kept the name AND a piece of the original, which is the fork shape this
|
|
748
1574
|
rule most wants to see.
|
|
749
1575
|
|
|
750
|
-
### Guards — `basalt/query-dual-import` removed
|
|
751
|
-
|
|
752
|
-
**Retired, not merely relaxed** — its `RETIRED_RULE_IDS` entry (kept recognized, so an existing
|
|
753
|
-
`theme-allow query-dual-import` still reads as a dead waiver rather than a typo) and its
|
|
754
|
-
`PLUGIN_RULE_GRACE` ledger row are both deleted. Its whole premise — a raw `@tanstack/react-query`
|
|
755
|
-
import beside `basalt-ui/query` — went unreachable the moment `./query` dropped (see § Consolidation
|
|
756
|
-
above): a consumer now MUST import `@tanstack/react-query` directly, and the rule warned on every
|
|
757
|
-
one of those imports instead of the drift it was written to catch (90+ false positives on a single
|
|
758
|
-
consumer). `query-fn-unwrap` is unaffected — it is a different rule, still `warn` (grace → 1.30.0).
|
|
759
|
-
|
|
760
|
-
### `basalt-ui/data` — a typed facet id, and a required `getItemKey`
|
|
761
|
-
|
|
762
|
-
**`BasaltDataTable`: a mistyped `facets[].columnId` used to render no pill, silently — `if
|
|
763
|
-
(!column) return null`.** It now throws in dev, naming the id and every known column id
|
|
764
|
-
(`facet columnId "…" matches no column`); production still degrades to no pill for that one facet.
|
|
765
|
-
`DataTableFacet` also gained a type parameter — `DataTableFacet<T = unknown>` — so `columnId` is
|
|
766
|
-
typed off the row shape wherever TanStack's `accessor`/manual `id` inference allows it
|
|
767
|
-
(`Extract<keyof T, string> | (string & {})`); the default keeps a bare `DataTableFacet[]`
|
|
768
|
-
compiling unchanged. `BasaltDataTableProps.facets` is now `DataTableFacet<T>[]`, and
|
|
769
|
-
`initialColumnPinning` moved from TanStack's own `ColumnPinningState` to a new, `T`-derived
|
|
770
|
-
`DataTableColumnPinning<T>` (structurally assignable to `ColumnPinningState`, so no cast is
|
|
771
|
-
needed at either end). Both are additive for existing call sites — an id that was already a real
|
|
772
|
-
column keeps compiling and rendering exactly as before.
|
|
773
|
-
|
|
774
|
-
**`BasaltVirtualList.getItemKey` is now required.** The index fallback hid stale rows across a
|
|
775
|
-
mutation: an insert/delete shifts every index below it, so the OLD item kept rendering at each
|
|
776
|
-
shifted index until an unrelated re-render happened to catch it up — in the one component whose
|
|
777
|
-
job is a long mutable list. Pass a real per-item key (`(item) => item.id`); there is no
|
|
778
|
-
replacement for the index fallback.
|
|
779
|
-
|
|
780
|
-
**`BasaltVirtualList` gained a ref.** `ref?: Ref<BasaltVirtualListHandle>` exposes
|
|
781
|
-
`scrollToIndex`/`scrollToOffset`/`scrollToEnd` (delegating to the internal
|
|
782
|
-
`@tanstack/react-virtual` instance) plus `getVirtualizer()` as the full escape hatch — mirrors
|
|
783
|
-
`./data/table`'s `useReactTable` re-export as the table's own. Additive; omitting `ref` changes
|
|
784
|
-
nothing.
|
|
785
|
-
|
|
786
|
-
### `basalt-ui/commands` — `BasaltOverlays` no longer WRAPS your app in `ModalsProvider`
|
|
787
|
-
|
|
788
|
-
**Same props, same imperative API, one structural change: every layer — `ModalsProvider`
|
|
789
|
-
included — now renders as a SIBLING of `children` instead of an ancestor.** The old shape put the
|
|
790
|
-
whole app under a `React.lazy` Suspense boundary (`<Suspense fallback={<>{app}</>}>
|
|
791
|
-
<LazyModalsProvider>{app}</LazyModalsProvider></Suspense>`), and `React.lazy` suspends on its first
|
|
792
|
-
render even when the module is already warm. Two consequences, both real: the app's FIRST COMMIT was
|
|
793
|
-
deferred past a microtask — long enough for TanStack Router's async `loadMatches` to `setState` onto
|
|
794
|
-
a not-yet-mounted fiber ("Can't perform a React state update on a component that hasn't mounted
|
|
795
|
-
yet", once per full page load) — and the app tree mounted TWICE, once inside the fallback and once
|
|
796
|
-
resolved, so every effect in the app ran twice on boot.
|
|
797
|
-
|
|
798
|
-
Nothing to change for the overlay APIs. `modals.open` / `modals.openConfirmModal` (and so basalt's
|
|
799
|
-
own `overlays.open` / `overlays.close`) go through `@mantine/modals`' window CustomEvent bus, which
|
|
800
|
-
`ModalsProvider` subscribes to — being inside it was never a requirement, and the provider renders
|
|
801
|
-
its own managed `Modal` regardless of where it sits.
|
|
802
|
-
|
|
803
|
-
**The one thing a sibling cannot serve is React CONTEXT**: `useModals()` from `@mantine/modals`
|
|
804
|
-
called inside your app now throws its "was called outside of context" error. If you use it (or
|
|
805
|
-
`openContextModal`, which needs the provider's `modals` map that `BasaltOverlays` never passed
|
|
806
|
-
anyway), mount your own provider and turn basalt's off — two providers would both answer the event
|
|
807
|
-
bus and open every modal twice:
|
|
808
|
-
|
|
809
|
-
```tsx
|
|
810
|
-
<BasaltOverlays modals={false}>
|
|
811
|
-
<ModalsProvider modals={CONTEXT_MODALS}>
|
|
812
|
-
<App />
|
|
813
|
-
</ModalsProvider>
|
|
814
|
-
</BasaltOverlays>
|
|
815
|
-
```
|
|
816
|
-
|
|
817
|
-
### `basalt-ui/controls` — the mobile `Filters (n)` sheet renders panel rows, not its own list
|
|
818
|
-
|
|
819
|
-
**The sheet's `SheetOptionList` (44px rows, a hairline between them, one per option) is gone.**
|
|
820
|
-
`useFilterSurface() === 'sheet'` now resolves to the SAME `PanelRow`/`PanelChoice`/facet-list body
|
|
821
|
-
`'panel'` already renders (`docs/CONTROLS-SPEC.md` §3: "sheet = panel rows inside a Drawer") — label
|
|
822
|
-
above, full-width control below, folding past `PANEL_TRACK_MAX` (3, a `Select`) or `max` (a
|
|
823
|
-
`MultiSelectFilter`'s `Show N more`) the same way the aside already did. A set with many options no
|
|
824
|
-
longer grows the sheet to a full column of option rows. `RangeFilter`'s sheet form is the panel's one
|
|
825
|
-
`Select` (presets plus a `Custom range…` row revealing the injected picker) instead of a
|
|
826
|
-
`SheetOptionList` with the picker behind a separate disclosure row.
|
|
827
|
-
|
|
828
|
-
Also fixed in the same pass: a `fullWidth` `SegmentedControl` (`PanelChoice`'s ≤3-option form, and
|
|
829
|
-
`ViewTabs`) now splits its options EVENLY — Mantine's own `.control` carries no `min-width`
|
|
830
|
-
override, so the initial `auto` floored each option at its content width and a `fullWidth` track
|
|
831
|
-
split unevenly; a long label now ellipsizes inside its equal share instead of forcing the column
|
|
832
|
-
wide. The active segment's fill is a second ink-mix step
|
|
833
|
-
(`color-mix(in srgb, var(--vx-ink) 12%, var(--vx-surface-panel))`), not a flat `--vx-surface-panel`,
|
|
834
|
-
so it stays visible against its own track when the whole control sits on a panel-coloured surface (a
|
|
835
|
-
`PanelRow`), where a flat panel fill read as barely-there next to the 6%-tinted track around it.
|
|
836
|
-
|
|
837
|
-
| Removed | Replacement | Note |
|
|
838
|
-
| ---------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
839
|
-
| `SheetOptionList`, `SheetField`, `SheetDisclosure` (`basalt-ui/controls`) | `PanelRow` / `PanelChoice` (already exported) | Were never part of the public `./controls` barrel — internal to `filter-sheet.tsx` — so this only breaks a consumer test that imported them directly out of `basalt-ui/controls/filter-sheet` or hooked a `role="radio"` `<fieldset>` DOM shape inside the sheet (now a `PanelChoice` `SegmentedControl`, `role="radiogroup"`, or a `Select`). |
|
|
840
|
-
| `SheetRow`, `sheetRowClassNames` (`basalt-ui/controls`) | none | Also internal-only. `ToggleFilter`'s sheet form is now a `PanelRow` whose `Switch` rides the label line, the same shape the panel surface always used. |
|
|
841
|
-
| `.sheetOption` / `.sheetList` / `.sheetField` / `.sheetRow` / `.sheetDisclosureBody` / `.sheetLabel` (`controls.module.css`) | `panel-row.module.css`'s `.row`/`.head`/`.control`/`.label` | CSS-module classes were never a public contract; named for a consumer that vendored a copy of the sheet's old CSS. |
|
|
842
|
-
|
|
843
|
-
### The shell scrollport — Main scrolls, not the window
|
|
844
|
-
|
|
845
|
-
`BasaltShell` dropped Mantine's `layout="alt"` (the header now spans the full viewport width, above
|
|
846
|
-
the sidebar and the aside, instead of being inset beside the sidebar) and gave `AppShell.Main` a
|
|
847
|
-
bounded height with `overflow: auto`. Main is therefore THE scrolling element: the document's
|
|
848
|
-
`scrollHeight` equals the viewport height, the browser scrollbar sits on Main's own right edge
|
|
849
|
-
(inside the aside, where the content is) instead of at the far edge of the window, and
|
|
850
|
-
`window.scrollY` is pinned at 0.
|
|
851
|
-
|
|
852
|
-
Main carries two handles: `data-basalt-scrollport` (basalt's own, and what `scrollParentOf` looks
|
|
853
|
-
for) and `data-scroll-restoration-id="basalt-main"` (the attribute `@tanstack/router-core`'s scroll
|
|
854
|
-
restoration reads, so a router-driven app restores this element rather than the window).
|
|
855
|
-
|
|
856
|
-
What to change in a consumer:
|
|
857
|
-
|
|
858
|
-
| If your app… | Do this |
|
|
859
|
-
| ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
860
|
-
| reads `window.scrollY` / listens for `scroll` on `window` | `const port = scrollParentOf(el)` and read `port.scrollTop` / listen on `port` — `null` still means the window, so shell-less code is unchanged |
|
|
861
|
-
| scrolls with `window.scrollTo` / `document.scrollingElement` | target `document.querySelector('[data-basalt-scrollport]')` first, with the document as the fallback |
|
|
862
|
-
| uses window-based scroll restoration | point it at `[data-scroll-restoration-id="basalt-main"]`; TanStack Router picks it up with no config |
|
|
863
|
-
| overrides `--vx-space-sticky-header-clearance` | the token NO LONGER includes the app-shell header height — it is `anchors.stackMd` (12px at level 0), pure breathing room, because BOTH the header and the page-bar band are regions outside the scrollport |
|
|
864
|
-
| passes `BasaltDataTable stickyHeaderOffset` inside a shell | drop it entirely. Nothing overhangs the scrollport any more, so a sticky table head wants `top: 0`, which is the default |
|
|
865
|
-
| computes a viewport-filling body from `--basalt-page-bar-h` | inside a shell that is just `100%` now — the band is outside Main rather than scrolling with it. The var is still published |
|
|
866
|
-
| passes `mobileNav.getScrollElement` only to reach the page scroller | you can drop it — the default resolves the scrollport now |
|
|
867
|
-
|
|
868
|
-
Three rendering changes ride along, all visible without any code change:
|
|
869
|
-
|
|
870
|
-
- **The brand moved out of the sidebar and into the header.** The header spans the full viewport
|
|
871
|
-
width now, so the sidebar's `appShellHeaderHeight` brand row painted as a SECOND 48px band under
|
|
872
|
-
the header seam. It is the header's leading zone instead — `--app-shell-navbar-offset` wide, so it
|
|
873
|
-
still tracks the rail collapse and still ends on the sidebar\|main seam — and the sidebar column
|
|
874
|
-
starts at `SidebarSearch`. `brand`, `brand.menu` and the collapse toggle are unchanged; only the
|
|
875
|
-
markup's home moved. Two consequences for a consumer mounting `AppSidebar` DIRECTLY (rather than
|
|
876
|
-
through `BasaltShell`, which is the supported path): it no longer paints a brand row, and its
|
|
877
|
-
`onToggleCollapse` prop is no longer read there — `BasaltShell` feeds it to the header instead.
|
|
878
|
-
- **`PageBar` row 2 is a shell-owned BAND**, not a row in the page: it portals into an outlet
|
|
879
|
-
`BasaltShell` renders between the header and the scrollport, exactly as row 1 portals into the
|
|
880
|
-
header. It spans Main's width, carries `--vx-surface-bg` and one `--vx-divider` hairline, and is a
|
|
881
|
-
zero-height, seam-less box on a route with no `PageBar`. It is no longer sticky and no longer
|
|
882
|
-
depends on where you wrote `<PageBar>` — nested in a `Stack` or written as Main's direct child now
|
|
883
|
-
produce identical geometry. Its padding-block tightened from `--vx-space-stack-sm` to
|
|
884
|
-
`--vx-space-stack-xs`, so the bar is 8px shorter.
|
|
885
|
-
- **`BasaltDataTable` contains itself horizontally by default.** A table with neither `maxHeight`
|
|
886
|
-
nor `minWidth` used to render a bare `<table>`, which sizes to its own min-content — a five-column
|
|
887
|
-
table measured 448px inside a 390px viewport and dragged the whole page sideways. Every table now
|
|
888
|
-
renders inside `Table.ScrollContainer type="native"` (`minWidth: 0` when you set none), so a wide
|
|
889
|
-
table scrolls inside its own card. `stickyHeader` with neither prop cannot take that container —
|
|
890
|
-
an `overflow-x` box computes `overflow-y` to `auto` and becomes the header's scrollport, and with
|
|
891
|
-
no height cap it has no scroll range, so the header would go inert — so it takes a wrapper whose
|
|
892
|
-
overflow is **measured** instead: while the table fits its container the wrapper declares no
|
|
893
|
-
overflow at all and the page-sticky header sticks exactly as before; once the table is wider the
|
|
894
|
-
wrapper flips to `overflow-x: auto`, the columns stay reachable, and the header is inert at that
|
|
895
|
-
width. The flip is a live `ResizeObserver` reading in both directions, so the wrapper goes back to
|
|
896
|
-
bare when the space returns (the window widened, the sidebar collapsed, the aside closed), and it
|
|
897
|
-
publishes `data-contained="true|false"`. The dev warning on that shape now states the trade rather
|
|
898
|
-
than a defect and points at `maxHeight` for a table that must both scroll and stick. Pinning takes
|
|
899
|
-
the same wrapper: the `overflow-x: auto` `Box` a pinned, uncapped, sticky table used to get is
|
|
900
|
-
gone, because it was the inert-header shape it was meant to avoid. **The TOOLBAR was the other
|
|
901
|
-
half of the same defect and is fixed with it**: the toolbar `Group` is the header row's flex item
|
|
902
|
-
(`CtlSlot` between them is `display: contents`) and carried `flex: 0 0 auto`, so a 220px search
|
|
903
|
-
beside a 230px facet pill row was an unshrinkable 461px box in a 302px column — `AppShell.Main`
|
|
904
|
-
measured `scrollWidth` 505 against `clientWidth` 390 at 390x844. It is now `flex: 0 1 auto` with
|
|
905
|
-
`min-width: 0`, and the search's `w={220}` became a flex BASIS (`flex: 0 1 220px`, floored at
|
|
906
|
-
`12ch`, capped at `100%`). The field still resolves to 220px wherever 220 fits and still never
|
|
907
|
-
grows with the table; below that the search and the pills wrap onto their own lines.
|
|
908
|
-
|
|
909
|
-
### Consolidation (targeting 1.29.0)
|
|
910
|
-
|
|
911
|
-
**Dropped subpaths — three, all folded into `.`/`./provider`, none deleted outright.**
|
|
912
|
-
|
|
913
|
-
<!-- C1 symbol list -->
|
|
914
|
-
|
|
915
|
-
- `./query` — dropped. `createBasaltQueryClient`, `unwrap`, `BasaltQueryDevtools`,
|
|
916
|
-
`toErrorMessage`, `errorStatus` now on `.` (root barrel) — import from `basalt-ui` instead of
|
|
917
|
-
`basalt-ui/query`. The nine raw TanStack re-exports it carried (`QueryClientProvider`,
|
|
918
|
-
`QueryErrorResetBoundary`, `useQueryErrorResetBoundary`, `useQuery`, `useSuspenseQuery`,
|
|
919
|
-
`useMutation`, `useQueryClient`, `useInfiniteQuery`, `queryOptions`) are gone from basalt entirely —
|
|
920
|
-
import them from `@tanstack/react-query`, which the root entry already requires.
|
|
921
|
-
- `./data/table` — **narrowed, not dropped**: `BasaltDataTable`, its types and `createColumnHelper`
|
|
922
|
-
stay; the TanStack pass-throughs (`useReactTable`, `flexRender`, `getCoreRowModel`,
|
|
923
|
-
`getSortedRowModel`, `getFilteredRowModel`, `getPaginationRowModel`, `ColumnDef`, `SortingState`,
|
|
924
|
-
`ColumnHelper`, `PaginationState`, `ColumnPinningState`, `RowSelectionState`, `ColumnFiltersState`)
|
|
925
|
-
are imported from `@tanstack/react-table` directly.
|
|
926
|
-
- `./connectivity` — dropped. `ConnectivityProvider` (auto-mounted by `BasaltProvider`, no
|
|
927
|
-
consumer-side import needed), `useConnectivity`, `ConnectivityIndicator` now on `.`.
|
|
928
|
-
- `./data` (bare) — dropped. It never carried its own exports beyond re-exporting `./data/table`
|
|
929
|
-
and `./data/virtual` — import the narrow subpath you need directly; nothing renamed.
|
|
930
|
-
- `./controls-dates` — **unaffected**, stays a separate subpath (inlining it into `./controls`
|
|
931
|
-
would pull `@mantine/dates` into a subpath that must resolve without it).
|
|
932
|
-
|
|
933
|
-
**Deleted `@deprecated` shims** (every one slated for 1.29.0 removal in an earlier minor's
|
|
934
|
-
deprecation row — see that minor's own `MIGRATING.md` section for the original `@deprecated`
|
|
935
|
-
annotation and rationale):
|
|
936
|
-
|
|
937
|
-
- `state.ts`'s legacy connectivity export — use `./provider`'s `useConnectivity` instead.
|
|
938
|
-
- `BasaltProvider`'s flat `sseUrl` / `healthUrl` / `healthIntervalMs` props — use
|
|
939
|
-
`connectivity={{ sseUrl, healthUrl, healthIntervalMs }}`.
|
|
940
|
-
- `QueryStateVariant` and the `variant` prop on `QueryState`/`LoadingState`/`ErrorState`/`EmptyState`
|
|
941
|
-
— use `QueryStateTier` and `tier` (identical values).
|
|
942
|
-
- `createSearchParamStore` / `createMultiSearchParamStore` (`basalt-ui/router-tanstack`) and the
|
|
943
|
-
legacy branches they kept alive in the field vocabulary — use `createSearchStore` with
|
|
944
|
-
`field.enum` / `field.multi`.
|
|
945
|
-
- `field` on `basalt-ui/forms` — use `inputProps(form, path)` + `fieldKey(form, path)`.
|
|
946
|
-
- `BasaltNotifications` / `BasaltNotificationsProps` (`basalt-ui/notifications`) — mount
|
|
947
|
-
`<BasaltOverlays notifications />` from `basalt-ui/commands` instead. **Doctrine exception,
|
|
948
|
-
recorded**: this one never carried a `deprecated-export` row; it was superseded in 1.2x and its own
|
|
949
|
-
JSDoc warned against mounting it next to `BasaltOverlays`, so it leaves in the same minor as the
|
|
950
|
-
other shims. The `duplicate-notifications-mount` lint rule that only existed to catch the
|
|
951
|
-
double-mount is retired with it.
|
|
952
|
-
- `ConnectivityIndicatorProps` stays on the root barrel (it is `BasaltProps`).
|
|
953
|
-
|
|
954
|
-
**CLI** (`basalt-ui`): `info` and `check-coverage` are gone — `check-coverage` is a repo-internal
|
|
955
|
-
script (`bun scripts/check-coverage.ts`), not a consumer command; `info` graded a consumer's
|
|
956
|
-
`vite.config.ts` by parsing it and nobody read the output. `doctor` checks THIS package only: the
|
|
957
|
-
workspace-wide `ai` major-parity walk (with `!`-exclusions and `aiMajorSkewReason`) and the
|
|
958
|
-
`basaltAppPlugin({ icons })` file check are dropped. One resolver remains for every command —
|
|
959
|
-
`BASALT_CWD`, then cwd, plus a declared `basalt.roots`; nothing is inferred from siblings or parents.
|
|
960
|
-
|
|
961
|
-
- `ZonedLine`/`Bars`' `ZoneSpec` aliases — use the canonical `AxisConfig`-scoped zone type each
|
|
962
|
-
kind now exports directly.
|
|
963
|
-
- `inputProps`'s `key` return field (already shape-changed in an earlier Unreleased entry above) —
|
|
964
|
-
fully removed this minor; use `fieldKey(form, name)` alongside `inputProps(form, name)`.
|
|
965
|
-
|
|
966
|
-
**On notice for 1.30.0** — a public export with exactly one consumer (the playground) as of this
|
|
967
|
-
audit. Adopt-or-delete: gains a real consumer by 1.30.0 or is removed. Not a promise either way;
|
|
968
|
-
record your adoption in `docs/ARGO-MIGRATION-LEARNINGS.md` if you pick one up.
|
|
969
|
-
|
|
970
|
-
- **Threads/agent-chat composite tier**: `ThreadWorkspace`, `ThreadFeed`, `ThreadFeedRow`,
|
|
971
|
-
`ThreadDetailPanel`, `ThreadOutcomeCard`, `ThreadsStoreAdapter`, `createThreadsStore`,
|
|
972
|
-
`createAdapterThreadsStore`, `threadsStoreAdapterContract`, `threadPartRenderers`,
|
|
973
|
-
`definePartRenderers`.
|
|
974
|
-
- **Chart kind**: `Heatmap` only — `DualPanel` has an argo consumer and `MirroredBars`/`BandStrip`
|
|
975
|
-
are consumed by linewatch, so those three are NOT on notice.
|
|
976
|
-
- **Nav/shell**: `PageAside`, `PanelRow`, `SettingsRow` (playground-only as of this audit).
|
|
977
|
-
- **Controls**: `SliderControl`, `SearchFilter`, `ToggleFilter`, `CompareFilter`, `OverflowMenu`,
|
|
978
|
-
`ControlGroup`.
|
|
979
|
-
|
|
980
|
-
**`common/**`— the public common primitives, as a unit.**`src/common/\*\*`(shipped from the
|
|
981
|
-
maturation round) is now the one place base prop/error/ref vocabulary lives:`BasaltProps`,
|
|
982
|
-
`SlotStylesProps`, `cx`, `mergeRefs`/`assignRef`, the prefixed `errors.ts` table
|
|
983
|
-
(`toErrorMessage`/`errorStatus`), `useValidateProps`, `assertRequiredProps`. Every basalt component
|
|
984
|
-
extends `BasaltProps`for its own prop type rather than redeclaring`className`/`style`; a consumer
|
|
985
|
-
composing over a basalt component should do the same.
|
|
986
|
-
|
|
987
|
-
### Additions (targeting 1.29.0)
|
|
988
|
-
|
|
989
|
-
C5 consolidation — every item below names the argo lines it lets a consumer delete
|
|
990
|
-
(`.claude/maturation/consolidation-plan.md` §C5, `audit-f-argo-consumer.md`). All additive; nothing
|
|
991
|
-
in this block removes or renames an existing export.
|
|
992
|
-
|
|
993
|
-
- **`basalt-ui/format`** — new subpath, Mantine-free and React-free: `money`, `percent`, `integer`,
|
|
994
|
-
`compact`, `deltaPct`, `duration` (seconds → `"1h 02m"`; `{ unit: 'minutes' }` for a
|
|
995
|
-
minutes-based input), `durationClock` (`"1:02:03"`), `clock`, `relativeTime`, `weekday`, `km`,
|
|
996
|
-
`kcal` — plus the original chart-formatter names (`fmtCompact`/`fmtPercent`/`fmtCurrency`/
|
|
997
|
-
`fmtInt`/`fmtAxisDate`/`fmtTooltipDate`/`NON_FINITE`/`formatters`), now re-exported from here;
|
|
998
|
-
`./charts`' `utils/format.ts` is an internal re-export shim, not a second implementation.
|
|
999
|
-
- **`ChartState.empty`** widens to `boolean | string` — a string is both truthy and the label
|
|
1000
|
-
`ChartEmpty` paints. `ChartCard` gains `state?: ChartState`, `placeholderHeight?: number`
|
|
1001
|
-
(default 240) and `stateAction?: ReactNode`, and `children` is now optional so a title-less
|
|
1002
|
-
pending card can be a route's own `<Suspense fallback>`. Resolution order is the same
|
|
1003
|
-
`resolveChartState` precedence every kind already has — pending → error → empty — and
|
|
1004
|
-
`state={{}}`/omitting `state` renders `children` unchanged. `ChartCenter`'s `width` widens to
|
|
1005
|
-
`number | string` (`'100%'` for a non-measured block body).
|
|
1006
|
-
- **`DualPanel`'s `fillBetween` gains `aboveFill?: string`** — the above-side fill color; omitted,
|
|
1007
|
-
both sides keep sharing `fill` (unchanged behaviour). **`AxisBottomNumeric`** exported from
|
|
1008
|
-
`./charts` — the numeric twin of `AxisLeftNumeric`, for a bespoke continuous-x plot.
|
|
1009
|
-
- **`useBreakpoint(name, edge?, options?)`** on the root barrel — SSR/hydration-safe media query
|
|
1010
|
-
over `theme.breakpoints`, built on `useSyncExternalStore` (not `@mantine/hooks`' `useMediaQuery`,
|
|
1011
|
-
which mismatches during hydration). `edge` is `'min'` (default, "at least this wide") or `'max'`.
|
|
1012
|
-
`page-aside.tsx` now composes this hook's own internals instead of a private duplicate.
|
|
1013
|
-
- **`--vx-space-touch-target` / `VX.spaceTouchTarget`** — the WCAG touch-target floor (44px),
|
|
1014
|
-
density-exempt. The one `SPACE_FIXED` member that IS emitted as a CSS var (every other one stays
|
|
1015
|
-
JS-only) — a consumer's own `@media (pointer: coarse)` CSS-module rule reads it directly.
|
|
1016
|
-
- **`SettingsMenuItem.active?: boolean`** — renders a trailing check glyph and `aria-current` in
|
|
1017
|
-
every projection (the sidebar's flat rows and gear menu, the mobile More sheet).
|
|
1018
|
-
- **`basalt-ui/commands` gains imperative shell handles**: `setColorScheme(scheme)` and
|
|
1019
|
-
`toggleSidebar()`, wired automatically — `BasaltProvider` registers the Mantine color-scheme
|
|
1020
|
-
setter, `BasaltShell` registers the collapse toggle, so a command's `run` calls either with zero
|
|
1021
|
-
`__root.tsx` bridge wiring (unlike the hand-rolled `color-scheme-bridge.ts`/`sidebar-bridge.ts`
|
|
1022
|
-
shape this seeds from). `Slot<K, Constraint>` (`register.ts`) changed its internal resolution
|
|
1023
|
-
from a whole-interface mapped-type match to an indexed-access form — same public behaviour,
|
|
1024
|
-
fixes the commands↔overlays same-file type cycle a command calling `overlays.open(...)` used to
|
|
1025
|
-
hit while `defineCommands`'s own type was still being inferred.
|
|
1026
|
-
- **`BasaltDevDock`** on the root barrel — a fixed, dev-only bottom drawer hosting TanStack Router
|
|
1027
|
-
devtools, TanStack Query devtools and the theme lab, each peer lazy-loaded. New optional peer:
|
|
1028
|
-
`@tanstack/react-router-devtools`.
|
|
1029
|
-
- **`BasaltErrorBoundaryProps`** is now exported; `onError` is optional (defaults to the same
|
|
1030
|
-
console-warn-in-dev fallback `BasaltProvider`'s own top-level `onError` uses); an omitted
|
|
1031
|
-
`fallback` now renders a minimal built-in default (a `PageTitle` + reload button) instead of
|
|
1032
|
-
`null` — pass `fallback={null}` explicitly for the old swallow-it behaviour. **`PageTitle`** is a
|
|
1033
|
-
new shell-less page-title primitive on the root barrel (a plain `<h1>`, not Mantine's `<Title>`,
|
|
1034
|
-
so it needs no `in-body-page-title` waiver) — the remedy for the `in-body-page-title` waiver a
|
|
1035
|
-
shell-less error/auth page used to need.
|
|
1036
|
-
- **`unwrap` gains a second overload** — the original took only a `Promise` of the `{ data, error }`
|
|
1037
|
-
envelope; it now also accepts the ALREADY-RESOLVED envelope directly (`unwrap(await api.x.get())`,
|
|
1038
|
-
or as a bare `.then(unwrap)` callback), inferring which overload from whether the argument IS a
|
|
1039
|
-
`Promise`. The absence guard also now throws on `undefined` data, not just `null`.
|
|
1040
|
-
|
|
1041
1576
|
## 1.26.0 — the control tier, the page bar and the store
|
|
1042
1577
|
|
|
1043
1578
|
**One export removed and two deprecated — see § Stores below; two shell PROPS removed — see
|
|
@@ -1057,7 +1592,7 @@ happened to the nine rows below.
|
|
|
1057
1592
|
(`shadow-basalt-export`). Both ledgers now hold only the wave-6 control guards listed under
|
|
1058
1593
|
§ Guards — six plugin rules and two guard kinds, each `{ since: '1.26.0', promote: '1.27.0' }` —
|
|
1059
1594
|
which the C16 gate forced to promote or be deleted at 1.27.0. Five rules and one kind promoted there;
|
|
1060
|
-
the C1 pair was re-dated to 1.30.0 against a measurement (see
|
|
1595
|
+
the C1 pair was re-dated to 1.30.0 against a measurement (see 1.27.0 § Guards above). A theme-allow escape written against
|
|
1061
1596
|
any of these still works unchanged; only the SEVERITY of an unwaived finding moves from `warn` to
|
|
1062
1597
|
`error`.
|
|
1063
1598
|
|