basalt-ui 1.25.0 → 1.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +8 -6
- package/CHANGELOG.md +26 -0
- package/MIGRATING.md +815 -23
- package/README.md +45 -32
- package/agent/rules/basalt-batteries.md +205 -0
- package/agent/rules/basalt-charts.md +125 -555
- package/agent/rules/basalt-controls.md +160 -0
- package/agent/rules/basalt-mantine.md +146 -439
- package/agent/rules/basalt-state.md +118 -96
- package/agent/rules/basalt-tokens.md +126 -288
- package/agent/skills/basalt-app/SKILL.md +70 -195
- package/agent/skills/basalt-charts/SKILL.md +54 -234
- package/agent/skills/basalt-design/SKILL.md +56 -248
- package/agent/templates/CLAUDE-block.md.tpl +31 -40
- package/agent/templates/DESIGN.md.tpl +22 -51
- package/configs/oxlint-plugin.js +1465 -36
- package/configs/oxlint-plugin.test.ts +1190 -11
- package/configs/oxlint.json +11 -3
- package/dist/charts/cursor/resolve.d.ts +18 -0
- package/dist/charts/cursor/resolve.d.ts.map +1 -1
- package/dist/charts/cursor/resolve.js +15 -6
- package/dist/charts/cursor/scope.d.ts +6 -3
- package/dist/charts/cursor/scope.d.ts.map +1 -1
- package/dist/charts/cursor/store.d.ts +5 -1
- package/dist/charts/cursor/store.d.ts.map +1 -1
- package/dist/charts/cursor/store.js +4 -4
- package/dist/charts/hooks/useChartCursor.d.ts.map +1 -1
- package/dist/charts/hooks/useChartCursor.js +9 -8
- package/dist/charts/index.d.ts +2 -2
- package/dist/charts/index.d.ts.map +1 -1
- package/dist/charts/primitives/ChartCard.d.ts +34 -9
- package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
- package/dist/charts/primitives/ChartCard.js +29 -114
- package/dist/charts/primitives/ChartCard.test.d.ts +2 -0
- package/dist/charts/primitives/ChartCard.test.d.ts.map +1 -0
- package/dist/charts/sparklines/BarSparkline.d.ts +26 -2
- package/dist/charts/sparklines/BarSparkline.d.ts.map +1 -1
- package/dist/charts/sparklines/BarSparkline.js +18 -4
- package/dist/cli/check-coverage.test.d.ts +2 -0
- package/dist/cli/check-coverage.test.d.ts.map +1 -0
- package/dist/cli/index.d.ts +67 -4
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +225 -7
- package/dist/connectivity/connectivity-indicator.d.ts +4 -1
- package/dist/connectivity/connectivity-indicator.d.ts.map +1 -1
- package/dist/connectivity/connectivity-indicator.js +1 -1
- package/dist/content/index.d.ts +5 -3
- package/dist/content/index.d.ts.map +1 -1
- package/dist/content/index.js +4 -2
- package/dist/content/prose.module.css +7 -28
- package/dist/controls/actions.d.ts +169 -0
- package/dist/controls/actions.d.ts.map +1 -0
- package/dist/controls/actions.js +287 -0
- package/dist/controls/actions.module.css +62 -0
- package/dist/controls/actions.module.css.d.ts +13 -0
- package/dist/controls/actions.test.d.ts +2 -0
- package/dist/controls/actions.test.d.ts.map +1 -0
- package/dist/controls/actions.type-guard.test.d.ts +2 -0
- package/dist/controls/actions.type-guard.test.d.ts.map +1 -0
- package/dist/controls/compare-filter.d.ts +41 -0
- package/dist/controls/compare-filter.d.ts.map +1 -0
- package/dist/controls/compare-filter.js +23 -0
- package/dist/controls/control-group.d.ts +19 -0
- package/dist/controls/control-group.d.ts.map +1 -0
- package/dist/controls/control-group.js +20 -0
- package/dist/controls/control-group.test.d.ts +2 -0
- package/dist/controls/control-group.test.d.ts.map +1 -0
- package/dist/controls/controls.module.css +542 -0
- package/dist/controls/controls.module.css.d.ts +35 -0
- package/dist/controls/controls.router.test.d.ts +2 -0
- package/dist/controls/controls.router.test.d.ts.map +1 -0
- package/dist/controls/enum-filter.d.ts +39 -0
- package/dist/controls/enum-filter.d.ts.map +1 -0
- package/dist/controls/enum-filter.js +93 -0
- package/dist/controls/filter-context.d.ts +60 -0
- package/dist/controls/filter-context.d.ts.map +1 -0
- package/dist/controls/filter-context.js +69 -0
- package/dist/controls/filter-pill.d.ts +28 -0
- package/dist/controls/filter-pill.d.ts.map +1 -0
- package/dist/controls/filter-pill.js +70 -0
- package/dist/controls/filter-set.d.ts +13 -0
- package/dist/controls/filter-set.d.ts.map +1 -0
- package/dist/controls/filter-set.js +125 -0
- package/dist/controls/filter-set.test.d.ts +2 -0
- package/dist/controls/filter-set.test.d.ts.map +1 -0
- package/dist/controls/filter-sheet.d.ts +117 -0
- package/dist/controls/filter-sheet.d.ts.map +1 -0
- package/dist/controls/filter-sheet.js +146 -0
- package/dist/controls/glyphs.d.ts +27 -0
- package/dist/controls/glyphs.d.ts.map +1 -0
- package/dist/controls/glyphs.js +49 -0
- package/dist/controls/index.d.ts +20 -0
- package/dist/controls/index.d.ts.map +1 -0
- package/dist/controls/index.js +46 -0
- package/dist/controls/multi-select-filter.d.ts +23 -0
- package/dist/controls/multi-select-filter.d.ts.map +1 -0
- package/dist/controls/multi-select-filter.js +89 -0
- package/dist/controls/number-filter.d.ts +26 -0
- package/dist/controls/number-filter.d.ts.map +1 -0
- package/dist/controls/number-filter.js +143 -0
- package/dist/controls/peer-boundary.test.d.ts +2 -0
- package/dist/controls/peer-boundary.test.d.ts.map +1 -0
- package/dist/controls/props-contract.test.d.ts +2 -0
- package/dist/controls/props-contract.test.d.ts.map +1 -0
- package/dist/controls/range-filter.d.ts +43 -0
- package/dist/controls/range-filter.d.ts.map +1 -0
- package/dist/controls/range-filter.js +135 -0
- package/dist/controls/range-filter.type-guard.test.d.ts +2 -0
- package/dist/controls/range-filter.type-guard.test.d.ts.map +1 -0
- package/dist/controls/search-filter.d.ts +10 -0
- package/dist/controls/search-filter.d.ts.map +1 -0
- package/dist/controls/search-filter.js +81 -0
- package/dist/controls/select-filter.d.ts +74 -0
- package/dist/controls/select-filter.d.ts.map +1 -0
- package/dist/controls/select-filter.js +10 -0
- package/dist/controls/sync-button.d.ts +38 -0
- package/dist/controls/sync-button.d.ts.map +1 -0
- package/dist/controls/sync-button.js +119 -0
- package/dist/controls/sync-button.module.css +63 -0
- package/dist/controls/sync-button.module.css.d.ts +11 -0
- package/dist/controls/sync-button.test.d.ts +2 -0
- package/dist/controls/sync-button.test.d.ts.map +1 -0
- package/dist/controls/toggle-filter.d.ts +9 -0
- package/dist/controls/toggle-filter.d.ts.map +1 -0
- package/dist/controls/toggle-filter.js +42 -0
- package/dist/controls/view-tabs.d.ts +23 -0
- package/dist/controls/view-tabs.d.ts.map +1 -0
- package/dist/controls/view-tabs.js +59 -0
- package/dist/controls-dates/date-range-picker.d.ts +4 -0
- package/dist/controls-dates/date-range-picker.d.ts.map +1 -0
- package/dist/controls-dates/date-range-picker.js +18 -0
- package/dist/controls-dates/index.d.ts +9 -0
- package/dist/controls-dates/index.d.ts.map +1 -0
- package/dist/controls-dates/index.js +4 -0
- package/dist/dashboard/delta-badge.d.ts +6 -12
- package/dist/dashboard/delta-badge.d.ts.map +1 -1
- package/dist/dashboard/delta-badge.js +1 -50
- package/dist/dashboard/index.d.ts +1 -1
- package/dist/dashboard/index.d.ts.map +1 -1
- package/dist/dashboard/index.js +3 -1
- package/dist/dashboard/settings-section.d.ts +6 -4
- package/dist/dashboard/settings-section.d.ts.map +1 -1
- package/dist/dashboard/settings-section.js +23 -16
- package/dist/dashboard/settings-section.module.css +10 -0
- package/dist/dashboard/settings-section.module.css.d.ts +1 -0
- package/dist/dashboard/settings-section.test.d.ts +2 -0
- package/dist/dashboard/settings-section.test.d.ts.map +1 -0
- package/dist/dashboard/stat-card.d.ts +95 -9
- package/dist/dashboard/stat-card.d.ts.map +1 -1
- package/dist/dashboard/stat-card.js +108 -49
- package/dist/dashboard/stat-card.module.css +153 -0
- package/dist/dashboard/stat-card.module.css.d.ts +15 -0
- package/dist/data/data-table.d.ts +37 -15
- package/dist/data/data-table.d.ts.map +1 -1
- package/dist/data/data-table.js +96 -58
- package/dist/guard/grace.test.d.ts +20 -0
- package/dist/guard/grace.test.d.ts.map +1 -0
- package/dist/guard/index.d.ts +66 -3
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +78 -7
- package/dist/guard/types.d.ts +3 -3
- package/dist/guard/types.d.ts.map +1 -1
- package/dist/index.d.ts +13 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -10
- package/dist/nav/types.d.ts +78 -2
- package/dist/nav/types.d.ts.map +1 -1
- package/dist/notifications/bell.d.ts +4 -1
- package/dist/notifications/bell.d.ts.map +1 -1
- package/dist/notifications/index.d.ts +1 -1
- package/dist/router-tanstack/field.d.ts +14 -0
- package/dist/router-tanstack/field.d.ts.map +1 -0
- package/dist/router-tanstack/field.js +4 -0
- package/dist/router-tanstack/index.d.ts +6 -2
- package/dist/router-tanstack/index.d.ts.map +1 -1
- package/dist/router-tanstack/index.js +4 -0
- package/dist/router-tanstack/multi-search-param-store.d.ts +4 -52
- package/dist/router-tanstack/multi-search-param-store.d.ts.map +1 -1
- package/dist/router-tanstack/multi-search-param-store.js +19 -27
- package/dist/router-tanstack/search-param-store.d.ts +3 -87
- package/dist/router-tanstack/search-param-store.d.ts.map +1 -1
- package/dist/router-tanstack/search-param-store.js +16 -45
- package/dist/router-tanstack/search-store.d.ts +84 -0
- package/dist/router-tanstack/search-store.d.ts.map +1 -0
- package/dist/router-tanstack/search-store.js +271 -0
- package/dist/router-tanstack/search-store.router.test.d.ts +2 -0
- package/dist/router-tanstack/search-store.router.test.d.ts.map +1 -0
- package/dist/router-tanstack/search-store.test.d.ts +2 -0
- package/dist/router-tanstack/search-store.test.d.ts.map +1 -0
- package/dist/router-tanstack/use-nav.d.ts +0 -2
- package/dist/router-tanstack/use-nav.d.ts.map +1 -1
- package/dist/router-tanstack/use-nav.js +34 -12
- package/dist/router-tanstack/use-nav.test.d.ts +2 -0
- package/dist/router-tanstack/use-nav.test.d.ts.map +1 -0
- package/dist/section/index.d.ts +2 -0
- package/dist/section/index.d.ts.map +1 -0
- package/dist/section/index.js +4 -0
- package/dist/section/section.d.ts +24 -0
- package/dist/section/section.d.ts.map +1 -0
- package/dist/section/section.js +126 -0
- package/dist/section/section.module.css +104 -0
- package/dist/section/section.module.css.d.ts +11 -0
- package/dist/section/section.test.d.ts +2 -0
- package/dist/section/section.test.d.ts.map +1 -0
- package/dist/shell/app-breadcrumbs.d.ts.map +1 -1
- package/dist/shell/app-breadcrumbs.js +18 -15
- package/dist/shell/app-header.module.css +77 -37
- package/dist/shell/app-header.module.css.d.ts +2 -1
- package/dist/shell/app-mobile-nav.d.ts +9 -2
- package/dist/shell/app-mobile-nav.d.ts.map +1 -1
- package/dist/shell/app-mobile-nav.js +98 -11
- package/dist/shell/app-mobile-nav.module.css +8 -0
- package/dist/shell/app-mobile-nav.module.css.d.ts +1 -0
- package/dist/shell/app-sidebar.d.ts +55 -28
- package/dist/shell/app-sidebar.d.ts.map +1 -1
- package/dist/shell/app-sidebar.js +116 -67
- package/dist/shell/app-sidebar.module.css +303 -20
- package/dist/shell/app-sidebar.module.css.d.ts +19 -1
- package/dist/shell/index.d.ts +55 -21
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +40 -18
- package/dist/shell/mobile-nav-model.d.ts +13 -2
- package/dist/shell/mobile-nav-model.d.ts.map +1 -1
- package/dist/shell/mobile-nav-model.js +15 -1
- package/dist/shell/nav-count-badge.d.ts +1 -1
- package/dist/shell/page-bar.d.ts +51 -0
- package/dist/shell/page-bar.d.ts.map +1 -0
- package/dist/shell/page-bar.js +154 -0
- package/dist/shell/page-bar.module.css +178 -0
- package/dist/shell/page-bar.module.css.d.ts +17 -0
- package/dist/shell/page-bar.test.d.ts +2 -0
- package/dist/shell/page-bar.test.d.ts.map +1 -0
- package/dist/shell/sidebar-block-model.d.ts +48 -0
- package/dist/shell/sidebar-block-model.d.ts.map +1 -0
- package/dist/shell/sidebar-block-model.js +39 -0
- package/dist/shell/sidebar-blocks.d.ts +32 -0
- package/dist/shell/sidebar-blocks.d.ts.map +1 -0
- package/dist/shell/sidebar-blocks.js +169 -0
- package/dist/shell/sidebar-search.d.ts +13 -1
- package/dist/shell/sidebar-search.d.ts.map +1 -1
- package/dist/shell/sidebar-search.js +34 -1
- package/dist/shell/sidebar-search.module.css +26 -0
- package/dist/shell/sidebar-search.module.css.d.ts +2 -0
- package/dist/state/fields.d.ts +565 -0
- package/dist/state/fields.d.ts.map +1 -0
- package/dist/state/fields.js +405 -0
- package/dist/state/fields.test.d.ts +2 -0
- package/dist/state/fields.test.d.ts.map +1 -0
- package/dist/state/persisted.d.ts +60 -0
- package/dist/state/persisted.d.ts.map +1 -0
- package/dist/state/persisted.js +123 -0
- package/dist/state/persisted.test.d.ts +2 -0
- package/dist/state/persisted.test.d.ts.map +1 -0
- package/dist/state.d.ts +11 -56
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +14 -120
- package/dist/styles.css +13 -0
- package/dist/surfaces.d.ts +106 -24
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +156 -19
- package/dist/theme/controls.module.css +16 -1
- package/dist/theme/ctl-theme.d.ts +32 -0
- package/dist/theme/ctl-theme.d.ts.map +1 -0
- package/dist/theme/ctl-theme.js +54 -0
- package/dist/theme/ctl-theme.test.d.ts +2 -0
- package/dist/theme/ctl-theme.test.d.ts.map +1 -0
- package/dist/theme/ctl-tier-coverage.test.d.ts +2 -0
- package/dist/theme/ctl-tier-coverage.test.d.ts.map +1 -0
- package/dist/theme/divider-law.test.d.ts +2 -0
- package/dist/theme/divider-law.test.d.ts.map +1 -0
- package/dist/theme/icon-slot.d.ts +44 -0
- package/dist/theme/icon-slot.d.ts.map +1 -0
- package/dist/theme/icon-slot.js +21 -0
- package/dist/theme/icon-slot.module.css +47 -0
- package/dist/theme/icon-slot.module.css.d.ts +8 -0
- package/dist/theme/icon-slot.test.d.ts +2 -0
- package/dist/theme/icon-slot.test.d.ts.map +1 -0
- package/dist/theme/index.d.ts +1 -0
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/index.js +82 -5
- package/dist/theme/layout-rhythm-css.test.d.ts +2 -0
- package/dist/theme/layout-rhythm-css.test.d.ts.map +1 -0
- package/dist/theme/segmented-control.module.css +20 -0
- package/dist/theme/shadow-surfaces.test.d.ts.map +1 -1
- package/dist/tokens/index.d.ts +12 -0
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +38 -8
- package/dist/tokens/palette.d.ts +97 -61
- package/dist/tokens/palette.d.ts.map +1 -1
- package/dist/tokens/palette.js +76 -38
- package/dist/tokens.css +10 -2
- package/dist/widget-header/delta-badge.d.ts +13 -0
- package/dist/widget-header/delta-badge.d.ts.map +1 -0
- package/dist/widget-header/delta-badge.js +29 -0
- package/dist/widget-header/delta-badge.test.d.ts +2 -0
- package/dist/widget-header/delta-badge.test.d.ts.map +1 -0
- package/dist/widget-header/index.d.ts +3 -0
- package/dist/widget-header/index.d.ts.map +1 -0
- package/dist/widget-header/index.js +6 -0
- package/dist/widget-header/no-mantine.test.d.ts +2 -0
- package/dist/widget-header/no-mantine.test.d.ts.map +1 -0
- package/dist/widget-header/widget-header.d.ts +67 -0
- package/dist/widget-header/widget-header.d.ts.map +1 -0
- package/dist/widget-header/widget-header.js +89 -0
- package/dist/widget-header/widget-header.module.css +350 -0
- package/dist/widget-header/widget-header.module.css.d.ts +25 -0
- package/dist/widget-header/widget-header.test.d.ts +2 -0
- package/dist/widget-header/widget-header.test.d.ts.map +1 -0
- package/llms.txt +30 -17
- package/package.json +14 -1
- package/src/charts/cursor/resolve.test.ts +29 -1
- package/src/charts/cursor/resolve.ts +38 -6
- package/src/charts/cursor/scope.tsx +6 -3
- package/src/charts/cursor/store.test.ts +19 -1
- package/src/charts/cursor/store.ts +10 -5
- package/src/charts/hooks/useChartCursor.ts +16 -8
- package/src/charts/index.ts +2 -2
- package/src/charts/primitives/CartesianChart.test.tsx +124 -0
- package/src/charts/primitives/ChartCard.test.tsx +66 -0
- package/src/charts/primitives/ChartCard.tsx +86 -146
- package/src/charts/sparklines/BarSparkline.tsx +43 -5
- package/src/cli/check-coverage.test.ts +297 -0
- package/src/cli/check-theme.test.ts +38 -14
- package/src/cli/index.ts +392 -16
- package/src/cli/init-sync.test.ts +97 -0
- package/src/connectivity/connectivity-indicator.tsx +16 -9
- package/src/content/index.ts +16 -3
- package/src/content/prose.module.css +7 -28
- package/src/content/prose.module.css.test.ts +8 -24
- package/src/controls/actions.module.css +62 -0
- package/src/controls/actions.module.css.d.ts +13 -0
- package/src/controls/actions.test.tsx +252 -0
- package/src/controls/actions.tsx +655 -0
- package/src/controls/actions.type-guard.test.ts +63 -0
- package/src/controls/compare-filter.tsx +64 -0
- package/src/controls/control-group.test.tsx +229 -0
- package/src/controls/control-group.tsx +63 -0
- package/src/controls/controls.module.css +542 -0
- package/src/controls/controls.module.css.d.ts +35 -0
- package/src/controls/controls.router.test.tsx +1053 -0
- package/src/controls/enum-filter.tsx +170 -0
- package/src/controls/filter-context.tsx +158 -0
- package/src/controls/filter-pill.tsx +120 -0
- package/src/controls/filter-set.test.tsx +472 -0
- package/src/controls/filter-set.tsx +225 -0
- package/src/controls/filter-sheet.tsx +285 -0
- package/src/controls/glyphs.tsx +81 -0
- package/src/controls/index.ts +49 -0
- package/src/controls/multi-select-filter.tsx +140 -0
- package/src/controls/number-filter.tsx +291 -0
- package/src/controls/peer-boundary.test.ts +85 -0
- package/src/controls/props-contract.test.tsx +137 -0
- package/src/controls/range-filter.tsx +237 -0
- package/src/controls/range-filter.type-guard.test.tsx +122 -0
- package/src/controls/search-filter.tsx +123 -0
- package/src/controls/select-filter.tsx +89 -0
- package/src/controls/sync-button.module.css +63 -0
- package/src/controls/sync-button.module.css.d.ts +11 -0
- package/src/controls/sync-button.test.tsx +183 -0
- package/src/controls/sync-button.tsx +197 -0
- package/src/controls/toggle-filter.tsx +60 -0
- package/src/controls/view-tabs.tsx +110 -0
- package/src/controls-dates/date-range-picker.tsx +42 -0
- package/src/controls-dates/index.ts +8 -0
- package/src/dashboard/delta-badge.tsx +4 -74
- package/src/dashboard/index.ts +6 -1
- package/src/dashboard/settings-section.module.css +10 -0
- package/src/dashboard/settings-section.module.css.d.ts +1 -0
- package/src/dashboard/settings-section.test.tsx +56 -0
- package/src/dashboard/settings-section.tsx +33 -30
- package/src/dashboard/stat-card.module.css +153 -0
- package/src/dashboard/stat-card.module.css.d.ts +15 -0
- package/src/dashboard/stat-card.test.tsx +298 -1
- package/src/dashboard/stat-card.tsx +289 -67
- package/src/data/data-table.test.tsx +96 -11
- package/src/data/data-table.tsx +228 -84
- package/src/guard/check-source.test.ts +159 -13
- package/src/guard/grace.test.ts +158 -0
- package/src/guard/index.ts +185 -29
- package/src/guard/types.ts +4 -2
- package/src/index.ts +51 -5
- package/src/nav/types.ts +84 -2
- package/src/notifications/bell.tsx +6 -3
- package/src/notifications/index.ts +1 -1
- package/src/router-tanstack/field.ts +35 -0
- package/src/router-tanstack/index.ts +28 -2
- package/src/router-tanstack/multi-search-param-store.test.ts +71 -3
- package/src/router-tanstack/multi-search-param-store.ts +46 -77
- package/src/router-tanstack/search-param-store.ts +42 -159
- package/src/router-tanstack/search-store.router.test.tsx +882 -0
- package/src/router-tanstack/search-store.test.ts +578 -0
- package/src/router-tanstack/search-store.ts +546 -0
- package/src/router-tanstack/use-nav.test.tsx +187 -0
- package/src/router-tanstack/use-nav.ts +72 -14
- package/src/section/index.ts +1 -0
- package/src/section/section.module.css +104 -0
- package/src/section/section.module.css.d.ts +11 -0
- package/src/section/section.test.tsx +204 -0
- package/src/section/section.tsx +220 -0
- package/src/shell/app-breadcrumbs.tsx +44 -31
- package/src/shell/app-header.module.css +77 -37
- package/src/shell/app-header.module.css.d.ts +2 -1
- package/src/shell/app-mobile-nav.module.css +8 -0
- package/src/shell/app-mobile-nav.module.css.d.ts +1 -0
- package/src/shell/app-mobile-nav.tsx +147 -8
- package/src/shell/app-sidebar-account.tsx +2 -2
- package/src/shell/app-sidebar.module.css +303 -20
- package/src/shell/app-sidebar.module.css.d.ts +19 -1
- package/src/shell/app-sidebar.test.tsx +456 -41
- package/src/shell/app-sidebar.tsx +276 -140
- package/src/shell/index.test.tsx +291 -5
- package/src/shell/index.tsx +137 -38
- package/src/shell/mobile-nav-model.test.ts +228 -7
- package/src/shell/mobile-nav-model.ts +53 -3
- package/src/shell/nav-count-badge.tsx +1 -1
- package/src/shell/page-bar.module.css +178 -0
- package/src/shell/page-bar.module.css.d.ts +17 -0
- package/src/shell/page-bar.test.tsx +316 -0
- package/src/shell/page-bar.tsx +315 -0
- package/src/shell/sidebar-block-model.ts +83 -0
- package/src/shell/sidebar-blocks.tsx +301 -0
- package/src/shell/sidebar-search.module.css +26 -0
- package/src/shell/sidebar-search.module.css.d.ts +2 -0
- package/src/shell/sidebar-search.tsx +71 -2
- package/src/state/fields.test.tsx +512 -0
- package/src/state/fields.ts +1150 -0
- package/src/state/persisted.test.ts +91 -0
- package/src/state/persisted.ts +261 -0
- package/src/state.ts +52 -240
- package/src/styles.css +13 -0
- package/src/surfaces.ts +204 -36
- package/src/theme/controls.module.css +16 -1
- package/src/theme/ctl-theme.test.tsx +275 -0
- package/src/theme/ctl-theme.tsx +138 -0
- package/src/theme/ctl-tier-coverage.test.ts +170 -0
- package/src/theme/density-relations.test.ts +106 -51
- package/src/theme/divider-law.test.ts +259 -0
- package/src/theme/icon-slot.module.css +47 -0
- package/src/theme/icon-slot.module.css.d.ts +8 -0
- package/src/theme/icon-slot.test.tsx +275 -0
- package/src/theme/icon-slot.tsx +59 -0
- package/src/theme/index.ts +165 -3
- package/src/theme/layout-rhythm-css.test.ts +72 -0
- package/src/theme/radius.test.ts +11 -1
- package/src/theme/segmented-control.module.css +20 -0
- package/src/theme/shadow-surfaces.test.ts +20 -7
- package/src/theme/spacing.test.ts +75 -27
- package/src/theme/use-basalt-spacing.ts +2 -2
- package/src/tokens/density.test.ts +7 -25
- package/src/tokens/index.ts +48 -8
- package/src/tokens/palette.ts +107 -73
- package/src/widget-header/delta-badge.test.tsx +60 -0
- package/src/widget-header/delta-badge.tsx +57 -0
- package/src/widget-header/index.ts +2 -0
- package/src/widget-header/no-mantine.test.ts +33 -0
- package/src/widget-header/widget-header.module.css +350 -0
- package/src/widget-header/widget-header.module.css.d.ts +25 -0
- package/src/widget-header/widget-header.test.tsx +187 -0
- package/src/widget-header/widget-header.tsx +213 -0
- package/agent/rules/basalt-agent.md +0 -452
- package/agent/rules/basalt-app.md +0 -246
- package/agent/rules/basalt-commands.md +0 -320
- package/agent/rules/basalt-content.md +0 -409
- package/agent/rules/basalt-data.md +0 -329
- package/agent/rules/basalt-forms.md +0 -186
- package/agent/rules/basalt-notifications.md +0 -264
- package/agent/rules/basalt-query.md +0 -187
- package/agent/rules/basalt-router.md +0 -336
- package/dist/content/article-filter-bar.d.ts +0 -40
- package/dist/content/article-filter-bar.d.ts.map +0 -1
- package/dist/content/article-filter-bar.js +0 -56
- package/dist/shell/page-header.d.ts +0 -13
- package/dist/shell/page-header.d.ts.map +0 -1
- package/dist/shell/page-header.js +0 -23
- package/src/content/article-filter-bar.tsx +0 -88
- package/src/shell/page-header.tsx +0 -35
|
@@ -1,36 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* `blocks` — the declared-data seam that replaced `navExtra` (law C13, `docs/CONTROLS-SPEC.md`
|
|
3
|
+
* §2.3), plus the two other things the sidebar now owns: the `brand.menu` workspace switcher and
|
|
4
|
+
* the flat-vs-menu settings footer.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* (`.root[data-collapsed] .navExtra { display: none }`) is exercised by the guard rules it
|
|
14
|
-
* shares with `.childList`/`.sectionBand`, not by this test.
|
|
15
|
-
* 3. `sections={[]}` plus only `navExtra` renders cleanly: the nav Stack holds exactly the
|
|
16
|
-
* extra content, no orphan section wrapper.
|
|
17
|
-
* 4. Omitting the prop reproduces today's DOM exactly — the nav Stack's child count matches
|
|
18
|
-
* `sections.length`, with no stray wrapper appended for the (absent) slot. This is the
|
|
19
|
-
* regression guard for every existing consumer.
|
|
6
|
+
* What is asserted here and what deliberately is not: CSS-module class hashes are unavailable under
|
|
7
|
+
* `bun test` (see `stat-card.test.tsx` for the same constraint), and jsdom evaluates no media query,
|
|
8
|
+
* so the collapsed-rail RULES are not testable. What IS testable is the DOM the rules select
|
|
9
|
+
* against — `data-rail` on every block, a dot node when a count earns one, and the ring on the
|
|
10
|
+
* settings row (the one rail projection that is JS-gated, because it moves the mark to a different
|
|
11
|
+
* node). A regression that reintroduces a JS `collapsed` check for the CSS-only halves fails the
|
|
12
|
+
* "stays mounted when collapsed" assertions instead of only breaking at the rail width in a browser.
|
|
20
13
|
*/
|
|
21
14
|
import { MantineProvider } from '@mantine/core'
|
|
22
|
-
import { render, screen } from '@testing-library/react'
|
|
15
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
23
16
|
import { describe, expect, test } from 'bun:test'
|
|
24
|
-
import type { ReactNode } from 'react'
|
|
25
17
|
import { AppSidebar } from './app-sidebar'
|
|
26
18
|
import type { AppSidebarProps } from './app-sidebar'
|
|
19
|
+
import type { SidebarBlock } from '../nav/types'
|
|
27
20
|
|
|
28
21
|
const BRAND = { name: 'Argo' }
|
|
29
22
|
const ONE_SECTION: AppSidebarProps['sections'] = [
|
|
30
23
|
{ label: 'Main', items: [{ key: 'home', label: 'Home', icon: null }] },
|
|
31
24
|
]
|
|
32
25
|
|
|
33
|
-
function renderSidebar(props: Partial<AppSidebarProps>
|
|
26
|
+
function renderSidebar(props: Partial<AppSidebarProps>) {
|
|
34
27
|
return render(
|
|
35
28
|
<MantineProvider>
|
|
36
29
|
<AppSidebar
|
|
@@ -44,17 +37,68 @@ function renderSidebar(props: Partial<AppSidebarProps> & { navExtra?: ReactNode
|
|
|
44
37
|
)
|
|
45
38
|
}
|
|
46
39
|
|
|
47
|
-
/** The nav `ScrollArea`'s content Stack — the direct parent of `sections` and `
|
|
40
|
+
/** The nav `ScrollArea`'s content Stack — the direct parent of `sections` and the `'nav'` blocks. */
|
|
48
41
|
function navStack(container: HTMLElement): Element {
|
|
49
42
|
const stack = container.querySelector('.mantine-ScrollArea-content > .mantine-Stack-root')
|
|
50
43
|
if (!stack) throw new Error('expected the nav ScrollArea content Stack to render')
|
|
51
44
|
return stack
|
|
52
45
|
}
|
|
53
46
|
|
|
54
|
-
|
|
55
|
-
|
|
47
|
+
const AWAITING: SidebarBlock = {
|
|
48
|
+
kind: 'list',
|
|
49
|
+
key: 'awaiting',
|
|
50
|
+
label: 'Awaiting action',
|
|
51
|
+
count: 3,
|
|
52
|
+
items: [
|
|
53
|
+
{ key: 'a', label: 'Review PR', tone: 'warn' },
|
|
54
|
+
{ key: 'b', label: 'Sign contract', tone: 'bad', href: '/contract' },
|
|
55
|
+
{ key: 'c', label: 'Reply to Jo', meta: '2h' },
|
|
56
|
+
],
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
describe('sidebar blocks — rendering per kind', () => {
|
|
60
|
+
test("a 'list' block renders its label, count badge and rows", () => {
|
|
61
|
+
renderSidebar({ blocks: [AWAITING] })
|
|
62
|
+
expect(screen.getByText('Awaiting action')).toBeTruthy()
|
|
63
|
+
expect(screen.getByText('3')).toBeTruthy()
|
|
64
|
+
expect(screen.getByText('Review PR')).toBeTruthy()
|
|
65
|
+
expect(screen.getByText('Reply to Jo')).toBeTruthy()
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('a row with nowhere to go is NOT a link — the Recents shape stays plain text', () => {
|
|
69
|
+
renderSidebar({
|
|
70
|
+
blocks: [
|
|
71
|
+
{
|
|
72
|
+
kind: 'list',
|
|
73
|
+
key: 'recents',
|
|
74
|
+
label: 'Recents',
|
|
75
|
+
items: [
|
|
76
|
+
{ key: 'r1', label: 'Q3 plan' },
|
|
77
|
+
{ key: 'r2', label: 'Q4 plan', href: '/q4' },
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
})
|
|
82
|
+
// A keyboard walk must not stop on a dead `<a>`; the sibling WITH an href proves the same
|
|
83
|
+
// renderer still produces one when there is somewhere to go.
|
|
84
|
+
expect(screen.getByText('Q3 plan').closest('a')).toBeNull()
|
|
85
|
+
expect(screen.getByText('Q4 plan').closest('a')).not.toBeNull()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test("a 'progress' block renders label, `n of m` and a Progress track", () => {
|
|
89
|
+
const { container } = renderSidebar({
|
|
90
|
+
blocks: [
|
|
91
|
+
{ kind: 'progress', key: 'onboarding', label: 'Getting started', value: 1, total: 5 },
|
|
92
|
+
],
|
|
93
|
+
})
|
|
94
|
+
expect(screen.getByText('Getting started')).toBeTruthy()
|
|
95
|
+
expect(screen.getByText('1 of 5')).toBeTruthy()
|
|
96
|
+
expect(container.querySelector('.mantine-Progress-root')).not.toBeNull()
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test("a 'custom' block mounts the node verbatim, inside the nav scroll region", () => {
|
|
56
100
|
const { container } = renderSidebar({
|
|
57
|
-
|
|
101
|
+
blocks: [{ kind: 'custom', key: 'tree', node: <div data-testid="nav-extra">Extra</div> }],
|
|
58
102
|
})
|
|
59
103
|
const extra = screen.getByTestId('nav-extra')
|
|
60
104
|
const stack = navStack(container)
|
|
@@ -62,33 +106,404 @@ describe('navExtra', () => {
|
|
|
62
106
|
expect(stack.lastElementChild?.contains(extra)).toBe(true)
|
|
63
107
|
})
|
|
64
108
|
|
|
65
|
-
test('
|
|
109
|
+
test('a custom block stays mounted when collapsed — hiding it is CSS-only, not a JS gate', () => {
|
|
66
110
|
const { container } = renderSidebar({
|
|
67
111
|
collapsed: true,
|
|
68
|
-
|
|
112
|
+
blocks: [{ kind: 'custom', key: 'tree', node: <div data-testid="nav-extra">Extra</div> }],
|
|
69
113
|
})
|
|
70
|
-
|
|
71
|
-
const stack = navStack(container)
|
|
72
|
-
// Presence, not visibility — jsdom never evaluates the `min-width: sm` media query that hides
|
|
73
|
-
// this in the rail. A regression here (reintroducing `!collapsed &&` in app-sidebar.tsx) would
|
|
74
|
-
// fail this assertion instead of only breaking silently in a real browser at the rail width.
|
|
75
|
-
expect(stack.contains(extra)).toBe(true)
|
|
114
|
+
expect(navStack(container).contains(screen.getByTestId('nav-extra'))).toBe(true)
|
|
76
115
|
})
|
|
77
116
|
|
|
78
|
-
test('sections={[]} plus
|
|
117
|
+
test('sections={[]} plus one block renders cleanly — no orphan section wrapper', () => {
|
|
79
118
|
const { container } = renderSidebar({
|
|
80
119
|
sections: [],
|
|
81
|
-
|
|
120
|
+
blocks: [{ kind: 'custom', key: 'tree', node: <div data-testid="nav-extra">Extra</div> }],
|
|
82
121
|
})
|
|
83
|
-
|
|
84
|
-
expect(screen.getByTestId('nav-extra')).toBeTruthy()
|
|
85
|
-
expect(stack.children.length).toBe(1)
|
|
122
|
+
expect(navStack(container).children.length).toBe(1)
|
|
86
123
|
})
|
|
87
124
|
|
|
88
|
-
test("omitting
|
|
125
|
+
test("omitting blocks reproduces today's DOM — no stray wrapper for the unused slot", () => {
|
|
89
126
|
const { container } = renderSidebar({})
|
|
90
|
-
expect(
|
|
91
|
-
|
|
92
|
-
|
|
127
|
+
expect(navStack(container).children.length).toBe(ONE_SECTION.length)
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
describe('sidebar blocks — placement', () => {
|
|
132
|
+
test("'nav' is the default for list and custom: both land in the scroll region", () => {
|
|
133
|
+
const { container } = renderSidebar({
|
|
134
|
+
blocks: [AWAITING, { kind: 'custom', key: 'tree', node: <span>tree</span> }],
|
|
135
|
+
})
|
|
136
|
+
expect(navStack(container).children.length).toBe(ONE_SECTION.length + 2)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test("'bottom' pins above the settings footer, OUTSIDE the nav scroll region", () => {
|
|
140
|
+
const { container } = renderSidebar({
|
|
141
|
+
blocks: [{ ...AWAITING, placement: 'bottom' }],
|
|
142
|
+
settingsMenuItems: [{ key: 'theme', label: 'Theme', onClick: () => {} }],
|
|
143
|
+
})
|
|
144
|
+
expect(navStack(container).children.length).toBe(ONE_SECTION.length)
|
|
145
|
+
const label = screen.getByText('Awaiting action')
|
|
146
|
+
expect(label.closest('.mantine-ScrollArea-root')).toBeNull()
|
|
147
|
+
// Above the settings row, not below it.
|
|
148
|
+
const footer = label.closest('.mantine-Stack-root')
|
|
149
|
+
const settings = screen.getByLabelText('Theme')
|
|
150
|
+
expect(footer?.contains(settings)).toBe(true)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
test('a progress block is bottom-placed with no placement stated', () => {
|
|
154
|
+
const { container } = renderSidebar({
|
|
155
|
+
blocks: [{ kind: 'progress', key: 'p', label: 'Getting started', value: 1, total: 5 }],
|
|
156
|
+
})
|
|
157
|
+
expect(navStack(container).children.length).toBe(ONE_SECTION.length)
|
|
158
|
+
expect(screen.getByText('Getting started').closest('.mantine-ScrollArea-root')).toBeNull()
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
describe('sidebar blocks — Show more', () => {
|
|
163
|
+
const LONG: SidebarBlock = {
|
|
164
|
+
kind: 'list',
|
|
165
|
+
key: 'long',
|
|
166
|
+
label: 'Awaiting action',
|
|
167
|
+
max: 3,
|
|
168
|
+
items: Array.from({ length: 6 }, (_, i) => ({ key: `i${i}`, label: `Item ${i}` })),
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
test('max shows the first N and a Show more toggle; the toggle reveals the rest', () => {
|
|
172
|
+
renderSidebar({ blocks: [LONG] })
|
|
173
|
+
expect(screen.getByText('Item 2')).toBeTruthy()
|
|
174
|
+
expect(screen.queryByText('Item 3')).toBeNull()
|
|
175
|
+
|
|
176
|
+
fireEvent.click(screen.getByText('Show more'))
|
|
177
|
+
expect(screen.getByText('Item 5')).toBeTruthy()
|
|
178
|
+
expect(screen.getByText('Show less')).toBeTruthy()
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
test('no max, or a max past the item count, renders no toggle at all', () => {
|
|
182
|
+
renderSidebar({ blocks: [AWAITING, { ...LONG, key: 'long2', max: 99 }] })
|
|
183
|
+
expect(screen.queryByText('Show more')).toBeNull()
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
test('Show more is EPHEMERAL — it persists nothing (unlike the fold)', () => {
|
|
187
|
+
localStorage.clear()
|
|
188
|
+
renderSidebar({ blocks: [LONG] })
|
|
189
|
+
fireEvent.click(screen.getByText('Show more'))
|
|
190
|
+
expect(localStorage.getItem('basalt:sidebar-block:long')).toBeNull()
|
|
191
|
+
})
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The fold keys are a CONTRACT, not an implementation detail: a consumer mirroring the state reads
|
|
196
|
+
* them with `readPersistedValue` from `basalt-ui/state`, and renaming one silently resets every
|
|
197
|
+
* user's sidebar. Section folds were a `useState` keyed by label through 1.25.0 — every reload
|
|
198
|
+
* re-opened a section the user had closed.
|
|
199
|
+
*/
|
|
200
|
+
describe('sidebar blocks — persisted folds', () => {
|
|
201
|
+
test('a block fold writes the versioned envelope at basalt:sidebar-block:<key>', () => {
|
|
202
|
+
localStorage.clear()
|
|
203
|
+
renderSidebar({ blocks: [{ ...AWAITING, key: 'awaiting-fold', collapsible: true }] })
|
|
204
|
+
|
|
205
|
+
fireEvent.click(screen.getByText('Awaiting action'))
|
|
206
|
+
|
|
207
|
+
expect(localStorage.getItem('basalt:sidebar-block:awaiting-fold')).toBe(
|
|
208
|
+
JSON.stringify({ v: 1, value: true }),
|
|
209
|
+
)
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
test('a nav-section fold writes at basalt:sidebar-section:<label-slug>', () => {
|
|
213
|
+
localStorage.clear()
|
|
214
|
+
renderSidebar({
|
|
215
|
+
sections: [
|
|
216
|
+
{ label: 'Tools & More', collapsible: true, items: [{ key: 't', label: 'T', icon: null }] },
|
|
217
|
+
],
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
fireEvent.click(screen.getByText('Tools & More'))
|
|
221
|
+
|
|
222
|
+
expect(localStorage.getItem('basalt:sidebar-section:tools-more')).toBe(
|
|
223
|
+
JSON.stringify({ v: 1, value: true }),
|
|
224
|
+
)
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
test('a persisted fold is restored on the next mount', () => {
|
|
228
|
+
localStorage.clear()
|
|
229
|
+
localStorage.setItem('basalt:sidebar-section:restored', JSON.stringify({ v: 1, value: true }))
|
|
230
|
+
renderSidebar({
|
|
231
|
+
sections: [
|
|
232
|
+
{
|
|
233
|
+
label: 'Restored',
|
|
234
|
+
collapsible: true,
|
|
235
|
+
items: [{ key: 't', label: 'Folded', icon: null }],
|
|
236
|
+
},
|
|
237
|
+
],
|
|
238
|
+
})
|
|
239
|
+
expect(screen.getByText('Restored').closest('button')?.getAttribute('aria-expanded')).toBe(
|
|
240
|
+
'false',
|
|
241
|
+
)
|
|
242
|
+
})
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
/** The `data-rail` attribute of the nth block — the whole contract with the rail stylesheet. */
|
|
246
|
+
const railOf = (container: HTMLElement, index = 0): string | null | undefined =>
|
|
247
|
+
container.querySelectorAll('[data-rail]')[index]?.getAttribute('data-rail')
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Rail projection. `data-rail` is the whole contract between the resolver and the stylesheet — the
|
|
251
|
+
* CSS selects on it (`.root[data-collapsed] .block[data-rail='hidden']`), so an attribute that
|
|
252
|
+
* stops matching the resolver is a rail that silently stops projecting.
|
|
253
|
+
*/
|
|
254
|
+
describe('sidebar blocks — rail projection', () => {
|
|
255
|
+
test("a list with a count is data-rail='dot' and renders the dot node", () => {
|
|
256
|
+
const { container } = renderSidebar({
|
|
257
|
+
blocks: [{ ...AWAITING, icon: <span data-testid="block-icon" /> }],
|
|
258
|
+
})
|
|
259
|
+
expect(railOf(container)).toBe('dot')
|
|
260
|
+
// The dot lives inside the icon wrapper — that is what the rail shows instead of the badge.
|
|
261
|
+
expect(screen.getByTestId('block-icon').parentElement?.children).toHaveLength(2)
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
test("a list with NO count is data-rail='hidden' — a dot with no number behind it means nothing", () => {
|
|
265
|
+
const { container } = renderSidebar({
|
|
266
|
+
blocks: [
|
|
267
|
+
{ kind: 'list', key: 'recents', label: 'Recents', items: [{ key: 'a', label: 'A' }] },
|
|
268
|
+
],
|
|
269
|
+
})
|
|
270
|
+
expect(railOf(container)).toBe('hidden')
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
test("rail:'hidden' overrides the count default", () => {
|
|
274
|
+
const { container } = renderSidebar({
|
|
275
|
+
blocks: [{ ...AWAITING, icon: <span />, rail: 'hidden' }],
|
|
276
|
+
})
|
|
277
|
+
expect(railOf(container)).toBe('hidden')
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
test('a count with NO icon leaves the rail — a dot has to sit on something', () => {
|
|
281
|
+
const { container } = renderSidebar({ blocks: [AWAITING] })
|
|
282
|
+
expect(railOf(container)).toBe('hidden')
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
test('a custom block is always hidden in the rail, as navExtra was', () => {
|
|
286
|
+
const { container } = renderSidebar({
|
|
287
|
+
blocks: [{ kind: 'custom', key: 'tree', node: <span>tree</span> }],
|
|
288
|
+
})
|
|
289
|
+
expect(railOf(container)).toBe('hidden')
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
test("a progress block is data-rail='ring', and the ring renders on the SETTINGS row when collapsed", () => {
|
|
293
|
+
const { container } = renderSidebar({
|
|
294
|
+
collapsed: true,
|
|
295
|
+
blocks: [{ kind: 'progress', key: 'p', label: 'Getting started', value: 1, total: 5 }],
|
|
296
|
+
settingsMenuItems: [{ key: 'theme', label: 'Theme', onClick: () => {} }],
|
|
297
|
+
})
|
|
298
|
+
expect(railOf(container)).toBe('ring')
|
|
299
|
+
const ring = container.querySelector('[data-basalt-rail-ring]')
|
|
300
|
+
expect(ring).not.toBeNull()
|
|
301
|
+
expect(screen.getByLabelText('Theme').contains(ring as Node)).toBe(true)
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
test('expanded, there is no ring — the block paints its own track instead', () => {
|
|
305
|
+
const { container } = renderSidebar({
|
|
306
|
+
blocks: [{ kind: 'progress', key: 'p', label: 'Getting started', value: 1, total: 5 }],
|
|
307
|
+
settingsMenuItems: [{ key: 'theme', label: 'Theme', onClick: () => {} }],
|
|
308
|
+
})
|
|
309
|
+
expect(container.querySelector('[data-basalt-rail-ring]')).toBeNull()
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
test("rail:'hidden' on a progress block puts no ring on the settings row", () => {
|
|
313
|
+
const { container } = renderSidebar({
|
|
314
|
+
collapsed: true,
|
|
315
|
+
blocks: [
|
|
316
|
+
{
|
|
317
|
+
kind: 'progress',
|
|
318
|
+
key: 'p',
|
|
319
|
+
label: 'Getting started',
|
|
320
|
+
value: 1,
|
|
321
|
+
total: 5,
|
|
322
|
+
rail: 'hidden',
|
|
323
|
+
},
|
|
324
|
+
],
|
|
325
|
+
settingsMenuItems: [{ key: 'theme', label: 'Theme', onClick: () => {} }],
|
|
326
|
+
})
|
|
327
|
+
expect(container.querySelector('[data-basalt-rail-ring]')).toBeNull()
|
|
328
|
+
})
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
describe('brand.menu — the workspace switcher', () => {
|
|
332
|
+
const MENU = [
|
|
333
|
+
{ key: 'switch', label: 'Switch workspace', onClick: () => {} },
|
|
334
|
+
{ key: 'new', label: 'New workspace', onClick: () => {} },
|
|
335
|
+
]
|
|
336
|
+
|
|
337
|
+
test('no menu: the brand name is plain text, not a control', () => {
|
|
338
|
+
renderSidebar({})
|
|
339
|
+
expect(screen.queryByLabelText('Argo workspace')).toBeNull()
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
test('with a menu the brand row becomes a `Name ▾` trigger opening the entries', async () => {
|
|
343
|
+
let picked = 0
|
|
344
|
+
renderSidebar({
|
|
345
|
+
brand: { name: 'Argo', menu: [{ ...MENU[0]!, onClick: () => (picked += 1) }, MENU[1]!] },
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
fireEvent.click(screen.getByLabelText('Argo workspace'))
|
|
349
|
+
await waitFor(() => expect(document.querySelector('[role="menu"]')).not.toBeNull())
|
|
350
|
+
expect(screen.getByText('New workspace')).toBeTruthy()
|
|
351
|
+
|
|
352
|
+
fireEvent.click(screen.getByText('Switch workspace'))
|
|
353
|
+
expect(picked).toBe(1)
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
test('an empty menu array is no menu — never a trigger that opens nothing', () => {
|
|
357
|
+
renderSidebar({ brand: { name: 'Argo', menu: [] } })
|
|
358
|
+
expect(screen.queryByLabelText('Argo workspace')).toBeNull()
|
|
359
|
+
})
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* The footer threshold. A menu that opens to show two rows costs a click for nothing; eight flat
|
|
364
|
+
* rows cost the nav its height. Three is the line (`docs/CONTROLS-SPEC.md` §2.3), and it is
|
|
365
|
+
* basalt's — there is no prop.
|
|
366
|
+
*/
|
|
367
|
+
describe('settingsMenuItems — flat at three or fewer', () => {
|
|
368
|
+
const three = [
|
|
369
|
+
{ key: 'settings', label: 'Settings', onClick: () => {} },
|
|
370
|
+
{ key: 'integrations', label: 'Integrations', onClick: () => {} },
|
|
371
|
+
{ key: 'invite', label: 'Invite teammates', onClick: () => {} },
|
|
372
|
+
]
|
|
373
|
+
|
|
374
|
+
test('three entries render as three link rows, each firing its own handler', () => {
|
|
375
|
+
let fired = ''
|
|
376
|
+
renderSidebar({
|
|
377
|
+
settingsMenuItems: three.map((entry) => ({ ...entry, onClick: () => (fired = entry.key) })),
|
|
378
|
+
})
|
|
379
|
+
expect(screen.getByText('Integrations')).toBeTruthy()
|
|
380
|
+
expect(screen.getByText('Invite teammates')).toBeTruthy()
|
|
381
|
+
fireEvent.click(screen.getByLabelText('Invite teammates'))
|
|
382
|
+
expect(fired).toBe('invite')
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
test('four entries collapse into the single gear menu', async () => {
|
|
386
|
+
renderSidebar({
|
|
387
|
+
settingsMenuItems: [...three, { key: 'devtools', label: 'Devtools', onClick: () => {} }],
|
|
388
|
+
})
|
|
389
|
+
// Only the gear trigger is present until it opens.
|
|
390
|
+
expect(screen.queryByText('Integrations')).toBeNull()
|
|
391
|
+
fireEvent.click(screen.getByLabelText('Settings'))
|
|
392
|
+
await waitFor(() => expect(document.querySelector('[role="menu"]')).not.toBeNull())
|
|
393
|
+
expect(screen.getByText('Integrations')).toBeTruthy()
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
test('the version rides the flat rows as a faint label, and the menu as a Menu.Label', async () => {
|
|
397
|
+
renderSidebar({ brand: { name: 'Argo', version: '2.1.0' }, settingsMenuItems: three })
|
|
398
|
+
expect(screen.getByText('Argo v2.1.0')).toBeTruthy()
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
test('no entries at all renders no footer row', () => {
|
|
402
|
+
renderSidebar({})
|
|
403
|
+
expect(screen.queryByLabelText('Settings')).toBeNull()
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* The count is not the whole question — three rows that are each a control (a theme radio group,
|
|
408
|
+
* a devtools switch) read as a widget pile flat, and the count rule cannot see the difference.
|
|
409
|
+
* `settingsMenu` is that override and nothing more: it moves the form, never the entries.
|
|
410
|
+
*/
|
|
411
|
+
describe('settingsMenu forces the form', () => {
|
|
412
|
+
test("'menu' collapses two entries into the gear dropdown", async () => {
|
|
413
|
+
renderSidebar({ settingsMenuItems: three.slice(0, 2), settingsMenu: 'menu' })
|
|
414
|
+
expect(screen.queryByText('Integrations')).toBeNull()
|
|
415
|
+
fireEvent.click(screen.getByLabelText('Settings'))
|
|
416
|
+
await waitFor(() => expect(document.querySelector('[role="menu"]')).not.toBeNull())
|
|
417
|
+
expect(screen.getByText('Integrations')).toBeTruthy()
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
test("'flat' keeps four entries as four link rows", () => {
|
|
421
|
+
renderSidebar({
|
|
422
|
+
settingsMenuItems: [...three, { key: 'devtools', label: 'Devtools', onClick: () => {} }],
|
|
423
|
+
settingsMenu: 'flat',
|
|
424
|
+
})
|
|
425
|
+
// No dropdown to open — every entry is its own row, and the gear trigger is not one of them.
|
|
426
|
+
expect(document.querySelector('[role="menu"]')).toBeNull()
|
|
427
|
+
expect(screen.getByText('Devtools')).toBeTruthy()
|
|
428
|
+
expect(screen.getByText('Integrations')).toBeTruthy()
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
test("'auto' is the default — the count rule, unchanged", () => {
|
|
432
|
+
renderSidebar({ settingsMenuItems: three, settingsMenu: 'auto' })
|
|
433
|
+
expect(screen.getByText('Integrations')).toBeTruthy()
|
|
434
|
+
})
|
|
435
|
+
})
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
describe('search.actions', () => {
|
|
439
|
+
test('no actions: the trigger is the row, with no wrapper and no extra buttons', () => {
|
|
440
|
+
const { container } = renderSidebar({ search: { onOpen: () => {} } })
|
|
441
|
+
expect(container.querySelectorAll('button')).toHaveLength(2) // search trigger + collapse toggle
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
test('two actions render as icon-only buttons at the icon tier, right of the ⌘K row', () => {
|
|
445
|
+
let created = 0
|
|
446
|
+
renderSidebar({
|
|
447
|
+
search: {
|
|
448
|
+
onOpen: () => {},
|
|
449
|
+
actions: [
|
|
450
|
+
{ key: 'new', label: 'New note', icon: <span />, onClick: () => (created += 1) },
|
|
451
|
+
{ key: 'filter', label: 'Filter', icon: <span /> },
|
|
452
|
+
],
|
|
453
|
+
},
|
|
454
|
+
})
|
|
455
|
+
const newButton = screen.getByLabelText('New note')
|
|
456
|
+
expect(newButton.getAttribute('data-size')).toBe('icon')
|
|
457
|
+
expect(screen.getByLabelText('Filter')).toBeTruthy()
|
|
458
|
+
fireEvent.click(newButton)
|
|
459
|
+
expect(created).toBe(1)
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
test('an action with no icon falls back to its first grapheme, label as the accessible name', () => {
|
|
463
|
+
renderSidebar({
|
|
464
|
+
search: { onOpen: () => {}, actions: [{ key: 'new', label: 'New note' }] },
|
|
465
|
+
})
|
|
466
|
+
expect(screen.getByLabelText('New note').textContent).toBe('N')
|
|
467
|
+
})
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
describe('nav item active/ancestor state (useNav exclusivity — router-tanstack/use-nav.test.tsx)', () => {
|
|
471
|
+
test('only the active row carries aria-current="page"', () => {
|
|
472
|
+
renderSidebar({
|
|
473
|
+
sections: [
|
|
474
|
+
{
|
|
475
|
+
label: 'Main',
|
|
476
|
+
items: [
|
|
477
|
+
{ key: 'home', label: 'Home', icon: null, active: true },
|
|
478
|
+
{ key: 'charts', label: 'Charts', icon: null, active: false },
|
|
479
|
+
],
|
|
480
|
+
},
|
|
481
|
+
],
|
|
482
|
+
})
|
|
483
|
+
expect(screen.getByText('Home').closest('a')?.getAttribute('aria-current')).toBe('page')
|
|
484
|
+
expect(screen.getByText('Charts').closest('a')?.hasAttribute('aria-current')).toBe(false)
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
test('an ancestor row carries data-ancestor and no aria-current', () => {
|
|
488
|
+
renderSidebar({
|
|
489
|
+
sections: [
|
|
490
|
+
{
|
|
491
|
+
label: 'Main',
|
|
492
|
+
items: [
|
|
493
|
+
{
|
|
494
|
+
key: 'dashboard',
|
|
495
|
+
label: 'Dashboard',
|
|
496
|
+
icon: null,
|
|
497
|
+
active: false,
|
|
498
|
+
ancestor: true,
|
|
499
|
+
children: [{ key: 'sessions', label: 'Sessions', icon: null, active: true }],
|
|
500
|
+
},
|
|
501
|
+
],
|
|
502
|
+
},
|
|
503
|
+
],
|
|
504
|
+
})
|
|
505
|
+
const dashboardAnchor = screen.getByText('Dashboard').closest('a')
|
|
506
|
+
expect(dashboardAnchor?.hasAttribute('data-ancestor')).toBe(true)
|
|
507
|
+
expect(dashboardAnchor?.hasAttribute('aria-current')).toBe(false)
|
|
93
508
|
})
|
|
94
509
|
})
|