basalt-ui 1.25.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 +18 -0
- package/MIGRATING.md +546 -20
- 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 +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/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 +37 -15
- package/dist/data/data-table.d.ts.map +1 -1
- package/dist/data/data-table.js +88 -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 +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/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 +96 -11
- package/src/data/data-table.tsx +220 -84
- 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 -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
|
@@ -13,9 +13,26 @@
|
|
|
13
13
|
* - a `disabled` destination that is silently dropped instead of rendered disabled (rule 11).
|
|
14
14
|
*/
|
|
15
15
|
import { describe, expect, test } from 'bun:test'
|
|
16
|
-
import { MOBILE_MORE_KEY, projectMobileNav } from './mobile-nav-model'
|
|
16
|
+
import { MOBILE_MORE_KEY, blockRowCount, projectMobileNav } from './mobile-nav-model'
|
|
17
17
|
import type { ProjectMobileNavOptions } from './mobile-nav-model'
|
|
18
|
-
import
|
|
18
|
+
import {
|
|
19
|
+
sidebarBlockFoldKey,
|
|
20
|
+
sidebarBlockMobile,
|
|
21
|
+
sidebarBlockPlacement,
|
|
22
|
+
sidebarBlockRail,
|
|
23
|
+
sidebarBlockVisibleCount,
|
|
24
|
+
sidebarSectionFoldKey,
|
|
25
|
+
slugifyLabel,
|
|
26
|
+
} from './sidebar-block-model'
|
|
27
|
+
import type {
|
|
28
|
+
MobileNavGroup,
|
|
29
|
+
MobileNavModel,
|
|
30
|
+
MobileNavSlot,
|
|
31
|
+
SidebarCustomBlock,
|
|
32
|
+
SidebarItem,
|
|
33
|
+
SidebarListBlock,
|
|
34
|
+
SidebarProgressBlock,
|
|
35
|
+
} from '../nav/types'
|
|
19
36
|
|
|
20
37
|
// ── fixtures ─────────────────────────────────────────────────────────────────────────────────────
|
|
21
38
|
|
|
@@ -258,9 +275,34 @@ describe('projectMobileNav', () => {
|
|
|
258
275
|
expect(rowKeys(more.groups)).toEqual(['reports', 'daily', 'weekly'])
|
|
259
276
|
})
|
|
260
277
|
|
|
261
|
-
/**
|
|
262
|
-
*
|
|
263
|
-
|
|
278
|
+
/**
|
|
279
|
+
* Rule 12 — a slot is active when the active destination is one it COVERS, and coverage and
|
|
280
|
+
* activeness read the SAME set. See `hasActiveDestination`'s doc for why they used to differ and
|
|
281
|
+
* what that cost (two `aria-current="page"` tabs at `/dashboard/sessions`, measured in Chrome).
|
|
282
|
+
*
|
|
283
|
+
* A SECTION tab covers its whole pruned tree, so the rollup rule 12 was written for still holds
|
|
284
|
+
* there — that is the case below.
|
|
285
|
+
*/
|
|
286
|
+
test('12. a SECTION slot is active when a NESTED child is the active destination', () => {
|
|
287
|
+
const model = projectMobileNav([
|
|
288
|
+
{
|
|
289
|
+
label: 'Main',
|
|
290
|
+
mobile: { tab: true },
|
|
291
|
+
items: [item('reports', { children: [item('daily', { active: true })] })],
|
|
292
|
+
},
|
|
293
|
+
{ label: 'Other', items: [item('other', { mobile: 'tab' })] },
|
|
294
|
+
])
|
|
295
|
+
|
|
296
|
+
expect(slotAt(model, 0).active).toBe(true)
|
|
297
|
+
expect(slotAt(model, 1).active).toBe(false)
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* The exclusivity half, and the one that was missing: an ITEM tab covers only the destination its
|
|
302
|
+
* tap navigates to, so a nested child that lives in the overflow lights the OVERFLOW slot instead.
|
|
303
|
+
* Lighting both was the defect — a bar cannot tell a reader they are in two places.
|
|
304
|
+
*/
|
|
305
|
+
test('12. an ITEM tab does NOT light for a child it cannot navigate to — that child’s slot does', () => {
|
|
264
306
|
const model = projectMobileNav([
|
|
265
307
|
{
|
|
266
308
|
label: 'Main',
|
|
@@ -271,8 +313,67 @@ describe('projectMobileNav', () => {
|
|
|
271
313
|
},
|
|
272
314
|
])
|
|
273
315
|
|
|
274
|
-
|
|
275
|
-
expect(
|
|
316
|
+
const active = model.slots.filter((slot) => slot.active)
|
|
317
|
+
expect(active.length).toBe(1)
|
|
318
|
+
expect(active[0]?.key).toBe('daily')
|
|
319
|
+
expect(slotAt(model, 0).active).toBe(false)
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* The invariant, stated once over every shape this model produces: AT MOST ONE slot is active.
|
|
324
|
+
* `useNav` already guarantees exactly one active DESTINATION (`use-nav.test.tsx`); this is the
|
|
325
|
+
* projection's half of the same law, and it is what a rendered bar actually shows.
|
|
326
|
+
*/
|
|
327
|
+
test('12. at most ONE slot is active, for every destination in a mixed definition', () => {
|
|
328
|
+
const sections = (activeKey: string) => [
|
|
329
|
+
{
|
|
330
|
+
label: 'Overview',
|
|
331
|
+
items: [
|
|
332
|
+
item('dashboard', {
|
|
333
|
+
mobile: 'tab' as const,
|
|
334
|
+
active: activeKey === 'dashboard',
|
|
335
|
+
children: [
|
|
336
|
+
item('sessions', { active: activeKey === 'sessions' }),
|
|
337
|
+
item('traffic', { active: activeKey === 'traffic' }),
|
|
338
|
+
],
|
|
339
|
+
}),
|
|
340
|
+
item('activity', { mobile: 'tab' as const, active: activeKey === 'activity' }),
|
|
341
|
+
],
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
label: 'Batteries',
|
|
345
|
+
mobile: { tab: true as const },
|
|
346
|
+
items: [
|
|
347
|
+
item('query', { active: activeKey === 'query' }),
|
|
348
|
+
item('router', { active: activeKey === 'router' }),
|
|
349
|
+
item('forms', { active: activeKey === 'forms' }),
|
|
350
|
+
],
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
label: 'System',
|
|
354
|
+
items: [
|
|
355
|
+
item('settings', { active: activeKey === 'settings' }),
|
|
356
|
+
item('user', { active: activeKey === 'user' }),
|
|
357
|
+
],
|
|
358
|
+
},
|
|
359
|
+
]
|
|
360
|
+
|
|
361
|
+
const keys = [
|
|
362
|
+
'dashboard',
|
|
363
|
+
'sessions',
|
|
364
|
+
'traffic',
|
|
365
|
+
'activity',
|
|
366
|
+
'query',
|
|
367
|
+
'router',
|
|
368
|
+
'forms',
|
|
369
|
+
'settings',
|
|
370
|
+
'user',
|
|
371
|
+
] as const
|
|
372
|
+
for (const key of keys) {
|
|
373
|
+
const model = projectMobileNav(sections(key))
|
|
374
|
+
const active = model.slots.filter((slot) => slot.active)
|
|
375
|
+
expect({ key, count: active.length }).toEqual({ key, count: 1 })
|
|
376
|
+
}
|
|
276
377
|
})
|
|
277
378
|
|
|
278
379
|
/** §2.4 — a five-slot bar is ~72px wide, so the bar label is `short` when the consumer supplied
|
|
@@ -294,3 +395,123 @@ describe('projectMobileNav', () => {
|
|
|
294
395
|
expect(slotAt(model, 1).short).toBe('Activity')
|
|
295
396
|
})
|
|
296
397
|
})
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* `blockRowCount` — the `accountRowCount` sibling for sidebar blocks (`docs/CONTROLS-SPEC.md`
|
|
401
|
+
* §2.3). It exists for one reason: `BasaltShell` needs the More-surface row count BEFORE the
|
|
402
|
+
* projection runs, because that count is what picks `menu` vs `sheet` against `menuMax`. A second,
|
|
403
|
+
* independent estimate of the same rows is exactly how a "1 row" account shipped a 9-row menu into
|
|
404
|
+
* headroom sized for 6.
|
|
405
|
+
*/
|
|
406
|
+
/** A three-item list block, the shape every row-count case counts against. */
|
|
407
|
+
const list = (extra: Partial<SidebarListBlock> = {}): SidebarListBlock => ({
|
|
408
|
+
kind: 'list',
|
|
409
|
+
key: extra.key ?? 'awaiting',
|
|
410
|
+
label: 'Awaiting action',
|
|
411
|
+
items: [
|
|
412
|
+
{ key: 'a', label: 'A' },
|
|
413
|
+
{ key: 'b', label: 'B' },
|
|
414
|
+
{ key: 'c', label: 'C' },
|
|
415
|
+
],
|
|
416
|
+
...extra,
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
describe('blockRowCount', () => {
|
|
420
|
+
test('undefined and an empty list are both zero', () => {
|
|
421
|
+
expect(blockRowCount(undefined)).toBe(0)
|
|
422
|
+
expect(blockRowCount([])).toBe(0)
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
test('ONE row per list block, never one per item — the row opens a nested sheet', () => {
|
|
426
|
+
expect(blockRowCount([list(), list({ key: 'second' })])).toBe(2)
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
test("mobile:'hidden' drops a list; a progress block is hidden until it asks otherwise", () => {
|
|
430
|
+
expect(blockRowCount([list({ mobile: 'hidden' })])).toBe(0)
|
|
431
|
+
const progress: SidebarProgressBlock = {
|
|
432
|
+
kind: 'progress',
|
|
433
|
+
key: 'onboarding',
|
|
434
|
+
label: 'Getting started',
|
|
435
|
+
value: 1,
|
|
436
|
+
total: 5,
|
|
437
|
+
}
|
|
438
|
+
expect(blockRowCount([progress])).toBe(0)
|
|
439
|
+
expect(blockRowCount([{ ...progress, mobile: 'more' }])).toBe(1)
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
test('a custom block never reaches mobile — it is desktop-only by kind', () => {
|
|
443
|
+
expect(blockRowCount([{ kind: 'custom', key: 'tree', node: null }])).toBe(0)
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
/** Same rule the renderer applies: an empty list would open a sheet with nothing in it. */
|
|
447
|
+
test('an empty list block contributes no row', () => {
|
|
448
|
+
expect(blockRowCount([list({ items: [] })])).toBe(0)
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
/** The threshold this number exists to move, asserted end to end against the projection. */
|
|
452
|
+
test('block rows push a six-destination More over the threshold into a sheet', () => {
|
|
453
|
+
const sections = [{ label: 'Main', items: [item('a', { mobile: 'tab' }), ...items(6)] }]
|
|
454
|
+
expect(slotAt(projectMobileNav(sections), 1).kind).toBe('menu')
|
|
455
|
+
expect(
|
|
456
|
+
slotAt(projectMobileNav(sections, { extraMoreRows: blockRowCount([list()]) }), 1).kind,
|
|
457
|
+
).toBe('sheet')
|
|
458
|
+
})
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* The block-default resolvers. Three call sites read them — the sidebar (placement + rail), the
|
|
463
|
+
* renderer (paint) and `blockRowCount` (the count above) — and a default re-derived per call site
|
|
464
|
+
* is how the count and the renderer drift apart.
|
|
465
|
+
*/
|
|
466
|
+
describe('sidebar block defaults', () => {
|
|
467
|
+
const listBlock: SidebarListBlock = { kind: 'list', key: 'k', label: 'L', items: [] }
|
|
468
|
+
const progressBlock: SidebarProgressBlock = {
|
|
469
|
+
kind: 'progress',
|
|
470
|
+
key: 'p',
|
|
471
|
+
label: 'P',
|
|
472
|
+
value: 1,
|
|
473
|
+
total: 5,
|
|
474
|
+
}
|
|
475
|
+
const customBlock: SidebarCustomBlock = { kind: 'custom', key: 'c', node: null }
|
|
476
|
+
|
|
477
|
+
test('placement: list/custom default to nav, progress to bottom', () => {
|
|
478
|
+
expect(sidebarBlockPlacement(listBlock)).toBe('nav')
|
|
479
|
+
expect(sidebarBlockPlacement(customBlock)).toBe('nav')
|
|
480
|
+
expect(sidebarBlockPlacement(progressBlock)).toBe('bottom')
|
|
481
|
+
expect(sidebarBlockPlacement({ ...listBlock, placement: 'bottom' })).toBe('bottom')
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
test('rail: a dot only for a list with BOTH a count and an icon, a ring for progress', () => {
|
|
485
|
+
expect(sidebarBlockRail(listBlock)).toBe('hidden')
|
|
486
|
+
expect(sidebarBlockRail({ ...listBlock, count: 3, icon: null })).toBe('dot')
|
|
487
|
+
expect(sidebarBlockRail({ ...listBlock, count: 3, icon: null, rail: 'hidden' })).toBe('hidden')
|
|
488
|
+
// Neither half alone earns one — and an explicit `rail: 'dot'` cannot conjure the node either.
|
|
489
|
+
expect(sidebarBlockRail({ ...listBlock, count: 3 })).toBe('hidden')
|
|
490
|
+
expect(sidebarBlockRail({ ...listBlock, icon: null })).toBe('hidden')
|
|
491
|
+
expect(sidebarBlockRail({ ...listBlock, rail: 'dot' })).toBe('hidden')
|
|
492
|
+
expect(sidebarBlockRail(progressBlock)).toBe('ring')
|
|
493
|
+
expect(sidebarBlockRail({ ...progressBlock, rail: 'hidden' })).toBe('hidden')
|
|
494
|
+
expect(sidebarBlockRail(customBlock)).toBe('hidden')
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
test('mobile: a list is reachable from More by default, progress and custom are not', () => {
|
|
498
|
+
expect(sidebarBlockMobile(listBlock)).toBe('more')
|
|
499
|
+
expect(sidebarBlockMobile({ ...listBlock, mobile: 'hidden' })).toBe('hidden')
|
|
500
|
+
expect(sidebarBlockMobile(progressBlock)).toBe('hidden')
|
|
501
|
+
expect(sidebarBlockMobile({ ...progressBlock, mobile: 'more' })).toBe('more')
|
|
502
|
+
expect(sidebarBlockMobile(customBlock)).toBe('hidden')
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
test('visible count honours max, and a max past the count changes nothing', () => {
|
|
506
|
+
expect(sidebarBlockVisibleCount(6, 3)).toBe(3)
|
|
507
|
+
expect(sidebarBlockVisibleCount(6, undefined)).toBe(6)
|
|
508
|
+
expect(sidebarBlockVisibleCount(2, 9)).toBe(2)
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
/** The fold keys are a consumer-readable contract — a rename resets every user's sidebar. */
|
|
512
|
+
test('the fold keys namespace to basalt:sidebar-block:<key> / basalt:sidebar-section:<slug>', () => {
|
|
513
|
+
expect(sidebarBlockFoldKey('awaiting')).toBe('sidebar-block:awaiting')
|
|
514
|
+
expect(sidebarSectionFoldKey('Tools & More')).toBe('sidebar-section:tools-more')
|
|
515
|
+
expect(slugifyLabel(' Awaiting Action! ')).toBe('awaiting-action')
|
|
516
|
+
})
|
|
517
|
+
})
|
|
@@ -19,9 +19,11 @@ import type {
|
|
|
19
19
|
MobileNavModel,
|
|
20
20
|
MobileNavSlot,
|
|
21
21
|
NavMobilePlacement,
|
|
22
|
+
SidebarBlock,
|
|
22
23
|
SidebarItem,
|
|
23
24
|
SidebarSection,
|
|
24
25
|
} from '../nav/types'
|
|
26
|
+
import { sidebarBlockMobile } from './sidebar-block-model'
|
|
25
27
|
|
|
26
28
|
/** Hard cap on bar slots, INCLUDING More. Five 72px slots is the widest a 360px viewport holds. */
|
|
27
29
|
export const MOBILE_MAX_TABS_DEFAULT = 5
|
|
@@ -39,13 +41,33 @@ export const MOBILE_MORE_KEY = '__more'
|
|
|
39
41
|
export type ProjectMobileNavOptions = {
|
|
40
42
|
config?: MobileNavConfig | undefined
|
|
41
43
|
/**
|
|
42
|
-
* Extra rows the More surface will hold — account + settings +
|
|
44
|
+
* Extra rows the More surface will hold — account + settings + sidebar blocks. Drives both
|
|
43
45
|
* `needsMore` and the menu-vs-sheet threshold. The model cannot see those rows (they are
|
|
44
46
|
* Mantine-rendered chrome, not `SidebarItem`s), so the shell counts them and passes the total.
|
|
45
47
|
*/
|
|
46
48
|
extraMoreRows?: number
|
|
47
49
|
}
|
|
48
50
|
|
|
51
|
+
/**
|
|
52
|
+
* How many More-surface rows `blocks` contribute — the `accountRowCount` sibling for sidebar
|
|
53
|
+
* blocks, and for the same reason: `BasaltShell` needs the number BEFORE the projection runs (it
|
|
54
|
+
* feeds `extraMoreRows`, which picks `menu` vs `sheet` against `menuMax`) while the renderer is
|
|
55
|
+
* what decides how many rows a block actually becomes.
|
|
56
|
+
*
|
|
57
|
+
* ONE row per block, never one per item: a list block projects to a single `Awaiting action · 3`
|
|
58
|
+
* row that opens a nested sheet of its items. Counting the items instead would push a 12-item
|
|
59
|
+
* block's More surface to a sheet on the strength of rows that surface never paints.
|
|
60
|
+
*/
|
|
61
|
+
export function blockRowCount(blocks: readonly SidebarBlock[] | undefined): number {
|
|
62
|
+
if (blocks === undefined) return 0
|
|
63
|
+
return blocks.filter((block) => {
|
|
64
|
+
if (sidebarBlockMobile(block) !== 'more') return false
|
|
65
|
+
// An empty list block would open a sheet with nothing in it — same failure as the `loading`
|
|
66
|
+
// account that used to conjure a More slot (`accountRowCount`).
|
|
67
|
+
return block.kind !== 'list' || block.items.length > 0
|
|
68
|
+
}).length
|
|
69
|
+
}
|
|
70
|
+
|
|
49
71
|
const DEV = process.env['NODE_ENV'] !== 'production'
|
|
50
72
|
|
|
51
73
|
/**
|
|
@@ -67,12 +89,38 @@ function isSectionHidden(section: SidebarSection): boolean {
|
|
|
67
89
|
return section.mobile === false
|
|
68
90
|
}
|
|
69
91
|
|
|
70
|
-
/**
|
|
92
|
+
/**
|
|
93
|
+
* §2.3 rule 12 — a slot is active when the active destination is one the slot COVERS.
|
|
94
|
+
*
|
|
95
|
+
* "Covers" is the load-bearing word, and it is what makes exclusivity structural: a slot's `covered`
|
|
96
|
+
* set is exactly the destinations it can navigate to, and activeness now reads that same set. Two
|
|
97
|
+
* slots therefore cannot both be lit for one location, because no destination is in two `covered`
|
|
98
|
+
* sets — the overflow is built by removing everything the bar already covers.
|
|
99
|
+
*
|
|
100
|
+
* It used to roll up unconditionally over `children`, and `itemCandidate.covered` was
|
|
101
|
+
* `new Set([item.key])` — the item's own key ONLY. The two disagreed, and the disagreement was the
|
|
102
|
+
* bug: at `/dashboard/sessions` the `Dashboard` link slot lit through the rollup while `Sessions`
|
|
103
|
+
* itself, uncovered, sat in the overflow and lit the `More` slot as well. Measured in Chrome: two
|
|
104
|
+
* `aria-current="page"` tabs in a five-slot bar.
|
|
105
|
+
*
|
|
106
|
+
* The rollup itself survives where coverage backs it — a SECTION slot covers its whole pruned tree
|
|
107
|
+
* (`collectKeys` recurses), so a nested destination inside a section tab still lights that tab, which
|
|
108
|
+
* is the "you are nowhere" case rule 12 was written for. What changed is an ITEM tab with children:
|
|
109
|
+
* it covers only itself, its children stay reachable in the overflow, and the slot that can actually
|
|
110
|
+
* reach the open route is the one that lights. That is strictly better than the alternative — folding
|
|
111
|
+
* the children into the parent's coverage would light the parent and make the child unreachable from
|
|
112
|
+
* the bar at all.
|
|
113
|
+
*/
|
|
71
114
|
function hasActiveDestination(item: SidebarItem): boolean {
|
|
72
115
|
if (item.active) return true
|
|
73
116
|
return item.children?.some(hasActiveDestination) ?? false
|
|
74
117
|
}
|
|
75
118
|
|
|
119
|
+
/** The half of rule 12 an ITEM slot reads: only the destination the tap actually navigates to. */
|
|
120
|
+
function isActiveDestination(item: SidebarItem): boolean {
|
|
121
|
+
return item.active === true
|
|
122
|
+
}
|
|
123
|
+
|
|
76
124
|
/** Rows a group tree will render. Nesting costs a row per node — children are indented rows. */
|
|
77
125
|
function countRows(items: readonly SidebarItem[]): number {
|
|
78
126
|
return items.reduce((total, item) => total + 1 + countRows(item.children ?? []), 0)
|
|
@@ -139,13 +187,15 @@ function itemCandidate(item: SidebarItem): Candidate {
|
|
|
139
187
|
return {
|
|
140
188
|
key: item.key,
|
|
141
189
|
covered: new Set([item.key]),
|
|
190
|
+
// `isActiveDestination`, not the rollup: this slot COVERS one key, so it reads one key
|
|
191
|
+
// (`hasActiveDestination`'s doc has the whole accounting).
|
|
142
192
|
toSlot: () => ({
|
|
143
193
|
kind: 'link',
|
|
144
194
|
key: item.key,
|
|
145
195
|
label: item.label,
|
|
146
196
|
short: item.short ?? item.label,
|
|
147
197
|
icon: item.icon,
|
|
148
|
-
active:
|
|
198
|
+
active: isActiveDestination(item),
|
|
149
199
|
item,
|
|
150
200
|
}),
|
|
151
201
|
}
|
|
@@ -8,7 +8,7 @@ import { Badge } from '@mantine/core'
|
|
|
8
8
|
import { VX } from '../tokens'
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* Sidebar nav count badge (docs/DESIGN-SPEC.md §5): mono
|
|
11
|
+
* Sidebar nav count badge (docs/DESIGN-SPEC.md §5): mono micro (11px), ink-8% bg, radius 5, height 16,
|
|
12
12
|
* padding 0 5px, muted text; `marginLeft: auto` pins it to the row end on any render path.
|
|
13
13
|
* `styles` (inline) rather than a token color prop, since none of Mantine's variant/color
|
|
14
14
|
* combinations land on the ink-tint idiom. Returns `null` for a zero/empty count so the badge slot
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* `PageBar` layout (docs/CONTROLS-SPEC.md §2.1).
|
|
3
|
+
*
|
|
4
|
+
* Law C7 in CSS form: `flex-wrap: nowrap` on every row and NO `overflow-x` anywhere. A home that
|
|
5
|
+
* scrolls sideways hides its own controls; the fold is computed instead — `More` for actions
|
|
6
|
+
* (`ActionGroup`), a `Filters (n)` sheet for filters (`FilterSet`'s job, wave 4). The filter
|
|
7
|
+
* container below only supplies `min-width: 0` so that fold has a box to measure against.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/* The shell-less bar: both rows, sticky at the top of the document. One layer under the AppShell
|
|
11
|
+
* header (`--app-shell-header-z-index`, default 100) so it never wins a stacking fight with ordinary
|
|
12
|
+
* in-flow content — Mantine's Input section, Indicator, sticky table headers, and Slider thumbs all
|
|
13
|
+
* sit at `z-index: 1` or higher, which used to paint over a bare `z-index: 1` bar while scrolling. */
|
|
14
|
+
.bar {
|
|
15
|
+
position: sticky;
|
|
16
|
+
top: 0;
|
|
17
|
+
z-index: calc(var(--app-shell-header-z-index, 100) - 1);
|
|
18
|
+
display: flex;
|
|
19
|
+
flex-direction: column;
|
|
20
|
+
gap: var(--vx-space-stack-xs);
|
|
21
|
+
background-color: var(--vx-surface-bg);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/*
|
|
25
|
+
* Row 2 inside a shell: in the page flow, sticky under the 48px header. `--app-shell-header-height`
|
|
26
|
+
* is Mantine's own AppShell variable, so the offset tracks the header at any density with no React
|
|
27
|
+
* state and no measurement. `z-index` sits one layer below the header itself (see `.bar` above) —
|
|
28
|
+
* high enough to stay above ordinary page content, never above the AppShell header.
|
|
29
|
+
*/
|
|
30
|
+
.row2Sticky {
|
|
31
|
+
position: sticky;
|
|
32
|
+
top: var(--app-shell-header-height, 0px);
|
|
33
|
+
z-index: calc(var(--app-shell-header-z-index, 100) - 1);
|
|
34
|
+
background-color: var(--vx-surface-bg);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.row1 {
|
|
38
|
+
display: flex;
|
|
39
|
+
flex-wrap: nowrap;
|
|
40
|
+
align-items: center;
|
|
41
|
+
gap: var(--vx-space-control-gap);
|
|
42
|
+
min-width: 0;
|
|
43
|
+
min-height: var(--vx-space-page-bar-row-height);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.lead {
|
|
47
|
+
display: flex;
|
|
48
|
+
flex-wrap: nowrap;
|
|
49
|
+
align-items: center;
|
|
50
|
+
gap: var(--vx-space-control-gap);
|
|
51
|
+
flex-shrink: 0;
|
|
52
|
+
min-width: 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.icon {
|
|
56
|
+
display: inline-flex;
|
|
57
|
+
align-items: center;
|
|
58
|
+
color: var(--vx-muted);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/*
|
|
62
|
+
* The shell-less page title (law C8: the breadcrumb names the page inside a shell, this does it
|
|
63
|
+
* outside one). Head-font treatment, same 88%/550 as every `WidgetHeader` title.
|
|
64
|
+
*/
|
|
65
|
+
.title {
|
|
66
|
+
margin: 0;
|
|
67
|
+
font-family: var(--basalt-font-head);
|
|
68
|
+
font-size: var(--vx-text-xl);
|
|
69
|
+
font-weight: 550;
|
|
70
|
+
font-stretch: 88%;
|
|
71
|
+
color: var(--vx-ink);
|
|
72
|
+
white-space: nowrap;
|
|
73
|
+
overflow: hidden;
|
|
74
|
+
text-overflow: ellipsis;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/*
|
|
78
|
+
* Icons inside a row-1 button are 16px and sit 6px from their label. Mantine spends
|
|
79
|
+
* `--mantine-spacing-xs` (11px) on a Button's section gap regardless of the size tier, which is the
|
|
80
|
+
* BODY tier's number — at 30px it detached the glyph from the word it belongs to. Scoped to the row
|
|
81
|
+
* rather than themed on `Button` globally: a `size="md"` button in a form still wants Mantine's gap.
|
|
82
|
+
*/
|
|
83
|
+
.row1 :global(.mantine-Button-section[data-position='left']),
|
|
84
|
+
.row2 :global(.mantine-Button-section[data-position='left']) {
|
|
85
|
+
margin-inline-end: var(--vx-space-control-gap);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.row1 :global(.mantine-Button-section) > svg,
|
|
89
|
+
.row1 :global(.mantine-ActionIcon-root) > svg {
|
|
90
|
+
width: 1rem;
|
|
91
|
+
height: 1rem;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* Actions and sync are right-aligned; the lead (or the breadcrumb, in a shell) holds the left. */
|
|
95
|
+
.row1End {
|
|
96
|
+
display: flex;
|
|
97
|
+
flex-wrap: nowrap;
|
|
98
|
+
align-items: center;
|
|
99
|
+
justify-content: flex-end;
|
|
100
|
+
gap: var(--vx-space-control-gap);
|
|
101
|
+
flex: 1 1 auto;
|
|
102
|
+
min-width: 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/*
|
|
106
|
+
* Row 2's rhythm is 8px · pills · 8px, NOT a 40px reserved row.
|
|
107
|
+
*
|
|
108
|
+
* `min-height: var(--vx-space-page-bar-row-height)` (40px) left a 30px pill floating inside a 40px
|
|
109
|
+
* box with the 5px above and below it belonging to nothing — and because the row sat in a `Stack`
|
|
110
|
+
* that already spaced it, the visible gap between the header and the pills read as ~19px while the
|
|
111
|
+
* gap from the pills to the first card read as ~19px too. Symmetric padding on the row itself is the
|
|
112
|
+
* same total height (46px vs 40px) with both halves owned by one declaration, so the sticky bar's
|
|
113
|
+
* published height covers exactly the band it paints.
|
|
114
|
+
*/
|
|
115
|
+
.row2 {
|
|
116
|
+
display: flex;
|
|
117
|
+
flex-wrap: nowrap;
|
|
118
|
+
align-items: center;
|
|
119
|
+
gap: var(--vx-space-control-gap);
|
|
120
|
+
min-width: 0;
|
|
121
|
+
padding-block: var(--vx-space-stack-sm);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.filters {
|
|
125
|
+
display: flex;
|
|
126
|
+
flex-wrap: nowrap;
|
|
127
|
+
align-items: center;
|
|
128
|
+
gap: var(--vx-space-control-gap);
|
|
129
|
+
flex: 1 1 auto;
|
|
130
|
+
min-width: 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.filtersEnd {
|
|
134
|
+
display: flex;
|
|
135
|
+
flex-wrap: nowrap;
|
|
136
|
+
align-items: center;
|
|
137
|
+
gap: var(--vx-space-control-gap);
|
|
138
|
+
margin-inline-start: auto;
|
|
139
|
+
flex-shrink: 0;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/*
|
|
143
|
+
* Law C15 — every touch target inside a home is at least `touchControlHeight` (36px at level 0)
|
|
144
|
+
* below `sm`, while the VISIBLE box stays the 30px `ctl` tier. `::after` and not `::before`:
|
|
145
|
+
* Mantine's Button owns `::before` for its loading overlay (`@mantine/core/styles/Button.css`),
|
|
146
|
+
* so a hit area drawn there would fight it. `pointer-events` stays default so the overlay is the
|
|
147
|
+
* one receiving the tap.
|
|
148
|
+
*
|
|
149
|
+
* `overflow: visible` is LOAD-BEARING, not tidying. Mantine's Button and ActionIcon roots both set
|
|
150
|
+
* `overflow: hidden` (`Button.css`, `ActionIcon.css`), which clips an overlay that by definition
|
|
151
|
+
* extends past the 30px box — so without this the 36px target simply does not exist and a tap 3px
|
|
152
|
+
* above the control misses. What `overflow: hidden` buys those roots is clipping the loading
|
|
153
|
+
* overlay's blur, and that overlay is only painted under `[data-loading]`, which is excluded below.
|
|
154
|
+
*/
|
|
155
|
+
@media (max-width: 47.99375em) {
|
|
156
|
+
.row1 :is(button, a),
|
|
157
|
+
.row2 :is(button, a) {
|
|
158
|
+
position: relative;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.row1 :is(button, a):not([data-loading]),
|
|
162
|
+
.row2 :is(button, a):not([data-loading]) {
|
|
163
|
+
overflow: visible;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.row1 :is(button, a)::after,
|
|
167
|
+
.row2 :is(button, a)::after {
|
|
168
|
+
content: '';
|
|
169
|
+
position: absolute;
|
|
170
|
+
top: 50%;
|
|
171
|
+
left: 50%;
|
|
172
|
+
translate: -50% -50%;
|
|
173
|
+
width: 100%;
|
|
174
|
+
height: 100%;
|
|
175
|
+
min-width: var(--vx-space-touch-control-height);
|
|
176
|
+
min-height: var(--vx-space-touch-control-height);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed CSS-module declaration (the S4 CSS-modules path). Named string props — not an index
|
|
3
|
+
* signature — so dot access stays verbatim under the package's strict flags.
|
|
4
|
+
*/
|
|
5
|
+
declare const classes: {
|
|
6
|
+
readonly bar: string
|
|
7
|
+
readonly row2Sticky: string
|
|
8
|
+
readonly row1: string
|
|
9
|
+
readonly lead: string
|
|
10
|
+
readonly icon: string
|
|
11
|
+
readonly title: string
|
|
12
|
+
readonly row2: string
|
|
13
|
+
readonly row1End: string
|
|
14
|
+
readonly filters: string
|
|
15
|
+
readonly filtersEnd: string
|
|
16
|
+
}
|
|
17
|
+
export default classes
|