basalt-ui 1.25.0 → 1.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +8 -6
- package/CHANGELOG.md +26 -0
- package/MIGRATING.md +815 -23
- package/README.md +45 -32
- package/agent/rules/basalt-batteries.md +205 -0
- package/agent/rules/basalt-charts.md +125 -555
- package/agent/rules/basalt-controls.md +160 -0
- package/agent/rules/basalt-mantine.md +146 -439
- package/agent/rules/basalt-state.md +118 -96
- package/agent/rules/basalt-tokens.md +126 -288
- package/agent/skills/basalt-app/SKILL.md +70 -195
- package/agent/skills/basalt-charts/SKILL.md +54 -234
- package/agent/skills/basalt-design/SKILL.md +56 -248
- package/agent/templates/CLAUDE-block.md.tpl +31 -40
- package/agent/templates/DESIGN.md.tpl +22 -51
- package/configs/oxlint-plugin.js +1465 -36
- package/configs/oxlint-plugin.test.ts +1190 -11
- package/configs/oxlint.json +11 -3
- package/dist/charts/cursor/resolve.d.ts +18 -0
- package/dist/charts/cursor/resolve.d.ts.map +1 -1
- package/dist/charts/cursor/resolve.js +15 -6
- package/dist/charts/cursor/scope.d.ts +6 -3
- package/dist/charts/cursor/scope.d.ts.map +1 -1
- package/dist/charts/cursor/store.d.ts +5 -1
- package/dist/charts/cursor/store.d.ts.map +1 -1
- package/dist/charts/cursor/store.js +4 -4
- package/dist/charts/hooks/useChartCursor.d.ts.map +1 -1
- package/dist/charts/hooks/useChartCursor.js +9 -8
- package/dist/charts/index.d.ts +2 -2
- package/dist/charts/index.d.ts.map +1 -1
- package/dist/charts/primitives/ChartCard.d.ts +34 -9
- package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
- package/dist/charts/primitives/ChartCard.js +29 -114
- package/dist/charts/primitives/ChartCard.test.d.ts +2 -0
- package/dist/charts/primitives/ChartCard.test.d.ts.map +1 -0
- package/dist/charts/sparklines/BarSparkline.d.ts +26 -2
- package/dist/charts/sparklines/BarSparkline.d.ts.map +1 -1
- package/dist/charts/sparklines/BarSparkline.js +18 -4
- package/dist/cli/check-coverage.test.d.ts +2 -0
- package/dist/cli/check-coverage.test.d.ts.map +1 -0
- package/dist/cli/index.d.ts +67 -4
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +225 -7
- package/dist/connectivity/connectivity-indicator.d.ts +4 -1
- package/dist/connectivity/connectivity-indicator.d.ts.map +1 -1
- package/dist/connectivity/connectivity-indicator.js +1 -1
- package/dist/content/index.d.ts +5 -3
- package/dist/content/index.d.ts.map +1 -1
- package/dist/content/index.js +4 -2
- package/dist/content/prose.module.css +7 -28
- package/dist/controls/actions.d.ts +169 -0
- package/dist/controls/actions.d.ts.map +1 -0
- package/dist/controls/actions.js +287 -0
- package/dist/controls/actions.module.css +62 -0
- package/dist/controls/actions.module.css.d.ts +13 -0
- package/dist/controls/actions.test.d.ts +2 -0
- package/dist/controls/actions.test.d.ts.map +1 -0
- package/dist/controls/actions.type-guard.test.d.ts +2 -0
- package/dist/controls/actions.type-guard.test.d.ts.map +1 -0
- package/dist/controls/compare-filter.d.ts +41 -0
- package/dist/controls/compare-filter.d.ts.map +1 -0
- package/dist/controls/compare-filter.js +23 -0
- package/dist/controls/control-group.d.ts +19 -0
- package/dist/controls/control-group.d.ts.map +1 -0
- package/dist/controls/control-group.js +20 -0
- package/dist/controls/control-group.test.d.ts +2 -0
- package/dist/controls/control-group.test.d.ts.map +1 -0
- package/dist/controls/controls.module.css +542 -0
- package/dist/controls/controls.module.css.d.ts +35 -0
- package/dist/controls/controls.router.test.d.ts +2 -0
- package/dist/controls/controls.router.test.d.ts.map +1 -0
- package/dist/controls/enum-filter.d.ts +39 -0
- package/dist/controls/enum-filter.d.ts.map +1 -0
- package/dist/controls/enum-filter.js +93 -0
- package/dist/controls/filter-context.d.ts +60 -0
- package/dist/controls/filter-context.d.ts.map +1 -0
- package/dist/controls/filter-context.js +69 -0
- package/dist/controls/filter-pill.d.ts +28 -0
- package/dist/controls/filter-pill.d.ts.map +1 -0
- package/dist/controls/filter-pill.js +70 -0
- package/dist/controls/filter-set.d.ts +13 -0
- package/dist/controls/filter-set.d.ts.map +1 -0
- package/dist/controls/filter-set.js +125 -0
- package/dist/controls/filter-set.test.d.ts +2 -0
- package/dist/controls/filter-set.test.d.ts.map +1 -0
- package/dist/controls/filter-sheet.d.ts +117 -0
- package/dist/controls/filter-sheet.d.ts.map +1 -0
- package/dist/controls/filter-sheet.js +146 -0
- package/dist/controls/glyphs.d.ts +27 -0
- package/dist/controls/glyphs.d.ts.map +1 -0
- package/dist/controls/glyphs.js +49 -0
- package/dist/controls/index.d.ts +20 -0
- package/dist/controls/index.d.ts.map +1 -0
- package/dist/controls/index.js +46 -0
- package/dist/controls/multi-select-filter.d.ts +23 -0
- package/dist/controls/multi-select-filter.d.ts.map +1 -0
- package/dist/controls/multi-select-filter.js +89 -0
- package/dist/controls/number-filter.d.ts +26 -0
- package/dist/controls/number-filter.d.ts.map +1 -0
- package/dist/controls/number-filter.js +143 -0
- package/dist/controls/peer-boundary.test.d.ts +2 -0
- package/dist/controls/peer-boundary.test.d.ts.map +1 -0
- package/dist/controls/props-contract.test.d.ts +2 -0
- package/dist/controls/props-contract.test.d.ts.map +1 -0
- package/dist/controls/range-filter.d.ts +43 -0
- package/dist/controls/range-filter.d.ts.map +1 -0
- package/dist/controls/range-filter.js +135 -0
- package/dist/controls/range-filter.type-guard.test.d.ts +2 -0
- package/dist/controls/range-filter.type-guard.test.d.ts.map +1 -0
- package/dist/controls/search-filter.d.ts +10 -0
- package/dist/controls/search-filter.d.ts.map +1 -0
- package/dist/controls/search-filter.js +81 -0
- package/dist/controls/select-filter.d.ts +74 -0
- package/dist/controls/select-filter.d.ts.map +1 -0
- package/dist/controls/select-filter.js +10 -0
- package/dist/controls/sync-button.d.ts +38 -0
- package/dist/controls/sync-button.d.ts.map +1 -0
- package/dist/controls/sync-button.js +119 -0
- package/dist/controls/sync-button.module.css +63 -0
- package/dist/controls/sync-button.module.css.d.ts +11 -0
- package/dist/controls/sync-button.test.d.ts +2 -0
- package/dist/controls/sync-button.test.d.ts.map +1 -0
- package/dist/controls/toggle-filter.d.ts +9 -0
- package/dist/controls/toggle-filter.d.ts.map +1 -0
- package/dist/controls/toggle-filter.js +42 -0
- package/dist/controls/view-tabs.d.ts +23 -0
- package/dist/controls/view-tabs.d.ts.map +1 -0
- package/dist/controls/view-tabs.js +59 -0
- package/dist/controls-dates/date-range-picker.d.ts +4 -0
- package/dist/controls-dates/date-range-picker.d.ts.map +1 -0
- package/dist/controls-dates/date-range-picker.js +18 -0
- package/dist/controls-dates/index.d.ts +9 -0
- package/dist/controls-dates/index.d.ts.map +1 -0
- package/dist/controls-dates/index.js +4 -0
- package/dist/dashboard/delta-badge.d.ts +6 -12
- package/dist/dashboard/delta-badge.d.ts.map +1 -1
- package/dist/dashboard/delta-badge.js +1 -50
- package/dist/dashboard/index.d.ts +1 -1
- package/dist/dashboard/index.d.ts.map +1 -1
- package/dist/dashboard/index.js +3 -1
- package/dist/dashboard/settings-section.d.ts +6 -4
- package/dist/dashboard/settings-section.d.ts.map +1 -1
- package/dist/dashboard/settings-section.js +23 -16
- package/dist/dashboard/settings-section.module.css +10 -0
- package/dist/dashboard/settings-section.module.css.d.ts +1 -0
- package/dist/dashboard/settings-section.test.d.ts +2 -0
- package/dist/dashboard/settings-section.test.d.ts.map +1 -0
- package/dist/dashboard/stat-card.d.ts +95 -9
- package/dist/dashboard/stat-card.d.ts.map +1 -1
- package/dist/dashboard/stat-card.js +108 -49
- package/dist/dashboard/stat-card.module.css +153 -0
- package/dist/dashboard/stat-card.module.css.d.ts +15 -0
- package/dist/data/data-table.d.ts +37 -15
- package/dist/data/data-table.d.ts.map +1 -1
- package/dist/data/data-table.js +96 -58
- package/dist/guard/grace.test.d.ts +20 -0
- package/dist/guard/grace.test.d.ts.map +1 -0
- package/dist/guard/index.d.ts +66 -3
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +78 -7
- package/dist/guard/types.d.ts +3 -3
- package/dist/guard/types.d.ts.map +1 -1
- package/dist/index.d.ts +13 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -10
- package/dist/nav/types.d.ts +78 -2
- package/dist/nav/types.d.ts.map +1 -1
- package/dist/notifications/bell.d.ts +4 -1
- package/dist/notifications/bell.d.ts.map +1 -1
- package/dist/notifications/index.d.ts +1 -1
- package/dist/router-tanstack/field.d.ts +14 -0
- package/dist/router-tanstack/field.d.ts.map +1 -0
- package/dist/router-tanstack/field.js +4 -0
- package/dist/router-tanstack/index.d.ts +6 -2
- package/dist/router-tanstack/index.d.ts.map +1 -1
- package/dist/router-tanstack/index.js +4 -0
- package/dist/router-tanstack/multi-search-param-store.d.ts +4 -52
- package/dist/router-tanstack/multi-search-param-store.d.ts.map +1 -1
- package/dist/router-tanstack/multi-search-param-store.js +19 -27
- package/dist/router-tanstack/search-param-store.d.ts +3 -87
- package/dist/router-tanstack/search-param-store.d.ts.map +1 -1
- package/dist/router-tanstack/search-param-store.js +16 -45
- package/dist/router-tanstack/search-store.d.ts +84 -0
- package/dist/router-tanstack/search-store.d.ts.map +1 -0
- package/dist/router-tanstack/search-store.js +271 -0
- package/dist/router-tanstack/search-store.router.test.d.ts +2 -0
- package/dist/router-tanstack/search-store.router.test.d.ts.map +1 -0
- package/dist/router-tanstack/search-store.test.d.ts +2 -0
- package/dist/router-tanstack/search-store.test.d.ts.map +1 -0
- package/dist/router-tanstack/use-nav.d.ts +0 -2
- package/dist/router-tanstack/use-nav.d.ts.map +1 -1
- package/dist/router-tanstack/use-nav.js +34 -12
- package/dist/router-tanstack/use-nav.test.d.ts +2 -0
- package/dist/router-tanstack/use-nav.test.d.ts.map +1 -0
- package/dist/section/index.d.ts +2 -0
- package/dist/section/index.d.ts.map +1 -0
- package/dist/section/index.js +4 -0
- package/dist/section/section.d.ts +24 -0
- package/dist/section/section.d.ts.map +1 -0
- package/dist/section/section.js +126 -0
- package/dist/section/section.module.css +104 -0
- package/dist/section/section.module.css.d.ts +11 -0
- package/dist/section/section.test.d.ts +2 -0
- package/dist/section/section.test.d.ts.map +1 -0
- package/dist/shell/app-breadcrumbs.d.ts.map +1 -1
- package/dist/shell/app-breadcrumbs.js +18 -15
- package/dist/shell/app-header.module.css +77 -37
- package/dist/shell/app-header.module.css.d.ts +2 -1
- package/dist/shell/app-mobile-nav.d.ts +9 -2
- package/dist/shell/app-mobile-nav.d.ts.map +1 -1
- package/dist/shell/app-mobile-nav.js +98 -11
- package/dist/shell/app-mobile-nav.module.css +8 -0
- package/dist/shell/app-mobile-nav.module.css.d.ts +1 -0
- package/dist/shell/app-sidebar.d.ts +55 -28
- package/dist/shell/app-sidebar.d.ts.map +1 -1
- package/dist/shell/app-sidebar.js +116 -67
- package/dist/shell/app-sidebar.module.css +303 -20
- package/dist/shell/app-sidebar.module.css.d.ts +19 -1
- package/dist/shell/index.d.ts +55 -21
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +40 -18
- package/dist/shell/mobile-nav-model.d.ts +13 -2
- package/dist/shell/mobile-nav-model.d.ts.map +1 -1
- package/dist/shell/mobile-nav-model.js +15 -1
- package/dist/shell/nav-count-badge.d.ts +1 -1
- package/dist/shell/page-bar.d.ts +51 -0
- package/dist/shell/page-bar.d.ts.map +1 -0
- package/dist/shell/page-bar.js +154 -0
- package/dist/shell/page-bar.module.css +178 -0
- package/dist/shell/page-bar.module.css.d.ts +17 -0
- package/dist/shell/page-bar.test.d.ts +2 -0
- package/dist/shell/page-bar.test.d.ts.map +1 -0
- package/dist/shell/sidebar-block-model.d.ts +48 -0
- package/dist/shell/sidebar-block-model.d.ts.map +1 -0
- package/dist/shell/sidebar-block-model.js +39 -0
- package/dist/shell/sidebar-blocks.d.ts +32 -0
- package/dist/shell/sidebar-blocks.d.ts.map +1 -0
- package/dist/shell/sidebar-blocks.js +169 -0
- package/dist/shell/sidebar-search.d.ts +13 -1
- package/dist/shell/sidebar-search.d.ts.map +1 -1
- package/dist/shell/sidebar-search.js +34 -1
- package/dist/shell/sidebar-search.module.css +26 -0
- package/dist/shell/sidebar-search.module.css.d.ts +2 -0
- package/dist/state/fields.d.ts +565 -0
- package/dist/state/fields.d.ts.map +1 -0
- package/dist/state/fields.js +405 -0
- package/dist/state/fields.test.d.ts +2 -0
- package/dist/state/fields.test.d.ts.map +1 -0
- package/dist/state/persisted.d.ts +60 -0
- package/dist/state/persisted.d.ts.map +1 -0
- package/dist/state/persisted.js +123 -0
- package/dist/state/persisted.test.d.ts +2 -0
- package/dist/state/persisted.test.d.ts.map +1 -0
- package/dist/state.d.ts +11 -56
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +14 -120
- package/dist/styles.css +13 -0
- package/dist/surfaces.d.ts +106 -24
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +156 -19
- package/dist/theme/controls.module.css +16 -1
- package/dist/theme/ctl-theme.d.ts +32 -0
- package/dist/theme/ctl-theme.d.ts.map +1 -0
- package/dist/theme/ctl-theme.js +54 -0
- package/dist/theme/ctl-theme.test.d.ts +2 -0
- package/dist/theme/ctl-theme.test.d.ts.map +1 -0
- package/dist/theme/ctl-tier-coverage.test.d.ts +2 -0
- package/dist/theme/ctl-tier-coverage.test.d.ts.map +1 -0
- package/dist/theme/divider-law.test.d.ts +2 -0
- package/dist/theme/divider-law.test.d.ts.map +1 -0
- package/dist/theme/icon-slot.d.ts +44 -0
- package/dist/theme/icon-slot.d.ts.map +1 -0
- package/dist/theme/icon-slot.js +21 -0
- package/dist/theme/icon-slot.module.css +47 -0
- package/dist/theme/icon-slot.module.css.d.ts +8 -0
- package/dist/theme/icon-slot.test.d.ts +2 -0
- package/dist/theme/icon-slot.test.d.ts.map +1 -0
- package/dist/theme/index.d.ts +1 -0
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/index.js +82 -5
- package/dist/theme/layout-rhythm-css.test.d.ts +2 -0
- package/dist/theme/layout-rhythm-css.test.d.ts.map +1 -0
- package/dist/theme/segmented-control.module.css +20 -0
- package/dist/theme/shadow-surfaces.test.d.ts.map +1 -1
- package/dist/tokens/index.d.ts +12 -0
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +38 -8
- package/dist/tokens/palette.d.ts +97 -61
- package/dist/tokens/palette.d.ts.map +1 -1
- package/dist/tokens/palette.js +76 -38
- package/dist/tokens.css +10 -2
- package/dist/widget-header/delta-badge.d.ts +13 -0
- package/dist/widget-header/delta-badge.d.ts.map +1 -0
- package/dist/widget-header/delta-badge.js +29 -0
- package/dist/widget-header/delta-badge.test.d.ts +2 -0
- package/dist/widget-header/delta-badge.test.d.ts.map +1 -0
- package/dist/widget-header/index.d.ts +3 -0
- package/dist/widget-header/index.d.ts.map +1 -0
- package/dist/widget-header/index.js +6 -0
- package/dist/widget-header/no-mantine.test.d.ts +2 -0
- package/dist/widget-header/no-mantine.test.d.ts.map +1 -0
- package/dist/widget-header/widget-header.d.ts +67 -0
- package/dist/widget-header/widget-header.d.ts.map +1 -0
- package/dist/widget-header/widget-header.js +89 -0
- package/dist/widget-header/widget-header.module.css +350 -0
- package/dist/widget-header/widget-header.module.css.d.ts +25 -0
- package/dist/widget-header/widget-header.test.d.ts +2 -0
- package/dist/widget-header/widget-header.test.d.ts.map +1 -0
- package/llms.txt +30 -17
- package/package.json +14 -1
- package/src/charts/cursor/resolve.test.ts +29 -1
- package/src/charts/cursor/resolve.ts +38 -6
- package/src/charts/cursor/scope.tsx +6 -3
- package/src/charts/cursor/store.test.ts +19 -1
- package/src/charts/cursor/store.ts +10 -5
- package/src/charts/hooks/useChartCursor.ts +16 -8
- package/src/charts/index.ts +2 -2
- package/src/charts/primitives/CartesianChart.test.tsx +124 -0
- package/src/charts/primitives/ChartCard.test.tsx +66 -0
- package/src/charts/primitives/ChartCard.tsx +86 -146
- package/src/charts/sparklines/BarSparkline.tsx +43 -5
- package/src/cli/check-coverage.test.ts +297 -0
- package/src/cli/check-theme.test.ts +38 -14
- package/src/cli/index.ts +392 -16
- package/src/cli/init-sync.test.ts +97 -0
- package/src/connectivity/connectivity-indicator.tsx +16 -9
- package/src/content/index.ts +16 -3
- package/src/content/prose.module.css +7 -28
- package/src/content/prose.module.css.test.ts +8 -24
- package/src/controls/actions.module.css +62 -0
- package/src/controls/actions.module.css.d.ts +13 -0
- package/src/controls/actions.test.tsx +252 -0
- package/src/controls/actions.tsx +655 -0
- package/src/controls/actions.type-guard.test.ts +63 -0
- package/src/controls/compare-filter.tsx +64 -0
- package/src/controls/control-group.test.tsx +229 -0
- package/src/controls/control-group.tsx +63 -0
- package/src/controls/controls.module.css +542 -0
- package/src/controls/controls.module.css.d.ts +35 -0
- package/src/controls/controls.router.test.tsx +1053 -0
- package/src/controls/enum-filter.tsx +170 -0
- package/src/controls/filter-context.tsx +158 -0
- package/src/controls/filter-pill.tsx +120 -0
- package/src/controls/filter-set.test.tsx +472 -0
- package/src/controls/filter-set.tsx +225 -0
- package/src/controls/filter-sheet.tsx +285 -0
- package/src/controls/glyphs.tsx +81 -0
- package/src/controls/index.ts +49 -0
- package/src/controls/multi-select-filter.tsx +140 -0
- package/src/controls/number-filter.tsx +291 -0
- package/src/controls/peer-boundary.test.ts +85 -0
- package/src/controls/props-contract.test.tsx +137 -0
- package/src/controls/range-filter.tsx +237 -0
- package/src/controls/range-filter.type-guard.test.tsx +122 -0
- package/src/controls/search-filter.tsx +123 -0
- package/src/controls/select-filter.tsx +89 -0
- package/src/controls/sync-button.module.css +63 -0
- package/src/controls/sync-button.module.css.d.ts +11 -0
- package/src/controls/sync-button.test.tsx +183 -0
- package/src/controls/sync-button.tsx +197 -0
- package/src/controls/toggle-filter.tsx +60 -0
- package/src/controls/view-tabs.tsx +110 -0
- package/src/controls-dates/date-range-picker.tsx +42 -0
- package/src/controls-dates/index.ts +8 -0
- package/src/dashboard/delta-badge.tsx +4 -74
- package/src/dashboard/index.ts +6 -1
- package/src/dashboard/settings-section.module.css +10 -0
- package/src/dashboard/settings-section.module.css.d.ts +1 -0
- package/src/dashboard/settings-section.test.tsx +56 -0
- package/src/dashboard/settings-section.tsx +33 -30
- package/src/dashboard/stat-card.module.css +153 -0
- package/src/dashboard/stat-card.module.css.d.ts +15 -0
- package/src/dashboard/stat-card.test.tsx +298 -1
- package/src/dashboard/stat-card.tsx +289 -67
- package/src/data/data-table.test.tsx +96 -11
- package/src/data/data-table.tsx +228 -84
- package/src/guard/check-source.test.ts +159 -13
- package/src/guard/grace.test.ts +158 -0
- package/src/guard/index.ts +185 -29
- package/src/guard/types.ts +4 -2
- package/src/index.ts +51 -5
- package/src/nav/types.ts +84 -2
- package/src/notifications/bell.tsx +6 -3
- package/src/notifications/index.ts +1 -1
- package/src/router-tanstack/field.ts +35 -0
- package/src/router-tanstack/index.ts +28 -2
- package/src/router-tanstack/multi-search-param-store.test.ts +71 -3
- package/src/router-tanstack/multi-search-param-store.ts +46 -77
- package/src/router-tanstack/search-param-store.ts +42 -159
- package/src/router-tanstack/search-store.router.test.tsx +882 -0
- package/src/router-tanstack/search-store.test.ts +578 -0
- package/src/router-tanstack/search-store.ts +546 -0
- package/src/router-tanstack/use-nav.test.tsx +187 -0
- package/src/router-tanstack/use-nav.ts +72 -14
- package/src/section/index.ts +1 -0
- package/src/section/section.module.css +104 -0
- package/src/section/section.module.css.d.ts +11 -0
- package/src/section/section.test.tsx +204 -0
- package/src/section/section.tsx +220 -0
- package/src/shell/app-breadcrumbs.tsx +44 -31
- package/src/shell/app-header.module.css +77 -37
- package/src/shell/app-header.module.css.d.ts +2 -1
- package/src/shell/app-mobile-nav.module.css +8 -0
- package/src/shell/app-mobile-nav.module.css.d.ts +1 -0
- package/src/shell/app-mobile-nav.tsx +147 -8
- package/src/shell/app-sidebar-account.tsx +2 -2
- package/src/shell/app-sidebar.module.css +303 -20
- package/src/shell/app-sidebar.module.css.d.ts +19 -1
- package/src/shell/app-sidebar.test.tsx +456 -41
- package/src/shell/app-sidebar.tsx +276 -140
- package/src/shell/index.test.tsx +291 -5
- package/src/shell/index.tsx +137 -38
- package/src/shell/mobile-nav-model.test.ts +228 -7
- package/src/shell/mobile-nav-model.ts +53 -3
- package/src/shell/nav-count-badge.tsx +1 -1
- package/src/shell/page-bar.module.css +178 -0
- package/src/shell/page-bar.module.css.d.ts +17 -0
- package/src/shell/page-bar.test.tsx +316 -0
- package/src/shell/page-bar.tsx +315 -0
- package/src/shell/sidebar-block-model.ts +83 -0
- package/src/shell/sidebar-blocks.tsx +301 -0
- package/src/shell/sidebar-search.module.css +26 -0
- package/src/shell/sidebar-search.module.css.d.ts +2 -0
- package/src/shell/sidebar-search.tsx +71 -2
- package/src/state/fields.test.tsx +512 -0
- package/src/state/fields.ts +1150 -0
- package/src/state/persisted.test.ts +91 -0
- package/src/state/persisted.ts +261 -0
- package/src/state.ts +52 -240
- package/src/styles.css +13 -0
- package/src/surfaces.ts +204 -36
- package/src/theme/controls.module.css +16 -1
- package/src/theme/ctl-theme.test.tsx +275 -0
- package/src/theme/ctl-theme.tsx +138 -0
- package/src/theme/ctl-tier-coverage.test.ts +170 -0
- package/src/theme/density-relations.test.ts +106 -51
- package/src/theme/divider-law.test.ts +259 -0
- package/src/theme/icon-slot.module.css +47 -0
- package/src/theme/icon-slot.module.css.d.ts +8 -0
- package/src/theme/icon-slot.test.tsx +275 -0
- package/src/theme/icon-slot.tsx +59 -0
- package/src/theme/index.ts +165 -3
- package/src/theme/layout-rhythm-css.test.ts +72 -0
- package/src/theme/radius.test.ts +11 -1
- package/src/theme/segmented-control.module.css +20 -0
- package/src/theme/shadow-surfaces.test.ts +20 -7
- package/src/theme/spacing.test.ts +75 -27
- package/src/theme/use-basalt-spacing.ts +2 -2
- package/src/tokens/density.test.ts +7 -25
- package/src/tokens/index.ts +48 -8
- package/src/tokens/palette.ts +107 -73
- package/src/widget-header/delta-badge.test.tsx +60 -0
- package/src/widget-header/delta-badge.tsx +57 -0
- package/src/widget-header/index.ts +2 -0
- package/src/widget-header/no-mantine.test.ts +33 -0
- package/src/widget-header/widget-header.module.css +350 -0
- package/src/widget-header/widget-header.module.css.d.ts +25 -0
- package/src/widget-header/widget-header.test.tsx +187 -0
- package/src/widget-header/widget-header.tsx +213 -0
- package/agent/rules/basalt-agent.md +0 -452
- package/agent/rules/basalt-app.md +0 -246
- package/agent/rules/basalt-commands.md +0 -320
- package/agent/rules/basalt-content.md +0 -409
- package/agent/rules/basalt-data.md +0 -329
- package/agent/rules/basalt-forms.md +0 -186
- package/agent/rules/basalt-notifications.md +0 -264
- package/agent/rules/basalt-query.md +0 -187
- package/agent/rules/basalt-router.md +0 -336
- package/dist/content/article-filter-bar.d.ts +0 -40
- package/dist/content/article-filter-bar.d.ts.map +0 -1
- package/dist/content/article-filter-bar.js +0 -56
- package/dist/shell/page-header.d.ts +0 -13
- package/dist/shell/page-header.d.ts.map +0 -1
- package/dist/shell/page-header.js +0 -23
- package/src/content/article-filter-bar.tsx +0 -88
- package/src/shell/page-header.tsx +0 -35
package/MIGRATING.md
CHANGED
|
@@ -32,13 +32,755 @@ at 1.3.0, `./agent-chat` at 1.10.0.
|
|
|
32
32
|
|
|
33
33
|
## Unreleased
|
|
34
34
|
|
|
35
|
-
**
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
**Nothing removed. Two exports added (`NumberFilter`, and three members on the number handle),
|
|
36
|
+
one member added to every `FieldHandle` (`clear()`), four `StatCard` props added, and enforcement
|
|
37
|
+
tightened: five oxlint rules plus one guard kind became `error`.** The store half of this minor also
|
|
38
|
+
fixed four inference and notification defects consumers hit while porting, plus two ways a fallback
|
|
39
|
+
got pinned into localStorage — see § Stores below. Every level change honours the `theme-allow`
|
|
40
|
+
grammar unchanged; only the severity of an unwaived finding moves.
|
|
41
|
+
|
|
42
|
+
### Stores — the `custom` flag, patched writes, lazy fallbacks, derived windows
|
|
43
|
+
|
|
44
|
+
**Additive, except one type that got narrower on purpose.** `field.range({ presets, fallback })`
|
|
45
|
+
written INLINE inside `createSearchStore({ fields })` used to infer `custom` as `boolean` — the
|
|
46
|
+
contextual `AnyField` return type won over the argument — so every value and search type carried a
|
|
47
|
+
`'custom'` preset the field itself rejects at runtime, and `RangeFilter field={…}` needed a cast or
|
|
48
|
+
an explicit `custom: false`. `field.range` is now three overloads: an omitted or `false` flag is
|
|
49
|
+
`false`, a literal `true` is `true`, a value typed `boolean` stays widened.
|
|
50
|
+
|
|
51
|
+
- **Delete the workaround, not the field.** An explicit `custom: false` still works (same
|
|
52
|
+
overload); a cast on `<RangeFilter field={…} />` can go, and `RangeFilterProps<P, C>` is generic
|
|
53
|
+
over the flag so a preset-only handle binds directly.
|
|
54
|
+
- **What can newly type-error:** code that read `'custom'` out of a range whose field never declared
|
|
55
|
+
it — `if (search.range === 'custom')` on a preset-only field, or a `RangeValue<P | 'custom'>`
|
|
56
|
+
annotation over one. That branch was always dead at runtime; the type just stopped pretending.
|
|
57
|
+
- **…and one more, in a consumer's own WRAPPER.** A prop typed `FieldHandle<RangeField<P>>` — `C`
|
|
58
|
+
defaulting to `boolean` — no longer accepts a handle from a `field.range` without `custom: true`,
|
|
59
|
+
because that handle's setter is now `RangeValue<P>`-typed and a setter is contravariant. Pin
|
|
60
|
+
`FieldHandle<RangeField<P, false>>` for the preset-only shape, `RangeField<P, true>` for a
|
|
61
|
+
picker-backed one, or make the wrapper generic over the flag
|
|
62
|
+
(`<P extends string, C extends boolean>` + `FieldHandle<RangeField<P, C>>`), which is what
|
|
63
|
+
`RangeFilter` does. An existing `as FieldHandle<RangeField<P>>` cast keeps compiling.
|
|
64
|
+
|
|
65
|
+
**`toWindow()` resolves derived presets.** `field.range({ window: { '3m': (now) => ({ from, to }) } })`
|
|
66
|
+
makes `toWindow({ preset: '3m' })` return `{ from, to }` (ISO dates, resolved at call time with the
|
|
67
|
+
current `Date`) while `3m` stays one preset in the URL. A preset with no resolver keeps
|
|
68
|
+
`{ window: preset }`, in the same field — which is what retires the last hand-rolled
|
|
69
|
+
`presetToParams` a consumer kept beside the store for `3m` / `6m` / `1y` / `ytd`.
|
|
70
|
+
|
|
71
|
+
**The resolved presets are gone from the return TYPE too**, which is the half that decides whether
|
|
72
|
+
the switch is deleted or the cast just moves: the field now carries its resolver keys (`RangeField`'s
|
|
73
|
+
fifth type parameter, inferred from `window`), and `toWindow` returns
|
|
74
|
+
`{ window: Exclude<P, W> } | { from; to }`. A `resolveWindow` whose target is
|
|
75
|
+
`{ window: '7d' | '30d' | '90d' | 'all' } | { from; to }` becomes a one-line delegation with no
|
|
76
|
+
`as`. One guard survives on a `custom: true` field: a `'custom'` preset that arrives WITHOUT dates
|
|
77
|
+
resolves to `{ window: 'custom' }`, so `'custom'` stays in the union by construction.
|
|
78
|
+
|
|
79
|
+
**A field setter takes an optional second argument.** `set(next, { patch })` merges extra search
|
|
80
|
+
params into the SAME navigate — URL lane only, for keys the store does not own (`{ patch: {
|
|
81
|
+
detailDate: undefined } }` clears a sibling param the page put there). The field's own params always
|
|
82
|
+
win over the patch, so it cannot corrupt the value being set. A local/memory-lane write has no
|
|
83
|
+
navigate to merge into and ignores it.
|
|
84
|
+
|
|
85
|
+
Two limits are now enforced rather than documented. A patch key **another field of the same store
|
|
86
|
+
owns** is refused — it would reach the URL while the store's own write path never touched the mirror,
|
|
87
|
+
so the next paramless visit and every `linkSearch` link resolve the OLD value: `createSearchStore`
|
|
88
|
+
throws in dev, and in production keeps the write and logs it once. Write that field through its own
|
|
89
|
+
setter instead. And a patched write **from a route that does not validate the field** persists (A1)
|
|
90
|
+
but drops the patch with it — there is no navigate to merge into — which now warns once per field in
|
|
91
|
+
dev instead of doing nothing.
|
|
92
|
+
|
|
93
|
+
**A fallback may be a thunk.** `field.*({ fallback: () => T })` (every kind — `field.number`'s codec
|
|
94
|
+
froze its thunk at store definition in the first cut of this minor and does not now) is resolved at
|
|
95
|
+
READ time and re-resolved while nothing is written, and is never persisted on its own — a write
|
|
96
|
+
stores what the control produced, as before. **Local and memory lanes only:** `createSearchStore` now
|
|
97
|
+
THROWS at definition for a thunk on a URL-lane field, because `validateSearch` would evaluate it on
|
|
98
|
+
every navigation and pin the result into the URL. Move the field off the URL lane (`{ url: false }`,
|
|
99
|
+
or `createLocalStore`) or pass a value.
|
|
100
|
+
|
|
101
|
+
**`FieldHandle` gained `clear()`, and a reset now UNSETS.** `useReset()` used to write
|
|
102
|
+
`entry.codec.fallback` into the mirror for every persisted field, and every control's reset called
|
|
103
|
+
`setValue(field.fallback)` — both of which pin a RESOLVED thunk: pressing `Reset all` over
|
|
104
|
+
`field.string({ fallback: () => todayIso() }, { url: false })` stored today's date, so tomorrow the
|
|
105
|
+
field read a value nobody chose and counted as active in `Filters (n)`. Both paths now delete the
|
|
106
|
+
key instead (the memory lane always did), and `clear()` is the per-field door: the persist lane
|
|
107
|
+
deletes its key, the memory lane drops its value, the URL lane navigates back to the fallback
|
|
108
|
+
params.
|
|
109
|
+
|
|
110
|
+
Nothing to change at a call site — every shipped control's reset calls it. A consumer that
|
|
111
|
+
hand-rolled a reset should swap `setValue(field.fallback)` for `field.clear()`; a hand-built
|
|
112
|
+
`FieldHandle` (a facet column, a test double) must add the member, which is the one type-error this
|
|
113
|
+
adds.
|
|
114
|
+
|
|
115
|
+
**`createPersistedState` notifies across instances (bug fix).** Two instances over one key — a
|
|
116
|
+
page's store and a widget's own state naming the same key, or a store re-exported from two modules —
|
|
117
|
+
kept per-instance listener sets, so a write through one left the other's components rendering the
|
|
118
|
+
stale value **in the same tab** while the cross-tab `storage` path worked. Subscribers are now
|
|
119
|
+
registered per storage key. No API change; if you were re-rendering something by hand to work around
|
|
120
|
+
this, delete it.
|
|
121
|
+
|
|
122
|
+
**`RangeFilter` warns once in dev** when the field declares `custom: true` and no `customPicker` was
|
|
123
|
+
injected: the custom window is then unreachable (no popover picker, no `Custom range…` row in the
|
|
124
|
+
sheet). Preset-only remains a legal configuration — the picker is injected precisely because
|
|
125
|
+
`@mantine/dates` may be absent — so this is a warning, not a type error.
|
|
126
|
+
|
|
127
|
+
### Controls — `NumberFilter`, the `field.number` lane
|
|
128
|
+
|
|
129
|
+
**One export added, nothing removed.** `field.number` has existed since the store landed and had no
|
|
130
|
+
control, so both consumers that needed one wrote around it in opposite directions: linewatch kept a
|
|
131
|
+
raw `SegmentedControl` over `minDuration` (a `control-outside-home` warn, law C1), and argo widened
|
|
132
|
+
`nights` into a string enum — which puts `'3'` in the URL and makes every downstream comparison a
|
|
133
|
+
parse.
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
import { NumberFilter } from 'basalt-ui/controls'
|
|
137
|
+
|
|
138
|
+
// A preset set — a pill plus a radio list, the same body every enum filter renders.
|
|
139
|
+
<NumberFilter field={booking.field.nights} label="Nights"
|
|
140
|
+
options={[{ value: 1, label: '1 night' }, { value: 7, label: 'A week' }]} />
|
|
141
|
+
|
|
142
|
+
// No `options` → a pill whose popover holds a `ctl` NumberInput; the sheet renders it full-width.
|
|
143
|
+
<NumberFilter field={lines.field.minDuration} label="Min duration" step={30} />
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Three properties worth knowing before porting a call site:
|
|
147
|
+
|
|
148
|
+
- **The URL keeps a NUMBER.** `options` takes `{ value: number; label: string }`, and a numeral
|
|
149
|
+
STRING there is a type error — the whole reason to stop widening a threshold into an enum.
|
|
150
|
+
- **The stepper applies on blur or Enter, never per keystroke.** A number is typed digit by digit, so
|
|
151
|
+
a live write would navigate on `4`, `42`, `420` — three loader runs for one intended threshold,
|
|
152
|
+
two of them values nobody meant. `SearchFilter` debounces; a number has an explicit commit point.
|
|
153
|
+
- **`min`/`max`/`int` come off the HANDLE, not the call site.** They are deliberately not props: the
|
|
154
|
+
field is what validates the URL, and a second copy at the call site is a second answer to the same
|
|
155
|
+
question. The number handle republishes all three (see § Stores — the number handle below), so the
|
|
156
|
+
stepper stops at the field's limit and an `int` field refuses decimals outright. A TYPED value is
|
|
157
|
+
clamped to those bounds when it commits, so the box reads what the store holds even when the store
|
|
158
|
+
does not move — a second out-of-range `9999` against `max: 600` over a stored `600` writes nothing,
|
|
159
|
+
and the readout still corrects itself to `600`. The codec's clamp stays the backstop for a value
|
|
160
|
+
that never came through the box at all — a hand-typed URL, a stale deep link.
|
|
161
|
+
|
|
162
|
+
### Stores — the number handle republishes its bounds
|
|
163
|
+
|
|
164
|
+
**Three members added to `FieldHandle<NumberField>`, nothing removed.** `min`, `max` and `int` now
|
|
165
|
+
sit on the handle, filled from the field declaration (`NumberHandleExtras`, the shape
|
|
166
|
+
`RangeHandleExtras`/`toWindow` already had). A control never sees the field descriptor — only the
|
|
167
|
+
handle — so before this the only way one could learn its own limits was for the call site to pass
|
|
168
|
+
them a second time.
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
const nights = store.field.nights // field.number({ fallback: 2, min: 1, max: 14, int: true })
|
|
172
|
+
nights.min // 1 — `undefined` when the field declared none
|
|
173
|
+
nights.max // 14
|
|
174
|
+
nights.int // true — `false`, never `undefined`, on a number handle
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Non-number handles carry all three as `undefined`, the same way `toWindow` is `undefined` off a
|
|
178
|
+
range handle. That is a type-level fact, not just a runtime one, and it matters for one collision in
|
|
179
|
+
particular: `StringField` has its own `max`, which is NOT republished — a string handle's `max` is
|
|
180
|
+
`undefined`.
|
|
181
|
+
|
|
182
|
+
Nothing about the clamp changed. The codec has always clamped a number to the declared bounds on
|
|
183
|
+
write, and still does; the handle is a readout of that law, which is what lets `NumberFilter` bound
|
|
184
|
+
its stepper instead of letting the user watch the correction happen.
|
|
185
|
+
|
|
186
|
+
### `StatCard` — `unit`, `breakdown` and the delta's own format
|
|
187
|
+
|
|
188
|
+
**Four props added, nothing removed or renamed; every one defaults to today's rendering when
|
|
189
|
+
omitted.**
|
|
190
|
+
`value` is a pre-formatted `string`, so a card had exactly one text channel under the hero row
|
|
191
|
+
(`subtitle`) and three consumers wanting a unit AND a basis AND a split hand-rolled the card instead
|
|
192
|
+
— which is the fork `shadow-basalt-export` reports as a `HeroCard`.
|
|
193
|
+
|
|
194
|
+
| Prop | Type | Renders |
|
|
195
|
+
| ------------- | ------------------------------------------------------------------ | ----------------------------------------------------------------------------- |
|
|
196
|
+
| `unit` | `string` | after the value on the hero row — muted, mono, `--vx-text-sm` |
|
|
197
|
+
| `breakdown` | `readonly { label: string; value: string; tone?: StatCardTone }[]` | compact rows under the hero, one `controlHeightTag` line each |
|
|
198
|
+
| `deltaFormat` | `(delta: number) => string` | the delta chip's label. Default unchanged: `Math.abs(delta).toFixed(1) + '%'` |
|
|
199
|
+
| `deltaGlyph` | `boolean` | the chip's ▲/▼. Default `true`; a zero delta never shows one |
|
|
200
|
+
|
|
201
|
+
`unit` is forwarded to `WidgetHeader`, which gained the same prop, so `ChartCard` and `Section`
|
|
202
|
+
headers get it too. It is NOT the basis: `412` + `TSS` is the pair, and `7-day rolling` is still
|
|
203
|
+
`subtitle`. A thousands separator, a currency symbol or a `%` still belong in `value`.
|
|
204
|
+
|
|
205
|
+
`breakdown` draws **no hairlines** — §2.1 puts a horizontal rule between OPTION rows and nowhere
|
|
206
|
+
else, and `theme/divider-law.test.ts` inventories every one basalt draws. The rows separate by
|
|
207
|
+
weight (muted label, mono ink value) instead. Keep it to two or three: past that the card is a
|
|
208
|
+
table, and a table is `BasaltDataTable` in a `Section`. A row's `tone` reads the same per-scheme
|
|
209
|
+
`--vx-status-*` solid the card's own rail does, and omitting it is untinted, never `'good'`.
|
|
210
|
+
|
|
211
|
+
`deltaFormat` exists because **a delta is not always a percentage, and `StatCard` claimed it was**:
|
|
212
|
+
`delta={0.3}` on a pace or speed card rendered `▲0.3%`, a wrong unit on a KPI — worse than no chip —
|
|
213
|
+
so the one consumer that needed it kept the card hand-rolled and took the `shadow-basalt-export` warn
|
|
214
|
+
instead. `DeltaBadge` had `format` all along; neither `WidgetHeader` nor `StatCard` forwarded it. Both now do.
|
|
215
|
+
`ChartCard`/`Section` are unchanged: each declares its own header props and forwards them one by
|
|
216
|
+
one, so a delta-bearing `ChartCard` or `Section` still prints the percentage — ask if you need it
|
|
217
|
+
there.
|
|
218
|
+
|
|
219
|
+
```tsx
|
|
220
|
+
// A percentage, unchanged — no prop, no move.
|
|
221
|
+
<StatCard title="Volume" value="1,204" delta={4.2} deltaPeriod="WoW" />
|
|
222
|
+
|
|
223
|
+
// An absolute delta. The formatter gets the SIGNED number, so it may print the sign itself;
|
|
224
|
+
// `deltaGlyph={false}` then stops the ▼ saying the same thing twice. No `deltaLabel` prop exists —
|
|
225
|
+
// the function IS the escape hatch, and `delta` stays the number that drives the tone.
|
|
226
|
+
<StatCard title="Pace" value="5:31" unit="/km" delta={-12} deltaGlyph={false}
|
|
227
|
+
deltaFormat={(s) => `${s < 0 ? '−' : '+'}0:${String(Math.abs(s)).padStart(2, '0')} /km`} />
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Guards — the 1.27.0 promotions, one narrowing and one new exemption
|
|
231
|
+
|
|
232
|
+
**Five plugin rules and one guard kind became `error` in the shipped preset.** Their
|
|
233
|
+
`PLUGIN_RULE_GRACE` / `GRACE_PERIOD_KINDS` entries are deleted, which IS the promotion (C16). A
|
|
234
|
+
`theme-allow` written against any of them still works unchanged; only the severity of an UNWAIVED
|
|
235
|
+
finding moves.
|
|
236
|
+
|
|
237
|
+
| Promoted to `error` | Waiver, if the finding is deliberate |
|
|
238
|
+
| -------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
239
|
+
| `basalt/control-size-literal` | `theme-allow control-size-literal — <why>` above the node, or `theme-allow-file` for the file |
|
|
240
|
+
| `basalt/in-body-page-title` | `theme-allow in-body-page-title — <why>` — ONE id, so it waives the guard kind's lane too |
|
|
241
|
+
| `in-body-page-title` (kind) | the same annotation; the two lanes share the id by construction |
|
|
242
|
+
| `basalt/responsive-twin` | `theme-allow responsive-twin — <why>`; the real fix is deleting one mount |
|
|
243
|
+
| `basalt/search-literal-link` | `theme-allow search-literal-link — <why>`; the real fix is `search: <store>.linkSearch` |
|
|
244
|
+
| `basalt/use-search-from-literal` | `theme-allow use-search-from-literal — <why>`; the real fix is the param as a prop |
|
|
245
|
+
|
|
246
|
+
The commonest in-body-title case that is NOT a defect: **a shell-less surface naming itself** — an
|
|
247
|
+
auth gate, an error boundary, a print view. There is no breadcrumb to carry the name, so write
|
|
248
|
+
`theme-allow in-body-page-title — shell-less surface`.
|
|
249
|
+
|
|
250
|
+
**`control-size-literal` also gained one exemption in the same minor, and it removes a waiver rather
|
|
251
|
+
than adding one: a slot owned by `ChartCard` no longer reports.** `ChartCard` lives inside the
|
|
252
|
+
Mantine-free chart layer, so its `actions` slot writes `data-basalt-tier` by hand and cannot mount
|
|
253
|
+
the tier theme at all — a `Switch` there with no `size` renders at Mantine's default, not at `ctl`.
|
|
254
|
+
The rule's message ("the HOME sets the tier, drop the prop") was simply false in that one slot, and
|
|
255
|
+
because the rule fires on the `size` ATTRIBUTE it could not tell a correct `size="ctl"` from the
|
|
256
|
+
`size="xs"` it exists to catch. Every other home still reports, and the owner test reads the
|
|
257
|
+
IMPORTED name so `ChartCard as Card` is exempt too. `hand-rolled-filter` is deliberately NOT
|
|
258
|
+
exempted there: a raw `Select` in `ChartCard.actions` is still a filter that should take a `field`.
|
|
259
|
+
A HOISTED binding is exempt only when EVERY basalt home it was handed to is `ChartCard` — one
|
|
260
|
+
`const acts = <Button size="xs"/>` given to both `<ChartCard actions>` and `<Section actions>` still
|
|
261
|
+
renders in a tiered slot, and keying the exemption on a single owner made the verdict depend on which
|
|
262
|
+
attribute came later in the file.
|
|
263
|
+
|
|
264
|
+
**`basalt/control-outside-home` and `raw-selection-control` did NOT promote. They are re-dated to
|
|
265
|
+
1.28.0, and the reason is a measurement rather than caution:** the wave-7 consumer run left 9 warns
|
|
266
|
+
in argo, and every one is a control inside a modal/form module whose `<Modal>` is rendered by the
|
|
267
|
+
PARENT route — law C1's cross-file case, which is advisory by declaration because no scan of one
|
|
268
|
+
file can see it. Both lanes now exempt a file whose BASENAME matches
|
|
269
|
+
`*-{modal,drawer,popover,panel,form}.tsx`, which is the convention those nine already follow.
|
|
270
|
+
Outside the convention, an overlay body declares itself:
|
|
271
|
+
`theme-allow-file control-outside-home — overlay`. The trade is stated plainly: a whole file goes
|
|
272
|
+
unscanned on a naming convention, so a `Select` that genuinely belongs in a page bar goes unreported
|
|
273
|
+
if it lives in `filters-panel.tsx` — the same bargain the `@mantine/form` exemption already buys,
|
|
274
|
+
and a smaller one than promoting a rule with 9 known false positives.
|
|
275
|
+
|
|
276
|
+
**`basalt/shadow-basalt-export` narrowed, and it stays a permanent advisory `warn`.** An ALIAS hit is
|
|
277
|
+
now skipped when the file imports the basalt export it renames **and REFERENCES that binding as a
|
|
278
|
+
value**: a `HeroCard` that composes `StatCard` is a wrapper, not a fork, and it was already following
|
|
279
|
+
the advice the message gives. Composition, not import — three shapes stay reported, and they are the
|
|
280
|
+
ones a fork actually writes: a type-only `import type { StatCard }` (or an inline `type` specifier)
|
|
281
|
+
feeding `ComponentProps<typeof StatCard>`, a VALUE import used only in a type position, and a dead
|
|
282
|
+
import left behind after the body was re-rolled. None of them render anything. A reference does not
|
|
283
|
+
have to be a JSX tag — `component={StatCard}` and `createElement(StatCard)` compose it too. The
|
|
284
|
+
provenance test reads the IMPORTED name, so `import { StatCard as Base }` counts. The name-COLLISION
|
|
285
|
+
half is deliberately NOT exempted the same way — a local `StatCard` beside an
|
|
286
|
+
`import { StatCard as Base }` kept the name AND a piece of the original, which is the fork shape this
|
|
287
|
+
rule most wants to see.
|
|
288
|
+
|
|
289
|
+
## 1.26.0 — the control tier, the page bar and the store
|
|
290
|
+
|
|
291
|
+
**One export removed and two deprecated — see § Stores below; two shell PROPS removed — see
|
|
292
|
+
§ Sidebar. Two other behaviour changes: the grace ledgers changed shape, and nine
|
|
293
|
+
long-stale entries (D4, `docs/archive/CONTROLS-SYNTHESIS.md`) are promoted.** C16
|
|
294
|
+
(`docs/CONTROLS-SPEC.md` §1) is the new law behind both: a grace entry now carries `{ since,
|
|
295
|
+
promote, why }` (semver strings) instead of a bare promotion-note string, and a version-gated test
|
|
296
|
+
fails the build once `package.json`'s version reaches an entry's `promote` while the entry is still
|
|
297
|
+
in the ledger — and `make release` refuses to cut a release whose COMPUTED version has reached one,
|
|
298
|
+
which is the half that fires before the release rather than one minor after it. That is what stops a
|
|
299
|
+
rule sitting at `warn` for five minors with nothing tracking the promise, which is exactly what
|
|
300
|
+
happened to the nine rows below.
|
|
301
|
+
|
|
302
|
+
**`PLUGIN_RULE_GRACE`** (`configs/oxlint-plugin.js`) **and `GRACE_PERIOD_KINDS`**
|
|
303
|
+
(`src/guard/index.ts`) no longer carry any PRE-EXISTING entry — every one either promoted to `error`
|
|
304
|
+
(the table below) or moved to the new, permanent `PLUGIN_RULE_ADVISORY` ledger
|
|
305
|
+
(`shadow-basalt-export`). Both ledgers now hold only the wave-6 control guards listed under
|
|
306
|
+
§ Guards — six plugin rules and two guard kinds, each `{ since: '1.26.0', promote: '1.27.0' }` —
|
|
307
|
+
which the C16 gate forced to promote or be deleted at 1.27.0. Five rules and one kind promoted there;
|
|
308
|
+
the C1 pair was re-dated to 1.28.0 against a measurement (see `## Unreleased` above). A theme-allow escape written against
|
|
309
|
+
any of these still works unchanged; only the SEVERITY of an unwaived finding moves from `warn` to
|
|
310
|
+
`error`.
|
|
311
|
+
|
|
312
|
+
| Rule / kind (promoted) | What now errors | Escape hatch |
|
|
313
|
+
| -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
314
|
+
| `basalt/hand-rolled-plot` | An unwaived chart-assembly node (`AxisLeftNumeric`, `AxisRightNumeric`, `AxisBottomDate`, `HoverOverlay`, `Crosshair`) outside a file composing `CartesianChart` | `theme-allow hand-rolled-plot — <why>` per node, or `theme-allow-file hand-rolled-plot — <why>` for a declared exception |
|
|
315
|
+
| `basalt/chart-legend-literal` | A hand-written array literal passed to `ChartLegend`'s `items` | Derive the legend from `series` (`deriveLegend`, or let `ChartFrame`/`CartesianChart` do it), or `theme-allow chart-legend-literal — <why>` |
|
|
316
|
+
| `basalt/hand-rolled-shell` | A hand-rolled app-shell assembly node instead of `BasaltShell` | `theme-allow hand-rolled-shell — <why>`, or `theme-allow-file` for the whole file |
|
|
317
|
+
| `theme-allow-unscoped` (guard kind) | A bare `// theme-allow` with no rule id | Name the kind(s): `theme-allow <kind> — <why>` |
|
|
318
|
+
| `surface-shadow-override` (guard kind) | A hand-composed `boxShadow` reaching a `--vx-*` token | Use the shipped shadow tokens, or waive with a reason |
|
|
319
|
+
| `css-raw-surface` (guard kind) | A raw surface color in kebab CSS (`.module.css`) | Same remedy as the JS/TSX form — a `--vx-*` var, or waive |
|
|
320
|
+
| `inline-font-size` (guard kind) | A hardcoded numeric font size in an inline `style` object | `VX.text.*` / `--vx-text-*`, or waive |
|
|
321
|
+
| `hidden-inline-style` (guard kind) | A style object defined once and spread into `style={...}` (evades the guard's inline-object scan) | Inline the object at the call site, or waive |
|
|
322
|
+
|
|
323
|
+
**`basalt/shadow-basalt-export` is unaffected** — it moves ledgers (`PLUGIN_RULE_GRACE` →
|
|
324
|
+
`PLUGIN_RULE_ADVISORY`), not severity. It stays `warn` in the shipped preset, permanently, and is
|
|
325
|
+
never subject to the C16 gate: see "Lint and guard rules that tightened" below for why.
|
|
326
|
+
|
|
327
|
+
Consumers who never overrode `basalt.severity` for any of the eight kinds/rules above and never
|
|
328
|
+
waived them see these findings for the first time as `error`; measure against the shipped preset
|
|
329
|
+
before upgrading if that's a concern (`basalt-ui check-theme --audit-allows`, `oxlint .`).
|
|
330
|
+
|
|
331
|
+
### Composers — WidgetHeader-backed renames (docs/CONTROLS-SPEC.md §2.2)
|
|
332
|
+
|
|
333
|
+
`StatCard`, `ChartCard`, `Section` (new), `SettingsSection`/`DangerZone` and `BasaltDataTable` now
|
|
334
|
+
each compose the wave-1 `WidgetHeader` primitive for their title row instead of hand-rolling one —
|
|
335
|
+
several props renamed or were added to line up with it.
|
|
336
|
+
|
|
337
|
+
| Component | Removed / renamed | Replacement |
|
|
338
|
+
| -------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
339
|
+
| `StatCard` | `label` | `title` |
|
|
340
|
+
| `StatCard` | `menu` | `actions` (wrapped in `CtlSlot`) |
|
|
341
|
+
| `StatCard` | — | adds `icon`, `info`, `subtitle` (forwarded to the composed `WidgetHeader` — the info glyph beside the title, the muted unit line under the hero row), `sparklinePlacement?: 'bleed' \| 'right'` (default `'bleed'`, collapses to it below `sm`) |
|
|
342
|
+
| `ChartCard` | `tooltip` | `info` |
|
|
343
|
+
| `ChartCard` | `extra` | `actions` (carries `data-basalt-tier="widget"`, no `CtlSlot` — `./charts` stays Mantine-free) |
|
|
344
|
+
| `ChartCard` | `title: string` (required) | `title?: string` — the header now renders only when title/info/value/actions/icon/count is set |
|
|
345
|
+
| `ChartCard` | — | adds `icon`, `value`, `delta`, `deltaPeriod`, `count` |
|
|
346
|
+
| `SettingsSection` / `DangerZone` | `description` | `subtitle` |
|
|
347
|
+
| `SettingsSection` / `DangerZone` | — | adds `actions` (wrapped in `CtlSlot`) |
|
|
348
|
+
| `BasaltDataTable` | `toolbarActions` | `actions?: ReactNode` — the toolbar (search + facets + `actions`) is now wrapped in `CtlSlot`. Typed `ReactNode`, not `BarAction[]` — a table toolbar is not a `PageBar`/`Section`, and `ActionGroup`'s shell-extras semantics (primary/secondary, mobile folding) must not leak into it |
|
|
349
|
+
| `BasaltDataTable` | fixed `w={220/200/180/110}` on toolbar/pagination controls | removed — the controls size by content |
|
|
350
|
+
| `BasaltDataTable` | — | adds `title`, `icon`, `subtitle` — when `title` is set, renders `WidgetHeader tier="widget"` above the toolbar, `count` always `table.getRowCount()` (C11) |
|
|
351
|
+
| `BasaltDataTable` | `facets` rendered as raw `Select`/`MultiSelect` | rendered as `EnumFilter`/`MultiSelectFilter` pills inside a `FilterSet` (`docs/CONTROLS-SPEC.md` §3) — a synthetic `"All"` option stands in for a single-select facet's cleared state, since a closed enum field has no member for "unset" |
|
|
352
|
+
|
|
353
|
+
New export: **`Section`** (`.`, Mantine-coupled) — `WidgetHeaderProps` minus `tier`, plus `tabs?`,
|
|
354
|
+
`collapsible?`, `persistKey?`, `defaultOpen?`, `summary?`, `id?`, `children`. No `variant`, no
|
|
355
|
+
border/background — one shaded container level per page belongs to the cards inside a `Section`, not
|
|
356
|
+
to `Section` itself. Fold state persists at `basalt:section:<persistKey>` via `createPersistedState`
|
|
357
|
+
when `persistKey` is given, else local `useState`; the header stays drawn when collapsed, only the
|
|
358
|
+
body unmounts. `defaultOpen` (default `true`) picks the state a FIRST visit lands on and is outranked
|
|
359
|
+
by a persisted value, so a section the reader closed stays closed. `summary` renders under the header
|
|
360
|
+
and survives a collapse — a folded section still states its headline figures, which is what makes
|
|
361
|
+
folding it a real option rather than a way to lose the numbers.
|
|
362
|
+
|
|
363
|
+
### Homes — `PageBar` replaces the page-action portal
|
|
364
|
+
|
|
365
|
+
**Three exports removed with no deprecation window, one prop retyped, three tokens and one CSS
|
|
366
|
+
class deleted.** `PageActions` gave a page a `ReactNode` slot in the header and nothing else: no
|
|
367
|
+
second row, no overflow policy, no mobile projection. Every consumer then hand-rolled the rest —
|
|
368
|
+
a sticky filter row measured into a `--lw-header-h`, a horizontally scrolling action strip, a
|
|
369
|
+
responsive twin under `visibleFrom`/`hiddenFrom`. `PageBar` is that whole shape, typed
|
|
370
|
+
(`docs/CONTROLS-SPEC.md` §2.1).
|
|
371
|
+
|
|
372
|
+
| Removed / changed | Replacement |
|
|
373
|
+
| -------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
|
|
374
|
+
| `PageActions` | `<PageBar actions={{ primary, secondary }} sync={…} tabs={…} filters={…} filtersEnd={…} />` |
|
|
375
|
+
| `PageActionsOutlet` | none — the outlet is internal to `BasaltShell` now |
|
|
376
|
+
| `PageHeaderProvider` | none — the provider is internal to `BasaltShell` now |
|
|
377
|
+
| `BasaltShellProps.globalActions: ReactNode` | `GlobalAction[]` — `{ key, node, mobile?: 'bar' \| 'more' \| 'hidden' }`; the first two default to `'bar'` |
|
|
378
|
+
| tokens `appShellHeaderMobileHeight`, `appHeaderMobileActionsHeight`, `stickyHeaderClearanceMobile` | `appShellHeaderHeight` / `stickyHeaderClearance` — one value at every viewport |
|
|
379
|
+
| vars `--vx-space-app-header-mobile-actions-height`, `--vx-space-sticky-header-clearance-mobile` | `--vx-space-sticky-header-clearance` |
|
|
380
|
+
| CSS `.pageActions` (`shell/app-header.module.css`) and its `nowrap` override | none — the portal target no longer scrolls; overflow folds into `ActionGroup`'s `More`/kebab |
|
|
381
|
+
|
|
382
|
+
What changes at runtime even if you only rename:
|
|
383
|
+
|
|
384
|
+
- **The app-shell header is 48px on a phone too.** It used to wrap to two rows (97px) and reserve
|
|
385
|
+
the second one on every route, empty or not (law C14). A page-level filter/tab row now renders
|
|
386
|
+
in the page FLOW, sticky under the header, so a route with no filters is 49px shorter on mobile.
|
|
387
|
+
- **The header never scrolls sideways.** Actions past three fold into a `More` menu on desktop and
|
|
388
|
+
into one kebab below `sm` — computed from `BarAction[]`, which is why the slot had to become data
|
|
389
|
+
(law C7). There is exactly ONE kebab per header, and `PageBar`'s ROW 1 owns it: the shell's
|
|
390
|
+
`mobile: 'more'` global actions land there, and so do `filtersEnd`'s items (row 2 shows those on
|
|
391
|
+
desktop only — below `sm` row 2 is tabs + the first pill + `Filters (n)`, per spec §2.1). A route
|
|
392
|
+
with no `PageBar` row-1 actions gets the kebab from the shell instead. An `ActionGroup` you mount
|
|
393
|
+
yourself — in a `Section` or `ChartCard` `actions` slot — never inherits the global rows.
|
|
394
|
+
- **`--basalt-page-bar-h` is published on `documentElement`** (ResizeObserver where available,
|
|
395
|
+
`height > 0` guard)
|
|
396
|
+
whenever a row 2 renders, and removed when it unmounts. It is written in the LAYOUT phase, so it
|
|
397
|
+
exists at the first paint — a cold load of `/page#anchor` lands below the bar rather than under
|
|
398
|
+
it. A `100dvh` body becomes
|
|
399
|
+
`calc(100dvh - var(--app-shell-header-height) - var(--basalt-page-bar-h, 0px))` — delete any
|
|
400
|
+
hand-rolled measure-and-publish effect and any hardcoded header-height fallback.
|
|
401
|
+
- **A shell-less app reads `title`/`icon`.** Inside a `BasaltShell` the breadcrumb names the page
|
|
402
|
+
and both are ignored; outside one they lead row 1, and the whole bar sticks at `top: 0`.
|
|
403
|
+
- **`className` lands on the bar ROOT in both forms** — the shell-less
|
|
404
|
+
`[data-basalt-page-bar="standalone"]`, or row 2's sticky wrapper `[data-basalt-page-bar="shell"]`.
|
|
405
|
+
It is the seam for the two things only the consumer's own layout knows: bleeding the sticky bar
|
|
406
|
+
across a container's gutters, and a hairline under it. Scope that CSS through the class; a global
|
|
407
|
+
`[data-basalt-page-bar]` rule reaches every page in the app, including the ones wanting neither.
|
|
408
|
+
|
|
409
|
+
`ActionGroup`, `OverflowMenu` and `SyncButton` ship on the new `basalt-ui/controls` subpath; only
|
|
410
|
+
the TYPES a `PageBar`/`BasaltShell` prop mentions (`BarAction`, `ActionGroupProps`, `GlobalAction`)
|
|
411
|
+
are re-exported from the root.
|
|
412
|
+
|
|
413
|
+
### Stores — `createSearchStore` replaces the enum-only pair
|
|
414
|
+
|
|
415
|
+
`createSearchParamStore` and `createMultiSearchParamStore` still work: both are now `@deprecated`
|
|
416
|
+
thin wrappers over `createSearchStore`, returning the same four members
|
|
417
|
+
(`validateSearch`, `useStore`, `readStored`, `linkSearch`) with the same signatures and reading the
|
|
418
|
+
same single-value localStorage envelope. **They are removed in 1.29.0.**
|
|
419
|
+
|
|
420
|
+
| Removed / deprecated | Replacement |
|
|
421
|
+
| ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
422
|
+
| `createSearchParamStore({ key, param, values, fallback })` | `createSearchStore({ key, fields: { <param>: field.enum(values, fallback) } })` — `@deprecated`, removed 1.29.0 |
|
|
423
|
+
| `createMultiSearchParamStore({ key, param, values })` | `createSearchStore({ key, fields: { <param>: field.multi(values, fallback) } })` — `@deprecated`, removed 1.29.0 |
|
|
424
|
+
| `<store>.useStore()` | `<store>.field.<name>.use()` — reads the URL FIRST, then the mirror; writes both lanes |
|
|
425
|
+
| `useOnlineStatus` (`.` and `./state`) | `useConnectivity()` (`.`, auto-mounted by `BasaltProvider`) — **removed, not deprecated** (A12) |
|
|
426
|
+
| `createSearchSchemaStore` | never shipped; `createSearchStore` is what those "planned" paragraphs were waiting for |
|
|
427
|
+
|
|
428
|
+
New on `./router-tanstack`: `createSearchStore`, `field` (`enum` / `multi` / `range` / `number` /
|
|
429
|
+
`boolean` / `string`), and the `FieldHandle` / `AnyField` / `FieldValue` / `RangeValue` / lane types.
|
|
430
|
+
New on `./state` (router-free, same field vocabulary): `createLocalStore`, `field`, those same types.
|
|
431
|
+
`FieldHandle` is what every 1.26.0 control takes instead of `value`/`onChange`
|
|
432
|
+
(`docs/CONTROLS-SPEC.md` §3–§4).
|
|
433
|
+
|
|
434
|
+
Three things to know when you migrate a real store:
|
|
435
|
+
|
|
436
|
+
- **A deep link now wins over the mirror.** `?range=7d` reads back `7d` even when localStorage says
|
|
437
|
+
`30d`. The old `useStore` read localStorage, so a shared link opened on the wrong window — the bug
|
|
438
|
+
three consumers shipped (A8). If you were compensating for that by hand, delete the compensation.
|
|
439
|
+
- **The storage layout changed for the NEW factory only.** `createSearchStore` keeps one entry per
|
|
440
|
+
STORE (`basalt:<key>` = `{ fieldName: value }`); the old factories kept one bare value per param.
|
|
441
|
+
A store migrated from a wrapper to `createSearchStore` therefore ignores whatever the browser
|
|
442
|
+
already had for that key and starts on its fallback once. The wrappers themselves keep the old
|
|
443
|
+
layout byte-for-byte, so an un-migrated consumer sees no reset — including a stored EMPTY multi
|
|
444
|
+
selection, which still reads back as empty: "an empty array means absent" is a URL rule in the
|
|
445
|
+
deprecated store, never a storage rule, so a deliberately cleared filter stays cleared.
|
|
446
|
+
- **A write from outside the owning route persists only** — it no longer needs a hand-rolled
|
|
447
|
+
`navigate` beside it, and it no longer silently writes a param the route does not validate.
|
|
448
|
+
`validateSearch` picks the persisted value up on the next visit (A1). The one combination with
|
|
449
|
+
nowhere to go — a `persist: false` field written from a route that does not validate it — warns
|
|
450
|
+
once per field in dev rather than dropping the write silently.
|
|
451
|
+
|
|
452
|
+
**Every navigate a store issues carries `resetScroll: false`** — a field write and `useReset`
|
|
453
|
+
alike. A filter lives halfway down a page as often as it lives in the bar, and the router treats a
|
|
454
|
+
same-route search write as a navigation, so the default scrolled the reader back to the top on every
|
|
455
|
+
change. Delete any `resetScroll: false` you were passing beside a hand-rolled `navigate`.
|
|
456
|
+
|
|
457
|
+
Lanes are declared once per field: `{ url: false }` is the local-only lane (per-chart selects, a
|
|
458
|
+
compact toggle — or use `createLocalStore`), `{ persist: false }` the URL-only lane (pagination, a
|
|
459
|
+
deliberately unpersisted threshold), `{ history: 'push' }` opts one field into a history entry.
|
|
460
|
+
**`{ url: false, persist: false }` is the IN-MEMORY lane** rather than the one combination that
|
|
461
|
+
dropped its write: shared across every mount of that store for the session, gone on reload, never
|
|
462
|
+
written to or read from localStorage. It behaves identically in both factories — `createSearchStore`
|
|
463
|
+
counts such a field in `useActiveCount()` and clears it in `useReset()`, and it never appears in
|
|
464
|
+
`useValues()`/`validateSearch`, which are the URL lane by definition. In a `createLocalStore` the
|
|
465
|
+
`url` flag is ignored, so `{ persist: false }` alone lands there. The dev warning about a write with
|
|
466
|
+
nowhere to go now fires only for its remaining real case: a `{ persist: false }` field that IS on the
|
|
467
|
+
URL lane, written from a route that does not validate the param. `createLocalStore` also carries **`labels()`**,
|
|
468
|
+
chainable and identical to `createSearchStore`'s, so `SelectFilter`/`ViewTabs` read option labels off
|
|
469
|
+
a local store too.
|
|
470
|
+
A `field.range` keeps THREE URL params (preset + `from` + `to`, renamable via `params`), so existing
|
|
471
|
+
deep links and loaders keep their shape, and `field.<name>.toWindow(v)` replaces a hand-rolled
|
|
472
|
+
`presetToParams`.
|
|
473
|
+
|
|
474
|
+
### Sidebar — `sidebarBlocks` replaces the two `ReactNode` extras
|
|
475
|
+
|
|
476
|
+
**Two props removed with no deprecation window, and two widened.** `sidebarNavExtra` and
|
|
477
|
+
`mobileNav.moreExtra` were the same gap solved twice and badly: the first rendered on desktop only
|
|
478
|
+
and vanished in the collapsed rail, the second was one anonymous row at the bottom of the More
|
|
479
|
+
surface, and neither could be projected because basalt could not see inside a `ReactNode`. Every
|
|
480
|
+
consumer that wanted an "Awaiting action" list therefore wrote it twice, once per prop, and got no
|
|
481
|
+
rail badge either way. `SidebarBlock[]` is that whole shape as data (law C13,
|
|
482
|
+
`docs/CONTROLS-SPEC.md` §2.3).
|
|
483
|
+
|
|
484
|
+
| Removed / changed | Replacement |
|
|
485
|
+
| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
486
|
+
| `BasaltShellProps.sidebarNavExtra` | `sidebarBlocks={[{ kind: 'custom', key, node }]}` — same DOM position (last child of the nav scroll region), same CSS-only hiding in the rail, still desktop-only |
|
|
487
|
+
| `AppSidebarProps.navExtra` | `blocks={[{ kind: 'custom', key, node }]}` |
|
|
488
|
+
| `MobileNavConfig.moreExtra` | `sidebarBlocks={[{ kind: 'list', key, label, items }]}` — one More-sheet row (`Awaiting action · 3`) opening a nested sheet of its items, instead of one unlabelled row of raw content |
|
|
489
|
+
| `useNav(NAV, { moreExtra })` | drop the option; declare the block on `BasaltShell` |
|
|
490
|
+
| `BasaltShellProps.brand` | widened to `BrandConfig & { menu?: AccountMenuItem[] }` — additive; `menu` makes the brand row a `Name ▾` workspace switcher |
|
|
491
|
+
| `BasaltShellProps.search` | widened to `SidebarSearchConfig & { actions?: SidebarSearchActions }` — additive; one or two icon-only buttons right of the ⌘K row |
|
|
492
|
+
|
|
493
|
+
What changes at runtime even if you declare no blocks at all:
|
|
494
|
+
|
|
495
|
+
- **Nav-section folds are now PERSISTED**, at `basalt:sidebar-section:<label-slug>` (e.g.
|
|
496
|
+
`basalt:sidebar-section:tools`). They were a `useState` keyed by label, so every reload re-opened
|
|
497
|
+
a section the user had closed. `defaultCollapsed` is now the SEED for that key rather than a value
|
|
498
|
+
that overrides the user on every mount. Block folds live at `basalt:sidebar-block:<key>`; both are
|
|
499
|
+
the standard `createPersistedState` envelope (`{ v: 1, value: boolean }`), readable with
|
|
500
|
+
`readPersistedValue` from `basalt-ui/state`. A "Show more" toggle is deliberately NOT persisted.
|
|
501
|
+
- **`settingsMenuItems` renders FLAT at three entries or fewer** — one link row each, instead of a
|
|
502
|
+
gear "Settings" menu you had to open to see two rows. Four or more keeps the menu. `brand.version`
|
|
503
|
+
rides the flat rows as a faint label and the dropdown as a `Menu.Label`, as before. New
|
|
504
|
+
`BasaltShellProps.settingsMenu?: 'auto' | 'flat' | 'menu'` (`AppSidebarProps` too, default
|
|
505
|
+
`'auto'` = that count rule) forces the form: pass `'menu'` when the three rows are CONTROLS rather
|
|
506
|
+
than destinations — a theme radio group and a devtools switch read as a widget pile flat, and the
|
|
507
|
+
count cannot tell them from three links.
|
|
508
|
+
- **The footer is one wrapper deeper.** `settingsMenuItems` without `account` used to render its
|
|
509
|
+
`Group` as the footer element itself; both now sit inside one footer `Stack` alongside any
|
|
510
|
+
`placement: 'bottom'` block. No visual change — relevant only to a stylesheet reaching in by
|
|
511
|
+
structure.
|
|
512
|
+
- **`useNav` now marks exactly one destination `active`** — the deepest matching route wins, instead
|
|
513
|
+
of every prefix-matching row (a parent AND its child) reading active at once. A parent on the
|
|
514
|
+
winner's path gets the new `SidebarItem.ancestor` instead: never active, never `aria-current`, only
|
|
515
|
+
a `data-ancestor` hook for CSS. A consumer that styled off two simultaneously-active rows will now
|
|
516
|
+
see one.
|
|
517
|
+
|
|
518
|
+
New types on `.`: `SidebarBlock`, `SidebarListBlock`, `SidebarProgressBlock`, `SidebarCustomBlock`,
|
|
519
|
+
`SidebarBlockItem`, `SidebarBlockTone`, `SidebarSearchActions`. `SidebarItem` gained `ancestor?:
|
|
520
|
+
boolean`.
|
|
521
|
+
|
|
522
|
+
### `DeltaBadge` — plain-element DOM, same props
|
|
523
|
+
|
|
524
|
+
Nothing removed or renamed — a DOM contract change only, ahead of `WidgetHeader`
|
|
525
|
+
(`docs/CONTROLS-SPEC.md` §2.2) composing it Mantine-free.
|
|
526
|
+
|
|
527
|
+
| Component | What changed | Why it matters |
|
|
528
|
+
| ------------ | --------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
|
529
|
+
| `DeltaBadge` | Renders a plain `<span>`, no `mantine-Badge-root`/`mantine-Badge-label` classes anymore | Same props, tone and format API — but a selector or snapshot keyed on Mantine's Badge classes breaks |
|
|
530
|
+
|
|
531
|
+
### Controls — `ArticleFilterBar` replaced by store-bound controls
|
|
532
|
+
|
|
533
|
+
Two new subpaths, `basalt-ui/controls` and `basalt-ui/controls-dates`, and ONE removal. The removed
|
|
534
|
+
component was controlled (`value`/`onChange`, its own `visibleFrom`/`hiddenFrom` twin); its
|
|
535
|
+
replacements take a `FieldHandle` and own the URL write, the localStorage mirror and the responsive
|
|
536
|
+
swap themselves (`docs/CONTROLS-SPEC.md` §3, laws C2/C9).
|
|
537
|
+
|
|
538
|
+
| Removed | Replacement | Import |
|
|
539
|
+
| ----------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------ |
|
|
540
|
+
| `ArticleFilterBar` | `FilterSet` + `ViewTabs` (category axis) + `MultiSelectFilter` (tags) | `basalt-ui/controls`, and unchanged from `basalt-ui/content` |
|
|
541
|
+
| `ArticleFilterBarProps` | `FilterSetProps` / `ViewTabsProps` / `MultiSelectFilterProps` | same |
|
|
542
|
+
|
|
543
|
+
All three replacements are re-exported from `basalt-ui/content` as well, so a content-only consumer
|
|
544
|
+
changes the component names and the props, not the import path.
|
|
545
|
+
|
|
546
|
+
Porting one call site:
|
|
547
|
+
|
|
548
|
+
```tsx
|
|
549
|
+
// before — the page owned the state, the labels and the mobile swap
|
|
550
|
+
const [category, setCategory] = useState('all')
|
|
551
|
+
<ArticleFilterBar categories={CATEGORIES} category={category} onCategoryChange={setCategory}
|
|
552
|
+
tags={ARTICLE_TAGS} selectedTags={tags} onTagsChange={setTags} />
|
|
553
|
+
|
|
554
|
+
// after — one store, two controls, no useState and no responsive branch
|
|
555
|
+
const articles = createSearchStore({
|
|
556
|
+
key: 'articles',
|
|
557
|
+
fields: { category: field.enum(CATEGORY_VALUES, 'all'), tags: field.multi(ARTICLE_TAGS, []) },
|
|
558
|
+
}).labels({ category: CATEGORY_LABELS })
|
|
559
|
+
|
|
560
|
+
<ViewTabs field={articles.field.category} />
|
|
561
|
+
<FilterSet>
|
|
562
|
+
<MultiSelectFilter field={articles.field.tags} label="All tags" />
|
|
563
|
+
</FilterSet>
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
New on `basalt-ui/controls`: `FilterSet`, `RangeFilter`, `CompareFilter`, `SelectFilter`,
|
|
567
|
+
`MultiSelectFilter`, `SearchFilter`, `ToggleFilter`, `ViewTabs`, `COMPARE_VALUES`, and the
|
|
568
|
+
action/sync family. New on `basalt-ui/controls-dates`: `DateRangePicker`.
|
|
569
|
+
|
|
570
|
+
**`SyncButton`'s `scope` now decides the SHAPE, not only where you mount it.** `scope: 'global'`
|
|
571
|
+
renders icon-only at every viewport (an `ActionIcon` with the spinning glyph, age and error in the
|
|
572
|
+
tooltip, `label` as the accessible name) — the shell header shares 48px with the breadcrumb and
|
|
573
|
+
`PageBar` row 1, and a labelled button there is what pushes a page's own actions into the kebab.
|
|
574
|
+
`scope: 'page'` is unchanged on desktop (labelled, age inline) and drops to icon-only below `sm`,
|
|
575
|
+
which is what `docs/CONTROLS-SPEC.md` §3 always specified. One mount either way, CSS-only swap (law
|
|
576
|
+
C9). Consequence to know: the accessible name is now `aria-label` in BOTH forms, so a test asserting
|
|
577
|
+
the name as `Sync 2m ago` (the age used to land in it) now reads `Sync`. Nothing to change at a call
|
|
578
|
+
site.
|
|
579
|
+
|
|
580
|
+
**`SelectFilter` and `MultiSelectFilter` take `options?: readonly FilterOption[]`** — a runtime
|
|
581
|
+
catalogue (`{ value, label, disabled? }`) that OVERRIDES `field.options` whole rather than merging,
|
|
582
|
+
so a value the catalogue has dropped loses its row. It is what a label carrying live data needs
|
|
583
|
+
(`EUR · 1.08`), and on `SelectFilter` it also opens a second field shape: a `FieldHandle<StringField>`
|
|
584
|
+
is legal WITH `options` — an id set no enum can close over, a project picker fed from a query — and
|
|
585
|
+
a type error without it. `FilterOption` is new on `basalt-ui/controls`. Additive; nothing to change.
|
|
586
|
+
|
|
587
|
+
**`FilterSet` renders the mobile `Filters (n)` pill only when a child is actually folded.** With
|
|
588
|
+
every child inside the `inline` budget (one filter at the default `inline: 1`) the sheet would hold a
|
|
589
|
+
copy of what is already on screen, so neither the trigger nor the drawer mounts. Nothing to change —
|
|
590
|
+
but a mobile test asserting the pill exists on a one-filter page now has to fold something
|
|
591
|
+
(`inline={0}`) or drop the assertion. `RangeFilter.label` (default `'Range'`) names the sheet heading
|
|
592
|
+
and the preset track's aria name; the pill itself still reads the VALUE.
|
|
593
|
+
|
|
594
|
+
**`data-numeric` is now a package-wide law, not a per-component rule.** `basalt-ui/styles.css`
|
|
595
|
+
declares `[data-numeric] { font-family: var(--basalt-font-mono); font-variant-numeric: tabular-nums }`
|
|
596
|
+
inside `@layer basalt`, so the attribute works on ANY element — `<Text data-numeric>`, a table cell,
|
|
597
|
+
a count tag — and not only on a `SegmentedControl`, where it was previously a module-scoped rule that
|
|
598
|
+
made the attribute look broken everywhere else. Nothing to change: adding the attribute is opt-in,
|
|
599
|
+
and the two module rules that also set a component `font-size` still apply on top. This retires the
|
|
600
|
+
per-consumer `theme-allow` + inline `fontFamily`/`fontSize` hack for numeric labels.
|
|
601
|
+
|
|
602
|
+
**`@mantine/dates` is a new OPTIONAL peer, and only `basalt-ui/controls-dates` needs it.**
|
|
603
|
+
`basalt-ui/controls` resolves and renders without it — proven from the packed tarball by
|
|
604
|
+
`scripts/pack-test.sh`. Pass the picker into `RangeFilter` rather than importing it from a shared
|
|
605
|
+
module:
|
|
606
|
+
|
|
607
|
+
```tsx
|
|
608
|
+
import { DateRangePicker } from 'basalt-ui/controls-dates'
|
|
609
|
+
;<RangeFilter field={analytics.field.range} customPicker={DateRangePicker} />
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
A consumer that uses `DateRangePicker` installs `@mantine/dates` and imports
|
|
613
|
+
`@mantine/dates/styles.layer.css` with the other Mantine layer bundles, before `basalt-ui/styles.css`.
|
|
614
|
+
|
|
615
|
+
### Guards — the control rules (docs/CONTROLS-SPEC.md §6)
|
|
616
|
+
|
|
617
|
+
**Nothing removed. Eight new oxlint rules and two new guard kinds. `Ships` below is the level as of
|
|
618
|
+
THIS minor** — eight land `error`, and the C1 pair (`control-outside-home` and its text lane) is the
|
|
619
|
+
only grace left, re-dated to 1.28.0 on a measurement (see § Guards — the promotions).\*\* Every one honours `theme-allow <id> — <why>` on the
|
|
620
|
+
node and `theme-allow-file <id> — <why>` on the file, the same grammar as the rest.
|
|
621
|
+
|
|
622
|
+
| Rule / kind | Fires on | Ships |
|
|
623
|
+
| ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
|
|
624
|
+
| `basalt/hand-rolled-filter` | A raw Mantine `Select`/`SegmentedControl`/… handed to a TIERED home slot (`actions`/`filters`/`tabs`/`sync`/`filtersEnd` on `PageBar`/`Section`/`WidgetHeader`/`ChartCard`/`StatCard`/`BasaltDataTable`/`SettingsSection`/`FilterSet`) | error |
|
|
625
|
+
| `basalt/page-bar-budget` | A second `PageBar` in the SAME returned tree, >4 `actions.secondary`, >3 `Section` actions, a second filled `Button`/`ActionIcon` in one slot (outside an overlay) | error |
|
|
626
|
+
| `basalt/control-outside-home` | The same raw control with no home at all — exempt under a settings row / overlay / composer, in an `@mantine/form` file, in a file that DEFINES a basalt control, or in a file basenamed `*-{modal,drawer,popover,panel,form}.tsx` | warn → 1.28.0 |
|
|
627
|
+
| `basalt/control-size-literal` | `size`/`w`/`fullWidth`/`visibleFrom`/`hiddenFrom` on anything inside a home slot (the slot sets the tier) | **error** |
|
|
628
|
+
| `basalt/in-body-page-title` | `<Title order={1\|2}>` outside prose/overlay context and outside a `content/` path | **error** |
|
|
629
|
+
| `basalt/responsive-twin` | The same control mounted twice, one `visibleFrom="X"` and one `hiddenFrom="X"` | **error** |
|
|
630
|
+
| `basalt/search-literal-link` | A `search:` object literal in a `linkOptions()` inside `defineNav()`/`navGroup()` | **error** |
|
|
631
|
+
| `basalt/use-search-from-literal` | `useSearch({ from: '<route>' })` | **error** |
|
|
632
|
+
| `in-body-page-title` (guard kind) | The text lane of the same law — SAME id, so one annotation waives both lanes | **error** |
|
|
633
|
+
| `raw-selection-control` (guard kind) | The text lane of `control-outside-home`, approximated by a 12-line host-tag window, plus the same overlay-basename exemption | warn → 1.28.0 |
|
|
634
|
+
|
|
635
|
+
**A `SettingsRow`'s `control` is not a tiered slot, so nothing in this table fires inside one.**
|
|
636
|
+
It is law C1's third home — the form row — and a form keeps Mantine's `md` tier
|
|
637
|
+
(`controlHeight` 42, unchanged). A raw `Select`/`Switch`/`Button` bound to a setting is correct
|
|
638
|
+
there, and its `size` prop is what holds the row at the form tier, so neither `hand-rolled-filter`
|
|
639
|
+
nor `control-size-literal` reaches into it; `control-outside-home` treats a settings row as a home
|
|
640
|
+
and stays silent as well. Consumer settings pages need no change.
|
|
641
|
+
|
|
642
|
+
**Three scoping facts, because each one is a rule NOT firing on code you already have.** The home
|
|
643
|
+
tag must resolve to a component imported from `basalt-ui` (or a basalt subpath) — your own
|
|
644
|
+
`Section`, `PageBar` or `StatCard` is not a tiered home, and nothing in this table reads it as one;
|
|
645
|
+
the one accepted gap is basalt's `Section` re-exported through your own barrel, which the rules
|
|
646
|
+
cannot see. An OVERLAY colocated in a slot (`Modal`, `Drawer`, `Popover.Dropdown`, `Menu.Dropdown`,
|
|
647
|
+
`SettingsRow`) is exempt — a `New` button beside the `<Modal>` it opens is how both are written, and
|
|
648
|
+
the overlay's own controls are not in the bar. And `control-size-literal` only reaches what the
|
|
649
|
+
slot's theme actually re-tiers: the raw filters, the basalt controls, and Mantine's
|
|
650
|
+
`Button`/`ActionIcon`/`Input`/`TextInput`/`Select`/`MultiSelect`/`SegmentedControl`/`NativeSelect` —
|
|
651
|
+
an icon's `size`, a count `Badge`, a `Loader`, an `Avatar` or a `Modal size="lg"` never report.
|
|
652
|
+
|
|
653
|
+
**Two existing rules widened, neither with a new id.** `basalt/raw-scroll-container` now also fires
|
|
654
|
+
on `overflowX: 'auto' \| 'scroll'` and on `<ScrollArea scrollbars="x">` **inside a home SLOT**
|
|
655
|
+
(`actions`/`filters`/`tabs`/`sync`/`filtersEnd`) — law C7, overflow folds into a `More` menu or a
|
|
656
|
+
`Filters (n)` sheet rather than scrolling sideways. A home's BODY is out of scope: a wide table, a
|
|
657
|
+
pinned-column grid or a horizontally scrolling code block in a `Section`/`ChartCard` is page
|
|
658
|
+
content, not a sideways-scrolling row of controls, and outside a slot `overflowX` is still not
|
|
659
|
+
policed at all.
|
|
660
|
+
oxlint severities are per rule ID, not per branch, so this widening inherits the rule's existing
|
|
661
|
+
`error` level with no grace runway of its own — the honest limitation, and the reason it is scoped to
|
|
662
|
+
homes rather than shipped repo-wide. `basalt/shadow-basalt-export` gains a rename table
|
|
663
|
+
(`PageHeader`/`FilterBar` → `PageBar`, `WindowSelector`/`RangeSelector`/`DateFilter` →
|
|
664
|
+
`RangeFilter`, `PageSection`/`SectionTitle`/`SectionHeading` → `Section`, `ViewSwitch`/`ViewToggle`
|
|
665
|
+
→ `ViewTabs`, `RefreshButton`/`SyncControl`/`SyncStatusButton` → `SyncButton`, `HeroCard`/`HeroStats`
|
|
666
|
+
→ `StatCard`), so a fork that RENAMED the export is visible too. It stays a permanent advisory
|
|
667
|
+
`warn`.
|
|
668
|
+
|
|
669
|
+
**`profile: 'tokens-only'` now disables 19 kinds, not 17** — both new kinds are Mantine-coupled.
|
|
670
|
+
|
|
671
|
+
**`SURFACES` gains `pluginRules`** (`src/surfaces.ts`, internal — not a published subpath): every
|
|
672
|
+
doctrine surface names the oxlint rules that enforce it, every registered rule maps to exactly one
|
|
673
|
+
surface, every guard KIND maps to at least one, and `basalt-ui check-coverage` asserts all three —
|
|
674
|
+
so a generated coverage header can neither over- nor under-report its lane. `check-coverage` also
|
|
675
|
+
takes `--write` / `--check` for the generated `<!-- basalt:coverage -->` header of each
|
|
676
|
+
`agent/rules/*.md` file; a rule file with no block yet is reported, not failed.
|
|
677
|
+
|
|
678
|
+
### Controls — the pill reads the VALUE, and four defaults moved
|
|
679
|
+
|
|
680
|
+
**A filter pill's TEXT is now always the selected option's label, at the field's default value too.**
|
|
681
|
+
It used to fall back to the filter's NAME while `field.isDefault(value)` held, which is how a bar
|
|
682
|
+
could read `Compare` over a field holding `'previous'` while the popover showed `Previous period`
|
|
683
|
+
selected. `label` is the popover/sheet heading and the accessible name; it is never printed on the
|
|
684
|
+
pill. Affects `SelectFilter`, `CompareFilter` and every control built on the shared enum body;
|
|
685
|
+
`RangeFilter` already read its value, and `MultiSelectFilter` keeps its own law (the group label
|
|
686
|
+
while the selection is empty or complete, `N <noun>` otherwise).
|
|
687
|
+
|
|
688
|
+
**Nothing to change at a call site.** What changes is the string a test or a screenshot sees: a
|
|
689
|
+
`SelectFilter field={currency} label="Currency"` at fallback `USD` now reads `USD`, not `Currency`.
|
|
690
|
+
If a bar reads badly at rest, the fix is the OPTION label (`store.labels()`), not the filter's name.
|
|
691
|
+
|
|
692
|
+
| Component / prop | Was | Now |
|
|
693
|
+
| ------------------------------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
694
|
+
| `CompareFilter` option labels | the raw values until `store.labels()` | `No comparison` / `Previous period` / `Same period last year`, exported as `COMPARE_LABELS`. A label a consumer set (`label !== value`) still wins. |
|
|
695
|
+
| `BasaltDataTable` → `withTableBorder` | `true` | `false`. Pass `true` for a table that needs a frame. The head rule and the between-row rules are untouched; the LAST row's rule is now dropped (it was an outer border). |
|
|
696
|
+
| `BasaltDataTable` single-select facet's "all" row | `All` | `Any <facet.label lowercased>` — the pill now reads it, and three facets all reading `All` said nothing. |
|
|
697
|
+
| `ViewTabs` phone `Select` (past 3 options) | greedy (216px measured, truncating a title) | capped at 9rem |
|
|
698
|
+
|
|
699
|
+
**New: `ControlGroup`** (`basalt-ui/controls`) — joins adjacent controls that act on ONE thing
|
|
700
|
+
(`‹ Today ›`, `− 1 +`) into a single box: no gap, one shared hairline per pair, radius on the outer
|
|
701
|
+
ends only. `gap?: 'none' | 'tight'`. `BarActionItem` gains `group?: true`, and a run of adjacent
|
|
702
|
+
`group: true` actions in an `ActionGroup` renders as one `ControlGroup`; a joined member that ships an
|
|
703
|
+
`icon` renders ICON-ONLY with its label demoted to the accessible name. `ActionGroup` also joins
|
|
704
|
+
adjacent ICON-ONLY entries on the mobile bar with no flag. `basalt/shadow-basalt-export` now names
|
|
705
|
+
`ButtonGroup` / `ButtonRow` / `JoinedButtons` as renamed forks of it.
|
|
706
|
+
|
|
707
|
+
### The `ctl` tier now covers Radio, Checkbox and Switch
|
|
708
|
+
|
|
709
|
+
All three defaulted to Mantine's `sm` — a 20px indicator beside the tier's 13.5px option label — in
|
|
710
|
+
every filter popover and in the mobile sheet. They are now in `CTL_THEME` (with their `.Group`s) and
|
|
711
|
+
`cssVariablesResolver` declares their `-ctl` vars: 16px indicator, 18×30 switch.
|
|
712
|
+
|
|
713
|
+
**One consequence worth knowing.** Mantine resolves a Radio's size as `props.size ? props.size :
|
|
714
|
+
ctx.size`, and a theme `defaultProps.size` is indistinguishable from an explicit prop to that check
|
|
715
|
+
— so inside a `<CtlSlot>` (any home's slot) a `<Radio.Group size="lg">` no longer sizes its children;
|
|
716
|
+
they stay `ctl`. A group that genuinely needs another size belongs in a BODY, which no slot wraps.
|
|
717
|
+
|
|
718
|
+
`basalt/control-size-literal` widened to cover the six new tags, so a `size` on a Radio/Checkbox/
|
|
719
|
+
Switch inside a home slot now reports. It is still `warn` on its 1.26.0 → 1.27.0 runway (it has never
|
|
720
|
+
shipped at any level, so widening it restarts nothing).
|
|
721
|
+
|
|
722
|
+
### Icons — one box, no call-site geometry
|
|
723
|
+
|
|
724
|
+
Every `icon` prop in the framework (`BarAction`, the filter pills and their trailing affordance,
|
|
725
|
+
`WidgetHeader`/`StatCard`, `SyncButton`, sidebar-block rows, the overflow fold's own glyphs) now
|
|
726
|
+
renders through ONE internal slot: a fixed `--vx-space-icon-size` (16px default) square that restates
|
|
727
|
+
the glyph's `width`/`height` at 100% and takes it off the text baseline. **No API change** — but a
|
|
728
|
+
consumer's `<svg width="24">` that used to set a control's row height now renders at 16px, and an
|
|
729
|
+
`<svg>` with no `width`/`viewBox` at all no longer paints at 300×150. Per-tier sizes come from the
|
|
730
|
+
box var (`WidgetHeader` sets 14px at `tier="widget"`), never from a prop.
|
|
731
|
+
|
|
732
|
+
### Mobile nav — at most ONE slot is active
|
|
733
|
+
|
|
734
|
+
A slot's activeness now reads exactly the destinations it COVERS. It used to roll up over `children`
|
|
735
|
+
unconditionally while an item slot covered only its own key, so at `/dashboard/sessions` the
|
|
736
|
+
`Dashboard` tab lit through the rollup and the `More` tab lit for `Sessions` sitting in its overflow
|
|
737
|
+
— two `aria-current="page"` tabs at once. A SECTION tab still lights for a nested destination (it
|
|
738
|
+
covers its whole tree); an ITEM tab with children no longer does, and the overflow slot that can
|
|
739
|
+
actually reach the open route lights instead.
|
|
740
|
+
|
|
741
|
+
`useNav`'s anchors now pass `activeOptions: { exact: true }` to TanStack's `Link`. A `Link` spreads
|
|
742
|
+
`{ 'data-status': 'active', 'aria-current': 'page' }` last — after `activeProps` and after every
|
|
743
|
+
caller prop — so its own fuzzy match could not be overridden from outside and re-created the very
|
|
744
|
+
double-highlight `useNav`'s resolver exists to prevent. basalt's own law is unchanged: an item whose
|
|
745
|
+
exact route is never visited still lights on a descendant route.
|
|
746
|
+
|
|
747
|
+
### Layout — two collisions and a gap
|
|
748
|
+
|
|
749
|
+
- **The app header**: exactly one side of row 1 is elastic, and it is the breadcrumb (the side whose
|
|
750
|
+
content can ellipsize). The control side is `flex: 0 0 auto` with no `min-width: 0` below it. Both
|
|
751
|
+
sides were shrinkable, and at 390px the sync button overflowed 30px INTO the page title, because a
|
|
752
|
+
`justify-content: flex-end` flex container overflows towards its start.
|
|
753
|
+
- **`WidgetHeader`'s hero row**: the value → `DeltaBadge` gap is the 8px rhythm step
|
|
754
|
+
(`--vx-space-stack-sm`), not 4px, and the badge is centred on the value rather than baseline-aligned
|
|
755
|
+
to it.
|
|
756
|
+
- **`WidgetHeader`'s title row** wraps below `sm`, so an `actions` slot too wide to sit beside the
|
|
757
|
+
title takes its own line instead of squeezing the heading to an ellipsis. A 30px kebab still fits
|
|
758
|
+
and still sits on the row. **Its metric row wraps too**: in a 2-up card grid at 390px
|
|
759
|
+
`$1294.9k ▲18.9%` rendered as `$129… ▲18.9%`, and truncating the number to protect the delta about
|
|
760
|
+
that number is the wrong way round. A short value still keeps its badge on the line.
|
|
761
|
+
- **`StatCard`'s `actions` slot is the 24px tier now, not 30px.** `CtlSlot` gained `tier?: 'ctl' |
|
|
762
|
+
'widget'` (default `'ctl'`, so no existing call site changes), and `StatCard` mounts its header slot
|
|
763
|
+
at `'widget'` — the `size="icon"` step `--vx-space-control-height-widget` has always named as
|
|
764
|
+
"`WidgetHeader tier="widget"` actions". A raw `ActionIcon` there rendered at the 30px `ctl` tier
|
|
765
|
+
inside a 28px header row and grew it to 30, so a KPI card WITH a kebab sat 2px below the card
|
|
766
|
+
beside it and every number in it with it. A consumer passing `<ActionIcon size="ctl">` explicitly
|
|
767
|
+
still gets 30px — and its card is 2px taller than its neighbours again, which is now visible rather
|
|
768
|
+
than accidental. The slot's `data-basalt-tier` marker reads `widget` there, the same value
|
|
769
|
+
`ChartCard` already writes by hand.
|
|
770
|
+
- **The breadcrumb's ANCESTOR crumbs are hidden below `sm`** — only the page crumb renders. At 390px
|
|
771
|
+
the elastic side truncated `Overview / Dashboard` to `O… / D…`: two ellipses and a separator in
|
|
772
|
+
place of the one word that says where the reader is. CSS-only (law C9), so no flash and no hook.
|
|
773
|
+
`AppBreadcrumbs` gained one wrapper element around those crumbs; its props are unchanged.
|
|
774
|
+
|
|
775
|
+
### Cursor — the shared cursor now partitions by x-domain kind, automatically
|
|
776
|
+
|
|
777
|
+
Nothing removed. `CursorState` gained one field.
|
|
778
|
+
|
|
779
|
+
| Component | What changed | Why it matters |
|
|
780
|
+
| ------------- | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
781
|
+
| `CursorState` | gains `kind: DomainKind \| null` | No consumer action needed. The shared cursor now classifies each chart's x-domain (`'time' \| 'linear' \| 'band'`, derived from its own keys) and a broadcast whose kind differs from a chart's own is invisible to it — a categorical chart can no longer be mistaken for a time chart. `ChartCursorScope` is no longer required to keep a categorical chart from following a time chart; it still isolates two charts that share the same domain kind but are semantically unrelated. |
|
|
782
|
+
|
|
783
|
+
## 1.24.0 — `QueryState`, table body chrome, four false greens
|
|
42
784
|
|
|
43
785
|
**Nothing removed or renamed. Twelve new runtime exports on `.`/`./query`, plus table props.** Every
|
|
44
786
|
API entry is additive; the behaviour changes are in the guard and the CLI, listed after them.
|
|
@@ -170,6 +912,32 @@ scaffold a second consumer: the refusal is keyed on the RESOLVED project and run
|
|
|
170
912
|
and no other consumer holds a single `.astro`, `.vue` or `.jsx` file, so grace would have covered
|
|
171
913
|
zero incumbent violations.
|
|
172
914
|
|
|
915
|
+
## 1.25.0 — `manualPagination` imposes a contract
|
|
916
|
+
|
|
917
|
+
**Nothing removed or renamed — a behaviour change, additive props only.** `manualPagination` made
|
|
918
|
+
`data` one server page but left every other client-side control armed: sorting reordered that page
|
|
919
|
+
under a header chevron while "Showing 1–25 of 412" presented it as a sort of all 412 — a plausible,
|
|
920
|
+
wrong answer with nothing on screen to give it away. argo found it on 1.24.0 and worked around it
|
|
921
|
+
with an explicit `enableSorting={false}`.
|
|
922
|
+
|
|
923
|
+
Adopting `manualPagination` now imposes a contract, checked from props at render:
|
|
924
|
+
|
|
925
|
+
| With `manualPagination` | Resolve it with |
|
|
926
|
+
| ------------------------- | ------------------------------------------------------------------------------------ |
|
|
927
|
+
| the pagination bar itself | `enablePagination` (without it `manualPagination` is inert) + `rowCount`/`pageCount` |
|
|
928
|
+
| sorting | the new `manualSorting` + sort in `onSortingChange`, or `enableSorting={false}` |
|
|
929
|
+
| `enableGlobalFilter` | the new `manualFiltering` + filter in `onGlobalFilterChange` |
|
|
930
|
+
| `facets` | `manualFiltering` + the new `onColumnFiltersChange` |
|
|
931
|
+
|
|
932
|
+
Unresolved, it **throws in dev** naming every breach at once; a production bundle degrades instead —
|
|
933
|
+
no sort headers, no filter controls, no "of N" it cannot stand behind, plus one `console.error`. A
|
|
934
|
+
bare `<BasaltDataTable data columns />` (no `manualPagination`) is byte-identical — no opt-out
|
|
935
|
+
needed for a client-side table.
|
|
936
|
+
|
|
937
|
+
Sibling defect fixed with it: the empty-state branch keyed off `data.length`, so a search or page
|
|
938
|
+
index matching nothing left `data` non-empty and rendered a `<tbody>` with no rows AND no message.
|
|
939
|
+
It now keys off the rendered row model.
|
|
940
|
+
|
|
173
941
|
## 1.23.1 — the band-state throw, the tag gate, a CLI that answers
|
|
174
942
|
|
|
175
943
|
**One type widened, nothing removed or renamed.** `BandStripSeries.formatValue` is now
|
|
@@ -231,7 +999,7 @@ instead of dumping help and letting the dump read like a choice.
|
|
|
231
999
|
- **`tokens:css --check` blanks the provenance line (line 2) before comparing**, so a version bump
|
|
232
1000
|
alone no longer forces a no-op commit in a tokens-only consumer, where the gate is byte-equality.
|
|
233
1001
|
A stale provenance line is now a note on an otherwise-passing check. The `@generated` header is
|
|
234
|
-
still emitted byte-identical — the line stays, it just stops gating. **Superseded
|
|
1002
|
+
still emitted byte-identical — the line stays, it just stops gating. **Superseded at 1.26.0:
|
|
235
1003
|
blanking the WHOLE line also stopped gating the regeneration command it carries. Only the version
|
|
236
1004
|
token is neutralized now.**
|
|
237
1005
|
|
|
@@ -402,7 +1170,7 @@ icons — it is not yet **sufficient**. If you went hybrid over the 404s, droppi
|
|
|
402
1170
|
half here costs you every icon unless your `public/` matches basalt's six filenames: at this
|
|
403
1171
|
release `icons` was still `false | { dir?: string }` over those six, so an app whose icon is
|
|
404
1172
|
`favicon.svg` could pick between a manifest naming two PNGs it never builds and a manifest with no
|
|
405
|
-
`icons` member at all. The array form that fixes it is in `##
|
|
1173
|
+
`icons` member at all. The array form that fixes it is in `## 1.26.0`. The option's JSDoc said
|
|
406
1174
|
"skips the head `<link>` icons"; it now says what the option does.
|
|
407
1175
|
|
|
408
1176
|
**Two `theme-allow` comment shapes were silently broken and now work.** Plainly: this is the fourth
|
|
@@ -410,7 +1178,7 @@ hole found in this one contract in three rounds. A thirteen-shape matrix — eve
|
|
|
410
1178
|
had been SEEN to write — is pinned in both halves, `src/guard/check-source.test.ts` and
|
|
411
1179
|
`configs/oxlint-plugin.test.ts`. **The claim that followed it here, that the two parsers could
|
|
412
1180
|
therefore no longer disagree, was false when it was written**: a list of collected anecdotes cannot
|
|
413
|
-
close a contract. Three more holes were open at this release. See `##
|
|
1181
|
+
close a contract. Three more holes were open at this release. See `## 1.26.0`.
|
|
414
1182
|
|
|
415
1183
|
| Shape | What it did | Now |
|
|
416
1184
|
| --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- |
|
|
@@ -583,7 +1351,7 @@ with the whole lint half off).
|
|
|
583
1351
|
|
|
584
1352
|
| Surface | Note |
|
|
585
1353
|
| ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
586
|
-
| `basalt.profile: 'tokens-only'` / `--tokens-only` | disables the
|
|
1354
|
+
| `basalt.profile: 'tokens-only'` / `--tokens-only` | disables the 19 kinds whose remedy is a Mantine component, prop or the React theme factory. `check-theme` requires it DECLARED; `doctor` infers it, because its profile only changes advice, never enforcement |
|
|
587
1355
|
| `basalt.include: [...]` | scan a named file outside `roots` — and the only route to a `.json`, which is never blanket-scanned |
|
|
588
1356
|
| `basalt.roots` + a `lint:basalt` script | written by `init` from the real layout; `init` on an existing app is a lint-debt event, not a no-op |
|
|
589
1357
|
| `tokens:css --check`, `--selector-class <c>` (+ `--light-class`) | drift gate; the Tailwind `<html class="dark">` convention. There is no `scheme: { class }` API — the class form is CLI-only |
|
|
@@ -697,23 +1465,47 @@ live in `configs/oxlint.json`, and since 1.20.0 their grace is tracked by `PLUGI
|
|
|
697
1465
|
named export beside the plugin — a test asserts it against the shipped preset in both directions, so
|
|
698
1466
|
deleting an entry forces the level flip in the same commit.
|
|
699
1467
|
|
|
700
|
-
| Rule
|
|
701
|
-
|
|
|
702
|
-
| `mantine-shade-index` (guard kind)
|
|
703
|
-
| `basalt/raw-scroll-container`
|
|
704
|
-
| `basalt/ai-sdk-major`
|
|
705
|
-
| `basalt/agent-no-raw-usechat`
|
|
706
|
-
| `basalt/agent-resume-guard`
|
|
707
|
-
| `basalt/raw-size-literal`
|
|
708
|
-
| `basalt/hand-rolled-plot`
|
|
709
|
-
| `basalt/chart-legend-literal`
|
|
710
|
-
| `basalt/shadow-basalt-export`
|
|
711
|
-
| `basalt/hand-rolled-shell`
|
|
1468
|
+
| Rule | Landed | Became `error` |
|
|
1469
|
+
| -------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------- |
|
|
1470
|
+
| `mantine-shade-index` (guard kind) | 1.7.0 as `warn` | **1.11.0** |
|
|
1471
|
+
| `basalt/raw-scroll-container` | ≤1.2.0 as `off` | `warn` 1.10.0 → **`error` 1.13.0** |
|
|
1472
|
+
| `basalt/ai-sdk-major` | 1.10.0 as `warn` | **1.13.0** |
|
|
1473
|
+
| `basalt/agent-no-raw-usechat` | 1.10.0 as `warn` | **1.13.0** |
|
|
1474
|
+
| `basalt/agent-resume-guard` | 1.10.0 as `warn` | **1.13.0** |
|
|
1475
|
+
| `basalt/raw-size-literal` | 1.7.0 as `warn` | **1.20.0** |
|
|
1476
|
+
| `basalt/hand-rolled-plot` | 1.21.0 as `warn` | **1.26.0** (grace had restarted at the 1.21.0 widening) |
|
|
1477
|
+
| `basalt/chart-legend-literal` | 1.20.0 as `warn` | **1.26.0** |
|
|
1478
|
+
| `basalt/shadow-basalt-export` | 1.20.0 as `warn` | ADVISORY — permanent `warn`, see below, never subject to the C16 gate |
|
|
1479
|
+
| `basalt/hand-rolled-shell` | 1.20.0 as `warn` | **1.26.0** |
|
|
1480
|
+
| `theme-allow-unscoped` (guard kind) | 1.20.0 as `warn` | **1.26.0** |
|
|
1481
|
+
| `surface-shadow-override` (guard kind) | 1.20.0 as `warn` | **1.26.0** |
|
|
1482
|
+
| `css-raw-surface` (guard kind) | 1.20.0 as `warn` | **1.26.0** |
|
|
1483
|
+
| `inline-font-size` (guard kind) | 1.20.0 as `warn` | **1.26.0** |
|
|
1484
|
+
| `hidden-inline-style` (guard kind) | 1.20.0 as `warn` | **1.26.0** |
|
|
1485
|
+
| `basalt/control-size-literal` | 1.26.0 as `warn` | **1.27.0** |
|
|
1486
|
+
| `basalt/in-body-page-title` | 1.26.0 as `warn` | **1.27.0** |
|
|
1487
|
+
| `in-body-page-title` (guard kind) | 1.26.0 as `warn` | **1.27.0** — same id as the rule above, so one waiver covers both |
|
|
1488
|
+
| `basalt/responsive-twin` | 1.26.0 as `warn` | **1.27.0** |
|
|
1489
|
+
| `basalt/search-literal-link` | 1.26.0 as `warn` | **1.27.0** |
|
|
1490
|
+
| `basalt/use-search-from-literal` | 1.26.0 as `warn` | **1.27.0** |
|
|
1491
|
+
| `basalt/control-outside-home` | 1.26.0 as `warn` | still `warn`, `promote: '1.28.0'` — re-dated on 9 measured cross-file overlay warns in argo |
|
|
1492
|
+
| `raw-selection-control` (guard kind) | 1.26.0 as `warn` | still `warn`, `promote: '1.28.0'` — the text lane of the row above |
|
|
712
1493
|
|
|
713
1494
|
`card-with-border`, `inline-display`, `raw-html-layout`, `raw-form-control`, `raw-font-family` and
|
|
714
1495
|
the other original guard kinds have been `error` since before 1.2.0 — they never had a grace minor.
|
|
715
1496
|
Guard findings only gained a severity field at all in 1.4.0; before that every finding was fatal.
|
|
716
1497
|
|
|
1498
|
+
**Both ledgers changed shape at 1.26.0 (C16, `docs/CONTROLS-SPEC.md` §1).** An entry
|
|
1499
|
+
used to be a bare promotion-note string with no expiry a machine could check — which is exactly how
|
|
1500
|
+
the nine rows above sat at `warn` for five minors although the doctrine below says "one minor". Both
|
|
1501
|
+
`PLUGIN_RULE_GRACE` (`configs/oxlint-plugin.js`) and `GRACE_PERIOD_KINDS` (`src/guard/index.ts`) now
|
|
1502
|
+
hold `{ since, promote, why }` (semver strings), and a version-gated test fails the build once
|
|
1503
|
+
`package.json`'s version reaches an entry's `promote` while the entry is still there — see
|
|
1504
|
+
"1.26.0" above for the full change. `shadow-basalt-export` moved to a sibling ledger,
|
|
1505
|
+
`PLUGIN_RULE_ADVISORY`, which the gate never checks: it was already documented as a possible
|
|
1506
|
+
permanent `warn`, and the version-gate doctrine only fits an entry that has an honest promotion
|
|
1507
|
+
date.
|
|
1508
|
+
|
|
717
1509
|
**Rule-id rename at 1.1.0:** `basalt/import-boundary` split into `basalt/visx-boundary`,
|
|
718
1510
|
`basalt/visx-tooltip` and `basalt/token-layer-boundary` (the last is repo-local and deliberately not
|
|
719
1511
|
in the shipped preset). A config still naming `import-boundary` after 1.1.0 disables nothing.
|