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
package/src/data/data-table.tsx
CHANGED
|
@@ -21,27 +21,18 @@
|
|
|
21
21
|
* <BasaltDataTable data={rows} columns={columns} />
|
|
22
22
|
*
|
|
23
23
|
* @example
|
|
24
|
-
* // With the toolbar + pagination bar:
|
|
24
|
+
* // With the title, the toolbar + pagination bar:
|
|
25
25
|
* <BasaltDataTable
|
|
26
|
+
* title="Top pages"
|
|
26
27
|
* data={rows}
|
|
27
28
|
* columns={columns}
|
|
28
29
|
* enableGlobalFilter
|
|
29
30
|
* facets={[{ columnId: 'department', label: 'Department', options: departmentOptions }]}
|
|
30
|
-
*
|
|
31
|
+
* actions={<Button>Export</Button>}
|
|
31
32
|
* enablePagination
|
|
32
33
|
* />
|
|
33
34
|
*/
|
|
34
|
-
import {
|
|
35
|
-
Box,
|
|
36
|
-
Group,
|
|
37
|
-
MultiSelect,
|
|
38
|
-
Pagination,
|
|
39
|
-
Select,
|
|
40
|
-
Skeleton,
|
|
41
|
-
Table,
|
|
42
|
-
Text,
|
|
43
|
-
TextInput,
|
|
44
|
-
} from '@mantine/core'
|
|
35
|
+
import { Box, Group, Pagination, Select, Skeleton, Table, Text, TextInput } from '@mantine/core'
|
|
45
36
|
import type { MantineSpacing } from '@mantine/core'
|
|
46
37
|
import type {
|
|
47
38
|
Column,
|
|
@@ -65,7 +56,17 @@ import {
|
|
|
65
56
|
} from '@tanstack/react-table'
|
|
66
57
|
import type { CSSProperties, ReactNode } from 'react'
|
|
67
58
|
import { useCallback, useMemo, useState } from 'react'
|
|
59
|
+
import { FilterSet } from '../controls'
|
|
60
|
+
// `EnumFilter` is deliberately NOT on the `./controls` barrel (`docs/CONTROLS-SPEC.md` §3 —
|
|
61
|
+
// reaching for it directly is hand-rolling a filter, `basalt/hand-rolled-filter`); a facet column
|
|
62
|
+
// is the one place inside the framework itself that legitimately builds a `FieldHandle` over
|
|
63
|
+
// something that is not a store field, so it reaches past the barrel on purpose.
|
|
64
|
+
import { EnumFilter } from '../controls/enum-filter'
|
|
65
|
+
import { MultiSelectFilter } from '../controls/multi-select-filter'
|
|
66
|
+
import { CtlSlot } from '../theme'
|
|
68
67
|
import { alpha, VX } from '../tokens'
|
|
68
|
+
import type { EnumField, FieldHandle, MultiField } from '../state'
|
|
69
|
+
import { WidgetHeader } from '../widget-header'
|
|
69
70
|
|
|
70
71
|
// ── Column alignment ──────────────────────────────────────────────────────────
|
|
71
72
|
|
|
@@ -113,17 +114,17 @@ function resolveAlign<T>(column: Column<T, unknown>): DataTableAlign | undefined
|
|
|
113
114
|
|
|
114
115
|
// ── Facets ────────────────────────────────────────────────────────────────────
|
|
115
116
|
|
|
116
|
-
/** A single selectable option inside a {@link DataTableFacet}'s
|
|
117
|
+
/** A single selectable option inside a {@link DataTableFacet}'s `EnumFilter`/`MultiSelectFilter` pill. */
|
|
117
118
|
export type DataTableFacetOption = {
|
|
118
119
|
/** The raw filter value, compared against the column's stringified cell value. */
|
|
119
120
|
value: string
|
|
120
|
-
/** Label shown in the
|
|
121
|
+
/** Label shown in the pill's popover. */
|
|
121
122
|
label: string
|
|
122
123
|
}
|
|
123
124
|
|
|
124
125
|
/**
|
|
125
|
-
* Declares one faceted column filter, rendered as
|
|
126
|
-
* `multiple` is set) inside the toolbar.
|
|
126
|
+
* Declares one faceted column filter, rendered as an `EnumFilter` pill (or `MultiSelectFilter`
|
|
127
|
+
* when `multiple` is set) inside a `FilterSet` in the toolbar (`docs/CONTROLS-SPEC.md` §3).
|
|
127
128
|
*
|
|
128
129
|
* @example
|
|
129
130
|
* const facets: DataTableFacet[] = [
|
|
@@ -135,14 +136,89 @@ export type DataTableFacetOption = {
|
|
|
135
136
|
export type DataTableFacet = {
|
|
136
137
|
/** The TanStack column id — the column's manual `id`, or its `accessorKey` string. */
|
|
137
138
|
columnId: string
|
|
138
|
-
/**
|
|
139
|
+
/** The pill's label — read at rest (single-select) and as the `Filters`/count fallback (multi). */
|
|
139
140
|
label: string
|
|
140
141
|
/** Selectable options for this facet. */
|
|
141
142
|
options: DataTableFacetOption[]
|
|
142
|
-
/** Render a
|
|
143
|
+
/** Render a `MultiSelectFilter` (any-of match) instead of an `EnumFilter` (exact match). @default false */
|
|
143
144
|
multiple?: boolean
|
|
144
145
|
}
|
|
145
146
|
|
|
147
|
+
/**
|
|
148
|
+
* The "no filter" member of a single-select facet's `FieldHandle`. A closed enum field always has
|
|
149
|
+
* a real member as its fallback (C4) — there is no "unset" — so a facet, whose underlying
|
|
150
|
+
* `column.getFilterValue()` genuinely can be `undefined`, needs one synthetic option to stand for
|
|
151
|
+
* that state. Rendered as the enum's first radio; picking it clears the column filter, same as every
|
|
152
|
+
* other value change.
|
|
153
|
+
*
|
|
154
|
+
* Its label is `Any <facet>`, and it used to be a bare `All`. The pill now reads the SELECTED
|
|
155
|
+
* option's label at every value including the fallback (`controls/enum-filter.tsx` — a bar reading
|
|
156
|
+
* `Compare` over a field holding `'previous'` is the bug that law fixes), and a toolbar with three
|
|
157
|
+
* facets would then have read `All` three times. `Any project` is the one label that works in both
|
|
158
|
+
* places at once: it is a legible popover row for "no constraint", and a legible pill readout of the
|
|
159
|
+
* state the filter is actually in. Derived from `facet.label`, so no facet has to declare it.
|
|
160
|
+
*/
|
|
161
|
+
const FACET_ALL_VALUE = '__basalt_facet_all__'
|
|
162
|
+
|
|
163
|
+
/** The synthetic row's label — see {@link FACET_ALL_VALUE}. */
|
|
164
|
+
function facetAllLabel(facet: DataTableFacet): string {
|
|
165
|
+
return `Any ${facet.label.toLowerCase()}`
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Presents a single-select facet column as a `FieldHandle<EnumField<string>>` so it can render
|
|
170
|
+
* through the shared `EnumFilter` pill instead of a hand-rolled `Select` (`basalt/hand-rolled-filter`).
|
|
171
|
+
* `use()` reads/writes `column.getFilterValue()`/`setFilterValue()` directly — no internal
|
|
172
|
+
* subscription needed, since the enclosing `BasaltDataTable` already re-renders on every
|
|
173
|
+
* `columnFilters` change and this handle is rebuilt fresh each render alongside its column.
|
|
174
|
+
*/
|
|
175
|
+
function facetEnumHandle<T>(
|
|
176
|
+
column: Column<T, unknown>,
|
|
177
|
+
facet: DataTableFacet,
|
|
178
|
+
): FieldHandle<EnumField<string>> {
|
|
179
|
+
return {
|
|
180
|
+
kind: 'enum',
|
|
181
|
+
fallback: FACET_ALL_VALUE,
|
|
182
|
+
options: [{ value: FACET_ALL_VALUE, label: facetAllLabel(facet) }, ...facet.options],
|
|
183
|
+
use: () => {
|
|
184
|
+
const current = (column.getFilterValue() as string | undefined) ?? FACET_ALL_VALUE
|
|
185
|
+
return [
|
|
186
|
+
current,
|
|
187
|
+
(next: string) => {
|
|
188
|
+
column.setFilterValue(next === FACET_ALL_VALUE ? undefined : next)
|
|
189
|
+
},
|
|
190
|
+
] as const
|
|
191
|
+
},
|
|
192
|
+
isDefault: (value) => value === FACET_ALL_VALUE,
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Presents a multi-select facet column as a `FieldHandle<MultiField<string>>`. Unlike the
|
|
198
|
+
* single-select case, `MultiField`'s own fallback — an empty selection — already means "no
|
|
199
|
+
* constraint", so no synthetic option is needed here.
|
|
200
|
+
*/
|
|
201
|
+
function facetMultiHandle<T>(
|
|
202
|
+
column: Column<T, unknown>,
|
|
203
|
+
facet: DataTableFacet,
|
|
204
|
+
): FieldHandle<MultiField<string>> {
|
|
205
|
+
return {
|
|
206
|
+
kind: 'multi',
|
|
207
|
+
fallback: [],
|
|
208
|
+
options: facet.options,
|
|
209
|
+
use: () => {
|
|
210
|
+
const current = (column.getFilterValue() as readonly string[] | undefined) ?? []
|
|
211
|
+
return [
|
|
212
|
+
current,
|
|
213
|
+
(next: readonly string[]) => {
|
|
214
|
+
column.setFilterValue(next.length > 0 ? [...next] : undefined)
|
|
215
|
+
},
|
|
216
|
+
] as const
|
|
217
|
+
},
|
|
218
|
+
isDefault: (value) => value.length === 0,
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
146
222
|
// ── Props ─────────────────────────────────────────────────────────────────────
|
|
147
223
|
|
|
148
224
|
/**
|
|
@@ -159,6 +235,19 @@ export type DataTableFacet = {
|
|
|
159
235
|
* }
|
|
160
236
|
*/
|
|
161
237
|
export type BasaltDataTableProps<T> = {
|
|
238
|
+
// ── Header (WidgetHeader, docs/CONTROLS-SPEC.md §2.2) ──────────────────────────
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Optional heading rendered above the toolbar via `WidgetHeader tier="widget"`. `count` always
|
|
242
|
+
* reads `table.getRowCount()` (C11) — never a raw `data.length` — so it tracks the row model
|
|
243
|
+
* (post-filter/-pagination) rather than the unfiltered input.
|
|
244
|
+
*/
|
|
245
|
+
title?: string
|
|
246
|
+
/** Optional leading icon, forwarded to `WidgetHeader`. */
|
|
247
|
+
icon?: ReactNode
|
|
248
|
+
/** Optional muted line rendered below the title row. */
|
|
249
|
+
subtitle?: string
|
|
250
|
+
|
|
162
251
|
/** Row data array. */
|
|
163
252
|
data: T[]
|
|
164
253
|
/**
|
|
@@ -262,9 +351,9 @@ export type BasaltDataTableProps<T> = {
|
|
|
262
351
|
/** Called whenever the internal global filter value changes. */
|
|
263
352
|
onGlobalFilterChange?: (value: string) => void
|
|
264
353
|
/**
|
|
265
|
-
* Faceted column filters, rendered in the toolbar as
|
|
266
|
-
* to TanStack's per-column `columnFilters` state. The toolbar
|
|
267
|
-
* non-empty, `enableGlobalFilter` is set, or `
|
|
354
|
+
* Faceted column filters, rendered in the toolbar as `EnumFilter`/`MultiSelectFilter` pills
|
|
355
|
+
* inside a `FilterSet`, wired to TanStack's per-column `columnFilters` state. The toolbar
|
|
356
|
+
* renders whenever this array is non-empty, `enableGlobalFilter` is set, or `actions` is passed.
|
|
268
357
|
* @example
|
|
269
358
|
* facets={[{ columnId: 'department', label: 'Department', options: departmentOptions }]}
|
|
270
359
|
*/
|
|
@@ -283,9 +372,10 @@ export type BasaltDataTableProps<T> = {
|
|
|
283
372
|
manualFiltering?: boolean
|
|
284
373
|
/**
|
|
285
374
|
* Right-aligned toolbar slot (e.g. an "Export" button). Renders the toolbar row even when no
|
|
286
|
-
* search input or facets are configured.
|
|
375
|
+
* search input or facets are configured. Renamed from `toolbarActions` — the toolbar (search +
|
|
376
|
+
* facets + this slot) is wrapped in `CtlSlot` (C1/C5), so its controls resolve to the `ctl` tier.
|
|
287
377
|
*/
|
|
288
|
-
|
|
378
|
+
actions?: ReactNode
|
|
289
379
|
|
|
290
380
|
// ── Pagination ────────────────────────────────────────────────────────────────
|
|
291
381
|
|
|
@@ -389,9 +479,20 @@ export type BasaltDataTableProps<T> = {
|
|
|
389
479
|
/** Row separators. Forwarded to Mantine `Table`. @default true (Mantine's own default) */
|
|
390
480
|
withRowBorders?: boolean
|
|
391
481
|
/**
|
|
392
|
-
* Outer table border
|
|
393
|
-
*
|
|
394
|
-
*
|
|
482
|
+
* Outer table border — a full box around the table.
|
|
483
|
+
*
|
|
484
|
+
* **`false` by default since this minor** (it was `true`). Three separate faults, all visible on
|
|
485
|
+
* the playground's `Controls (mobile)` page at 390px: its TOP edge sat directly under the table's
|
|
486
|
+
* own `WidgetHeader`, separating a header from its own content; its BOTTOM edge landed on the last
|
|
487
|
+
* row's own hairline, two rules at zero distance; and its left/right edges carried no information
|
|
488
|
+
* at all on a table that fills the page (`docs/DESIGN-SPEC.md` §8 — a border is a layout divider,
|
|
489
|
+
* and there is nothing here to divide). The head-row rule and the between-row rules stay, so the
|
|
490
|
+
* table still reads as a table.
|
|
491
|
+
*
|
|
492
|
+
* Pass `true` for a table that genuinely needs a frame — a small table floating in whitespace
|
|
493
|
+
* beside other content, where the box IS the grouping.
|
|
494
|
+
*
|
|
495
|
+
* @default false
|
|
395
496
|
*/
|
|
396
497
|
withTableBorder?: boolean
|
|
397
498
|
}
|
|
@@ -428,6 +529,23 @@ const RANGE_LABEL_STYLE: CSSProperties = {
|
|
|
428
529
|
|
|
429
530
|
const DEFAULT_PAGE_SIZE_OPTIONS = [10, 25, 50, 100]
|
|
430
531
|
|
|
532
|
+
/**
|
|
533
|
+
* The header row's two halves. The lead SHRINKS and the toolbar does not — a title can ellipsize, a
|
|
534
|
+
* 220px search field with a facet pill beside it cannot, and when both were shrinkable flex split
|
|
535
|
+
* the overflow between them and clipped the search's placeholder mid-word.
|
|
536
|
+
*/
|
|
537
|
+
const TABLE_HEADER_LEAD_STYLE: CSSProperties = { flex: '1 1 auto', minWidth: 0 }
|
|
538
|
+
const TABLE_TOOLBAR_STYLE: CSSProperties = { flex: '0 0 auto' }
|
|
539
|
+
/**
|
|
540
|
+
* The toolbar search's width. Mantine's Input grows to its container, which in a `justify:
|
|
541
|
+
* space-between` row means "whatever is left" — so the field was 600px on a wide table and 180px on a
|
|
542
|
+
* narrow one, for the same one-word query. A stated width also makes the toolbar's own minimum width
|
|
543
|
+
* knowable, which is what lets it wrap under the title below `sm` with no media query.
|
|
544
|
+
*/
|
|
545
|
+
const SEARCH_WIDTH = 220
|
|
546
|
+
/** The rows-per-page Select's width — see `SEARCH_WIDTH` for why a toolbar field states one. */
|
|
547
|
+
const PAGE_SIZE_WIDTH = 116
|
|
548
|
+
|
|
431
549
|
// ── Column pinning helpers ────────────────────────────────────────────────────
|
|
432
550
|
|
|
433
551
|
// Merges the left/center/right header-group triad TanStack exposes for pinning into the single
|
|
@@ -597,6 +715,9 @@ function enforceManualPaginationContract(breaches: ManualPaginationBreach[]): vo
|
|
|
597
715
|
* />
|
|
598
716
|
*/
|
|
599
717
|
export function BasaltDataTable<T>({
|
|
718
|
+
title,
|
|
719
|
+
icon,
|
|
720
|
+
subtitle,
|
|
600
721
|
data,
|
|
601
722
|
columns,
|
|
602
723
|
enableSorting = true,
|
|
@@ -616,7 +737,7 @@ export function BasaltDataTable<T>({
|
|
|
616
737
|
facets,
|
|
617
738
|
onColumnFiltersChange,
|
|
618
739
|
manualFiltering = false,
|
|
619
|
-
|
|
740
|
+
actions,
|
|
620
741
|
enablePagination = false,
|
|
621
742
|
pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS,
|
|
622
743
|
initialPagination,
|
|
@@ -633,7 +754,7 @@ export function BasaltDataTable<T>({
|
|
|
633
754
|
verticalSpacing,
|
|
634
755
|
horizontalSpacing,
|
|
635
756
|
withRowBorders,
|
|
636
|
-
withTableBorder =
|
|
757
|
+
withTableBorder = false,
|
|
637
758
|
}: BasaltDataTableProps<T>) {
|
|
638
759
|
const [sorting, setSorting] = useState<SortingState>(initialSorting ?? [])
|
|
639
760
|
const [globalFilter, setGlobalFilter] = useState(initialGlobalFilter ?? '')
|
|
@@ -773,7 +894,7 @@ export function BasaltDataTable<T>({
|
|
|
773
894
|
|
|
774
895
|
const showSearch = enableGlobalFilter && filteringEnabled
|
|
775
896
|
const showFacets = hasFacets && filteringEnabled
|
|
776
|
-
const showToolbar = showSearch || showFacets || Boolean(
|
|
897
|
+
const showToolbar = showSearch || showFacets || Boolean(actions)
|
|
777
898
|
const headerGroups = enablePinning ? getOrderedHeaderGroups(table) : table.getHeaderGroups()
|
|
778
899
|
// The rows actually rendered, not `data` — a filter or a page index that matches nothing leaves
|
|
779
900
|
// `data` non-empty while the body has nothing in it, and a blank body reads as a broken table.
|
|
@@ -906,63 +1027,81 @@ export function BasaltDataTable<T>({
|
|
|
906
1027
|
const scrolls = maxHeight !== undefined || minWidth !== undefined
|
|
907
1028
|
|
|
908
1029
|
const paginationState = table.getState().pagination
|
|
1030
|
+
// The CURRENT page size is always an option, even when it is not in `pageSizeOptions`. Mantine's
|
|
1031
|
+
// Select renders EMPTY when its `value` matches no row, so a table opened at
|
|
1032
|
+
// `initialPagination.pageSize: 5` against the default `[10, 25, 50, 100]` showed a blank
|
|
1033
|
+
// rows-per-page box — a control stating nothing, next to a range label stating `1–5`.
|
|
1034
|
+
const pageSizeRows = [...new Set([...pageSizeOptions, paginationState.pageSize])]
|
|
1035
|
+
.sort((a, b) => a - b)
|
|
1036
|
+
.map((size) => ({ value: String(size), label: `${size} / page` }))
|
|
909
1037
|
const total = table.getRowCount()
|
|
910
1038
|
const rangeStart = total === 0 ? 0 : paginationState.pageIndex * paginationState.pageSize + 1
|
|
911
1039
|
const rangeEnd = Math.min((paginationState.pageIndex + 1) * paginationState.pageSize, total)
|
|
912
1040
|
|
|
913
1041
|
return (
|
|
914
1042
|
<>
|
|
915
|
-
{
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
1043
|
+
{/*
|
|
1044
|
+
* ONE header row: the `WidgetHeader` (title · count) on the left, the toolbar right-aligned in
|
|
1045
|
+
* the SAME row. It was two stacked rows with `mb="xs"` on each, so a titled table with search
|
|
1046
|
+
* spent title → 11px → search → 11px → thead before a single datum appeared, and the toolbar
|
|
1047
|
+
* read as page furniture rather than as this table's controls.
|
|
1048
|
+
*
|
|
1049
|
+
* `wrap="wrap"` is the below-`sm` behaviour and needs no media query: the toolbar's minimum
|
|
1050
|
+
* width (a 220px search plus its pills) exceeds what is left beside a title on a phone, so flex
|
|
1051
|
+
* wraps it under the title on its own. `align="center"` puts the 30px toolbar controls on the
|
|
1052
|
+
* title's optical line rather than on its baseline.
|
|
1053
|
+
*/}
|
|
1054
|
+
{(title !== undefined || showToolbar) && (
|
|
1055
|
+
<Group justify="space-between" align="center" wrap="wrap" gap="xs" mb="xs">
|
|
1056
|
+
{title !== undefined ? (
|
|
1057
|
+
<Box style={TABLE_HEADER_LEAD_STYLE}>
|
|
1058
|
+
<WidgetHeader
|
|
1059
|
+
tier="widget"
|
|
1060
|
+
title={title}
|
|
1061
|
+
{...(icon !== undefined && { icon })}
|
|
1062
|
+
{...(subtitle !== undefined && { subtitle })}
|
|
1063
|
+
count={table.getRowCount()}
|
|
927
1064
|
/>
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
return (
|
|
936
|
-
<MultiSelect
|
|
937
|
-
key={facet.columnId}
|
|
938
|
-
size="xs"
|
|
939
|
-
radius="md"
|
|
940
|
-
placeholder={facet.label}
|
|
941
|
-
data={facet.options}
|
|
942
|
-
value={value}
|
|
943
|
-
onChange={(next) => column.setFilterValue(next.length > 0 ? next : undefined)}
|
|
944
|
-
clearable
|
|
945
|
-
w={200}
|
|
946
|
-
/>
|
|
947
|
-
)
|
|
948
|
-
}
|
|
949
|
-
const value = (column.getFilterValue() as string | undefined) ?? null
|
|
950
|
-
return (
|
|
951
|
-
<Select
|
|
952
|
-
key={facet.columnId}
|
|
953
|
-
size="xs"
|
|
1065
|
+
</Box>
|
|
1066
|
+
) : null}
|
|
1067
|
+
{showToolbar && (
|
|
1068
|
+
<CtlSlot>
|
|
1069
|
+
<Group gap="xs" wrap="wrap" align="center" style={TABLE_TOOLBAR_STYLE}>
|
|
1070
|
+
{showSearch && (
|
|
1071
|
+
<TextInput
|
|
954
1072
|
radius="md"
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
w={180}
|
|
1073
|
+
w={SEARCH_WIDTH}
|
|
1074
|
+
placeholder={globalFilterPlaceholder}
|
|
1075
|
+
leftSection={searchIcon}
|
|
1076
|
+
value={globalFilter}
|
|
1077
|
+
onChange={(event) => handleGlobalFilterChange(event.currentTarget.value)}
|
|
961
1078
|
/>
|
|
962
|
-
)
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1079
|
+
)}
|
|
1080
|
+
{showFacets && (
|
|
1081
|
+
<FilterSet>
|
|
1082
|
+
{facets?.map((facet) => {
|
|
1083
|
+
const column = table.getColumn(facet.columnId)
|
|
1084
|
+
if (!column) return null
|
|
1085
|
+
return facet.multiple ? (
|
|
1086
|
+
<MultiSelectFilter
|
|
1087
|
+
key={facet.columnId}
|
|
1088
|
+
field={facetMultiHandle(column, facet)}
|
|
1089
|
+
label={facet.label}
|
|
1090
|
+
/>
|
|
1091
|
+
) : (
|
|
1092
|
+
<EnumFilter
|
|
1093
|
+
key={facet.columnId}
|
|
1094
|
+
field={facetEnumHandle(column, facet)}
|
|
1095
|
+
label={facet.label}
|
|
1096
|
+
/>
|
|
1097
|
+
)
|
|
1098
|
+
})}
|
|
1099
|
+
</FilterSet>
|
|
1100
|
+
)}
|
|
1101
|
+
{actions}
|
|
1102
|
+
</Group>
|
|
1103
|
+
</CtlSlot>
|
|
1104
|
+
)}
|
|
966
1105
|
</Group>
|
|
967
1106
|
)}
|
|
968
1107
|
{scrolls ? (
|
|
@@ -986,16 +1125,13 @@ export function BasaltDataTable<T>({
|
|
|
986
1125
|
</Text>
|
|
987
1126
|
<Group gap="xs" align="center">
|
|
988
1127
|
<Select
|
|
989
|
-
size="
|
|
1128
|
+
size="ctl"
|
|
990
1129
|
radius="md"
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
label: `${size} / page`,
|
|
994
|
-
}))}
|
|
1130
|
+
w={PAGE_SIZE_WIDTH}
|
|
1131
|
+
data={pageSizeRows}
|
|
995
1132
|
value={String(paginationState.pageSize)}
|
|
996
1133
|
onChange={(value) => value && table.setPageSize(Number(value))}
|
|
997
1134
|
allowDeselect={false}
|
|
998
|
-
w={110}
|
|
999
1135
|
/>
|
|
1000
1136
|
<Pagination
|
|
1001
1137
|
size="sm"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Unit tests for checkSource — the pure (text, relPath, cfg) → Finding[] core.
|
|
3
3
|
*
|
|
4
|
-
* Covers all
|
|
4
|
+
* Covers all 27 guard kinds. Co-located with the guard, excluded from tsc
|
|
5
5
|
* (tsconfig exclude: src/**\/*.test.ts), run via `bun test`.
|
|
6
6
|
*
|
|
7
7
|
* The walker/reporter half is covered by the integration test in
|
|
@@ -1916,15 +1916,16 @@ describe('severity', () => {
|
|
|
1916
1916
|
expect(f.find((v) => v.kind === 'raw-spacing')?.severity).toBe('error')
|
|
1917
1917
|
})
|
|
1918
1918
|
|
|
1919
|
-
// The grace ledger.
|
|
1920
|
-
//
|
|
1919
|
+
// The grace ledger. These five kinds landed warn-only in the round-4 guard minor and sat there
|
|
1920
|
+
// for five minors with nothing tracking the promise (D4) — GRACE_PERIOD_KINDS is now empty and
|
|
1921
|
+
// the C16 version gate in grace.test.ts is what stops a future entry sitting unpromoted again.
|
|
1921
1922
|
it.each([
|
|
1922
1923
|
'theme-allow-unscoped',
|
|
1923
1924
|
'surface-shadow-override',
|
|
1924
1925
|
'css-raw-surface',
|
|
1925
1926
|
'inline-font-size',
|
|
1926
1927
|
'hidden-inline-style',
|
|
1927
|
-
])('%s is
|
|
1928
|
+
])('%s is promoted past its grace minor (error)', (kind) => {
|
|
1928
1929
|
const cases: Record<string, [string, string]> = {
|
|
1929
1930
|
'theme-allow-unscoped': ['const x = 1 // theme-allow', PATH],
|
|
1930
1931
|
'surface-shadow-override': ['const s = { boxShadow: `0 0 0 2px ${VX.accent}` }', PATH],
|
|
@@ -1937,7 +1938,7 @@ describe('severity', () => {
|
|
|
1937
1938
|
}
|
|
1938
1939
|
const [src, path] = cases[kind] as [string, string]
|
|
1939
1940
|
const hit = find(src, path).find((v) => v.kind === kind)
|
|
1940
|
-
expect(hit?.severity).toBe('
|
|
1941
|
+
expect(hit?.severity).toBe('error')
|
|
1941
1942
|
})
|
|
1942
1943
|
|
|
1943
1944
|
it('ships every OTHER kind past its grace period', () => {
|
|
@@ -2139,9 +2140,9 @@ describe('theme-allow scoping', () => {
|
|
|
2139
2140
|
expect(f).toHaveLength(0)
|
|
2140
2141
|
})
|
|
2141
2142
|
|
|
2142
|
-
it('ships theme-allow-unscoped as
|
|
2143
|
+
it('ships theme-allow-unscoped as an error (promoted, C16)', () => {
|
|
2143
2144
|
const f = find(`const x = 1 // theme-allow`)
|
|
2144
|
-
expect(f.every((v) => v.severity === '
|
|
2145
|
+
expect(f.every((v) => v.severity === 'error')).toBe(true)
|
|
2145
2146
|
})
|
|
2146
2147
|
|
|
2147
2148
|
// The annotation itself lives in a comment; a `theme-allow` in real code must not waive anything.
|
|
@@ -2370,9 +2371,9 @@ describe('surface-shadow-override', () => {
|
|
|
2370
2371
|
expect(kinds(f)).not.toContain('surface-shadow-override')
|
|
2371
2372
|
})
|
|
2372
2373
|
|
|
2373
|
-
it('
|
|
2374
|
+
it('is an error now that its grace minor has ended (promoted, C16)', () => {
|
|
2374
2375
|
const f = find('const s = { boxShadow: `0 0 0 2px ${VX.accent}` }')
|
|
2375
|
-
expect(f.find((v) => v.kind === 'surface-shadow-override')?.severity).toBe('
|
|
2376
|
+
expect(f.find((v) => v.kind === 'surface-shadow-override')?.severity).toBe('error')
|
|
2376
2377
|
})
|
|
2377
2378
|
})
|
|
2378
2379
|
|
|
@@ -2435,6 +2436,114 @@ describe('inline-font-size', () => {
|
|
|
2435
2436
|
})
|
|
2436
2437
|
})
|
|
2437
2438
|
|
|
2439
|
+
// ── 26. in-body-page-title (the text lane of law C8) ─────────────────────────
|
|
2440
|
+
|
|
2441
|
+
describe('in-body-page-title', () => {
|
|
2442
|
+
it('flags <Title order={1}> in the body', () => {
|
|
2443
|
+
expect(kinds(find(`<Title order={1}>Analytics</Title>`))).toContain('in-body-page-title')
|
|
2444
|
+
})
|
|
2445
|
+
|
|
2446
|
+
it('flags order={2} too', () => {
|
|
2447
|
+
expect(kinds(find(`<Title order={2}>Analytics</Title>`))).toContain('in-body-page-title')
|
|
2448
|
+
})
|
|
2449
|
+
|
|
2450
|
+
// The formatted default — the reason this kind is a bounded TAG scan and not a line regex.
|
|
2451
|
+
it('flags a Title formatted across lines', () => {
|
|
2452
|
+
const f = find(`<Title\n order={1}\n mb="sm"\n>\n Analytics\n</Title>`)
|
|
2453
|
+
expect(kinds(f)).toContain('in-body-page-title')
|
|
2454
|
+
})
|
|
2455
|
+
|
|
2456
|
+
it('does NOT flag order={3} — a section heading', () => {
|
|
2457
|
+
expect(kinds(find(`<Title order={3}>Sessions</Title>`))).not.toContain('in-body-page-title')
|
|
2458
|
+
})
|
|
2459
|
+
|
|
2460
|
+
it('does NOT flag a Title with no order', () => {
|
|
2461
|
+
expect(kinds(find(`<Title>Sessions</Title>`))).not.toContain('in-body-page-title')
|
|
2462
|
+
})
|
|
2463
|
+
|
|
2464
|
+
it('does NOT flag anything under a content/ path segment', () => {
|
|
2465
|
+
const f = find(`<Title order={1}>Doc</Title>`, 'src/content/Guide.tsx')
|
|
2466
|
+
expect(kinds(f)).not.toContain('in-body-page-title')
|
|
2467
|
+
})
|
|
2468
|
+
|
|
2469
|
+
// File-scoped and coarser than the plugin's node-level ancestry, on purpose: a document heading
|
|
2470
|
+
// told to become a breadcrumb is the WRONG advice, so the text lane errs silent.
|
|
2471
|
+
it('does NOT flag a file that renders Prose', () => {
|
|
2472
|
+
const f = find(`<Prose>\n <Title order={1}>Doc</Title>\n</Prose>`)
|
|
2473
|
+
expect(kinds(f)).not.toContain('in-body-page-title')
|
|
2474
|
+
})
|
|
2475
|
+
|
|
2476
|
+
it('honours a theme-allow', () => {
|
|
2477
|
+
const f = find(
|
|
2478
|
+
`// theme-allow in-body-page-title — a shell-less print view\n<Title order={1}>P</Title>`,
|
|
2479
|
+
)
|
|
2480
|
+
expect(kinds(f)).not.toContain('in-body-page-title')
|
|
2481
|
+
})
|
|
2482
|
+
|
|
2483
|
+
it('lands warn while the grace entry stands (C16)', () => {
|
|
2484
|
+
const f = find(`<Title order={1}>Analytics</Title>`)
|
|
2485
|
+
expect(f.find((x) => x.kind === 'in-body-page-title')?.severity).toBe('warn')
|
|
2486
|
+
})
|
|
2487
|
+
})
|
|
2488
|
+
|
|
2489
|
+
// ── 27. raw-selection-control (the text lane of law C1) ──────────────────────
|
|
2490
|
+
|
|
2491
|
+
describe('raw-selection-control', () => {
|
|
2492
|
+
it.each(['SegmentedControl', 'Select', 'MultiSelect', 'NativeSelect', 'TagsInput', 'Chip.Group'])(
|
|
2493
|
+
'flags a raw <%s>',
|
|
2494
|
+
(tag) => {
|
|
2495
|
+
expect(kinds(find(`<${tag} data={[]} />`))).toContain('raw-selection-control')
|
|
2496
|
+
},
|
|
2497
|
+
)
|
|
2498
|
+
|
|
2499
|
+
it('reports the tag it found, not the whole line', () => {
|
|
2500
|
+
const f = find(`<Select data={[]} />`)
|
|
2501
|
+
expect(f.find((x) => x.kind === 'raw-selection-control')?.token).toBe('<Select')
|
|
2502
|
+
})
|
|
2503
|
+
|
|
2504
|
+
it.each(['SettingsRow', 'Modal', 'Drawer', 'Menu.Dropdown'])(
|
|
2505
|
+
'does NOT flag one inside a %s window',
|
|
2506
|
+
(host) => {
|
|
2507
|
+
const f = find(`<${host}>\n <Select data={[]} />\n</${host}>`)
|
|
2508
|
+
expect(kinds(f)).not.toContain('raw-selection-control')
|
|
2509
|
+
},
|
|
2510
|
+
)
|
|
2511
|
+
|
|
2512
|
+
// The window is 12 lines; past it the approximation would start swallowing the NEXT row's control.
|
|
2513
|
+
it('DOES flag one 20 lines below the host tag — the window is bounded', () => {
|
|
2514
|
+
const f = find(`<Modal>\n${' <Text>x</Text>\n'.repeat(20)} <Select data={[]} />\n</Modal>`)
|
|
2515
|
+
expect(kinds(f)).toContain('raw-selection-control')
|
|
2516
|
+
})
|
|
2517
|
+
|
|
2518
|
+
it('does NOT flag anything in a file importing @mantine/form', () => {
|
|
2519
|
+
const f = find(`import { useForm } from '@mantine/form'\n<Select data={[]} />`)
|
|
2520
|
+
expect(kinds(f)).not.toContain('raw-selection-control')
|
|
2521
|
+
})
|
|
2522
|
+
|
|
2523
|
+
it('does NOT flag the file that DEFINES a basalt control', () => {
|
|
2524
|
+
const f = find(`export function ViewTabs() {\n return <SegmentedControl data={[]} />\n}`)
|
|
2525
|
+
expect(kinds(f)).not.toContain('raw-selection-control')
|
|
2526
|
+
})
|
|
2527
|
+
|
|
2528
|
+
it('does NOT flag a bound basalt control', () => {
|
|
2529
|
+
expect(kinds(find(`<SelectFilter field={f} label="Channel" />`))).not.toContain(
|
|
2530
|
+
'raw-selection-control',
|
|
2531
|
+
)
|
|
2532
|
+
})
|
|
2533
|
+
|
|
2534
|
+
it('honours a theme-allow', () => {
|
|
2535
|
+
const f = find(
|
|
2536
|
+
`{/* theme-allow raw-selection-control — a one-off admin picker */}\n<Select data={[]} />`,
|
|
2537
|
+
)
|
|
2538
|
+
expect(kinds(f)).not.toContain('raw-selection-control')
|
|
2539
|
+
})
|
|
2540
|
+
|
|
2541
|
+
it('lands warn while the grace entry stands (C16)', () => {
|
|
2542
|
+
const f = find(`<Select data={[]} />`)
|
|
2543
|
+
expect(f.find((x) => x.kind === 'raw-selection-control')?.severity).toBe('warn')
|
|
2544
|
+
})
|
|
2545
|
+
})
|
|
2546
|
+
|
|
2438
2547
|
// ── 26. hidden-inline-style ──────────────────────────────────────────────────
|
|
2439
2548
|
|
|
2440
2549
|
describe('hidden-inline-style', () => {
|
|
@@ -2475,12 +2584,12 @@ export const C = () => <div style={labelStyle} />`,
|
|
|
2475
2584
|
expect(kinds(f)).not.toContain('hidden-inline-style')
|
|
2476
2585
|
})
|
|
2477
2586
|
|
|
2478
|
-
it('
|
|
2587
|
+
it('is an error now that its grace minor has ended (promoted, C16)', () => {
|
|
2479
2588
|
const f = find(
|
|
2480
2589
|
`const wrapperStyle = { position: 'relative', width: '100%' }
|
|
2481
2590
|
export const C = () => <div style={wrapperStyle} />`,
|
|
2482
2591
|
)
|
|
2483
|
-
expect(f.find((v) => v.kind === 'hidden-inline-style')?.severity).toBe('
|
|
2592
|
+
expect(f.find((v) => v.kind === 'hidden-inline-style')?.severity).toBe('error')
|
|
2484
2593
|
})
|
|
2485
2594
|
})
|
|
2486
2595
|
|
|
@@ -2638,7 +2747,7 @@ describe('markup files (index.html / *.webmanifest)', () => {
|
|
|
2638
2747
|
* Round 9: `.astro` and `.vue` became scannable and fell through to the `ts` dialect, so `<!-- … -->`
|
|
2639
2748
|
* was never stripped — a `theme-allow` written in an HTML comment waived nothing there, and a color
|
|
2640
2749
|
* inside a commented-out block still reported. They resolve as `sfc` now: BOTH comment dialects,
|
|
2641
|
-
* and the full
|
|
2750
|
+
* and the full 27-kind set (not `MARKUP_KINDS` — an `.astro` template is JSX-shaped and a `.vue`
|
|
2642
2751
|
* `<script setup>` is real TS).
|
|
2643
2752
|
*/
|
|
2644
2753
|
describe('single-file components (.astro / .vue)', () => {
|
|
@@ -2770,11 +2879,15 @@ describe("profile: 'tokens-only'", () => {
|
|
|
2770
2879
|
'css-raw-surface': false,
|
|
2771
2880
|
'inline-font-size': false,
|
|
2772
2881
|
'hidden-inline-style': true,
|
|
2882
|
+
// The two wave-6 control kinds: `<Title>` is a Mantine component, and the remedy for either is
|
|
2883
|
+
// a Mantine-rendered home (PageBar / WidgetHeader) or a basalt control over @mantine/core.
|
|
2884
|
+
'in-body-page-title': true,
|
|
2885
|
+
'raw-selection-control': true,
|
|
2773
2886
|
}
|
|
2774
2887
|
|
|
2775
2888
|
it('classifies every kind in the registry — the table is exhaustive', () => {
|
|
2776
2889
|
expect(Object.keys(MANTINE_COUPLED).toSorted()).toEqual(Object.keys(GUARD_RULES).toSorted())
|
|
2777
|
-
expect(Object.keys(MANTINE_COUPLED)).toHaveLength(
|
|
2890
|
+
expect(Object.keys(MANTINE_COUPLED)).toHaveLength(27)
|
|
2778
2891
|
})
|
|
2779
2892
|
|
|
2780
2893
|
it('the disabled set is exactly the Mantine-coupled half — a complete partition', () => {
|