basalt-ui 1.24.0 → 1.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +8 -6
- package/CHANGELOG.md +25 -0
- package/MIGRATING.md +547 -21
- package/README.md +46 -33
- package/agent/rules/basalt-batteries.md +205 -0
- package/agent/rules/basalt-charts.md +125 -555
- package/agent/rules/basalt-controls.md +155 -0
- package/agent/rules/basalt-mantine.md +146 -439
- package/agent/rules/basalt-state.md +113 -96
- package/agent/rules/basalt-tokens.md +126 -288
- package/agent/skills/basalt-app/SKILL.md +70 -195
- package/agent/skills/basalt-charts/SKILL.md +54 -234
- package/agent/skills/basalt-design/SKILL.md +56 -248
- package/agent/templates/CLAUDE-block.md.tpl +31 -40
- package/agent/templates/DESIGN.md.tpl +22 -51
- package/configs/oxlint-plugin.js +1275 -31
- package/configs/oxlint-plugin.test.ts +947 -11
- package/configs/oxlint.json +11 -3
- package/dist/charts/cursor/resolve.d.ts +18 -0
- package/dist/charts/cursor/resolve.d.ts.map +1 -1
- package/dist/charts/cursor/resolve.js +15 -6
- package/dist/charts/cursor/scope.d.ts +6 -3
- package/dist/charts/cursor/scope.d.ts.map +1 -1
- package/dist/charts/cursor/store.d.ts +5 -1
- package/dist/charts/cursor/store.d.ts.map +1 -1
- package/dist/charts/cursor/store.js +4 -4
- package/dist/charts/hooks/useChartCursor.d.ts.map +1 -1
- package/dist/charts/hooks/useChartCursor.js +9 -8
- package/dist/charts/index.d.ts +2 -2
- package/dist/charts/index.d.ts.map +1 -1
- package/dist/charts/primitives/ChartCard.d.ts +34 -9
- package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
- package/dist/charts/primitives/ChartCard.js +29 -114
- package/dist/charts/primitives/ChartCard.test.d.ts +2 -0
- package/dist/charts/primitives/ChartCard.test.d.ts.map +1 -0
- package/dist/charts/sparklines/BarSparkline.d.ts +26 -2
- package/dist/charts/sparklines/BarSparkline.d.ts.map +1 -1
- package/dist/charts/sparklines/BarSparkline.js +18 -4
- package/dist/cli/check-coverage.test.d.ts +2 -0
- package/dist/cli/check-coverage.test.d.ts.map +1 -0
- package/dist/cli/index.d.ts +67 -4
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +225 -7
- package/dist/connectivity/connectivity-indicator.d.ts +4 -1
- package/dist/connectivity/connectivity-indicator.d.ts.map +1 -1
- package/dist/connectivity/connectivity-indicator.js +1 -1
- package/dist/content/index.d.ts +5 -3
- package/dist/content/index.d.ts.map +1 -1
- package/dist/content/index.js +4 -2
- package/dist/content/prose.module.css +7 -28
- package/dist/controls/actions.d.ts +169 -0
- package/dist/controls/actions.d.ts.map +1 -0
- package/dist/controls/actions.js +287 -0
- package/dist/controls/actions.module.css +62 -0
- package/dist/controls/actions.module.css.d.ts +13 -0
- package/dist/controls/actions.test.d.ts +2 -0
- package/dist/controls/actions.test.d.ts.map +1 -0
- package/dist/controls/actions.type-guard.test.d.ts +2 -0
- package/dist/controls/actions.type-guard.test.d.ts.map +1 -0
- package/dist/controls/compare-filter.d.ts +41 -0
- package/dist/controls/compare-filter.d.ts.map +1 -0
- package/dist/controls/compare-filter.js +23 -0
- package/dist/controls/control-group.d.ts +19 -0
- package/dist/controls/control-group.d.ts.map +1 -0
- package/dist/controls/control-group.js +20 -0
- package/dist/controls/control-group.test.d.ts +2 -0
- package/dist/controls/control-group.test.d.ts.map +1 -0
- package/dist/controls/controls.module.css +542 -0
- package/dist/controls/controls.module.css.d.ts +35 -0
- package/dist/controls/controls.router.test.d.ts +2 -0
- package/dist/controls/controls.router.test.d.ts.map +1 -0
- package/dist/controls/enum-filter.d.ts +30 -0
- package/dist/controls/enum-filter.d.ts.map +1 -0
- package/dist/controls/enum-filter.js +91 -0
- package/dist/controls/filter-context.d.ts +55 -0
- package/dist/controls/filter-context.d.ts.map +1 -0
- package/dist/controls/filter-context.js +69 -0
- package/dist/controls/filter-pill.d.ts +28 -0
- package/dist/controls/filter-pill.d.ts.map +1 -0
- package/dist/controls/filter-pill.js +70 -0
- package/dist/controls/filter-set.d.ts +13 -0
- package/dist/controls/filter-set.d.ts.map +1 -0
- package/dist/controls/filter-set.js +125 -0
- package/dist/controls/filter-set.test.d.ts +2 -0
- package/dist/controls/filter-set.test.d.ts.map +1 -0
- package/dist/controls/filter-sheet.d.ts +117 -0
- package/dist/controls/filter-sheet.d.ts.map +1 -0
- package/dist/controls/filter-sheet.js +146 -0
- package/dist/controls/glyphs.d.ts +27 -0
- package/dist/controls/glyphs.d.ts.map +1 -0
- package/dist/controls/glyphs.js +49 -0
- package/dist/controls/index.d.ts +19 -0
- package/dist/controls/index.d.ts.map +1 -0
- package/dist/controls/index.js +44 -0
- package/dist/controls/multi-select-filter.d.ts +23 -0
- package/dist/controls/multi-select-filter.d.ts.map +1 -0
- package/dist/controls/multi-select-filter.js +89 -0
- package/dist/controls/peer-boundary.test.d.ts +2 -0
- package/dist/controls/peer-boundary.test.d.ts.map +1 -0
- package/dist/controls/props-contract.test.d.ts +2 -0
- package/dist/controls/props-contract.test.d.ts.map +1 -0
- package/dist/controls/range-filter.d.ts +29 -0
- package/dist/controls/range-filter.d.ts.map +1 -0
- package/dist/controls/range-filter.js +120 -0
- package/dist/controls/search-filter.d.ts +10 -0
- package/dist/controls/search-filter.d.ts.map +1 -0
- package/dist/controls/search-filter.js +81 -0
- package/dist/controls/select-filter.d.ts +74 -0
- package/dist/controls/select-filter.d.ts.map +1 -0
- package/dist/controls/select-filter.js +10 -0
- package/dist/controls/sync-button.d.ts +38 -0
- package/dist/controls/sync-button.d.ts.map +1 -0
- package/dist/controls/sync-button.js +119 -0
- package/dist/controls/sync-button.module.css +63 -0
- package/dist/controls/sync-button.module.css.d.ts +11 -0
- package/dist/controls/sync-button.test.d.ts +2 -0
- package/dist/controls/sync-button.test.d.ts.map +1 -0
- package/dist/controls/toggle-filter.d.ts +9 -0
- package/dist/controls/toggle-filter.d.ts.map +1 -0
- package/dist/controls/toggle-filter.js +42 -0
- package/dist/controls/view-tabs.d.ts +23 -0
- package/dist/controls/view-tabs.d.ts.map +1 -0
- package/dist/controls/view-tabs.js +59 -0
- package/dist/controls-dates/date-range-picker.d.ts +4 -0
- package/dist/controls-dates/date-range-picker.d.ts.map +1 -0
- package/dist/controls-dates/date-range-picker.js +18 -0
- package/dist/controls-dates/index.d.ts +9 -0
- package/dist/controls-dates/index.d.ts.map +1 -0
- package/dist/controls-dates/index.js +4 -0
- package/dist/dashboard/delta-badge.d.ts +6 -12
- package/dist/dashboard/delta-badge.d.ts.map +1 -1
- package/dist/dashboard/delta-badge.js +1 -50
- package/dist/dashboard/query-state.d.ts +1 -1
- package/dist/dashboard/settings-section.d.ts +6 -4
- package/dist/dashboard/settings-section.d.ts.map +1 -1
- package/dist/dashboard/settings-section.js +23 -16
- package/dist/dashboard/settings-section.module.css +10 -0
- package/dist/dashboard/settings-section.module.css.d.ts +1 -0
- package/dist/dashboard/settings-section.test.d.ts +2 -0
- package/dist/dashboard/settings-section.test.d.ts.map +1 -0
- package/dist/dashboard/stat-card.d.ts +44 -9
- package/dist/dashboard/stat-card.d.ts.map +1 -1
- package/dist/dashboard/stat-card.js +80 -48
- package/dist/dashboard/stat-card.module.css +82 -0
- package/dist/dashboard/stat-card.module.css.d.ts +11 -0
- package/dist/data/data-table.d.ts +102 -20
- package/dist/data/data-table.d.ts.map +1 -1
- package/dist/data/data-table.js +166 -65
- package/dist/data/table.d.ts +1 -1
- package/dist/data/table.d.ts.map +1 -1
- package/dist/guard/grace.test.d.ts +20 -0
- package/dist/guard/grace.test.d.ts.map +1 -0
- package/dist/guard/index.d.ts +66 -3
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +82 -7
- package/dist/guard/types.d.ts +3 -3
- package/dist/guard/types.d.ts.map +1 -1
- package/dist/index.d.ts +12 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -10
- package/dist/nav/types.d.ts +78 -2
- package/dist/nav/types.d.ts.map +1 -1
- package/dist/notifications/bell.d.ts +4 -1
- package/dist/notifications/bell.d.ts.map +1 -1
- package/dist/notifications/index.d.ts +1 -1
- package/dist/router-tanstack/field.d.ts +14 -0
- package/dist/router-tanstack/field.d.ts.map +1 -0
- package/dist/router-tanstack/field.js +4 -0
- package/dist/router-tanstack/index.d.ts +6 -2
- package/dist/router-tanstack/index.d.ts.map +1 -1
- package/dist/router-tanstack/index.js +4 -0
- package/dist/router-tanstack/multi-search-param-store.d.ts +4 -52
- package/dist/router-tanstack/multi-search-param-store.d.ts.map +1 -1
- package/dist/router-tanstack/multi-search-param-store.js +19 -27
- package/dist/router-tanstack/search-param-store.d.ts +3 -87
- package/dist/router-tanstack/search-param-store.d.ts.map +1 -1
- package/dist/router-tanstack/search-param-store.js +16 -45
- package/dist/router-tanstack/search-store.d.ts +81 -0
- package/dist/router-tanstack/search-store.d.ts.map +1 -0
- package/dist/router-tanstack/search-store.js +187 -0
- package/dist/router-tanstack/search-store.router.test.d.ts +2 -0
- package/dist/router-tanstack/search-store.router.test.d.ts.map +1 -0
- package/dist/router-tanstack/search-store.test.d.ts +2 -0
- package/dist/router-tanstack/search-store.test.d.ts.map +1 -0
- package/dist/router-tanstack/use-nav.d.ts +0 -2
- package/dist/router-tanstack/use-nav.d.ts.map +1 -1
- package/dist/router-tanstack/use-nav.js +34 -12
- package/dist/router-tanstack/use-nav.test.d.ts +2 -0
- package/dist/router-tanstack/use-nav.test.d.ts.map +1 -0
- package/dist/section/index.d.ts +2 -0
- package/dist/section/index.d.ts.map +1 -0
- package/dist/section/index.js +4 -0
- package/dist/section/section.d.ts +24 -0
- package/dist/section/section.d.ts.map +1 -0
- package/dist/section/section.js +126 -0
- package/dist/section/section.module.css +104 -0
- package/dist/section/section.module.css.d.ts +11 -0
- package/dist/section/section.test.d.ts +2 -0
- package/dist/section/section.test.d.ts.map +1 -0
- package/dist/shell/app-breadcrumbs.d.ts.map +1 -1
- package/dist/shell/app-breadcrumbs.js +18 -15
- package/dist/shell/app-header.module.css +77 -37
- package/dist/shell/app-header.module.css.d.ts +2 -1
- package/dist/shell/app-mobile-nav.d.ts +9 -2
- package/dist/shell/app-mobile-nav.d.ts.map +1 -1
- package/dist/shell/app-mobile-nav.js +98 -11
- package/dist/shell/app-mobile-nav.module.css +8 -0
- package/dist/shell/app-mobile-nav.module.css.d.ts +1 -0
- package/dist/shell/app-sidebar.d.ts +55 -28
- package/dist/shell/app-sidebar.d.ts.map +1 -1
- package/dist/shell/app-sidebar.js +116 -67
- package/dist/shell/app-sidebar.module.css +303 -20
- package/dist/shell/app-sidebar.module.css.d.ts +19 -1
- package/dist/shell/index.d.ts +55 -21
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +40 -18
- package/dist/shell/mobile-nav-model.d.ts +13 -2
- package/dist/shell/mobile-nav-model.d.ts.map +1 -1
- package/dist/shell/mobile-nav-model.js +15 -1
- package/dist/shell/nav-count-badge.d.ts +1 -1
- package/dist/shell/page-bar.d.ts +51 -0
- package/dist/shell/page-bar.d.ts.map +1 -0
- package/dist/shell/page-bar.js +154 -0
- package/dist/shell/page-bar.module.css +178 -0
- package/dist/shell/page-bar.module.css.d.ts +17 -0
- package/dist/shell/page-bar.test.d.ts +2 -0
- package/dist/shell/page-bar.test.d.ts.map +1 -0
- package/dist/shell/sidebar-block-model.d.ts +48 -0
- package/dist/shell/sidebar-block-model.d.ts.map +1 -0
- package/dist/shell/sidebar-block-model.js +39 -0
- package/dist/shell/sidebar-blocks.d.ts +32 -0
- package/dist/shell/sidebar-blocks.d.ts.map +1 -0
- package/dist/shell/sidebar-blocks.js +169 -0
- package/dist/shell/sidebar-search.d.ts +13 -1
- package/dist/shell/sidebar-search.d.ts.map +1 -1
- package/dist/shell/sidebar-search.js +34 -1
- package/dist/shell/sidebar-search.module.css +26 -0
- package/dist/shell/sidebar-search.module.css.d.ts +2 -0
- package/dist/state/fields.d.ts +392 -0
- package/dist/state/fields.d.ts.map +1 -0
- package/dist/state/fields.js +369 -0
- package/dist/state/fields.test.d.ts +2 -0
- package/dist/state/fields.test.d.ts.map +1 -0
- package/dist/state/persisted.d.ts +48 -0
- package/dist/state/persisted.d.ts.map +1 -0
- package/dist/state/persisted.js +105 -0
- package/dist/state.d.ts +11 -56
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +14 -120
- package/dist/styles.css +13 -0
- package/dist/surfaces.d.ts +106 -24
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +155 -19
- package/dist/theme/controls.module.css +16 -1
- package/dist/theme/ctl-theme.d.ts +32 -0
- package/dist/theme/ctl-theme.d.ts.map +1 -0
- package/dist/theme/ctl-theme.js +54 -0
- package/dist/theme/ctl-theme.test.d.ts +2 -0
- package/dist/theme/ctl-theme.test.d.ts.map +1 -0
- package/dist/theme/ctl-tier-coverage.test.d.ts +2 -0
- package/dist/theme/ctl-tier-coverage.test.d.ts.map +1 -0
- package/dist/theme/divider-law.test.d.ts +2 -0
- package/dist/theme/divider-law.test.d.ts.map +1 -0
- package/dist/theme/icon-slot.d.ts +44 -0
- package/dist/theme/icon-slot.d.ts.map +1 -0
- package/dist/theme/icon-slot.js +21 -0
- package/dist/theme/icon-slot.module.css +47 -0
- package/dist/theme/icon-slot.module.css.d.ts +8 -0
- package/dist/theme/icon-slot.test.d.ts +2 -0
- package/dist/theme/icon-slot.test.d.ts.map +1 -0
- package/dist/theme/index.d.ts +1 -0
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/index.js +82 -5
- package/dist/theme/layout-rhythm-css.test.d.ts +2 -0
- package/dist/theme/layout-rhythm-css.test.d.ts.map +1 -0
- package/dist/theme/segmented-control.module.css +20 -0
- package/dist/theme/shadow-surfaces.test.d.ts.map +1 -1
- package/dist/tokens/index.d.ts +12 -0
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +38 -8
- package/dist/tokens/palette.d.ts +97 -61
- package/dist/tokens/palette.d.ts.map +1 -1
- package/dist/tokens/palette.js +76 -38
- package/dist/tokens.css +10 -2
- package/dist/widget-header/delta-badge.d.ts +13 -0
- package/dist/widget-header/delta-badge.d.ts.map +1 -0
- package/dist/widget-header/delta-badge.js +29 -0
- package/dist/widget-header/delta-badge.test.d.ts +2 -0
- package/dist/widget-header/delta-badge.test.d.ts.map +1 -0
- package/dist/widget-header/index.d.ts +3 -0
- package/dist/widget-header/index.d.ts.map +1 -0
- package/dist/widget-header/index.js +6 -0
- package/dist/widget-header/no-mantine.test.d.ts +2 -0
- package/dist/widget-header/no-mantine.test.d.ts.map +1 -0
- package/dist/widget-header/widget-header.d.ts +37 -0
- package/dist/widget-header/widget-header.d.ts.map +1 -0
- package/dist/widget-header/widget-header.js +77 -0
- package/dist/widget-header/widget-header.module.css +335 -0
- package/dist/widget-header/widget-header.module.css.d.ts +24 -0
- package/dist/widget-header/widget-header.test.d.ts +2 -0
- package/dist/widget-header/widget-header.test.d.ts.map +1 -0
- package/llms.txt +30 -17
- package/package.json +14 -1
- package/src/charts/cursor/resolve.test.ts +29 -1
- package/src/charts/cursor/resolve.ts +38 -6
- package/src/charts/cursor/scope.tsx +6 -3
- package/src/charts/cursor/store.test.ts +19 -1
- package/src/charts/cursor/store.ts +10 -5
- package/src/charts/hooks/useChartCursor.ts +16 -8
- package/src/charts/index.ts +2 -2
- package/src/charts/primitives/CartesianChart.test.tsx +124 -0
- package/src/charts/primitives/ChartCard.test.tsx +66 -0
- package/src/charts/primitives/ChartCard.tsx +86 -146
- package/src/charts/sparklines/BarSparkline.tsx +43 -5
- package/src/cli/check-coverage.test.ts +297 -0
- package/src/cli/check-theme.test.ts +38 -14
- package/src/cli/index.ts +392 -16
- package/src/cli/init-sync.test.ts +97 -0
- package/src/connectivity/connectivity-indicator.tsx +16 -9
- package/src/content/index.ts +16 -3
- package/src/content/prose.module.css +7 -28
- package/src/content/prose.module.css.test.ts +8 -24
- package/src/controls/actions.module.css +62 -0
- package/src/controls/actions.module.css.d.ts +13 -0
- package/src/controls/actions.test.tsx +252 -0
- package/src/controls/actions.tsx +655 -0
- package/src/controls/actions.type-guard.test.ts +63 -0
- package/src/controls/compare-filter.tsx +64 -0
- package/src/controls/control-group.test.tsx +229 -0
- package/src/controls/control-group.tsx +63 -0
- package/src/controls/controls.module.css +542 -0
- package/src/controls/controls.module.css.d.ts +35 -0
- package/src/controls/controls.router.test.tsx +637 -0
- package/src/controls/enum-filter.tsx +159 -0
- package/src/controls/filter-context.tsx +153 -0
- package/src/controls/filter-pill.tsx +120 -0
- package/src/controls/filter-set.test.tsx +472 -0
- package/src/controls/filter-set.tsx +225 -0
- package/src/controls/filter-sheet.tsx +285 -0
- package/src/controls/glyphs.tsx +81 -0
- package/src/controls/index.ts +48 -0
- package/src/controls/multi-select-filter.tsx +140 -0
- package/src/controls/peer-boundary.test.ts +85 -0
- package/src/controls/props-contract.test.tsx +106 -0
- package/src/controls/range-filter.tsx +184 -0
- package/src/controls/search-filter.tsx +123 -0
- package/src/controls/select-filter.tsx +89 -0
- package/src/controls/sync-button.module.css +63 -0
- package/src/controls/sync-button.module.css.d.ts +11 -0
- package/src/controls/sync-button.test.tsx +183 -0
- package/src/controls/sync-button.tsx +197 -0
- package/src/controls/toggle-filter.tsx +60 -0
- package/src/controls/view-tabs.tsx +110 -0
- package/src/controls-dates/date-range-picker.tsx +42 -0
- package/src/controls-dates/index.ts +8 -0
- package/src/dashboard/delta-badge.tsx +4 -74
- package/src/dashboard/query-state.tsx +1 -1
- package/src/dashboard/settings-section.module.css +10 -0
- package/src/dashboard/settings-section.module.css.d.ts +1 -0
- package/src/dashboard/settings-section.test.tsx +56 -0
- package/src/dashboard/settings-section.tsx +33 -30
- package/src/dashboard/stat-card.module.css +82 -0
- package/src/dashboard/stat-card.module.css.d.ts +11 -0
- package/src/dashboard/stat-card.test.tsx +67 -1
- package/src/dashboard/stat-card.tsx +209 -68
- package/src/data/data-table.test.tsx +282 -6
- package/src/data/data-table.tsx +429 -94
- package/src/data/table.ts +2 -0
- package/src/guard/check-source.test.ts +126 -13
- package/src/guard/grace.test.ts +149 -0
- package/src/guard/index.ts +172 -29
- package/src/guard/types.ts +4 -2
- package/src/index.ts +46 -5
- package/src/nav/types.ts +84 -2
- package/src/notifications/bell.tsx +6 -3
- package/src/notifications/index.ts +1 -1
- package/src/router-tanstack/field.ts +31 -0
- package/src/router-tanstack/index.ts +24 -2
- package/src/router-tanstack/multi-search-param-store.test.ts +71 -3
- package/src/router-tanstack/multi-search-param-store.ts +46 -77
- package/src/router-tanstack/search-param-store.ts +42 -159
- package/src/router-tanstack/search-store.router.test.tsx +661 -0
- package/src/router-tanstack/search-store.test.ts +418 -0
- package/src/router-tanstack/search-store.ts +380 -0
- package/src/router-tanstack/use-nav.test.tsx +187 -0
- package/src/router-tanstack/use-nav.ts +72 -14
- package/src/section/index.ts +1 -0
- package/src/section/section.module.css +104 -0
- package/src/section/section.module.css.d.ts +11 -0
- package/src/section/section.test.tsx +204 -0
- package/src/section/section.tsx +220 -0
- package/src/shell/app-breadcrumbs.tsx +44 -31
- package/src/shell/app-header.module.css +77 -37
- package/src/shell/app-header.module.css.d.ts +2 -1
- package/src/shell/app-mobile-nav.module.css +8 -0
- package/src/shell/app-mobile-nav.module.css.d.ts +1 -0
- package/src/shell/app-mobile-nav.tsx +147 -8
- package/src/shell/app-sidebar-account.tsx +2 -2
- package/src/shell/app-sidebar.module.css +303 -20
- package/src/shell/app-sidebar.module.css.d.ts +19 -1
- package/src/shell/app-sidebar.test.tsx +456 -41
- package/src/shell/app-sidebar.tsx +276 -140
- package/src/shell/index.test.tsx +291 -5
- package/src/shell/index.tsx +137 -38
- package/src/shell/mobile-nav-model.test.ts +228 -7
- package/src/shell/mobile-nav-model.ts +53 -3
- package/src/shell/nav-count-badge.tsx +1 -1
- package/src/shell/page-bar.module.css +178 -0
- package/src/shell/page-bar.module.css.d.ts +17 -0
- package/src/shell/page-bar.test.tsx +316 -0
- package/src/shell/page-bar.tsx +315 -0
- package/src/shell/sidebar-block-model.ts +83 -0
- package/src/shell/sidebar-blocks.tsx +301 -0
- package/src/shell/sidebar-search.module.css +26 -0
- package/src/shell/sidebar-search.module.css.d.ts +2 -0
- package/src/shell/sidebar-search.tsx +71 -2
- package/src/state/fields.test.tsx +258 -0
- package/src/state/fields.ts +892 -0
- package/src/state/persisted.ts +211 -0
- package/src/state.ts +46 -240
- package/src/styles.css +13 -0
- package/src/surfaces.ts +203 -36
- package/src/theme/controls.module.css +16 -1
- package/src/theme/ctl-theme.test.tsx +275 -0
- package/src/theme/ctl-theme.tsx +138 -0
- package/src/theme/ctl-tier-coverage.test.ts +170 -0
- package/src/theme/density-relations.test.ts +106 -51
- package/src/theme/divider-law.test.ts +259 -0
- package/src/theme/icon-slot.module.css +47 -0
- package/src/theme/icon-slot.module.css.d.ts +8 -0
- package/src/theme/icon-slot.test.tsx +275 -0
- package/src/theme/icon-slot.tsx +59 -0
- package/src/theme/index.ts +165 -3
- package/src/theme/layout-rhythm-css.test.ts +72 -0
- package/src/theme/radius.test.ts +11 -1
- package/src/theme/segmented-control.module.css +20 -0
- package/src/theme/shadow-surfaces.test.ts +20 -7
- package/src/theme/spacing.test.ts +75 -27
- package/src/theme/use-basalt-spacing.ts +2 -2
- package/src/tokens/density.test.ts +7 -25
- package/src/tokens/index.ts +48 -8
- package/src/tokens/palette.ts +107 -73
- package/src/widget-header/delta-badge.test.tsx +60 -0
- package/src/widget-header/delta-badge.tsx +57 -0
- package/src/widget-header/index.ts +2 -0
- package/src/widget-header/no-mantine.test.ts +33 -0
- package/src/widget-header/widget-header.module.css +335 -0
- package/src/widget-header/widget-header.module.css.d.ts +24 -0
- package/src/widget-header/widget-header.test.tsx +187 -0
- package/src/widget-header/widget-header.tsx +168 -0
- package/agent/rules/basalt-agent.md +0 -452
- package/agent/rules/basalt-app.md +0 -246
- package/agent/rules/basalt-commands.md +0 -320
- package/agent/rules/basalt-content.md +0 -409
- package/agent/rules/basalt-data.md +0 -269
- package/agent/rules/basalt-forms.md +0 -186
- package/agent/rules/basalt-notifications.md +0 -264
- package/agent/rules/basalt-query.md +0 -187
- package/agent/rules/basalt-router.md +0 -336
- package/dist/content/article-filter-bar.d.ts +0 -40
- package/dist/content/article-filter-bar.d.ts.map +0 -1
- package/dist/content/article-filter-bar.js +0 -56
- package/dist/shell/page-header.d.ts +0 -13
- package/dist/shell/page-header.d.ts.map +0 -1
- package/dist/shell/page-header.js +0 -23
- package/src/content/article-filter-bar.tsx +0 -88
- package/src/shell/page-header.tsx +0 -35
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sidebar-block law — every default a `SidebarBlock` leaves unstated, plus the two persisted
|
|
3
|
+
* fold-key grammars.
|
|
4
|
+
*
|
|
5
|
+
* PURE BY CONTRACT, for the same reason `mobile-nav-model.ts` is: three consumers read these
|
|
6
|
+
* defaults and none of them may disagree. `app-sidebar.tsx` decides where a block renders and
|
|
7
|
+
* whether the rail keeps it, `sidebar-blocks.tsx` paints it, and `mobile-nav-model.ts` counts the
|
|
8
|
+
* More-sheet rows it will produce — a count that picks `menu` vs `sheet` before any of it renders.
|
|
9
|
+
* A default re-derived per call site is how the count and the renderer drift apart (see
|
|
10
|
+
* `accountRowCount`'s doc for the same failure with account rows).
|
|
11
|
+
*/
|
|
12
|
+
import type { SidebarBlock } from '../nav/types'
|
|
13
|
+
|
|
14
|
+
/** Envelope version for a persisted fold flag. Bump only if the value stops being a boolean. */
|
|
15
|
+
export const FOLD_VERSION = 1
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* `'nav'` renders after the nav sections inside the scroll region; `'bottom'` pins above the
|
|
19
|
+
* settings footer. A progress block is `'bottom'` — it is a pinned status row, not a nav tail.
|
|
20
|
+
*/
|
|
21
|
+
export function sidebarBlockPlacement(block: SidebarBlock): 'nav' | 'bottom' {
|
|
22
|
+
if (block.kind === 'progress') return block.placement ?? 'bottom'
|
|
23
|
+
return block.placement ?? 'nav'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The collapsed-rail projection. A list block earns a dot only when it carries a `count` — a rail
|
|
28
|
+
* icon with no number behind it is a dot that means nothing — and a progress block a ring on the
|
|
29
|
+
* settings row. A `custom` block has no rail representation at all (arbitrary content in ~48px of
|
|
30
|
+
* icon column), which is exactly what `sidebarNavExtra` did before it.
|
|
31
|
+
*/
|
|
32
|
+
export function sidebarBlockRail(block: SidebarBlock): 'dot' | 'ring' | 'hidden' {
|
|
33
|
+
if (block.kind === 'custom') return 'hidden'
|
|
34
|
+
if (block.kind === 'progress') return block.rail ?? 'ring'
|
|
35
|
+
// A dot has to sit ON something. With no `icon` there is no rail node to badge, and with no
|
|
36
|
+
// `count` there is no number behind the dot — either way the block leaves the rail rather than
|
|
37
|
+
// occupying ~48px of icon column with a label the rail has already hidden. This overrides an
|
|
38
|
+
// explicit `rail: 'dot'` too: that combination is a config mistake, not a shape to render blank.
|
|
39
|
+
if (block.icon === undefined || block.count === undefined) return 'hidden'
|
|
40
|
+
return block.rail ?? 'dot'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The mobile projection. A list is reachable from More by default — below `sm` the sidebar does
|
|
45
|
+
* not render at all, so a hidden list is unreachable, not merely unshown. A progress row defaults
|
|
46
|
+
* to `'hidden'`: it is ambient status, and every More row costs headroom the menu-vs-sheet
|
|
47
|
+
* threshold is measured against.
|
|
48
|
+
*/
|
|
49
|
+
export function sidebarBlockMobile(block: SidebarBlock): 'more' | 'hidden' {
|
|
50
|
+
if (block.kind === 'custom') return 'hidden'
|
|
51
|
+
if (block.kind === 'progress') return block.mobile ?? 'hidden'
|
|
52
|
+
return block.mobile ?? 'more'
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Rows a list block paints, honouring `max` — the "Show more" toggle reveals the remainder. */
|
|
56
|
+
export function sidebarBlockVisibleCount(itemCount: number, max: number | undefined): number {
|
|
57
|
+
if (max === undefined || max >= itemCount) return itemCount
|
|
58
|
+
return Math.max(0, max)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A section label as a storage-key segment. Lowercase, every run of non-alphanumerics collapsed to
|
|
63
|
+
* one `-`, no leading/trailing separator — `'Awaiting action'` → `'awaiting-action'`.
|
|
64
|
+
*/
|
|
65
|
+
export function slugifyLabel(label: string): string {
|
|
66
|
+
return label
|
|
67
|
+
.toLowerCase()
|
|
68
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
69
|
+
.replace(/^-+|-+$/g, '')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* `createPersistedState` namespaces every key as `basalt:<key>`, so these return the UNNAMESPACED
|
|
74
|
+
* halves — the real storage keys are `basalt:sidebar-block:<key>` and
|
|
75
|
+
* `basalt:sidebar-section:<label-slug>` (`docs/CONTROLS-SPEC.md` §2.3).
|
|
76
|
+
*/
|
|
77
|
+
export function sidebarBlockFoldKey(blockKey: string): string {
|
|
78
|
+
return `sidebar-block:${blockKey}`
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function sidebarSectionFoldKey(label: string): string {
|
|
82
|
+
return `sidebar-section:${slugifyLabel(label)}`
|
|
83
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sidebar blocks — the renderer for `SidebarBlock[]` (`docs/CONTROLS-SPEC.md` §2.3, law C13).
|
|
3
|
+
*
|
|
4
|
+
* A block is the "Awaiting action" / "Recents" / "Getting started 1 of 5" shape every consumer
|
|
5
|
+
* hand-rolled into `sidebarNavExtra`: a micro-label, a short list of NON-destination rows, and
|
|
6
|
+
* sometimes a fold. Because it arrives as DATA, basalt owns the two projections a `ReactNode` slot
|
|
7
|
+
* could never express — the collapsed rail (a dot on the icon, a ring on the settings row) and the
|
|
8
|
+
* mobile More sheet (`app-mobile-nav.tsx`).
|
|
9
|
+
*
|
|
10
|
+
* Styling deliberately lives in `app-sidebar.module.css`, not in a module of its own: the
|
|
11
|
+
* collapsed-rail rules are `.root[data-collapsed] …` descendant selectors, and `.root` is that
|
|
12
|
+
* module's class. A second CSS module would hash `.block*` into a different name than the rail
|
|
13
|
+
* rules could ever select.
|
|
14
|
+
*/
|
|
15
|
+
import { Collapse, NavLink, Progress, Text, UnstyledButton } from '@mantine/core'
|
|
16
|
+
import { useState } from 'react'
|
|
17
|
+
import type { ReactNode } from 'react'
|
|
18
|
+
import type {
|
|
19
|
+
SidebarBlockItem,
|
|
20
|
+
SidebarBlockTone,
|
|
21
|
+
SidebarCustomBlock,
|
|
22
|
+
SidebarListBlock,
|
|
23
|
+
SidebarProgressBlock,
|
|
24
|
+
} from '../nav/types'
|
|
25
|
+
import { NavCountBadge } from './nav-count-badge'
|
|
26
|
+
import { createPersistedState } from '../state'
|
|
27
|
+
import {
|
|
28
|
+
FOLD_VERSION,
|
|
29
|
+
sidebarBlockFoldKey,
|
|
30
|
+
sidebarBlockRail,
|
|
31
|
+
sidebarBlockVisibleCount,
|
|
32
|
+
} from './sidebar-block-model'
|
|
33
|
+
import { IconSlot } from '../theme/icon-slot'
|
|
34
|
+
import classes from './app-sidebar.module.css'
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Fold/expand chevron. Lives here rather than in `app-sidebar.tsx` purely for direction: that
|
|
38
|
+
* module imports this one, so a glyph shared by a collapsible SECTION header and a collapsible
|
|
39
|
+
* BLOCK header has to sit on this side of the edge or the two files cycle.
|
|
40
|
+
*/
|
|
41
|
+
export function IconChevron({ open }: { open: boolean }) {
|
|
42
|
+
return (
|
|
43
|
+
<svg
|
|
44
|
+
width={12}
|
|
45
|
+
height={12}
|
|
46
|
+
viewBox="0 0 24 24"
|
|
47
|
+
fill="none"
|
|
48
|
+
stroke="currentColor"
|
|
49
|
+
strokeWidth={2}
|
|
50
|
+
strokeLinecap="round"
|
|
51
|
+
strokeLinejoin="round"
|
|
52
|
+
aria-hidden
|
|
53
|
+
>
|
|
54
|
+
{open ? <path d="M6 9l6 6l6 -6" /> : <path d="M9 6l6 6l-6 6" />}
|
|
55
|
+
</svg>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* One memoized `createPersistedState` store per fold key — the same module-scope factory pattern
|
|
61
|
+
* (and the same reason for it) as `collapseStore` in `index.tsx`: `createPersistedState` is a
|
|
62
|
+
* per-key FACTORY, so calling it during render would allocate a fresh store and a fresh
|
|
63
|
+
* `useSyncExternalStore` subscription on every commit. The FIRST caller's `initial` wins, which is
|
|
64
|
+
* what makes a `defaultCollapsed` a seed rather than a value that fights the persisted one.
|
|
65
|
+
*/
|
|
66
|
+
const foldStores = new Map<string, () => readonly [boolean, (next: boolean) => void]>()
|
|
67
|
+
|
|
68
|
+
function foldStore(
|
|
69
|
+
key: string,
|
|
70
|
+
initial: boolean,
|
|
71
|
+
): () => readonly [boolean, (next: boolean) => void] {
|
|
72
|
+
const cached = foldStores.get(key)
|
|
73
|
+
if (cached) return cached
|
|
74
|
+
const store = createPersistedState<boolean>({ key, version: FOLD_VERSION, initial })
|
|
75
|
+
foldStores.set(key, store)
|
|
76
|
+
return store
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A persisted fold flag (`true` = folded). Exactly one hook call regardless of `key`, so swapping
|
|
81
|
+
* keys mid-life never moves the hook count.
|
|
82
|
+
*
|
|
83
|
+
* Every fold in the sidebar goes through this — block folds at `basalt:sidebar-block:<key>` and
|
|
84
|
+
* nav-section folds at `basalt:sidebar-section:<label-slug>`. Through 1.25.0 the section folds were
|
|
85
|
+
* one `useState` keyed by label, so every reload re-opened a section the user had closed.
|
|
86
|
+
*/
|
|
87
|
+
export function usePersistedFold(
|
|
88
|
+
key: string,
|
|
89
|
+
initial: boolean,
|
|
90
|
+
): readonly [boolean, (next: boolean) => void] {
|
|
91
|
+
return foldStore(key, initial)()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Block micro-label. Reuses `.sectionLabel` — the SAME class a nav section header uses, so a block
|
|
96
|
+
* heading and a section heading cannot drift apart (docs/DESIGN-SPEC.md §3).
|
|
97
|
+
*/
|
|
98
|
+
function BlockLabel({ children }: { children: ReactNode }) {
|
|
99
|
+
return (
|
|
100
|
+
<Text component="div" className={classes.sectionLabel}>
|
|
101
|
+
{children}
|
|
102
|
+
</Text>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Status dot — the icon fallback for a toned row. Reads `--vx-status-*` through `data-tone`. */
|
|
107
|
+
export function SidebarBlockToneDot({ tone }: { tone: SidebarBlockTone }) {
|
|
108
|
+
return <span className={classes.blockToneDot} data-tone={tone} aria-hidden />
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** `value` of `total` as a ring — the collapsed rail's stand-in for a progress block. */
|
|
112
|
+
export function SidebarProgressRing({ value, total }: { value: number; total: number }) {
|
|
113
|
+
// r=8 in a 20x20 box: the circumference is a constant, so the arc is a dash offset rather than a
|
|
114
|
+
// path computed per render.
|
|
115
|
+
const CIRCUMFERENCE = 2 * Math.PI * 8
|
|
116
|
+
const ratio = total > 0 ? Math.min(1, Math.max(0, value / total)) : 0
|
|
117
|
+
return (
|
|
118
|
+
<span className={classes.blockRing} data-basalt-rail-ring>
|
|
119
|
+
<svg width={20} height={20} viewBox="0 0 20 20" aria-hidden>
|
|
120
|
+
<circle cx={10} cy={10} r={8} fill="none" stroke="var(--vx-divider)" strokeWidth={2} />
|
|
121
|
+
<circle
|
|
122
|
+
cx={10}
|
|
123
|
+
cy={10}
|
|
124
|
+
r={8}
|
|
125
|
+
fill="none"
|
|
126
|
+
stroke="var(--vx-accent)"
|
|
127
|
+
strokeWidth={2}
|
|
128
|
+
strokeLinecap="round"
|
|
129
|
+
strokeDasharray={CIRCUMFERENCE}
|
|
130
|
+
strokeDashoffset={CIRCUMFERENCE * (1 - ratio)}
|
|
131
|
+
transform="rotate(-90 10 10)"
|
|
132
|
+
/>
|
|
133
|
+
</svg>
|
|
134
|
+
</span>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* One list row. A row with no `Anchor`, `href` or `onClick` is NOT a link — the "Recents" shape is
|
|
140
|
+
* plain text, and wrapping it in an `<a>` with nowhere to go is what makes a keyboard walk through
|
|
141
|
+
* the sidebar land on dead stops.
|
|
142
|
+
*/
|
|
143
|
+
function BlockRow({ item }: { item: SidebarBlockItem }) {
|
|
144
|
+
// Through `IconSlot` like every other `icon` prop in the framework: a block row's leading mark is
|
|
145
|
+
// a consumer-supplied glyph, and the row's 20px rhythm cannot be at the mercy of what size that
|
|
146
|
+
// glyph declares for itself (`theme/icon-slot.tsx`). The tone dot is basalt's own and already
|
|
147
|
+
// square, but it goes through the same box so the two cases align to the same column.
|
|
148
|
+
const rawLead =
|
|
149
|
+
item.icon ?? (item.tone !== undefined ? <SidebarBlockToneDot tone={item.tone} /> : undefined)
|
|
150
|
+
const lead = rawLead === undefined ? undefined : <IconSlot>{rawLead}</IconSlot>
|
|
151
|
+
const meta =
|
|
152
|
+
item.meta !== undefined ? (
|
|
153
|
+
<Text component="span" className={classes.blockRowMeta}>
|
|
154
|
+
{item.meta}
|
|
155
|
+
</Text>
|
|
156
|
+
) : undefined
|
|
157
|
+
|
|
158
|
+
const interactive =
|
|
159
|
+
item.Anchor !== undefined || item.href !== undefined || item.onClick !== undefined
|
|
160
|
+
if (!interactive) {
|
|
161
|
+
return (
|
|
162
|
+
<div className={`${classes.blockRow} ${classes.blockRowStatic}`}>
|
|
163
|
+
{lead}
|
|
164
|
+
<Text component="span" className={classes.blockRowLabel}>
|
|
165
|
+
{item.label}
|
|
166
|
+
</Text>
|
|
167
|
+
{meta}
|
|
168
|
+
</div>
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const shared = {
|
|
173
|
+
classNames: { root: classes.blockRow },
|
|
174
|
+
label: item.label,
|
|
175
|
+
...(lead !== undefined && { leftSection: lead }),
|
|
176
|
+
...(meta !== undefined && { rightSection: meta }),
|
|
177
|
+
...(item.onClick !== undefined && { onClick: item.onClick }),
|
|
178
|
+
}
|
|
179
|
+
const Anchor = item.Anchor
|
|
180
|
+
if (Anchor) return <NavLink component={Anchor} {...shared} />
|
|
181
|
+
return <NavLink component="a" {...(item.href !== undefined && { href: item.href })} {...shared} />
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* A `kind: 'list'` block. Two independent states, deliberately: the FOLD is persisted (a user who
|
|
186
|
+
* closes "Recents" keeps it closed across reloads) while "Show more" is ephemeral — it is a
|
|
187
|
+
* momentary reach into a longer list, not a preference, and persisting it would quietly grow the
|
|
188
|
+
* sidebar for good.
|
|
189
|
+
*/
|
|
190
|
+
function ListBlock({ block }: { block: SidebarListBlock }) {
|
|
191
|
+
const [folded, setFolded] = usePersistedFold(sidebarBlockFoldKey(block.key), false)
|
|
192
|
+
const [showAll, setShowAll] = useState(false)
|
|
193
|
+
|
|
194
|
+
const rail = sidebarBlockRail(block)
|
|
195
|
+
const visible = showAll
|
|
196
|
+
? block.items.length
|
|
197
|
+
: sidebarBlockVisibleCount(block.items.length, block.max)
|
|
198
|
+
const hidden = block.items.length - visible
|
|
199
|
+
|
|
200
|
+
const label = (
|
|
201
|
+
<>
|
|
202
|
+
{block.icon !== undefined && (
|
|
203
|
+
<span className={classes.blockIcon}>
|
|
204
|
+
{block.icon}
|
|
205
|
+
{rail === 'dot' && <span className={classes.blockDot} aria-hidden />}
|
|
206
|
+
</span>
|
|
207
|
+
)}
|
|
208
|
+
<BlockLabel>{block.label}</BlockLabel>
|
|
209
|
+
{block.count ? <NavCountBadge count={block.count} /> : null}
|
|
210
|
+
</>
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
const body = (
|
|
214
|
+
<div className={classes.blockItems}>
|
|
215
|
+
{block.items.slice(0, visible).map((item) => (
|
|
216
|
+
<BlockRow key={item.key} item={item} />
|
|
217
|
+
))}
|
|
218
|
+
{hidden > 0 || showAll ? (
|
|
219
|
+
<UnstyledButton
|
|
220
|
+
className={classes.blockMore}
|
|
221
|
+
onClick={() => setShowAll(!showAll)}
|
|
222
|
+
aria-expanded={showAll}
|
|
223
|
+
>
|
|
224
|
+
{showAll ? 'Show less' : 'Show more'}
|
|
225
|
+
</UnstyledButton>
|
|
226
|
+
) : null}
|
|
227
|
+
</div>
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
<div className={classes.block} data-rail={rail}>
|
|
232
|
+
{block.collapsible === true ? (
|
|
233
|
+
<>
|
|
234
|
+
<UnstyledButton
|
|
235
|
+
className={`${classes.blockHeader} ${classes.blockHeaderButton}`}
|
|
236
|
+
onClick={() => setFolded(!folded)}
|
|
237
|
+
aria-expanded={!folded}
|
|
238
|
+
>
|
|
239
|
+
{label}
|
|
240
|
+
<IconChevron open={!folded} />
|
|
241
|
+
</UnstyledButton>
|
|
242
|
+
<Collapse expanded={!folded}>{body}</Collapse>
|
|
243
|
+
</>
|
|
244
|
+
) : (
|
|
245
|
+
<>
|
|
246
|
+
<div className={classes.blockHeader}>{label}</div>
|
|
247
|
+
{body}
|
|
248
|
+
</>
|
|
249
|
+
)}
|
|
250
|
+
</div>
|
|
251
|
+
)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** A `kind: 'progress'` block — the "Getting started 1 of 5" row pinned above the footer. */
|
|
255
|
+
function ProgressBlock({ block }: { block: SidebarProgressBlock }) {
|
|
256
|
+
const percent =
|
|
257
|
+
block.total > 0 ? Math.min(100, Math.max(0, (block.value / block.total) * 100)) : 0
|
|
258
|
+
const inner = (
|
|
259
|
+
<>
|
|
260
|
+
<div className={classes.blockProgressHead}>
|
|
261
|
+
<BlockLabel>{block.label}</BlockLabel>
|
|
262
|
+
<Text component="span" className={classes.blockRowMeta}>
|
|
263
|
+
{block.value} of {block.total}
|
|
264
|
+
</Text>
|
|
265
|
+
</div>
|
|
266
|
+
<Progress value={percent} size="xs" aria-label={block.label} />
|
|
267
|
+
</>
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
return (
|
|
271
|
+
<div className={classes.block} data-rail={sidebarBlockRail(block)}>
|
|
272
|
+
{block.onClick !== undefined ? (
|
|
273
|
+
<UnstyledButton className={classes.blockProgress} onClick={block.onClick}>
|
|
274
|
+
{inner}
|
|
275
|
+
</UnstyledButton>
|
|
276
|
+
) : (
|
|
277
|
+
<div className={classes.blockProgress}>{inner}</div>
|
|
278
|
+
)}
|
|
279
|
+
</div>
|
|
280
|
+
)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** A `kind: 'custom'` block — consumer content, hidden on the rail exactly as `navExtra` was. */
|
|
284
|
+
function CustomBlock({ block }: { block: SidebarCustomBlock }) {
|
|
285
|
+
return (
|
|
286
|
+
<div className={classes.block} data-rail="hidden">
|
|
287
|
+
{block.node}
|
|
288
|
+
</div>
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** One block, whichever kind it is. `AppSidebar` places it; this decides nothing but the paint. */
|
|
293
|
+
export function SidebarBlockView({
|
|
294
|
+
block,
|
|
295
|
+
}: {
|
|
296
|
+
block: SidebarListBlock | SidebarProgressBlock | SidebarCustomBlock
|
|
297
|
+
}) {
|
|
298
|
+
if (block.kind === 'list') return <ListBlock block={block} />
|
|
299
|
+
if (block.kind === 'progress') return <ProgressBlock block={block} />
|
|
300
|
+
return <CustomBlock block={block} />
|
|
301
|
+
}
|
|
@@ -5,6 +5,18 @@
|
|
|
5
5
|
* button as a matched pair, so the two must never drift onto different depth tokens.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
/*
|
|
9
|
+
* The row that appears ONLY when `search.actions` is supplied — one or two icon-only buttons beside
|
|
10
|
+
* the trigger. Without actions the trigger stays the row's sole node (no wrapper), so the DOM of
|
|
11
|
+
* every consumer that doesn't use them is untouched.
|
|
12
|
+
*/
|
|
13
|
+
.row {
|
|
14
|
+
display: flex;
|
|
15
|
+
align-items: center;
|
|
16
|
+
gap: var(--vx-space-sidebar-search-gap);
|
|
17
|
+
min-width: 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
8
20
|
.trigger {
|
|
9
21
|
display: flex;
|
|
10
22
|
align-items: center;
|
|
@@ -74,3 +86,17 @@
|
|
|
74
86
|
background-color: color-mix(in srgb, var(--vx-ink) 6%, transparent) !important;
|
|
75
87
|
color: var(--vx-ink) !important;
|
|
76
88
|
}
|
|
89
|
+
|
|
90
|
+
/* Trailing search-row action — the same ghost treatment as `.ghostIcon` / `.railBtn`, at the `icon`
|
|
91
|
+
* tier (24px) the component asks for explicitly. Never re-points `--ai-size`: unlike `.railBtn`,
|
|
92
|
+
* this button is NOT a second state of the trigger and must not track its height. */
|
|
93
|
+
.actionBtn {
|
|
94
|
+
flex: none;
|
|
95
|
+
color: var(--vx-faint) !important;
|
|
96
|
+
border-radius: var(--vx-radius-ctrl) !important;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.actionBtn:hover {
|
|
100
|
+
background-color: color-mix(in srgb, var(--vx-ink) 6%, transparent) !important;
|
|
101
|
+
color: var(--vx-ink) !important;
|
|
102
|
+
}
|
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
* (`noUncheckedIndexedAccess`, `noPropertyAccessFromIndexSignature`, `exactOptionalPropertyTypes`).
|
|
5
5
|
*/
|
|
6
6
|
declare const classes: {
|
|
7
|
+
readonly row: string
|
|
7
8
|
readonly trigger: string
|
|
8
9
|
readonly label: string
|
|
9
10
|
readonly railBtn: string
|
|
11
|
+
readonly actionBtn: string
|
|
10
12
|
}
|
|
11
13
|
export default classes
|
|
@@ -8,8 +8,22 @@
|
|
|
8
8
|
* surface + shadow-card, control radius, faint icon/label, mono `Kbd` shortcut hint.
|
|
9
9
|
*/
|
|
10
10
|
import { ActionIcon, Kbd, Text, UnstyledButton } from '@mantine/core'
|
|
11
|
+
import type { ReactNode } from 'react'
|
|
12
|
+
import { OverflowMenu } from '../controls/actions'
|
|
13
|
+
import type { BarAction } from '../controls/actions'
|
|
11
14
|
import classes from './sidebar-search.module.css'
|
|
12
15
|
|
|
16
|
+
/**
|
|
17
|
+
* The ⌘K row's trailing icon buttons — ONE or TWO, by type (`docs/CONTROLS-SPEC.md` §2.3).
|
|
18
|
+
*
|
|
19
|
+
* The tuple IS the enforcement: a third button pushes the search trigger under the width where its
|
|
20
|
+
* placeholder stays readable, and there is no third thing a sidebar search row is for. Each entry
|
|
21
|
+
* renders icon-only at the `icon` tier (24px, `--ai-size-icon`) with `label` demoted to the
|
|
22
|
+
* accessible name; a `kind: 'menu'` entry becomes an `OverflowMenu` kebab, a `kind: 'custom'` one
|
|
23
|
+
* is mounted verbatim.
|
|
24
|
+
*/
|
|
25
|
+
export type SidebarSearchActions = [BarAction] | [BarAction, BarAction]
|
|
26
|
+
|
|
13
27
|
export type SidebarSearchConfig = {
|
|
14
28
|
/** Opens the search palette — e.g. `openSpotlight` from `basalt-ui/commands`. */
|
|
15
29
|
onOpen: () => void
|
|
@@ -39,12 +53,55 @@ function IconSearch() {
|
|
|
39
53
|
)
|
|
40
54
|
}
|
|
41
55
|
|
|
56
|
+
/** The first grapheme of a label — the fallback body for an action shipping no icon. */
|
|
57
|
+
function initial(label: string): string {
|
|
58
|
+
return [...label][0]?.toUpperCase() ?? '?'
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* One trailing action as an icon-only button. `size="icon"` is EXPLICIT here, not inherited: the
|
|
63
|
+
* search row is chrome, not a control home, so no `CtlSlot` wraps it and nothing else would resolve
|
|
64
|
+
* the tier (`--ai-size-icon`, declared by `cssVariablesResolver` — spec §5).
|
|
65
|
+
*/
|
|
66
|
+
function SearchAction({ action }: { action: BarAction }): ReactNode {
|
|
67
|
+
if (action.kind === 'custom') return action.node
|
|
68
|
+
if (action.kind === 'menu') {
|
|
69
|
+
return <OverflowMenu actions={action.items} trigger="kebab" label={action.label} />
|
|
70
|
+
}
|
|
71
|
+
const shared = {
|
|
72
|
+
variant: 'subtle' as const,
|
|
73
|
+
size: 'icon',
|
|
74
|
+
className: classes.actionBtn,
|
|
75
|
+
'aria-label': action.label,
|
|
76
|
+
disabled: action.disabled === true,
|
|
77
|
+
...(action.onClick !== undefined && { onClick: action.onClick }),
|
|
78
|
+
}
|
|
79
|
+
const body = action.icon ?? initial(action.label)
|
|
80
|
+
const Anchor = action.Anchor
|
|
81
|
+
if (Anchor !== undefined) {
|
|
82
|
+
return (
|
|
83
|
+
<ActionIcon component={Anchor} {...shared}>
|
|
84
|
+
{body}
|
|
85
|
+
</ActionIcon>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
if (action.link !== undefined) {
|
|
89
|
+
return (
|
|
90
|
+
<ActionIcon component="a" href={action.link.to} {...shared}>
|
|
91
|
+
{body}
|
|
92
|
+
</ActionIcon>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
return <ActionIcon {...shared}>{body}</ActionIcon>
|
|
96
|
+
}
|
|
97
|
+
|
|
42
98
|
export function SidebarSearch({
|
|
43
99
|
onOpen,
|
|
44
100
|
placeholder = 'Search…',
|
|
45
101
|
shortcut,
|
|
102
|
+
actions,
|
|
46
103
|
collapsed,
|
|
47
|
-
}: SidebarSearchConfig & { collapsed?: boolean }) {
|
|
104
|
+
}: SidebarSearchConfig & { actions?: SidebarSearchActions; collapsed?: boolean }) {
|
|
48
105
|
// SSR-safe mac detection, matching the shell's other shortcut-hint logic.
|
|
49
106
|
const isMac = typeof navigator !== 'undefined' && /Mac|iPhone|iPod|iPad/i.test(navigator.platform)
|
|
50
107
|
const hint = shortcut ?? (isMac ? '⌘K' : 'Ctrl K')
|
|
@@ -63,7 +120,7 @@ export function SidebarSearch({
|
|
|
63
120
|
)
|
|
64
121
|
}
|
|
65
122
|
|
|
66
|
-
|
|
123
|
+
const trigger = (
|
|
67
124
|
<UnstyledButton
|
|
68
125
|
type="button"
|
|
69
126
|
className={classes.trigger}
|
|
@@ -77,4 +134,16 @@ export function SidebarSearch({
|
|
|
77
134
|
<Kbd size="xs">{hint}</Kbd>
|
|
78
135
|
</UnstyledButton>
|
|
79
136
|
)
|
|
137
|
+
|
|
138
|
+
// No wrapper without actions — the trigger stays the row's only node, exactly as it shipped.
|
|
139
|
+
if (actions === undefined) return trigger
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<div className={classes.row}>
|
|
143
|
+
{trigger}
|
|
144
|
+
{actions.map((action) => (
|
|
145
|
+
<SearchAction key={action.key} action={action} />
|
|
146
|
+
))}
|
|
147
|
+
</div>
|
|
148
|
+
)
|
|
80
149
|
}
|