basalt-ui 1.24.0 → 1.26.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 +25 -0
- package/MIGRATING.md +547 -21
- package/README.md +46 -33
- package/agent/rules/basalt-batteries.md +205 -0
- package/agent/rules/basalt-charts.md +125 -555
- package/agent/rules/basalt-controls.md +155 -0
- package/agent/rules/basalt-mantine.md +146 -439
- package/agent/rules/basalt-state.md +113 -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 +1275 -31
- package/configs/oxlint-plugin.test.ts +947 -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 +30 -0
- package/dist/controls/enum-filter.d.ts.map +1 -0
- package/dist/controls/enum-filter.js +91 -0
- package/dist/controls/filter-context.d.ts +55 -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 +19 -0
- package/dist/controls/index.d.ts.map +1 -0
- package/dist/controls/index.js +44 -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/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 +29 -0
- package/dist/controls/range-filter.d.ts.map +1 -0
- package/dist/controls/range-filter.js +120 -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/query-state.d.ts +1 -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 +44 -9
- package/dist/dashboard/stat-card.d.ts.map +1 -1
- package/dist/dashboard/stat-card.js +80 -48
- package/dist/dashboard/stat-card.module.css +82 -0
- package/dist/dashboard/stat-card.module.css.d.ts +11 -0
- package/dist/data/data-table.d.ts +102 -20
- package/dist/data/data-table.d.ts.map +1 -1
- package/dist/data/data-table.js +166 -65
- package/dist/data/table.d.ts +1 -1
- package/dist/data/table.d.ts.map +1 -1
- 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 +82 -7
- package/dist/guard/types.d.ts +3 -3
- package/dist/guard/types.d.ts.map +1 -1
- package/dist/index.d.ts +12 -2
- 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 +81 -0
- package/dist/router-tanstack/search-store.d.ts.map +1 -0
- package/dist/router-tanstack/search-store.js +187 -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 +392 -0
- package/dist/state/fields.d.ts.map +1 -0
- package/dist/state/fields.js +369 -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 +48 -0
- package/dist/state/persisted.d.ts.map +1 -0
- package/dist/state/persisted.js +105 -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 +155 -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 +37 -0
- package/dist/widget-header/widget-header.d.ts.map +1 -0
- package/dist/widget-header/widget-header.js +77 -0
- package/dist/widget-header/widget-header.module.css +335 -0
- package/dist/widget-header/widget-header.module.css.d.ts +24 -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 +637 -0
- package/src/controls/enum-filter.tsx +159 -0
- package/src/controls/filter-context.tsx +153 -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 +48 -0
- package/src/controls/multi-select-filter.tsx +140 -0
- package/src/controls/peer-boundary.test.ts +85 -0
- package/src/controls/props-contract.test.tsx +106 -0
- package/src/controls/range-filter.tsx +184 -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/query-state.tsx +1 -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 +82 -0
- package/src/dashboard/stat-card.module.css.d.ts +11 -0
- package/src/dashboard/stat-card.test.tsx +67 -1
- package/src/dashboard/stat-card.tsx +209 -68
- package/src/data/data-table.test.tsx +282 -6
- package/src/data/data-table.tsx +429 -94
- package/src/data/table.ts +2 -0
- package/src/guard/check-source.test.ts +126 -13
- package/src/guard/grace.test.ts +149 -0
- package/src/guard/index.ts +172 -29
- package/src/guard/types.ts +4 -2
- package/src/index.ts +46 -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 +31 -0
- package/src/router-tanstack/index.ts +24 -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 +661 -0
- package/src/router-tanstack/search-store.test.ts +418 -0
- package/src/router-tanstack/search-store.ts +380 -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 +258 -0
- package/src/state/fields.ts +892 -0
- package/src/state/persisted.ts +211 -0
- package/src/state.ts +46 -240
- package/src/styles.css +13 -0
- package/src/surfaces.ts +203 -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 +335 -0
- package/src/widget-header/widget-header.module.css.d.ts +24 -0
- package/src/widget-header/widget-header.test.tsx +187 -0
- package/src/widget-header/widget-header.tsx +168 -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 -269
- 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/src/shell/index.test.tsx
CHANGED
|
@@ -7,22 +7,37 @@
|
|
|
7
7
|
import { MantineProvider } from '@mantine/core'
|
|
8
8
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
9
9
|
import { describe, expect, test } from 'bun:test'
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
10
|
+
import { ActionGroup } from '../controls/actions'
|
|
11
|
+
import { BasaltShell, PageBar } from './index'
|
|
12
|
+
import type { BasaltAccountProps, SidebarBlock, SidebarSection } from './index'
|
|
12
13
|
|
|
13
14
|
const BRAND = { name: 'Argo' }
|
|
14
15
|
const ONE_SECTION: SidebarSection[] = [
|
|
15
16
|
{ label: 'Main', items: [{ key: 'home', label: 'Home', icon: null }] },
|
|
16
17
|
]
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
const AWAITING: SidebarBlock = {
|
|
20
|
+
kind: 'list',
|
|
21
|
+
key: 'awaiting',
|
|
22
|
+
label: 'Awaiting action',
|
|
23
|
+
count: 3,
|
|
24
|
+
items: [
|
|
25
|
+
{ key: 'a', label: 'Review PR' },
|
|
26
|
+
{ key: 'b', label: 'Sign contract' },
|
|
27
|
+
{ key: 'c', label: 'Reply to Jo' },
|
|
28
|
+
],
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('BasaltShell sidebarBlocks', () => {
|
|
32
|
+
test("a 'custom' block reaches the sidebar's nav scroll region through the hand-off", () => {
|
|
20
33
|
const { container } = render(
|
|
21
34
|
<MantineProvider>
|
|
22
35
|
<BasaltShell
|
|
23
36
|
brand={BRAND}
|
|
24
37
|
sections={ONE_SECTION}
|
|
25
|
-
|
|
38
|
+
sidebarBlocks={[
|
|
39
|
+
{ kind: 'custom', key: 'tree', node: <div data-testid="nav-extra">Extra</div> },
|
|
40
|
+
]}
|
|
26
41
|
/>
|
|
27
42
|
</MantineProvider>,
|
|
28
43
|
)
|
|
@@ -31,6 +46,77 @@ describe('BasaltShell sidebarNavExtra', () => {
|
|
|
31
46
|
expect(stack).not.toBeNull()
|
|
32
47
|
expect(stack?.contains(extra)).toBe(true)
|
|
33
48
|
})
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Both halves of the SAME prop, which is the point of C13: one declaration renders the desktop
|
|
52
|
+
* block and produces the mobile row. `sidebarNavExtra` + `mobileNav.moreExtra` needed two.
|
|
53
|
+
*/
|
|
54
|
+
test('the same block projects to ONE More row that opens a nested sheet of its items', async () => {
|
|
55
|
+
render(
|
|
56
|
+
<MantineProvider>
|
|
57
|
+
<BasaltShell brand={BRAND} sections={ONE_SECTION} sidebarBlocks={[AWAITING]} />
|
|
58
|
+
</MantineProvider>,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
fireEvent.click(screen.getByLabelText('More'))
|
|
62
|
+
await waitFor(() => expect(document.querySelector('[role="menu"]')).not.toBeNull())
|
|
63
|
+
// One row, stating its own count — not three rows, one per item. `Home` took the bar slot, so
|
|
64
|
+
// the block row is the whole More surface here.
|
|
65
|
+
const row = screen.getByText('Awaiting action · 3')
|
|
66
|
+
expect(document.querySelectorAll('.mantine-Menu-item')).toHaveLength(1)
|
|
67
|
+
|
|
68
|
+
fireEvent.click(row)
|
|
69
|
+
await waitFor(() => expect(document.querySelector('.mantine-Drawer-content')).not.toBeNull())
|
|
70
|
+
// Scoped to the sheet: the desktop block renders the same three labels, and a bare `getByText`
|
|
71
|
+
// would match both copies rather than proving the sheet holds them.
|
|
72
|
+
const body = document.querySelector('.mantine-Drawer-body')?.textContent ?? ''
|
|
73
|
+
expect(body).toContain('Review PR')
|
|
74
|
+
expect(body).toContain('Reply to Jo')
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test("mobile:'hidden' keeps the block off the More surface entirely", () => {
|
|
78
|
+
render(
|
|
79
|
+
<MantineProvider>
|
|
80
|
+
<BasaltShell
|
|
81
|
+
brand={BRAND}
|
|
82
|
+
sections={[
|
|
83
|
+
{ label: 'Main', items: [{ key: 'home', label: 'Home', icon: null, mobile: 'tab' }] },
|
|
84
|
+
]}
|
|
85
|
+
sidebarBlocks={[{ ...AWAITING, mobile: 'hidden' }]}
|
|
86
|
+
/>
|
|
87
|
+
</MantineProvider>,
|
|
88
|
+
)
|
|
89
|
+
// Nothing else feeds More, so a hidden block must not conjure the slot at all.
|
|
90
|
+
expect(screen.queryByLabelText('More')).toBeNull()
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Law C13's enforcement column is "tsc — `sidebarNavExtra` / `mobileNav.moreExtra` removed". These
|
|
96
|
+
* two `@ts-expect-error`s ARE that gate: they fail the build the day either prop comes back, which
|
|
97
|
+
* is the only way a type-level removal can be asserted from a test file.
|
|
98
|
+
*/
|
|
99
|
+
describe('BasaltShell — the removed ReactNode slots', () => {
|
|
100
|
+
test('sidebarNavExtra and mobileNav.moreExtra no longer type-check', () => {
|
|
101
|
+
const removedNavExtra = (
|
|
102
|
+
<BasaltShell
|
|
103
|
+
brand={BRAND}
|
|
104
|
+
sections={ONE_SECTION}
|
|
105
|
+
// @ts-expect-error sidebarNavExtra was replaced by `sidebarBlocks` kind 'custom' (C13)
|
|
106
|
+
sidebarNavExtra={<div />}
|
|
107
|
+
/>
|
|
108
|
+
)
|
|
109
|
+
const removedMoreExtra = (
|
|
110
|
+
<BasaltShell
|
|
111
|
+
brand={BRAND}
|
|
112
|
+
sections={ONE_SECTION}
|
|
113
|
+
// @ts-expect-error mobileNav.moreExtra was replaced by `sidebarBlocks` kind 'list' (C13)
|
|
114
|
+
mobileNav={{ moreExtra: <div /> }}
|
|
115
|
+
/>
|
|
116
|
+
)
|
|
117
|
+
expect(removedNavExtra).toBeTruthy()
|
|
118
|
+
expect(removedMoreExtra).toBeTruthy()
|
|
119
|
+
})
|
|
34
120
|
})
|
|
35
121
|
|
|
36
122
|
/**
|
|
@@ -140,3 +226,203 @@ describe('BasaltShell collapse persistence', () => {
|
|
|
140
226
|
expect(localStorage.getItem(`basalt:${key}`)).toBe(JSON.stringify({ v: 1, value: false }))
|
|
141
227
|
})
|
|
142
228
|
})
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Law C14 — an empty home renders nothing, so no route pays for a reserved row. This is the
|
|
232
|
+
* assertion the spec names as the law's gate: through 1.25.0 the mobile header was a 97px SUM whose
|
|
233
|
+
* second row (`appHeaderMobileActionsHeight`, 52px) was reserved on every route whether or not the
|
|
234
|
+
* page portalled anything into it. Both tokens are deleted; the header is `appShellHeaderHeight` at
|
|
235
|
+
* every width, and an empty `PageBar` adds no node in either place.
|
|
236
|
+
*/
|
|
237
|
+
describe('BasaltShell header height (law C14)', () => {
|
|
238
|
+
test('the AppShell header is ONE row — a single unconditional height declaration, no media override', () => {
|
|
239
|
+
render(
|
|
240
|
+
<MantineProvider>
|
|
241
|
+
<BasaltShell brand={BRAND} sections={ONE_SECTION} />
|
|
242
|
+
</MantineProvider>,
|
|
243
|
+
)
|
|
244
|
+
// Mantine emits the AppShell dimensions as a `<style>` block: one `:root` rule plus one
|
|
245
|
+
// `@media` rule per RESPONSIVE prop. A `{ base, sm }` header height therefore shows up twice;
|
|
246
|
+
// a single number shows up once, which is the invariant.
|
|
247
|
+
const css = [...document.querySelectorAll('style')]
|
|
248
|
+
.map((tag) => tag.textContent ?? '')
|
|
249
|
+
.find((text) => text.includes('--app-shell-header-height'))
|
|
250
|
+
expect(css).toBeDefined()
|
|
251
|
+
const heights = [...(css ?? '').matchAll(/--app-shell-header-height:\s*([^;]+)/g)].map(
|
|
252
|
+
(m) => m[1],
|
|
253
|
+
)
|
|
254
|
+
expect(heights).toHaveLength(1)
|
|
255
|
+
// 48px, expressed the way Mantine's own `rem()` does.
|
|
256
|
+
expect(heights[0]).toContain('3rem')
|
|
257
|
+
// The navbar and footer ARE responsive, so their overrides prove the query blocks still exist —
|
|
258
|
+
// the header simply is not among them any more.
|
|
259
|
+
expect(css).toContain('--app-shell-footer-height:0rem')
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
test('an empty PageBar contributes no node — not in the header, not in the page flow', () => {
|
|
263
|
+
render(
|
|
264
|
+
<MantineProvider>
|
|
265
|
+
<BasaltShell brand={BRAND} sections={ONE_SECTION}>
|
|
266
|
+
<PageBar />
|
|
267
|
+
</BasaltShell>
|
|
268
|
+
</MantineProvider>,
|
|
269
|
+
)
|
|
270
|
+
expect(document.querySelector('[data-basalt-page-bar]')).toBeNull()
|
|
271
|
+
})
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* `globalActions` is DECLARED DATA since 1.26.0 (`GlobalAction[]`, was `ReactNode`), which is what
|
|
276
|
+
* lets basalt project it onto mobile at all: the first two ride the bar, the rest fold into the
|
|
277
|
+
* header's ONE kebab, and a `'hidden'` one is dropped below `sm`. A `ReactNode` slot could express
|
|
278
|
+
* none of that, which is why every consumer hand-rolled a responsive twin instead.
|
|
279
|
+
*/
|
|
280
|
+
describe('BasaltShell globalActions mobile policy', () => {
|
|
281
|
+
const ACTIONS = [
|
|
282
|
+
{ key: 'timer', node: <span data-testid="g-timer" /> },
|
|
283
|
+
{ key: 'bell', node: <span data-testid="g-bell" /> },
|
|
284
|
+
{ key: 'theme', node: <span data-testid="g-theme" /> },
|
|
285
|
+
{ key: 'devtools', node: <span data-testid="g-devtools" />, mobile: 'hidden' as const },
|
|
286
|
+
]
|
|
287
|
+
|
|
288
|
+
const renderWithGlobals = () =>
|
|
289
|
+
render(
|
|
290
|
+
<MantineProvider>
|
|
291
|
+
<BasaltShell brand={BRAND} sections={ONE_SECTION} globalActions={ACTIONS} />
|
|
292
|
+
</MantineProvider>,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
test('the first two are mounted ONCE, unwrapped — visible at every width', () => {
|
|
296
|
+
renderWithGlobals()
|
|
297
|
+
for (const id of ['g-timer', 'g-bell']) {
|
|
298
|
+
const node = screen.getByTestId(id)
|
|
299
|
+
expect(node.closest('.mantine-visible-from-sm')).toBeNull()
|
|
300
|
+
expect(node.closest('.mantine-hidden-from-sm')).toBeNull()
|
|
301
|
+
}
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
test("the third defaults to 'more': desktop-only inline, plus the header's one kebab", () => {
|
|
305
|
+
renderWithGlobals()
|
|
306
|
+
expect(screen.getByTestId('g-theme').closest('.mantine-visible-from-sm')).not.toBeNull()
|
|
307
|
+
const kebabs = document.querySelectorAll('[aria-label="More actions"]')
|
|
308
|
+
expect(kebabs).toHaveLength(1)
|
|
309
|
+
expect(kebabs[0]?.closest('.mantine-hidden-from-sm')).not.toBeNull()
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
test("'hidden' drops it below sm and never reaches the kebab", () => {
|
|
313
|
+
renderWithGlobals()
|
|
314
|
+
expect(screen.getByTestId('g-devtools').closest('.mantine-visible-from-sm')).not.toBeNull()
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
test('a page ActionGroup takes over the kebab, so the header never shows two', () => {
|
|
318
|
+
render(
|
|
319
|
+
<MantineProvider>
|
|
320
|
+
<BasaltShell brand={BRAND} sections={ONE_SECTION} globalActions={ACTIONS}>
|
|
321
|
+
<PageBar actions={{ secondary: [{ key: 'export', label: 'Export' }] }} />
|
|
322
|
+
</BasaltShell>
|
|
323
|
+
</MantineProvider>,
|
|
324
|
+
)
|
|
325
|
+
expect(document.querySelectorAll('[aria-label="More actions"]')).toHaveLength(1)
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
test('filtersEnd folds into the SAME kebab — never a second one in row 2', () => {
|
|
329
|
+
// The bug this pins: every `ActionGroup` used to read the shell's `mobile: 'more'` globals and
|
|
330
|
+
// claim a kebab, so `PageBar.filtersEnd` (projected through its own group in row 2) grew a
|
|
331
|
+
// SECOND kebab that re-mounted the global node. Now only row 1 is `host: 'page'`.
|
|
332
|
+
render(
|
|
333
|
+
<MantineProvider>
|
|
334
|
+
<BasaltShell brand={BRAND} sections={ONE_SECTION} globalActions={ACTIONS}>
|
|
335
|
+
<PageBar
|
|
336
|
+
actions={{ secondary: [{ key: 'export', label: 'Export CSV' }] }}
|
|
337
|
+
filtersEnd={[{ key: 'metrics', label: 'Manage metrics' }]}
|
|
338
|
+
/>
|
|
339
|
+
</BasaltShell>
|
|
340
|
+
</MantineProvider>,
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
const kebabs = document.querySelectorAll('[aria-label="More actions"]')
|
|
344
|
+
expect(kebabs).toHaveLength(1)
|
|
345
|
+
// And it is the HEADER's, not row 2's.
|
|
346
|
+
expect(kebabs[0]?.closest('[data-basalt-page-bar="shell"]')).toBeNull()
|
|
347
|
+
expect(document.querySelector('.mantine-AppShell-header')?.contains(kebabs[0] as Node)).toBe(
|
|
348
|
+
true,
|
|
349
|
+
)
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
test('the global node reaches ONE dropdown, not two', async () => {
|
|
353
|
+
render(
|
|
354
|
+
<MantineProvider>
|
|
355
|
+
<BasaltShell brand={BRAND} sections={ONE_SECTION} globalActions={ACTIONS}>
|
|
356
|
+
<PageBar
|
|
357
|
+
actions={{ secondary: [{ key: 'export', label: 'Export CSV' }] }}
|
|
358
|
+
filtersEnd={[{ key: 'metrics', label: 'Manage metrics' }]}
|
|
359
|
+
/>
|
|
360
|
+
</BasaltShell>
|
|
361
|
+
</MantineProvider>,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
// One inline copy (the desktop-only `visibleFrom` box) before any dropdown opens.
|
|
365
|
+
expect(screen.getAllByTestId('g-theme')).toHaveLength(1)
|
|
366
|
+
fireEvent.click(screen.getByLabelText('More actions'))
|
|
367
|
+
await waitFor(() => expect(document.querySelector('[role="menu"]')).not.toBeNull())
|
|
368
|
+
// Exactly one more copy — the open dropdown's — so the node is never live in two menus at once.
|
|
369
|
+
expect(screen.getAllByTestId('g-theme')).toHaveLength(2)
|
|
370
|
+
expect(document.querySelectorAll('[role="menu"]')).toHaveLength(1)
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
test("filtersEnd alone still yields exactly one kebab, and it is the header's", () => {
|
|
374
|
+
render(
|
|
375
|
+
<MantineProvider>
|
|
376
|
+
<BasaltShell brand={BRAND} sections={ONE_SECTION} globalActions={ACTIONS}>
|
|
377
|
+
<PageBar filtersEnd={[{ key: 'metrics', label: 'Manage metrics' }]} />
|
|
378
|
+
</BasaltShell>
|
|
379
|
+
</MantineProvider>,
|
|
380
|
+
)
|
|
381
|
+
const kebabs = document.querySelectorAll('[aria-label="More actions"]')
|
|
382
|
+
expect(kebabs).toHaveLength(1)
|
|
383
|
+
expect(document.querySelector('.mantine-AppShell-header')?.contains(kebabs[0] as Node)).toBe(
|
|
384
|
+
true,
|
|
385
|
+
)
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
test('a PageBar with no row-1 actions and no filtersEnd hands the kebab BACK to the shell', () => {
|
|
389
|
+
render(
|
|
390
|
+
<MantineProvider>
|
|
391
|
+
<BasaltShell brand={BRAND} sections={ONE_SECTION} globalActions={ACTIONS}>
|
|
392
|
+
<PageBar tabs={<span data-testid="tabs" />} />
|
|
393
|
+
</BasaltShell>
|
|
394
|
+
</MantineProvider>,
|
|
395
|
+
)
|
|
396
|
+
const kebabs = document.querySelectorAll('[aria-label="More actions"]')
|
|
397
|
+
expect(kebabs).toHaveLength(1)
|
|
398
|
+
expect(kebabs[0]?.closest('.mantine-hidden-from-sm')).not.toBeNull()
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
test('an ActionGroup mounted in some OTHER home inherits no global rows and no claim', () => {
|
|
402
|
+
// A consumer's tier-2 kebab (a `Section`/`ChartCard` actions slot) must not swallow the shell's
|
|
403
|
+
// globals — nor steal the claim, which would leave the shell's own kebab unrendered.
|
|
404
|
+
render(
|
|
405
|
+
<MantineProvider>
|
|
406
|
+
<BasaltShell brand={BRAND} sections={ONE_SECTION} globalActions={ACTIONS}>
|
|
407
|
+
<ActionGroup secondary={[{ key: 'a', label: 'Section action' }]} />
|
|
408
|
+
</BasaltShell>
|
|
409
|
+
</MantineProvider>,
|
|
410
|
+
)
|
|
411
|
+
// Two kebabs is CORRECT here: the shell's (globals) and the section's (its own rows). What must
|
|
412
|
+
// not happen is the global node appearing in the section's.
|
|
413
|
+
const shellKebab = document
|
|
414
|
+
.querySelector('.mantine-AppShell-header')
|
|
415
|
+
?.querySelector('[aria-label="More actions"]')
|
|
416
|
+
expect(shellKebab).not.toBeNull()
|
|
417
|
+
expect(screen.getAllByTestId('g-theme')).toHaveLength(1)
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
test('no globalActions at all renders no kebab', () => {
|
|
421
|
+
render(
|
|
422
|
+
<MantineProvider>
|
|
423
|
+
<BasaltShell brand={BRAND} sections={ONE_SECTION} />
|
|
424
|
+
</MantineProvider>,
|
|
425
|
+
)
|
|
426
|
+
expect(document.querySelector('[aria-label="More actions"]')).toBeNull()
|
|
427
|
+
})
|
|
428
|
+
})
|
package/src/shell/index.tsx
CHANGED
|
@@ -17,25 +17,37 @@
|
|
|
17
17
|
* active item across `sections`, not from a router hook. Collapse is persisted via basalt's own
|
|
18
18
|
* `createPersistedState` (`../state`) keyed by `storageKey` — see `collapseStore`.
|
|
19
19
|
*/
|
|
20
|
-
import { AppShell } from '@mantine/core'
|
|
21
|
-
import { useMemo } from 'react'
|
|
20
|
+
import { AppShell, Box } from '@mantine/core'
|
|
21
|
+
import { Fragment, useMemo } from 'react'
|
|
22
22
|
import type { MouseEvent, ReactNode } from 'react'
|
|
23
23
|
import { AppSidebar } from './app-sidebar'
|
|
24
24
|
import { MobileNav, accountRowCount } from './app-mobile-nav'
|
|
25
|
-
import { projectMobileNav } from './mobile-nav-model'
|
|
25
|
+
import { blockRowCount, projectMobileNav } from './mobile-nav-model'
|
|
26
26
|
import { AppBreadcrumbs } from './app-breadcrumbs'
|
|
27
|
-
import {
|
|
28
|
-
import
|
|
29
|
-
import type {
|
|
30
|
-
import type {
|
|
31
|
-
import {
|
|
27
|
+
import { PageBarOutlet, PageBarProvider, usePageKebabClaimed } from './page-bar'
|
|
28
|
+
import { OverflowMenu, globalActionAsBarAction, globalActionMobile } from '../controls/actions'
|
|
29
|
+
import type { GlobalAction } from '../controls/actions'
|
|
30
|
+
import type { AccountMenuItem, BasaltAccountProps } from './account-types'
|
|
31
|
+
import type { SidebarSearchActions, SidebarSearchConfig } from './sidebar-search'
|
|
32
|
+
import type {
|
|
33
|
+
MobileNavConfig,
|
|
34
|
+
NavAnchor,
|
|
35
|
+
SidebarBlock,
|
|
36
|
+
SidebarItem,
|
|
37
|
+
SidebarSection,
|
|
38
|
+
} from '../nav/types'
|
|
39
|
+
import { CtlSlot, useBasaltSpacing } from '../theme'
|
|
32
40
|
import { createPersistedState } from '../state'
|
|
33
41
|
import headerClasses from './app-header.module.css'
|
|
34
42
|
import mobileNavClasses from './app-mobile-nav.module.css'
|
|
35
43
|
|
|
36
44
|
export { AppSidebar, type AppSidebarProps } from './app-sidebar'
|
|
37
45
|
export { NavCountBadge } from './nav-count-badge'
|
|
38
|
-
export {
|
|
46
|
+
export {
|
|
47
|
+
SidebarSearch,
|
|
48
|
+
type SidebarSearchConfig,
|
|
49
|
+
type SidebarSearchActions,
|
|
50
|
+
} from './sidebar-search'
|
|
39
51
|
export { SidebarAccount } from './app-sidebar-account'
|
|
40
52
|
export type {
|
|
41
53
|
AccountBadgeTone,
|
|
@@ -56,7 +68,7 @@ export {
|
|
|
56
68
|
MOBILE_MORE_KEY,
|
|
57
69
|
} from './mobile-nav-model'
|
|
58
70
|
export { AppBreadcrumbs } from './app-breadcrumbs'
|
|
59
|
-
export {
|
|
71
|
+
export { PageBar, type PageBarProps } from './page-bar'
|
|
60
72
|
|
|
61
73
|
/**
|
|
62
74
|
* The shared nav vocabulary lives in `src/nav/types.ts` so the headless router bridge can `import
|
|
@@ -70,6 +82,12 @@ export type {
|
|
|
70
82
|
NavSectionMobile,
|
|
71
83
|
SidebarItem,
|
|
72
84
|
SidebarSection,
|
|
85
|
+
SidebarBlock,
|
|
86
|
+
SidebarBlockItem,
|
|
87
|
+
SidebarBlockTone,
|
|
88
|
+
SidebarListBlock,
|
|
89
|
+
SidebarProgressBlock,
|
|
90
|
+
SidebarCustomBlock,
|
|
73
91
|
MobileNavConfig,
|
|
74
92
|
MobileNavGroup,
|
|
75
93
|
MobileNavModel,
|
|
@@ -98,8 +116,11 @@ export type SettingsMenuItem = {
|
|
|
98
116
|
}
|
|
99
117
|
|
|
100
118
|
export type BasaltShellProps = {
|
|
101
|
-
/**
|
|
102
|
-
|
|
119
|
+
/**
|
|
120
|
+
* Brand identity for the sidebar header. Supplying `menu` turns the brand row into a `Name ▾`
|
|
121
|
+
* workspace switcher — the rows are `AccountMenuItem`s, the shape the account menu already uses.
|
|
122
|
+
*/
|
|
123
|
+
brand: BrandConfig & { menu?: AccountMenuItem[] }
|
|
103
124
|
/** Grouped nav sections — the ONE definition, rendered as the sidebar and projected to the bar. */
|
|
104
125
|
sections: SidebarSection[]
|
|
105
126
|
/**
|
|
@@ -107,28 +128,54 @@ export type BasaltShellProps = {
|
|
|
107
128
|
* configured still produces a working bar (see `projectMobileNav`'s zero-config fallback).
|
|
108
129
|
*/
|
|
109
130
|
mobileNav?: MobileNavConfig
|
|
110
|
-
/** Persistent, shell-owned top-bar slot (timer, refresh, notifications). */
|
|
111
|
-
globalActions?: ReactNode
|
|
112
131
|
/**
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
* `
|
|
117
|
-
* `
|
|
132
|
+
* Persistent, shell-owned header actions (timer, notifications, a global `SyncButton`) — DECLARED
|
|
133
|
+
* DATA, not a `ReactNode` slot, so basalt owns the mobile projection: the first two ride the bar,
|
|
134
|
+
* the rest fold into the header's single kebab (`mobile: 'bar' | 'more' | 'hidden'`, see
|
|
135
|
+
* `GlobalAction`). That kebab is the SAME one `PageBar`'s ROW 1 opens — a route whose bar has
|
|
136
|
+
* row-1 actions (or `filtersEnd`) lends it, any other route gets it from the shell. An
|
|
137
|
+
* `ActionGroup` a consumer mounts in some other home never inherits these rows.
|
|
138
|
+
*/
|
|
139
|
+
globalActions?: GlobalAction[]
|
|
140
|
+
/**
|
|
141
|
+
* Sidebar blocks — DECLARED DATA (law C13, `docs/CONTROLS-SPEC.md` §2.3), which replaced both
|
|
142
|
+
* `sidebarNavExtra` and `mobileNav.moreExtra`. `placement: 'nav'` (the default for `list` and
|
|
143
|
+
* `custom`) appends after the nav sections inside the scroll region; `'bottom'` pins above the
|
|
144
|
+
* settings footer.
|
|
145
|
+
*
|
|
146
|
+
* Because they are data, basalt owns the projections the two `ReactNode` slots could not express:
|
|
147
|
+
* a `list` with a `count` becomes a dot on its icon in the collapsed rail, a `progress` block
|
|
148
|
+
* becomes a ring on the settings row, and a block with `mobile: 'more'` becomes one More-sheet
|
|
149
|
+
* row opening a nested sheet of its items. A `kind: 'custom'` block is desktop-only, exactly as
|
|
150
|
+
* `sidebarNavExtra` was.
|
|
151
|
+
*/
|
|
152
|
+
sidebarBlocks?: SidebarBlock[]
|
|
153
|
+
/**
|
|
154
|
+
* Entries appended to the sidebar settings footer — flat link rows at three or fewer, one gear
|
|
155
|
+
* menu at four or more (`AppSidebarProps.settingsMenuItems` documents the threshold).
|
|
118
156
|
*/
|
|
119
|
-
sidebarNavExtra?: ReactNode
|
|
120
|
-
/** Entries appended to the sidebar settings menu. */
|
|
121
157
|
settingsMenuItems?: SettingsMenuItem[]
|
|
122
158
|
/**
|
|
123
|
-
*
|
|
159
|
+
* Forces the footer form instead of taking the count rule above: `'flat'` always renders link
|
|
160
|
+
* rows, `'menu'` always renders the gear dropdown. `'auto'` (the default) is today's ≤3 → flat
|
|
161
|
+
* behaviour. Reach for it when the rows are CONTROLS rather than destinations — three of those
|
|
162
|
+
* (a theme radio group, a devtools switch) read as a widget pile in the footer even though the
|
|
163
|
+
* count says flat. Full rationale on `AppSidebarProps.settingsMenu`.
|
|
164
|
+
*
|
|
165
|
+
* @default 'auto'
|
|
166
|
+
*/
|
|
167
|
+
settingsMenu?: 'auto' | 'flat' | 'menu'
|
|
168
|
+
/**
|
|
169
|
+
* Optional account row rendered below the settings menu in the sidebar footer (see
|
|
124
170
|
* `SidebarAccount` / `BasaltAccountProps`). Omitting it reproduces today's footer unchanged.
|
|
125
171
|
*/
|
|
126
172
|
account?: BasaltAccountProps
|
|
127
173
|
/**
|
|
128
174
|
* Optional search field below the brand in the sidebar (fixed, above the nav scroll). Supply
|
|
129
|
-
* `onOpen`, e.g. `() => openSpotlight()` from basalt-ui/commands.
|
|
175
|
+
* `onOpen`, e.g. `() => openSpotlight()` from basalt-ui/commands. `actions` adds one or two
|
|
176
|
+
* icon-only buttons to the right of that row (a tuple by type — see `SidebarSearchActions`).
|
|
130
177
|
*/
|
|
131
|
-
search?: SidebarSearchConfig
|
|
178
|
+
search?: SidebarSearchConfig & { actions?: SidebarSearchActions }
|
|
132
179
|
/**
|
|
133
180
|
* localStorage key for the persisted sidebar-collapsed flag. Ignored when `collapsed` is set.
|
|
134
181
|
*
|
|
@@ -254,8 +301,9 @@ export function BasaltShell({
|
|
|
254
301
|
sections,
|
|
255
302
|
mobileNav,
|
|
256
303
|
globalActions,
|
|
257
|
-
|
|
304
|
+
sidebarBlocks,
|
|
258
305
|
settingsMenuItems,
|
|
306
|
+
settingsMenu,
|
|
259
307
|
storageKey = 'basalt-sidebar-collapsed',
|
|
260
308
|
collapsed: collapsedProp,
|
|
261
309
|
onCollapsedChange,
|
|
@@ -280,26 +328,29 @@ export function BasaltShell({
|
|
|
280
328
|
}
|
|
281
329
|
|
|
282
330
|
const activeCrumb = findActiveCrumb(sections)
|
|
283
|
-
// The account row
|
|
284
|
-
// mobile sidebar to reach them through any more), so they
|
|
285
|
-
// menu-vs-sheet threshold. `accountRowCount`
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
//
|
|
331
|
+
// The account row, every settings entry and every mobile-reachable sidebar block render as flat
|
|
332
|
+
// rows in the More surface (there is no mobile sidebar to reach them through any more), so they
|
|
333
|
+
// count toward BOTH `needsMore` and the menu-vs-sheet threshold. `accountRowCount` and
|
|
334
|
+
// `blockRowCount` are the SAME functions the renderer agrees with, not second estimates of them:
|
|
335
|
+
// an account is worth 0 rows while `loading` and up to seven once authenticated, a block is worth
|
|
336
|
+
// exactly one row however many items it holds, and §2.2's whole guarantee (`menuMax` rows fit the
|
|
337
|
+
// headroom above the bar, and the menu runs `flip: false` so it cannot escape upward) is
|
|
338
|
+
// arithmetic over this number.
|
|
289
339
|
const extraMoreRows =
|
|
290
|
-
accountRowCount(account) + (settingsMenuItems?.length ?? 0) + (
|
|
340
|
+
accountRowCount(account) + (settingsMenuItems?.length ?? 0) + blockRowCount(sidebarBlocks)
|
|
291
341
|
const model = useMemo(
|
|
292
342
|
() => projectMobileNav(sections, { config: mobileNav, extraMoreRows }),
|
|
293
343
|
[sections, mobileNav, extraMoreRows],
|
|
294
344
|
)
|
|
295
345
|
|
|
296
346
|
return (
|
|
297
|
-
<
|
|
347
|
+
<PageBarProvider globalActions={globalActions ?? []}>
|
|
298
348
|
<AppShell
|
|
299
349
|
h="100dvh"
|
|
300
350
|
layout="alt"
|
|
301
351
|
header={{
|
|
302
|
-
height
|
|
352
|
+
// ONE height at every width (law C14): nothing reserves a second mobile row any more.
|
|
353
|
+
height: step.appShellHeaderHeight,
|
|
303
354
|
}}
|
|
304
355
|
navbar={{
|
|
305
356
|
width: {
|
|
@@ -320,8 +371,8 @@ export function BasaltShell({
|
|
|
320
371
|
<div className={headerClasses.lead}>
|
|
321
372
|
<AppBreadcrumbs {...activeCrumb} />
|
|
322
373
|
</div>
|
|
323
|
-
<
|
|
324
|
-
|
|
374
|
+
<PageBarOutlet className={headerClasses.pageBar} />
|
|
375
|
+
<HeaderGlobalActions actions={globalActions ?? []} />
|
|
325
376
|
</div>
|
|
326
377
|
</AppShell.Header>
|
|
327
378
|
|
|
@@ -331,8 +382,9 @@ export function BasaltShell({
|
|
|
331
382
|
sections={sections}
|
|
332
383
|
collapsed={collapsed}
|
|
333
384
|
onToggleCollapse={toggleCollapse}
|
|
334
|
-
|
|
385
|
+
{...(sidebarBlocks !== undefined && { blocks: sidebarBlocks })}
|
|
335
386
|
{...(settingsMenuItems !== undefined && { settingsMenuItems })}
|
|
387
|
+
{...(settingsMenu !== undefined && { settingsMenu })}
|
|
336
388
|
{...(account !== undefined && { account })}
|
|
337
389
|
{...(search !== undefined && { search })}
|
|
338
390
|
/>
|
|
@@ -349,9 +401,56 @@ export function BasaltShell({
|
|
|
349
401
|
config={mobileNav}
|
|
350
402
|
{...(account !== undefined && { account })}
|
|
351
403
|
{...(settingsMenuItems !== undefined && { settingsMenuItems })}
|
|
404
|
+
{...(sidebarBlocks !== undefined && { blocks: sidebarBlocks })}
|
|
352
405
|
/>
|
|
353
406
|
</AppShell.Footer>
|
|
354
407
|
</AppShell>
|
|
355
|
-
</
|
|
408
|
+
</PageBarProvider>
|
|
409
|
+
)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* The shell's own `globalActions`, projected for both viewports.
|
|
414
|
+
*
|
|
415
|
+
* Each action is mounted EXACTLY ONCE inline, in declaration order — a `mobile: 'bar'` action with
|
|
416
|
+
* no wrapper (visible at every width), a `'more'`/`'hidden'` one inside a `visibleFrom="sm"` box.
|
|
417
|
+
* The `'more'` nodes appear a second time only inside the kebab dropdown, which Mantine mounts
|
|
418
|
+
* lazily on open. That asymmetry is why `GlobalAction`'s JSDoc points anything stateful at
|
|
419
|
+
* `mobile: 'bar'`.
|
|
420
|
+
*
|
|
421
|
+
* The kebab renders here only while NO page bar owns one (`usePageKebabClaimed`), so a header never
|
|
422
|
+
* shows two — the claim is what makes "one kebab per header" a fact rather than a convention.
|
|
423
|
+
*/
|
|
424
|
+
function HeaderGlobalActions({ actions }: { actions: readonly GlobalAction[] }): ReactNode {
|
|
425
|
+
const kebabClaimed = usePageKebabClaimed()
|
|
426
|
+
if (actions.length === 0) return null
|
|
427
|
+
|
|
428
|
+
const more = actions
|
|
429
|
+
.filter((action, index) => globalActionMobile(action, index) === 'more')
|
|
430
|
+
.map(globalActionAsBarAction)
|
|
431
|
+
|
|
432
|
+
return (
|
|
433
|
+
// `CtlSlot` here for the same reason a home wraps its own slot (law C5): `globalActions` are
|
|
434
|
+
// consumer NODES, so basalt cannot pass them a size — it can only provide the tier they resolve
|
|
435
|
+
// against. Without it a `<NotificationBell/>`'s internal `ActionIcon` fell back to Mantine's
|
|
436
|
+
// `md` (28px) beside a 30px page group.
|
|
437
|
+
<CtlSlot>
|
|
438
|
+
<div className={headerClasses.global}>
|
|
439
|
+
{actions.map((action, index) =>
|
|
440
|
+
globalActionMobile(action, index) === 'bar' ? (
|
|
441
|
+
<Fragment key={action.key}>{action.node}</Fragment>
|
|
442
|
+
) : (
|
|
443
|
+
<Box key={action.key} visibleFrom="sm">
|
|
444
|
+
{action.node}
|
|
445
|
+
</Box>
|
|
446
|
+
),
|
|
447
|
+
)}
|
|
448
|
+
{!kebabClaimed && more.length > 0 && (
|
|
449
|
+
<Box hiddenFrom="sm">
|
|
450
|
+
<OverflowMenu actions={more} trigger="kebab" label="More actions" />
|
|
451
|
+
</Box>
|
|
452
|
+
)}
|
|
453
|
+
</div>
|
|
454
|
+
</CtlSlot>
|
|
356
455
|
)
|
|
357
456
|
}
|