basalt-ui 1.25.0 → 1.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +8 -6
- package/CHANGELOG.md +18 -0
- package/MIGRATING.md +546 -20
- package/README.md +45 -32
- package/agent/rules/basalt-batteries.md +205 -0
- package/agent/rules/basalt-charts.md +125 -555
- package/agent/rules/basalt-controls.md +155 -0
- package/agent/rules/basalt-mantine.md +146 -439
- package/agent/rules/basalt-state.md +113 -96
- package/agent/rules/basalt-tokens.md +126 -288
- package/agent/skills/basalt-app/SKILL.md +70 -195
- package/agent/skills/basalt-charts/SKILL.md +54 -234
- package/agent/skills/basalt-design/SKILL.md +56 -248
- package/agent/templates/CLAUDE-block.md.tpl +31 -40
- package/agent/templates/DESIGN.md.tpl +22 -51
- package/configs/oxlint-plugin.js +1275 -31
- package/configs/oxlint-plugin.test.ts +947 -11
- package/configs/oxlint.json +11 -3
- package/dist/charts/cursor/resolve.d.ts +18 -0
- package/dist/charts/cursor/resolve.d.ts.map +1 -1
- package/dist/charts/cursor/resolve.js +15 -6
- package/dist/charts/cursor/scope.d.ts +6 -3
- package/dist/charts/cursor/scope.d.ts.map +1 -1
- package/dist/charts/cursor/store.d.ts +5 -1
- package/dist/charts/cursor/store.d.ts.map +1 -1
- package/dist/charts/cursor/store.js +4 -4
- package/dist/charts/hooks/useChartCursor.d.ts.map +1 -1
- package/dist/charts/hooks/useChartCursor.js +9 -8
- package/dist/charts/index.d.ts +2 -2
- package/dist/charts/index.d.ts.map +1 -1
- package/dist/charts/primitives/ChartCard.d.ts +34 -9
- package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
- package/dist/charts/primitives/ChartCard.js +29 -114
- package/dist/charts/primitives/ChartCard.test.d.ts +2 -0
- package/dist/charts/primitives/ChartCard.test.d.ts.map +1 -0
- package/dist/charts/sparklines/BarSparkline.d.ts +26 -2
- package/dist/charts/sparklines/BarSparkline.d.ts.map +1 -1
- package/dist/charts/sparklines/BarSparkline.js +18 -4
- package/dist/cli/check-coverage.test.d.ts +2 -0
- package/dist/cli/check-coverage.test.d.ts.map +1 -0
- package/dist/cli/index.d.ts +67 -4
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +225 -7
- package/dist/connectivity/connectivity-indicator.d.ts +4 -1
- package/dist/connectivity/connectivity-indicator.d.ts.map +1 -1
- package/dist/connectivity/connectivity-indicator.js +1 -1
- package/dist/content/index.d.ts +5 -3
- package/dist/content/index.d.ts.map +1 -1
- package/dist/content/index.js +4 -2
- package/dist/content/prose.module.css +7 -28
- package/dist/controls/actions.d.ts +169 -0
- package/dist/controls/actions.d.ts.map +1 -0
- package/dist/controls/actions.js +287 -0
- package/dist/controls/actions.module.css +62 -0
- package/dist/controls/actions.module.css.d.ts +13 -0
- package/dist/controls/actions.test.d.ts +2 -0
- package/dist/controls/actions.test.d.ts.map +1 -0
- package/dist/controls/actions.type-guard.test.d.ts +2 -0
- package/dist/controls/actions.type-guard.test.d.ts.map +1 -0
- package/dist/controls/compare-filter.d.ts +41 -0
- package/dist/controls/compare-filter.d.ts.map +1 -0
- package/dist/controls/compare-filter.js +23 -0
- package/dist/controls/control-group.d.ts +19 -0
- package/dist/controls/control-group.d.ts.map +1 -0
- package/dist/controls/control-group.js +20 -0
- package/dist/controls/control-group.test.d.ts +2 -0
- package/dist/controls/control-group.test.d.ts.map +1 -0
- package/dist/controls/controls.module.css +542 -0
- package/dist/controls/controls.module.css.d.ts +35 -0
- package/dist/controls/controls.router.test.d.ts +2 -0
- package/dist/controls/controls.router.test.d.ts.map +1 -0
- package/dist/controls/enum-filter.d.ts +30 -0
- package/dist/controls/enum-filter.d.ts.map +1 -0
- package/dist/controls/enum-filter.js +91 -0
- package/dist/controls/filter-context.d.ts +55 -0
- package/dist/controls/filter-context.d.ts.map +1 -0
- package/dist/controls/filter-context.js +69 -0
- package/dist/controls/filter-pill.d.ts +28 -0
- package/dist/controls/filter-pill.d.ts.map +1 -0
- package/dist/controls/filter-pill.js +70 -0
- package/dist/controls/filter-set.d.ts +13 -0
- package/dist/controls/filter-set.d.ts.map +1 -0
- package/dist/controls/filter-set.js +125 -0
- package/dist/controls/filter-set.test.d.ts +2 -0
- package/dist/controls/filter-set.test.d.ts.map +1 -0
- package/dist/controls/filter-sheet.d.ts +117 -0
- package/dist/controls/filter-sheet.d.ts.map +1 -0
- package/dist/controls/filter-sheet.js +146 -0
- package/dist/controls/glyphs.d.ts +27 -0
- package/dist/controls/glyphs.d.ts.map +1 -0
- package/dist/controls/glyphs.js +49 -0
- package/dist/controls/index.d.ts +19 -0
- package/dist/controls/index.d.ts.map +1 -0
- package/dist/controls/index.js +44 -0
- package/dist/controls/multi-select-filter.d.ts +23 -0
- package/dist/controls/multi-select-filter.d.ts.map +1 -0
- package/dist/controls/multi-select-filter.js +89 -0
- package/dist/controls/peer-boundary.test.d.ts +2 -0
- package/dist/controls/peer-boundary.test.d.ts.map +1 -0
- package/dist/controls/props-contract.test.d.ts +2 -0
- package/dist/controls/props-contract.test.d.ts.map +1 -0
- package/dist/controls/range-filter.d.ts +29 -0
- package/dist/controls/range-filter.d.ts.map +1 -0
- package/dist/controls/range-filter.js +120 -0
- package/dist/controls/search-filter.d.ts +10 -0
- package/dist/controls/search-filter.d.ts.map +1 -0
- package/dist/controls/search-filter.js +81 -0
- package/dist/controls/select-filter.d.ts +74 -0
- package/dist/controls/select-filter.d.ts.map +1 -0
- package/dist/controls/select-filter.js +10 -0
- package/dist/controls/sync-button.d.ts +38 -0
- package/dist/controls/sync-button.d.ts.map +1 -0
- package/dist/controls/sync-button.js +119 -0
- package/dist/controls/sync-button.module.css +63 -0
- package/dist/controls/sync-button.module.css.d.ts +11 -0
- package/dist/controls/sync-button.test.d.ts +2 -0
- package/dist/controls/sync-button.test.d.ts.map +1 -0
- package/dist/controls/toggle-filter.d.ts +9 -0
- package/dist/controls/toggle-filter.d.ts.map +1 -0
- package/dist/controls/toggle-filter.js +42 -0
- package/dist/controls/view-tabs.d.ts +23 -0
- package/dist/controls/view-tabs.d.ts.map +1 -0
- package/dist/controls/view-tabs.js +59 -0
- package/dist/controls-dates/date-range-picker.d.ts +4 -0
- package/dist/controls-dates/date-range-picker.d.ts.map +1 -0
- package/dist/controls-dates/date-range-picker.js +18 -0
- package/dist/controls-dates/index.d.ts +9 -0
- package/dist/controls-dates/index.d.ts.map +1 -0
- package/dist/controls-dates/index.js +4 -0
- package/dist/dashboard/delta-badge.d.ts +6 -12
- package/dist/dashboard/delta-badge.d.ts.map +1 -1
- package/dist/dashboard/delta-badge.js +1 -50
- package/dist/dashboard/settings-section.d.ts +6 -4
- package/dist/dashboard/settings-section.d.ts.map +1 -1
- package/dist/dashboard/settings-section.js +23 -16
- package/dist/dashboard/settings-section.module.css +10 -0
- package/dist/dashboard/settings-section.module.css.d.ts +1 -0
- package/dist/dashboard/settings-section.test.d.ts +2 -0
- package/dist/dashboard/settings-section.test.d.ts.map +1 -0
- package/dist/dashboard/stat-card.d.ts +44 -9
- package/dist/dashboard/stat-card.d.ts.map +1 -1
- package/dist/dashboard/stat-card.js +80 -48
- package/dist/dashboard/stat-card.module.css +82 -0
- package/dist/dashboard/stat-card.module.css.d.ts +11 -0
- package/dist/data/data-table.d.ts +37 -15
- package/dist/data/data-table.d.ts.map +1 -1
- package/dist/data/data-table.js +88 -58
- package/dist/guard/grace.test.d.ts +20 -0
- package/dist/guard/grace.test.d.ts.map +1 -0
- package/dist/guard/index.d.ts +66 -3
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +82 -7
- package/dist/guard/types.d.ts +3 -3
- package/dist/guard/types.d.ts.map +1 -1
- package/dist/index.d.ts +12 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -10
- package/dist/nav/types.d.ts +78 -2
- package/dist/nav/types.d.ts.map +1 -1
- package/dist/notifications/bell.d.ts +4 -1
- package/dist/notifications/bell.d.ts.map +1 -1
- package/dist/notifications/index.d.ts +1 -1
- package/dist/router-tanstack/field.d.ts +14 -0
- package/dist/router-tanstack/field.d.ts.map +1 -0
- package/dist/router-tanstack/field.js +4 -0
- package/dist/router-tanstack/index.d.ts +6 -2
- package/dist/router-tanstack/index.d.ts.map +1 -1
- package/dist/router-tanstack/index.js +4 -0
- package/dist/router-tanstack/multi-search-param-store.d.ts +4 -52
- package/dist/router-tanstack/multi-search-param-store.d.ts.map +1 -1
- package/dist/router-tanstack/multi-search-param-store.js +19 -27
- package/dist/router-tanstack/search-param-store.d.ts +3 -87
- package/dist/router-tanstack/search-param-store.d.ts.map +1 -1
- package/dist/router-tanstack/search-param-store.js +16 -45
- package/dist/router-tanstack/search-store.d.ts +81 -0
- package/dist/router-tanstack/search-store.d.ts.map +1 -0
- package/dist/router-tanstack/search-store.js +187 -0
- package/dist/router-tanstack/search-store.router.test.d.ts +2 -0
- package/dist/router-tanstack/search-store.router.test.d.ts.map +1 -0
- package/dist/router-tanstack/search-store.test.d.ts +2 -0
- package/dist/router-tanstack/search-store.test.d.ts.map +1 -0
- package/dist/router-tanstack/use-nav.d.ts +0 -2
- package/dist/router-tanstack/use-nav.d.ts.map +1 -1
- package/dist/router-tanstack/use-nav.js +34 -12
- package/dist/router-tanstack/use-nav.test.d.ts +2 -0
- package/dist/router-tanstack/use-nav.test.d.ts.map +1 -0
- package/dist/section/index.d.ts +2 -0
- package/dist/section/index.d.ts.map +1 -0
- package/dist/section/index.js +4 -0
- package/dist/section/section.d.ts +24 -0
- package/dist/section/section.d.ts.map +1 -0
- package/dist/section/section.js +126 -0
- package/dist/section/section.module.css +104 -0
- package/dist/section/section.module.css.d.ts +11 -0
- package/dist/section/section.test.d.ts +2 -0
- package/dist/section/section.test.d.ts.map +1 -0
- package/dist/shell/app-breadcrumbs.d.ts.map +1 -1
- package/dist/shell/app-breadcrumbs.js +18 -15
- package/dist/shell/app-header.module.css +77 -37
- package/dist/shell/app-header.module.css.d.ts +2 -1
- package/dist/shell/app-mobile-nav.d.ts +9 -2
- package/dist/shell/app-mobile-nav.d.ts.map +1 -1
- package/dist/shell/app-mobile-nav.js +98 -11
- package/dist/shell/app-mobile-nav.module.css +8 -0
- package/dist/shell/app-mobile-nav.module.css.d.ts +1 -0
- package/dist/shell/app-sidebar.d.ts +55 -28
- package/dist/shell/app-sidebar.d.ts.map +1 -1
- package/dist/shell/app-sidebar.js +116 -67
- package/dist/shell/app-sidebar.module.css +303 -20
- package/dist/shell/app-sidebar.module.css.d.ts +19 -1
- package/dist/shell/index.d.ts +55 -21
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +40 -18
- package/dist/shell/mobile-nav-model.d.ts +13 -2
- package/dist/shell/mobile-nav-model.d.ts.map +1 -1
- package/dist/shell/mobile-nav-model.js +15 -1
- package/dist/shell/nav-count-badge.d.ts +1 -1
- package/dist/shell/page-bar.d.ts +51 -0
- package/dist/shell/page-bar.d.ts.map +1 -0
- package/dist/shell/page-bar.js +154 -0
- package/dist/shell/page-bar.module.css +178 -0
- package/dist/shell/page-bar.module.css.d.ts +17 -0
- package/dist/shell/page-bar.test.d.ts +2 -0
- package/dist/shell/page-bar.test.d.ts.map +1 -0
- package/dist/shell/sidebar-block-model.d.ts +48 -0
- package/dist/shell/sidebar-block-model.d.ts.map +1 -0
- package/dist/shell/sidebar-block-model.js +39 -0
- package/dist/shell/sidebar-blocks.d.ts +32 -0
- package/dist/shell/sidebar-blocks.d.ts.map +1 -0
- package/dist/shell/sidebar-blocks.js +169 -0
- package/dist/shell/sidebar-search.d.ts +13 -1
- package/dist/shell/sidebar-search.d.ts.map +1 -1
- package/dist/shell/sidebar-search.js +34 -1
- package/dist/shell/sidebar-search.module.css +26 -0
- package/dist/shell/sidebar-search.module.css.d.ts +2 -0
- package/dist/state/fields.d.ts +392 -0
- package/dist/state/fields.d.ts.map +1 -0
- package/dist/state/fields.js +369 -0
- package/dist/state/fields.test.d.ts +2 -0
- package/dist/state/fields.test.d.ts.map +1 -0
- package/dist/state/persisted.d.ts +48 -0
- package/dist/state/persisted.d.ts.map +1 -0
- package/dist/state/persisted.js +105 -0
- package/dist/state.d.ts +11 -56
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +14 -120
- package/dist/styles.css +13 -0
- package/dist/surfaces.d.ts +106 -24
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +155 -19
- package/dist/theme/controls.module.css +16 -1
- package/dist/theme/ctl-theme.d.ts +32 -0
- package/dist/theme/ctl-theme.d.ts.map +1 -0
- package/dist/theme/ctl-theme.js +54 -0
- package/dist/theme/ctl-theme.test.d.ts +2 -0
- package/dist/theme/ctl-theme.test.d.ts.map +1 -0
- package/dist/theme/ctl-tier-coverage.test.d.ts +2 -0
- package/dist/theme/ctl-tier-coverage.test.d.ts.map +1 -0
- package/dist/theme/divider-law.test.d.ts +2 -0
- package/dist/theme/divider-law.test.d.ts.map +1 -0
- package/dist/theme/icon-slot.d.ts +44 -0
- package/dist/theme/icon-slot.d.ts.map +1 -0
- package/dist/theme/icon-slot.js +21 -0
- package/dist/theme/icon-slot.module.css +47 -0
- package/dist/theme/icon-slot.module.css.d.ts +8 -0
- package/dist/theme/icon-slot.test.d.ts +2 -0
- package/dist/theme/icon-slot.test.d.ts.map +1 -0
- package/dist/theme/index.d.ts +1 -0
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/index.js +82 -5
- package/dist/theme/layout-rhythm-css.test.d.ts +2 -0
- package/dist/theme/layout-rhythm-css.test.d.ts.map +1 -0
- package/dist/theme/segmented-control.module.css +20 -0
- package/dist/theme/shadow-surfaces.test.d.ts.map +1 -1
- package/dist/tokens/index.d.ts +12 -0
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +38 -8
- package/dist/tokens/palette.d.ts +97 -61
- package/dist/tokens/palette.d.ts.map +1 -1
- package/dist/tokens/palette.js +76 -38
- package/dist/tokens.css +10 -2
- package/dist/widget-header/delta-badge.d.ts +13 -0
- package/dist/widget-header/delta-badge.d.ts.map +1 -0
- package/dist/widget-header/delta-badge.js +29 -0
- package/dist/widget-header/delta-badge.test.d.ts +2 -0
- package/dist/widget-header/delta-badge.test.d.ts.map +1 -0
- package/dist/widget-header/index.d.ts +3 -0
- package/dist/widget-header/index.d.ts.map +1 -0
- package/dist/widget-header/index.js +6 -0
- package/dist/widget-header/no-mantine.test.d.ts +2 -0
- package/dist/widget-header/no-mantine.test.d.ts.map +1 -0
- package/dist/widget-header/widget-header.d.ts +37 -0
- package/dist/widget-header/widget-header.d.ts.map +1 -0
- package/dist/widget-header/widget-header.js +77 -0
- package/dist/widget-header/widget-header.module.css +335 -0
- package/dist/widget-header/widget-header.module.css.d.ts +24 -0
- package/dist/widget-header/widget-header.test.d.ts +2 -0
- package/dist/widget-header/widget-header.test.d.ts.map +1 -0
- package/llms.txt +30 -17
- package/package.json +14 -1
- package/src/charts/cursor/resolve.test.ts +29 -1
- package/src/charts/cursor/resolve.ts +38 -6
- package/src/charts/cursor/scope.tsx +6 -3
- package/src/charts/cursor/store.test.ts +19 -1
- package/src/charts/cursor/store.ts +10 -5
- package/src/charts/hooks/useChartCursor.ts +16 -8
- package/src/charts/index.ts +2 -2
- package/src/charts/primitives/CartesianChart.test.tsx +124 -0
- package/src/charts/primitives/ChartCard.test.tsx +66 -0
- package/src/charts/primitives/ChartCard.tsx +86 -146
- package/src/charts/sparklines/BarSparkline.tsx +43 -5
- package/src/cli/check-coverage.test.ts +297 -0
- package/src/cli/check-theme.test.ts +38 -14
- package/src/cli/index.ts +392 -16
- package/src/cli/init-sync.test.ts +97 -0
- package/src/connectivity/connectivity-indicator.tsx +16 -9
- package/src/content/index.ts +16 -3
- package/src/content/prose.module.css +7 -28
- package/src/content/prose.module.css.test.ts +8 -24
- package/src/controls/actions.module.css +62 -0
- package/src/controls/actions.module.css.d.ts +13 -0
- package/src/controls/actions.test.tsx +252 -0
- package/src/controls/actions.tsx +655 -0
- package/src/controls/actions.type-guard.test.ts +63 -0
- package/src/controls/compare-filter.tsx +64 -0
- package/src/controls/control-group.test.tsx +229 -0
- package/src/controls/control-group.tsx +63 -0
- package/src/controls/controls.module.css +542 -0
- package/src/controls/controls.module.css.d.ts +35 -0
- package/src/controls/controls.router.test.tsx +637 -0
- package/src/controls/enum-filter.tsx +159 -0
- package/src/controls/filter-context.tsx +153 -0
- package/src/controls/filter-pill.tsx +120 -0
- package/src/controls/filter-set.test.tsx +472 -0
- package/src/controls/filter-set.tsx +225 -0
- package/src/controls/filter-sheet.tsx +285 -0
- package/src/controls/glyphs.tsx +81 -0
- package/src/controls/index.ts +48 -0
- package/src/controls/multi-select-filter.tsx +140 -0
- package/src/controls/peer-boundary.test.ts +85 -0
- package/src/controls/props-contract.test.tsx +106 -0
- package/src/controls/range-filter.tsx +184 -0
- package/src/controls/search-filter.tsx +123 -0
- package/src/controls/select-filter.tsx +89 -0
- package/src/controls/sync-button.module.css +63 -0
- package/src/controls/sync-button.module.css.d.ts +11 -0
- package/src/controls/sync-button.test.tsx +183 -0
- package/src/controls/sync-button.tsx +197 -0
- package/src/controls/toggle-filter.tsx +60 -0
- package/src/controls/view-tabs.tsx +110 -0
- package/src/controls-dates/date-range-picker.tsx +42 -0
- package/src/controls-dates/index.ts +8 -0
- package/src/dashboard/delta-badge.tsx +4 -74
- package/src/dashboard/settings-section.module.css +10 -0
- package/src/dashboard/settings-section.module.css.d.ts +1 -0
- package/src/dashboard/settings-section.test.tsx +56 -0
- package/src/dashboard/settings-section.tsx +33 -30
- package/src/dashboard/stat-card.module.css +82 -0
- package/src/dashboard/stat-card.module.css.d.ts +11 -0
- package/src/dashboard/stat-card.test.tsx +67 -1
- package/src/dashboard/stat-card.tsx +209 -68
- package/src/data/data-table.test.tsx +96 -11
- package/src/data/data-table.tsx +220 -84
- package/src/guard/check-source.test.ts +126 -13
- package/src/guard/grace.test.ts +149 -0
- package/src/guard/index.ts +172 -29
- package/src/guard/types.ts +4 -2
- package/src/index.ts +46 -5
- package/src/nav/types.ts +84 -2
- package/src/notifications/bell.tsx +6 -3
- package/src/notifications/index.ts +1 -1
- package/src/router-tanstack/field.ts +31 -0
- package/src/router-tanstack/index.ts +24 -2
- package/src/router-tanstack/multi-search-param-store.test.ts +71 -3
- package/src/router-tanstack/multi-search-param-store.ts +46 -77
- package/src/router-tanstack/search-param-store.ts +42 -159
- package/src/router-tanstack/search-store.router.test.tsx +661 -0
- package/src/router-tanstack/search-store.test.ts +418 -0
- package/src/router-tanstack/search-store.ts +380 -0
- package/src/router-tanstack/use-nav.test.tsx +187 -0
- package/src/router-tanstack/use-nav.ts +72 -14
- package/src/section/index.ts +1 -0
- package/src/section/section.module.css +104 -0
- package/src/section/section.module.css.d.ts +11 -0
- package/src/section/section.test.tsx +204 -0
- package/src/section/section.tsx +220 -0
- package/src/shell/app-breadcrumbs.tsx +44 -31
- package/src/shell/app-header.module.css +77 -37
- package/src/shell/app-header.module.css.d.ts +2 -1
- package/src/shell/app-mobile-nav.module.css +8 -0
- package/src/shell/app-mobile-nav.module.css.d.ts +1 -0
- package/src/shell/app-mobile-nav.tsx +147 -8
- package/src/shell/app-sidebar-account.tsx +2 -2
- package/src/shell/app-sidebar.module.css +303 -20
- package/src/shell/app-sidebar.module.css.d.ts +19 -1
- package/src/shell/app-sidebar.test.tsx +456 -41
- package/src/shell/app-sidebar.tsx +276 -140
- package/src/shell/index.test.tsx +291 -5
- package/src/shell/index.tsx +137 -38
- package/src/shell/mobile-nav-model.test.ts +228 -7
- package/src/shell/mobile-nav-model.ts +53 -3
- package/src/shell/nav-count-badge.tsx +1 -1
- package/src/shell/page-bar.module.css +178 -0
- package/src/shell/page-bar.module.css.d.ts +17 -0
- package/src/shell/page-bar.test.tsx +316 -0
- package/src/shell/page-bar.tsx +315 -0
- package/src/shell/sidebar-block-model.ts +83 -0
- package/src/shell/sidebar-blocks.tsx +301 -0
- package/src/shell/sidebar-search.module.css +26 -0
- package/src/shell/sidebar-search.module.css.d.ts +2 -0
- package/src/shell/sidebar-search.tsx +71 -2
- package/src/state/fields.test.tsx +258 -0
- package/src/state/fields.ts +892 -0
- package/src/state/persisted.ts +211 -0
- package/src/state.ts +46 -240
- package/src/styles.css +13 -0
- package/src/surfaces.ts +203 -36
- package/src/theme/controls.module.css +16 -1
- package/src/theme/ctl-theme.test.tsx +275 -0
- package/src/theme/ctl-theme.tsx +138 -0
- package/src/theme/ctl-tier-coverage.test.ts +170 -0
- package/src/theme/density-relations.test.ts +106 -51
- package/src/theme/divider-law.test.ts +259 -0
- package/src/theme/icon-slot.module.css +47 -0
- package/src/theme/icon-slot.module.css.d.ts +8 -0
- package/src/theme/icon-slot.test.tsx +275 -0
- package/src/theme/icon-slot.tsx +59 -0
- package/src/theme/index.ts +165 -3
- package/src/theme/layout-rhythm-css.test.ts +72 -0
- package/src/theme/radius.test.ts +11 -1
- package/src/theme/segmented-control.module.css +20 -0
- package/src/theme/shadow-surfaces.test.ts +20 -7
- package/src/theme/spacing.test.ts +75 -27
- package/src/theme/use-basalt-spacing.ts +2 -2
- package/src/tokens/density.test.ts +7 -25
- package/src/tokens/index.ts +48 -8
- package/src/tokens/palette.ts +107 -73
- package/src/widget-header/delta-badge.test.tsx +60 -0
- package/src/widget-header/delta-badge.tsx +57 -0
- package/src/widget-header/index.ts +2 -0
- package/src/widget-header/no-mantine.test.ts +33 -0
- package/src/widget-header/widget-header.module.css +335 -0
- package/src/widget-header/widget-header.module.css.d.ts +24 -0
- package/src/widget-header/widget-header.test.tsx +187 -0
- package/src/widget-header/widget-header.tsx +168 -0
- package/agent/rules/basalt-agent.md +0 -452
- package/agent/rules/basalt-app.md +0 -246
- package/agent/rules/basalt-commands.md +0 -320
- package/agent/rules/basalt-content.md +0 -409
- package/agent/rules/basalt-data.md +0 -329
- package/agent/rules/basalt-forms.md +0 -186
- package/agent/rules/basalt-notifications.md +0 -264
- package/agent/rules/basalt-query.md +0 -187
- package/agent/rules/basalt-router.md +0 -336
- package/dist/content/article-filter-bar.d.ts +0 -40
- package/dist/content/article-filter-bar.d.ts.map +0 -1
- package/dist/content/article-filter-bar.js +0 -56
- package/dist/shell/page-header.d.ts +0 -13
- package/dist/shell/page-header.d.ts.map +0 -1
- package/dist/shell/page-header.js +0 -23
- package/src/content/article-filter-bar.tsx +0 -88
- package/src/shell/page-header.tsx +0 -35
|
@@ -13,6 +13,7 @@ import { createElement, useMemo } from 'react'
|
|
|
13
13
|
import type { ElementType, ReactNode } from 'react'
|
|
14
14
|
import { Link, useMatchRoute, useRouterState } from '@tanstack/react-router'
|
|
15
15
|
import type { MobileNavConfig, NavAnchor, SidebarItem, SidebarSection } from '../nav/types'
|
|
16
|
+
import { flattenNav } from './nav'
|
|
16
17
|
import type { AnyNavGroup, AnyNavItem, NavConfig, NavItemId, NavTabId } from './nav'
|
|
17
18
|
|
|
18
19
|
export type UseNavOptions<G extends ReadonlyArray<AnyNavGroup>> = {
|
|
@@ -24,8 +25,6 @@ export type UseNavOptions<G extends ReadonlyArray<AnyNavGroup>> = {
|
|
|
24
25
|
badges?: Partial<Record<NavItemId<G>, number | ReactNode>>
|
|
25
26
|
/** Override active detection entirely. */
|
|
26
27
|
isActive?: (item: AnyNavItem) => boolean
|
|
27
|
-
/** Passed straight through into the returned `mobileNav`. */
|
|
28
|
-
moreExtra?: ReactNode
|
|
29
28
|
getScrollElement?: () => HTMLElement | null
|
|
30
29
|
}
|
|
31
30
|
|
|
@@ -57,40 +56,101 @@ export function useNav<
|
|
|
57
56
|
const badgeOf = (id: string): number | ReactNode =>
|
|
58
57
|
(badges as Record<string, number | ReactNode> | undefined)?.[id]
|
|
59
58
|
|
|
60
|
-
|
|
59
|
+
// Per-item match, exactly as before: the override wins when supplied, otherwise a router
|
|
60
|
+
// prefix match (`'/'` stays exact). What changed is that this boolean no longer BECOMES
|
|
61
|
+
// `active` on its own — it only says "on the path", and the pass below picks ONE winner
|
|
62
|
+
// across the whole definition so a parent and its child can no longer both read `active`.
|
|
63
|
+
const matchOf = (item: AnyNavItem): boolean =>
|
|
61
64
|
isActiveOverride?.(item) ??
|
|
62
65
|
Boolean(
|
|
63
66
|
matchRoute({
|
|
64
67
|
to: item.link.to,
|
|
65
68
|
...(item.link.params !== undefined && { params: item.link.params }),
|
|
66
|
-
// Prefix match by default so a parent stays lit on its children; `'/'` would then match
|
|
67
|
-
// everything, so it is always exact.
|
|
68
69
|
fuzzy: item.exact !== true && item.link.to !== '/',
|
|
69
70
|
}),
|
|
70
71
|
)
|
|
71
72
|
|
|
73
|
+
// One pass over the WHOLE flattened definition (`flattenNav` — depth-first, parent then
|
|
74
|
+
// children, the same order every other reader of a nav config walks it) rather than a
|
|
75
|
+
// per-item independent check, which is what let a parent prefix-match stay lit alongside an
|
|
76
|
+
// exact-matching child. The most SPECIFIC match (most non-empty `/`-segments in `link.to`)
|
|
77
|
+
// wins; a tie (the playground's `reports`/`components` shape, both pointing at `/components`)
|
|
78
|
+
// keeps the FIRST item in definition order, since only a strictly later, strictly longer match
|
|
79
|
+
// ever replaces the current winner below.
|
|
80
|
+
const allItems = flattenNav(config)
|
|
81
|
+
const specificity = (to: string): number => to.split('/').filter(Boolean).length
|
|
82
|
+
const matchedById = new Map<string, boolean>()
|
|
83
|
+
let winnerId: string | undefined
|
|
84
|
+
let winnerTo: string | undefined
|
|
85
|
+
for (const item of allItems) {
|
|
86
|
+
const matched = matchOf(item)
|
|
87
|
+
matchedById.set(item.id, matched)
|
|
88
|
+
if (
|
|
89
|
+
matched &&
|
|
90
|
+
(winnerTo === undefined || specificity(item.link.to) > specificity(winnerTo))
|
|
91
|
+
) {
|
|
92
|
+
winnerId = item.id
|
|
93
|
+
winnerTo = item.link.to
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// `active` is true for the winner only. `ancestor` is true for a matched item that sits
|
|
98
|
+
// strictly on the winner's PATH (`winnerTo` starts with `item.link.to + '/'`) — a same-length
|
|
99
|
+
// tie loser matches nothing here (its own `to` cannot be a strict prefix of an equally long
|
|
100
|
+
// `winnerTo`), so it reads as neither active nor an ancestor, which is the point: two
|
|
101
|
+
// destinations sharing one route are siblings, not a hierarchy.
|
|
102
|
+
const resolveState = (item: AnyNavItem): { active: boolean; ancestor: boolean } => {
|
|
103
|
+
if (item.id === winnerId) return { active: true, ancestor: false }
|
|
104
|
+
const matched = matchedById.get(item.id) ?? false
|
|
105
|
+
const ancestor = winnerTo !== undefined && matched && winnerTo.startsWith(`${item.link.to}/`)
|
|
106
|
+
return { active: false, ancestor }
|
|
107
|
+
}
|
|
108
|
+
|
|
72
109
|
const toSidebarItem = (item: AnyNavItem): SidebarItem => {
|
|
73
110
|
// One anchor per destination. Basalt renders every pixel of chrome around it (desktop row,
|
|
74
111
|
// 56px slot, 44px sheet row) and only hosts this component, so `preload`, middle-click and
|
|
75
112
|
// back/forward all keep working. `Link as ElementType` is the single cast in the whole
|
|
76
113
|
// chain: the widened `AnyNavLink['search']` is `unknown`, which `LinkComponentProps` rejects.
|
|
77
114
|
// The consumer gets zero casts, which is the point.
|
|
115
|
+
//
|
|
116
|
+
// `activeOptions: { exact: true }` is what makes `resolveState` above the ONLY authority on
|
|
117
|
+
// active, and it is not a preference — a `Link` computes its own `isActive` (default
|
|
118
|
+
// `activeOptions.exact: false`, i.e. prefix) and spreads `{ 'data-status': 'active',
|
|
119
|
+
// 'aria-current': 'page' }` LAST, after `activeProps` and after every caller prop
|
|
120
|
+
// (`@tanstack/react-router/dist/esm/link.js` — `...isActive && STATIC_ACTIVE_PROPS` is the
|
|
121
|
+
// final entry of the returned object). So `aria-current` cannot be overridden from outside;
|
|
122
|
+
// the only lever is `isActive` itself. Left at its default, the router re-derived the exact
|
|
123
|
+
// fuzzy match this resolver exists to replace, and at `/dashboard/sessions` BOTH the parent
|
|
124
|
+
// and the child anchor carried `aria-current="page"` even with basalt's own model marking one
|
|
125
|
+
// of them an ancestor — verified in Chrome, and the reason the first attempt at this fix
|
|
126
|
+
// measured as no fix at all.
|
|
127
|
+
//
|
|
128
|
+
// Exact here does NOT narrow basalt's own law. `resolveState` still matches fuzzily and still
|
|
129
|
+
// lights an item whose exact route is never visited; that item is simply named by the
|
|
130
|
+
// `aria-current` `app-sidebar.tsx` stamps itself, which survives because the router's spread
|
|
131
|
+
// is conditional on ITS `isActive` being true.
|
|
78
132
|
const Anchor: NavAnchor = (props) =>
|
|
79
|
-
createElement(Link as ElementType, {
|
|
133
|
+
createElement(Link as ElementType, {
|
|
134
|
+
...item.link,
|
|
135
|
+
activeOptions: { exact: true },
|
|
136
|
+
...props,
|
|
137
|
+
})
|
|
80
138
|
|
|
81
139
|
const badge = badgeOf(item.id)
|
|
82
140
|
const children = item.children?.map(toSidebarItem)
|
|
141
|
+
const { active, ancestor } = resolveState(item)
|
|
83
142
|
|
|
84
143
|
return {
|
|
85
144
|
key: item.id,
|
|
86
145
|
label: item.label,
|
|
87
146
|
icon: item.icon,
|
|
88
147
|
Anchor,
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
active
|
|
148
|
+
// Exactly one destination in the WHOLE definition reads `active` (`resolveState` above).
|
|
149
|
+
// Rolling that up into a mobile SLOT (any descendant active → the slot reads active) is
|
|
150
|
+
// still `projectMobileNav`'s job, which reads `children` for that. `ancestor` is the
|
|
151
|
+
// desktop-only middle state: a matched item that sits on the winner's path but isn't it.
|
|
152
|
+
active,
|
|
153
|
+
...(ancestor && { ancestor }),
|
|
94
154
|
...(item.short !== undefined && { short: item.short }),
|
|
95
155
|
...(item.mobile !== undefined && { mobile: item.mobile }),
|
|
96
156
|
...(item.disabled !== undefined && { disabled: item.disabled }),
|
|
@@ -119,7 +179,6 @@ export function useNav<
|
|
|
119
179
|
}, [config, matchRoute, href, badges, isActiveOverride])
|
|
120
180
|
|
|
121
181
|
const mobile = config.mobile
|
|
122
|
-
const moreExtra = opts?.moreExtra
|
|
123
182
|
const getScrollElement = opts?.getScrollElement
|
|
124
183
|
|
|
125
184
|
const mobileNav = useMemo<MobileNavConfig>(() => {
|
|
@@ -137,10 +196,9 @@ export function useNav<
|
|
|
137
196
|
...(mobile?.maxTabs !== undefined && { maxTabs: mobile.maxTabs }),
|
|
138
197
|
...(mobile?.menuMax !== undefined && { menuMax: mobile.menuMax }),
|
|
139
198
|
...(mobile?.moreLabel !== undefined && { moreLabel: mobile.moreLabel }),
|
|
140
|
-
...(moreExtra !== undefined && { moreExtra }),
|
|
141
199
|
...(getScrollElement !== undefined && { getScrollElement }),
|
|
142
200
|
}
|
|
143
|
-
}, [config, mobile,
|
|
201
|
+
}, [config, mobile, getScrollElement])
|
|
144
202
|
|
|
145
203
|
return { sections, mobileNav }
|
|
146
204
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Section, type SectionProps } from './section'
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Section — the tier-2 heading composer (docs/CONTROLS-SPEC.md §2.2). No border/background of its
|
|
3
|
+
* own — one shaded container level per page belongs to the cards inside a Section
|
|
4
|
+
* (`ChartCard`/`StatCard`), not to the Section itself.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* 12px from the header to the body, and the same between two body children — one rhythm, read from
|
|
9
|
+
* the stack scale (`--vx-space-stack-md`) instead of the card-inset scale
|
|
10
|
+
* (`--mantine-spacing-sm`, 13px). A Section is a page-level GROUPING, so its gaps belong to the
|
|
11
|
+
* vertical rhythm, not to the inset ladder its cards use.
|
|
12
|
+
*/
|
|
13
|
+
.root {
|
|
14
|
+
display: flex;
|
|
15
|
+
flex-direction: column;
|
|
16
|
+
gap: var(--vx-space-stack-md, 0.75rem);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.body {
|
|
20
|
+
display: flex;
|
|
21
|
+
flex-direction: column;
|
|
22
|
+
gap: var(--vx-space-stack-md, 0.75rem);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/*
|
|
26
|
+
* `summary` — the always-visible row under the header. No border, no background (the Section owns
|
|
27
|
+
* neither); it is a plain flow row, so a consumer's `Group` of stats or one line of `Text` lands
|
|
28
|
+
* where the body would have started.
|
|
29
|
+
*/
|
|
30
|
+
.summary {
|
|
31
|
+
display: flex;
|
|
32
|
+
flex-direction: column;
|
|
33
|
+
gap: var(--mantine-spacing-xs, 0.625rem);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/*
|
|
37
|
+
* The collapse chevron — a plain control, not a Mantine ActionIcon, so it sits directly in the header
|
|
38
|
+
* row (its own CtlSlot fragment would nest a theme provider around one glyph). It is sized to the
|
|
39
|
+
* `ctl` tier BY HAND for exactly that reason: a 12px glyph in a 20px box was a different height from
|
|
40
|
+
* every other control in the same header, which is what made the section header read as ragged. 30px
|
|
41
|
+
* square, glyph centred, no padding — the box IS the tier.
|
|
42
|
+
*/
|
|
43
|
+
.chevron {
|
|
44
|
+
/* `relative` is the anchor for the C15 hit-area overlay in the `< sm` block at the bottom. */
|
|
45
|
+
position: relative;
|
|
46
|
+
display: inline-flex;
|
|
47
|
+
flex: none;
|
|
48
|
+
align-items: center;
|
|
49
|
+
justify-content: center;
|
|
50
|
+
/* `--vx-space-control-height-ctl` is a bare rem — the theme's `ctlSizeVars` (`theme/index.ts`)
|
|
51
|
+
* reconstructs every OTHER ctl-tier box as `calc(<rem> * var(--mantine-scale))` so it tracks
|
|
52
|
+
* Mantine's `theme.scale`; read raw here, this box used to stay fixed while a scaled Button/Input
|
|
53
|
+
* beside it grew or shrank, reintroducing the exact raggedness this control was sized to fix. */
|
|
54
|
+
width: calc(var(--vx-space-control-height-ctl) * var(--mantine-scale, 1));
|
|
55
|
+
height: calc(var(--vx-space-control-height-ctl) * var(--mantine-scale, 1));
|
|
56
|
+
appearance: none;
|
|
57
|
+
margin: 0;
|
|
58
|
+
padding: 0;
|
|
59
|
+
border: none;
|
|
60
|
+
background: none;
|
|
61
|
+
border-radius: var(--vx-radius-ctrl);
|
|
62
|
+
color: var(--vx-muted);
|
|
63
|
+
cursor: pointer;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.chevron:hover,
|
|
67
|
+
.chevron:focus-visible {
|
|
68
|
+
color: var(--vx-ink);
|
|
69
|
+
background: var(--vx-surface-subtle);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.chevron svg {
|
|
73
|
+
transition: transform 150ms ease;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.chevron[data-open='false'] svg {
|
|
77
|
+
transform: rotate(-90deg);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/*
|
|
81
|
+
* Law C15 — the chevron is a 12px glyph in a 20px box, which is a fine mouse target and an
|
|
82
|
+
* unusable thumb one. Below `sm` an `::after` overlay lifts the HIT area to
|
|
83
|
+
* `--vx-space-touch-control-height` (36px at level 0) while the visible glyph stays put — the same
|
|
84
|
+
* overlay `shell/page-bar.module.css` draws for the bar's own controls. `overflow: visible` is part
|
|
85
|
+
* of the fix, not tidying: an overlay that extends past the box is clipped by any `overflow:
|
|
86
|
+
* hidden` on the host (which is what made the identical overlay a no-op on Mantine's Button roots).
|
|
87
|
+
*/
|
|
88
|
+
@media (max-width: 47.99375em) {
|
|
89
|
+
.chevron {
|
|
90
|
+
overflow: visible;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.chevron::after {
|
|
94
|
+
content: '';
|
|
95
|
+
position: absolute;
|
|
96
|
+
top: 50%;
|
|
97
|
+
left: 50%;
|
|
98
|
+
translate: -50% -50%;
|
|
99
|
+
width: 100%;
|
|
100
|
+
height: 100%;
|
|
101
|
+
min-width: var(--vx-space-touch-control-height, 2.25rem);
|
|
102
|
+
min-height: var(--vx-space-touch-control-height, 2.25rem);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed CSS-module declaration (the S4 CSS-modules path). Named string props — not an index
|
|
3
|
+
* signature — so dot access stays verbatim under the package's strict flags.
|
|
4
|
+
*/
|
|
5
|
+
declare const classes: {
|
|
6
|
+
readonly root: string
|
|
7
|
+
readonly body: string
|
|
8
|
+
readonly summary: string
|
|
9
|
+
readonly chevron: string
|
|
10
|
+
}
|
|
11
|
+
export default classes
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Section` — the tier-2 heading composer (docs/CONTROLS-SPEC.md §2.2): the collapse contract
|
|
3
|
+
* (persisted vs local, header stays drawn, body unmounts, tabs hide), the scroll-anchor style, and
|
|
4
|
+
* the ≤3-actions dev warning (C6).
|
|
5
|
+
*/
|
|
6
|
+
import { MantineProvider } from '@mantine/core'
|
|
7
|
+
import { fireEvent, render, screen } from '@testing-library/react'
|
|
8
|
+
import { beforeEach, describe, expect, spyOn, test } from 'bun:test'
|
|
9
|
+
import type { ReactElement } from 'react'
|
|
10
|
+
import { Section } from './section'
|
|
11
|
+
|
|
12
|
+
function renderWith(node: ReactElement) {
|
|
13
|
+
return render(<MantineProvider>{node}</MantineProvider>)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
window.localStorage.clear()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
describe('Section composes WidgetHeader at the section tier', () => {
|
|
21
|
+
test('renders an h2 carrying the title', () => {
|
|
22
|
+
renderWith(
|
|
23
|
+
<Section title="Revenue">
|
|
24
|
+
<div>body</div>
|
|
25
|
+
</Section>,
|
|
26
|
+
)
|
|
27
|
+
expect(screen.getByRole('heading', { level: 2, name: 'Revenue' })).toBeDefined()
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
describe('collapsible', () => {
|
|
32
|
+
test('the header stays drawn when closed; only the body unmounts', () => {
|
|
33
|
+
renderWith(
|
|
34
|
+
<Section title="Usage" collapsible>
|
|
35
|
+
<div>body content</div>
|
|
36
|
+
</Section>,
|
|
37
|
+
)
|
|
38
|
+
expect(screen.getByRole('heading', { level: 2, name: 'Usage' })).toBeDefined()
|
|
39
|
+
expect(screen.getByText('body content')).toBeDefined()
|
|
40
|
+
|
|
41
|
+
fireEvent.click(screen.getByRole('button', { name: 'Collapse section' }))
|
|
42
|
+
|
|
43
|
+
expect(screen.getByRole('heading', { level: 2, name: 'Usage' })).toBeDefined()
|
|
44
|
+
expect(screen.queryByText('body content')).toBeNull()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('tabs hide while collapsed', () => {
|
|
48
|
+
renderWith(
|
|
49
|
+
<Section title="Usage" collapsible tabs={<div>the tabs</div>}>
|
|
50
|
+
<div>body</div>
|
|
51
|
+
</Section>,
|
|
52
|
+
)
|
|
53
|
+
expect(screen.getByText('the tabs')).toBeDefined()
|
|
54
|
+
fireEvent.click(screen.getByRole('button', { name: 'Collapse section' }))
|
|
55
|
+
expect(screen.queryByText('the tabs')).toBeNull()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('an unpersisted fold (no persistKey) does not touch localStorage', () => {
|
|
59
|
+
renderWith(
|
|
60
|
+
<Section title="Usage" collapsible>
|
|
61
|
+
<div>body</div>
|
|
62
|
+
</Section>,
|
|
63
|
+
)
|
|
64
|
+
fireEvent.click(screen.getByRole('button', { name: 'Collapse section' }))
|
|
65
|
+
expect(window.localStorage.length).toBe(0)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('persistKey persists the fold state under basalt:section:<key>', () => {
|
|
69
|
+
renderWith(
|
|
70
|
+
<Section title="Usage" collapsible persistKey="usage">
|
|
71
|
+
<div>body</div>
|
|
72
|
+
</Section>,
|
|
73
|
+
)
|
|
74
|
+
fireEvent.click(screen.getByRole('button', { name: 'Collapse section' }))
|
|
75
|
+
|
|
76
|
+
const raw = window.localStorage.getItem('basalt:section:usage')
|
|
77
|
+
expect(raw).not.toBeNull()
|
|
78
|
+
expect(JSON.parse(raw ?? '{}')).toMatchObject({ value: false })
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
describe('id — the scroll-anchor contract', () => {
|
|
83
|
+
test('sets the id and a scrollMarginTop clearing both sticky bars', () => {
|
|
84
|
+
const { container } = renderWith(
|
|
85
|
+
<Section id="usage" title="Usage">
|
|
86
|
+
<div>body</div>
|
|
87
|
+
</Section>,
|
|
88
|
+
)
|
|
89
|
+
const root = container.querySelector('#usage')
|
|
90
|
+
expect(root).not.toBeNull()
|
|
91
|
+
const style = root?.getAttribute('style') ?? ''
|
|
92
|
+
expect(style).toContain('scroll-margin-top')
|
|
93
|
+
expect(style).toContain('--app-shell-header-height')
|
|
94
|
+
expect(style).toContain('--basalt-page-bar-h')
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('omitting id sets neither an id nor the anchor style', () => {
|
|
98
|
+
const { container } = renderWith(
|
|
99
|
+
<Section title="Usage">
|
|
100
|
+
<div>body</div>
|
|
101
|
+
</Section>,
|
|
102
|
+
)
|
|
103
|
+
const root = container.firstElementChild
|
|
104
|
+
expect(root?.id).toBe('')
|
|
105
|
+
expect(root?.getAttribute('style')).toBeNull()
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
// Relies on the default test-runner NODE_ENV being non-production (`isDev()` true), matching
|
|
110
|
+
// `data/data-table.test.tsx`'s manualPagination dev-throw tests — no explicit env override needed.
|
|
111
|
+
describe('≤3 actions — the dev-only budget warning (C6)', () => {
|
|
112
|
+
// `Children.count` (like every `Children.*` helper) does not recurse into a Fragment passed as
|
|
113
|
+
// a single node — an ARRAY of actions is what the count actually sees, same shape a consumer
|
|
114
|
+
// building the list programmatically would pass.
|
|
115
|
+
test('4 actions warns', () => {
|
|
116
|
+
const warn = spyOn(console, 'warn').mockImplementation(() => {})
|
|
117
|
+
renderWith(
|
|
118
|
+
<Section
|
|
119
|
+
title="Usage"
|
|
120
|
+
actions={[1, 2, 3, 4].map((n) => (
|
|
121
|
+
<button key={n} type="button">
|
|
122
|
+
{n}
|
|
123
|
+
</button>
|
|
124
|
+
))}
|
|
125
|
+
>
|
|
126
|
+
<div>body</div>
|
|
127
|
+
</Section>,
|
|
128
|
+
)
|
|
129
|
+
expect(warn).toHaveBeenCalledTimes(1)
|
|
130
|
+
expect(String(warn.mock.calls[0]?.[0])).toContain('≤3 budget')
|
|
131
|
+
warn.mockRestore()
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test('3 actions does not warn', () => {
|
|
135
|
+
const warn = spyOn(console, 'warn').mockImplementation(() => {})
|
|
136
|
+
renderWith(
|
|
137
|
+
<Section
|
|
138
|
+
title="Usage"
|
|
139
|
+
actions={[1, 2, 3].map((n) => (
|
|
140
|
+
<button key={n} type="button">
|
|
141
|
+
{n}
|
|
142
|
+
</button>
|
|
143
|
+
))}
|
|
144
|
+
>
|
|
145
|
+
<div>body</div>
|
|
146
|
+
</Section>,
|
|
147
|
+
)
|
|
148
|
+
expect(warn).not.toHaveBeenCalled()
|
|
149
|
+
warn.mockRestore()
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
describe('defaultOpen', () => {
|
|
154
|
+
test('defaultOpen={false} opens collapsed, and the chevron still expands it', () => {
|
|
155
|
+
renderWith(
|
|
156
|
+
<Section title="Usage" collapsible defaultOpen={false}>
|
|
157
|
+
<div>body content</div>
|
|
158
|
+
</Section>,
|
|
159
|
+
)
|
|
160
|
+
expect(screen.queryByText('body content')).toBeNull()
|
|
161
|
+
|
|
162
|
+
fireEvent.click(screen.getByRole('button', { name: 'Expand section' }))
|
|
163
|
+
expect(screen.getByText('body content')).toBeDefined()
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
test('a persisted value outranks it — a section the reader opened stays open', () => {
|
|
167
|
+
window.localStorage.setItem('basalt:section:usage', JSON.stringify({ v: 1, value: true }))
|
|
168
|
+
renderWith(
|
|
169
|
+
<Section title="Usage" collapsible persistKey="usage" defaultOpen={false}>
|
|
170
|
+
<div>body content</div>
|
|
171
|
+
</Section>,
|
|
172
|
+
)
|
|
173
|
+
expect(screen.getByText('body content')).toBeDefined()
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
describe('summary — visible collapsed or not', () => {
|
|
178
|
+
test('a collapsed section still states its headline figures', () => {
|
|
179
|
+
renderWith(
|
|
180
|
+
<Section title="Usage" collapsible summary={<span>48,204 requests</span>}>
|
|
181
|
+
<div>body content</div>
|
|
182
|
+
</Section>,
|
|
183
|
+
)
|
|
184
|
+
expect(screen.getByText('48,204 requests')).toBeDefined()
|
|
185
|
+
|
|
186
|
+
fireEvent.click(screen.getByRole('button', { name: 'Collapse section' }))
|
|
187
|
+
|
|
188
|
+
expect(screen.queryByText('body content')).toBeNull()
|
|
189
|
+
expect(screen.getByText('48,204 requests')).toBeDefined()
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
test('it renders under the header, above where the body starts', () => {
|
|
193
|
+
renderWith(
|
|
194
|
+
<Section title="Usage" summary={<span>48,204 requests</span>}>
|
|
195
|
+
<div>body content</div>
|
|
196
|
+
</Section>,
|
|
197
|
+
)
|
|
198
|
+
const summary = screen.getByText('48,204 requests').parentElement
|
|
199
|
+
expect(
|
|
200
|
+
summary?.previousElementSibling?.contains(screen.getByRole('heading', { level: 2 })),
|
|
201
|
+
).toBe(true)
|
|
202
|
+
expect(summary?.nextElementSibling?.contains(screen.getByText('body content'))).toBe(true)
|
|
203
|
+
})
|
|
204
|
+
})
|