basalt-ui 1.24.0 → 1.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +8 -6
- package/CHANGELOG.md +25 -0
- package/MIGRATING.md +547 -21
- package/README.md +46 -33
- package/agent/rules/basalt-batteries.md +205 -0
- package/agent/rules/basalt-charts.md +125 -555
- package/agent/rules/basalt-controls.md +155 -0
- package/agent/rules/basalt-mantine.md +146 -439
- package/agent/rules/basalt-state.md +113 -96
- package/agent/rules/basalt-tokens.md +126 -288
- package/agent/skills/basalt-app/SKILL.md +70 -195
- package/agent/skills/basalt-charts/SKILL.md +54 -234
- package/agent/skills/basalt-design/SKILL.md +56 -248
- package/agent/templates/CLAUDE-block.md.tpl +31 -40
- package/agent/templates/DESIGN.md.tpl +22 -51
- package/configs/oxlint-plugin.js +1275 -31
- package/configs/oxlint-plugin.test.ts +947 -11
- package/configs/oxlint.json +11 -3
- package/dist/charts/cursor/resolve.d.ts +18 -0
- package/dist/charts/cursor/resolve.d.ts.map +1 -1
- package/dist/charts/cursor/resolve.js +15 -6
- package/dist/charts/cursor/scope.d.ts +6 -3
- package/dist/charts/cursor/scope.d.ts.map +1 -1
- package/dist/charts/cursor/store.d.ts +5 -1
- package/dist/charts/cursor/store.d.ts.map +1 -1
- package/dist/charts/cursor/store.js +4 -4
- package/dist/charts/hooks/useChartCursor.d.ts.map +1 -1
- package/dist/charts/hooks/useChartCursor.js +9 -8
- package/dist/charts/index.d.ts +2 -2
- package/dist/charts/index.d.ts.map +1 -1
- package/dist/charts/primitives/ChartCard.d.ts +34 -9
- package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
- package/dist/charts/primitives/ChartCard.js +29 -114
- package/dist/charts/primitives/ChartCard.test.d.ts +2 -0
- package/dist/charts/primitives/ChartCard.test.d.ts.map +1 -0
- package/dist/charts/sparklines/BarSparkline.d.ts +26 -2
- package/dist/charts/sparklines/BarSparkline.d.ts.map +1 -1
- package/dist/charts/sparklines/BarSparkline.js +18 -4
- package/dist/cli/check-coverage.test.d.ts +2 -0
- package/dist/cli/check-coverage.test.d.ts.map +1 -0
- package/dist/cli/index.d.ts +67 -4
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +225 -7
- package/dist/connectivity/connectivity-indicator.d.ts +4 -1
- package/dist/connectivity/connectivity-indicator.d.ts.map +1 -1
- package/dist/connectivity/connectivity-indicator.js +1 -1
- package/dist/content/index.d.ts +5 -3
- package/dist/content/index.d.ts.map +1 -1
- package/dist/content/index.js +4 -2
- package/dist/content/prose.module.css +7 -28
- package/dist/controls/actions.d.ts +169 -0
- package/dist/controls/actions.d.ts.map +1 -0
- package/dist/controls/actions.js +287 -0
- package/dist/controls/actions.module.css +62 -0
- package/dist/controls/actions.module.css.d.ts +13 -0
- package/dist/controls/actions.test.d.ts +2 -0
- package/dist/controls/actions.test.d.ts.map +1 -0
- package/dist/controls/actions.type-guard.test.d.ts +2 -0
- package/dist/controls/actions.type-guard.test.d.ts.map +1 -0
- package/dist/controls/compare-filter.d.ts +41 -0
- package/dist/controls/compare-filter.d.ts.map +1 -0
- package/dist/controls/compare-filter.js +23 -0
- package/dist/controls/control-group.d.ts +19 -0
- package/dist/controls/control-group.d.ts.map +1 -0
- package/dist/controls/control-group.js +20 -0
- package/dist/controls/control-group.test.d.ts +2 -0
- package/dist/controls/control-group.test.d.ts.map +1 -0
- package/dist/controls/controls.module.css +542 -0
- package/dist/controls/controls.module.css.d.ts +35 -0
- package/dist/controls/controls.router.test.d.ts +2 -0
- package/dist/controls/controls.router.test.d.ts.map +1 -0
- package/dist/controls/enum-filter.d.ts +30 -0
- package/dist/controls/enum-filter.d.ts.map +1 -0
- package/dist/controls/enum-filter.js +91 -0
- package/dist/controls/filter-context.d.ts +55 -0
- package/dist/controls/filter-context.d.ts.map +1 -0
- package/dist/controls/filter-context.js +69 -0
- package/dist/controls/filter-pill.d.ts +28 -0
- package/dist/controls/filter-pill.d.ts.map +1 -0
- package/dist/controls/filter-pill.js +70 -0
- package/dist/controls/filter-set.d.ts +13 -0
- package/dist/controls/filter-set.d.ts.map +1 -0
- package/dist/controls/filter-set.js +125 -0
- package/dist/controls/filter-set.test.d.ts +2 -0
- package/dist/controls/filter-set.test.d.ts.map +1 -0
- package/dist/controls/filter-sheet.d.ts +117 -0
- package/dist/controls/filter-sheet.d.ts.map +1 -0
- package/dist/controls/filter-sheet.js +146 -0
- package/dist/controls/glyphs.d.ts +27 -0
- package/dist/controls/glyphs.d.ts.map +1 -0
- package/dist/controls/glyphs.js +49 -0
- package/dist/controls/index.d.ts +19 -0
- package/dist/controls/index.d.ts.map +1 -0
- package/dist/controls/index.js +44 -0
- package/dist/controls/multi-select-filter.d.ts +23 -0
- package/dist/controls/multi-select-filter.d.ts.map +1 -0
- package/dist/controls/multi-select-filter.js +89 -0
- package/dist/controls/peer-boundary.test.d.ts +2 -0
- package/dist/controls/peer-boundary.test.d.ts.map +1 -0
- package/dist/controls/props-contract.test.d.ts +2 -0
- package/dist/controls/props-contract.test.d.ts.map +1 -0
- package/dist/controls/range-filter.d.ts +29 -0
- package/dist/controls/range-filter.d.ts.map +1 -0
- package/dist/controls/range-filter.js +120 -0
- package/dist/controls/search-filter.d.ts +10 -0
- package/dist/controls/search-filter.d.ts.map +1 -0
- package/dist/controls/search-filter.js +81 -0
- package/dist/controls/select-filter.d.ts +74 -0
- package/dist/controls/select-filter.d.ts.map +1 -0
- package/dist/controls/select-filter.js +10 -0
- package/dist/controls/sync-button.d.ts +38 -0
- package/dist/controls/sync-button.d.ts.map +1 -0
- package/dist/controls/sync-button.js +119 -0
- package/dist/controls/sync-button.module.css +63 -0
- package/dist/controls/sync-button.module.css.d.ts +11 -0
- package/dist/controls/sync-button.test.d.ts +2 -0
- package/dist/controls/sync-button.test.d.ts.map +1 -0
- package/dist/controls/toggle-filter.d.ts +9 -0
- package/dist/controls/toggle-filter.d.ts.map +1 -0
- package/dist/controls/toggle-filter.js +42 -0
- package/dist/controls/view-tabs.d.ts +23 -0
- package/dist/controls/view-tabs.d.ts.map +1 -0
- package/dist/controls/view-tabs.js +59 -0
- package/dist/controls-dates/date-range-picker.d.ts +4 -0
- package/dist/controls-dates/date-range-picker.d.ts.map +1 -0
- package/dist/controls-dates/date-range-picker.js +18 -0
- package/dist/controls-dates/index.d.ts +9 -0
- package/dist/controls-dates/index.d.ts.map +1 -0
- package/dist/controls-dates/index.js +4 -0
- package/dist/dashboard/delta-badge.d.ts +6 -12
- package/dist/dashboard/delta-badge.d.ts.map +1 -1
- package/dist/dashboard/delta-badge.js +1 -50
- package/dist/dashboard/query-state.d.ts +1 -1
- package/dist/dashboard/settings-section.d.ts +6 -4
- package/dist/dashboard/settings-section.d.ts.map +1 -1
- package/dist/dashboard/settings-section.js +23 -16
- package/dist/dashboard/settings-section.module.css +10 -0
- package/dist/dashboard/settings-section.module.css.d.ts +1 -0
- package/dist/dashboard/settings-section.test.d.ts +2 -0
- package/dist/dashboard/settings-section.test.d.ts.map +1 -0
- package/dist/dashboard/stat-card.d.ts +44 -9
- package/dist/dashboard/stat-card.d.ts.map +1 -1
- package/dist/dashboard/stat-card.js +80 -48
- package/dist/dashboard/stat-card.module.css +82 -0
- package/dist/dashboard/stat-card.module.css.d.ts +11 -0
- package/dist/data/data-table.d.ts +102 -20
- package/dist/data/data-table.d.ts.map +1 -1
- package/dist/data/data-table.js +166 -65
- package/dist/data/table.d.ts +1 -1
- package/dist/data/table.d.ts.map +1 -1
- package/dist/guard/grace.test.d.ts +20 -0
- package/dist/guard/grace.test.d.ts.map +1 -0
- package/dist/guard/index.d.ts +66 -3
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +82 -7
- package/dist/guard/types.d.ts +3 -3
- package/dist/guard/types.d.ts.map +1 -1
- package/dist/index.d.ts +12 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -10
- package/dist/nav/types.d.ts +78 -2
- package/dist/nav/types.d.ts.map +1 -1
- package/dist/notifications/bell.d.ts +4 -1
- package/dist/notifications/bell.d.ts.map +1 -1
- package/dist/notifications/index.d.ts +1 -1
- package/dist/router-tanstack/field.d.ts +14 -0
- package/dist/router-tanstack/field.d.ts.map +1 -0
- package/dist/router-tanstack/field.js +4 -0
- package/dist/router-tanstack/index.d.ts +6 -2
- package/dist/router-tanstack/index.d.ts.map +1 -1
- package/dist/router-tanstack/index.js +4 -0
- package/dist/router-tanstack/multi-search-param-store.d.ts +4 -52
- package/dist/router-tanstack/multi-search-param-store.d.ts.map +1 -1
- package/dist/router-tanstack/multi-search-param-store.js +19 -27
- package/dist/router-tanstack/search-param-store.d.ts +3 -87
- package/dist/router-tanstack/search-param-store.d.ts.map +1 -1
- package/dist/router-tanstack/search-param-store.js +16 -45
- package/dist/router-tanstack/search-store.d.ts +81 -0
- package/dist/router-tanstack/search-store.d.ts.map +1 -0
- package/dist/router-tanstack/search-store.js +187 -0
- package/dist/router-tanstack/search-store.router.test.d.ts +2 -0
- package/dist/router-tanstack/search-store.router.test.d.ts.map +1 -0
- package/dist/router-tanstack/search-store.test.d.ts +2 -0
- package/dist/router-tanstack/search-store.test.d.ts.map +1 -0
- package/dist/router-tanstack/use-nav.d.ts +0 -2
- package/dist/router-tanstack/use-nav.d.ts.map +1 -1
- package/dist/router-tanstack/use-nav.js +34 -12
- package/dist/router-tanstack/use-nav.test.d.ts +2 -0
- package/dist/router-tanstack/use-nav.test.d.ts.map +1 -0
- package/dist/section/index.d.ts +2 -0
- package/dist/section/index.d.ts.map +1 -0
- package/dist/section/index.js +4 -0
- package/dist/section/section.d.ts +24 -0
- package/dist/section/section.d.ts.map +1 -0
- package/dist/section/section.js +126 -0
- package/dist/section/section.module.css +104 -0
- package/dist/section/section.module.css.d.ts +11 -0
- package/dist/section/section.test.d.ts +2 -0
- package/dist/section/section.test.d.ts.map +1 -0
- package/dist/shell/app-breadcrumbs.d.ts.map +1 -1
- package/dist/shell/app-breadcrumbs.js +18 -15
- package/dist/shell/app-header.module.css +77 -37
- package/dist/shell/app-header.module.css.d.ts +2 -1
- package/dist/shell/app-mobile-nav.d.ts +9 -2
- package/dist/shell/app-mobile-nav.d.ts.map +1 -1
- package/dist/shell/app-mobile-nav.js +98 -11
- package/dist/shell/app-mobile-nav.module.css +8 -0
- package/dist/shell/app-mobile-nav.module.css.d.ts +1 -0
- package/dist/shell/app-sidebar.d.ts +55 -28
- package/dist/shell/app-sidebar.d.ts.map +1 -1
- package/dist/shell/app-sidebar.js +116 -67
- package/dist/shell/app-sidebar.module.css +303 -20
- package/dist/shell/app-sidebar.module.css.d.ts +19 -1
- package/dist/shell/index.d.ts +55 -21
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +40 -18
- package/dist/shell/mobile-nav-model.d.ts +13 -2
- package/dist/shell/mobile-nav-model.d.ts.map +1 -1
- package/dist/shell/mobile-nav-model.js +15 -1
- package/dist/shell/nav-count-badge.d.ts +1 -1
- package/dist/shell/page-bar.d.ts +51 -0
- package/dist/shell/page-bar.d.ts.map +1 -0
- package/dist/shell/page-bar.js +154 -0
- package/dist/shell/page-bar.module.css +178 -0
- package/dist/shell/page-bar.module.css.d.ts +17 -0
- package/dist/shell/page-bar.test.d.ts +2 -0
- package/dist/shell/page-bar.test.d.ts.map +1 -0
- package/dist/shell/sidebar-block-model.d.ts +48 -0
- package/dist/shell/sidebar-block-model.d.ts.map +1 -0
- package/dist/shell/sidebar-block-model.js +39 -0
- package/dist/shell/sidebar-blocks.d.ts +32 -0
- package/dist/shell/sidebar-blocks.d.ts.map +1 -0
- package/dist/shell/sidebar-blocks.js +169 -0
- package/dist/shell/sidebar-search.d.ts +13 -1
- package/dist/shell/sidebar-search.d.ts.map +1 -1
- package/dist/shell/sidebar-search.js +34 -1
- package/dist/shell/sidebar-search.module.css +26 -0
- package/dist/shell/sidebar-search.module.css.d.ts +2 -0
- package/dist/state/fields.d.ts +392 -0
- package/dist/state/fields.d.ts.map +1 -0
- package/dist/state/fields.js +369 -0
- package/dist/state/fields.test.d.ts +2 -0
- package/dist/state/fields.test.d.ts.map +1 -0
- package/dist/state/persisted.d.ts +48 -0
- package/dist/state/persisted.d.ts.map +1 -0
- package/dist/state/persisted.js +105 -0
- package/dist/state.d.ts +11 -56
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +14 -120
- package/dist/styles.css +13 -0
- package/dist/surfaces.d.ts +106 -24
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +155 -19
- package/dist/theme/controls.module.css +16 -1
- package/dist/theme/ctl-theme.d.ts +32 -0
- package/dist/theme/ctl-theme.d.ts.map +1 -0
- package/dist/theme/ctl-theme.js +54 -0
- package/dist/theme/ctl-theme.test.d.ts +2 -0
- package/dist/theme/ctl-theme.test.d.ts.map +1 -0
- package/dist/theme/ctl-tier-coverage.test.d.ts +2 -0
- package/dist/theme/ctl-tier-coverage.test.d.ts.map +1 -0
- package/dist/theme/divider-law.test.d.ts +2 -0
- package/dist/theme/divider-law.test.d.ts.map +1 -0
- package/dist/theme/icon-slot.d.ts +44 -0
- package/dist/theme/icon-slot.d.ts.map +1 -0
- package/dist/theme/icon-slot.js +21 -0
- package/dist/theme/icon-slot.module.css +47 -0
- package/dist/theme/icon-slot.module.css.d.ts +8 -0
- package/dist/theme/icon-slot.test.d.ts +2 -0
- package/dist/theme/icon-slot.test.d.ts.map +1 -0
- package/dist/theme/index.d.ts +1 -0
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/index.js +82 -5
- package/dist/theme/layout-rhythm-css.test.d.ts +2 -0
- package/dist/theme/layout-rhythm-css.test.d.ts.map +1 -0
- package/dist/theme/segmented-control.module.css +20 -0
- package/dist/theme/shadow-surfaces.test.d.ts.map +1 -1
- package/dist/tokens/index.d.ts +12 -0
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +38 -8
- package/dist/tokens/palette.d.ts +97 -61
- package/dist/tokens/palette.d.ts.map +1 -1
- package/dist/tokens/palette.js +76 -38
- package/dist/tokens.css +10 -2
- package/dist/widget-header/delta-badge.d.ts +13 -0
- package/dist/widget-header/delta-badge.d.ts.map +1 -0
- package/dist/widget-header/delta-badge.js +29 -0
- package/dist/widget-header/delta-badge.test.d.ts +2 -0
- package/dist/widget-header/delta-badge.test.d.ts.map +1 -0
- package/dist/widget-header/index.d.ts +3 -0
- package/dist/widget-header/index.d.ts.map +1 -0
- package/dist/widget-header/index.js +6 -0
- package/dist/widget-header/no-mantine.test.d.ts +2 -0
- package/dist/widget-header/no-mantine.test.d.ts.map +1 -0
- package/dist/widget-header/widget-header.d.ts +37 -0
- package/dist/widget-header/widget-header.d.ts.map +1 -0
- package/dist/widget-header/widget-header.js +77 -0
- package/dist/widget-header/widget-header.module.css +335 -0
- package/dist/widget-header/widget-header.module.css.d.ts +24 -0
- package/dist/widget-header/widget-header.test.d.ts +2 -0
- package/dist/widget-header/widget-header.test.d.ts.map +1 -0
- package/llms.txt +30 -17
- package/package.json +14 -1
- package/src/charts/cursor/resolve.test.ts +29 -1
- package/src/charts/cursor/resolve.ts +38 -6
- package/src/charts/cursor/scope.tsx +6 -3
- package/src/charts/cursor/store.test.ts +19 -1
- package/src/charts/cursor/store.ts +10 -5
- package/src/charts/hooks/useChartCursor.ts +16 -8
- package/src/charts/index.ts +2 -2
- package/src/charts/primitives/CartesianChart.test.tsx +124 -0
- package/src/charts/primitives/ChartCard.test.tsx +66 -0
- package/src/charts/primitives/ChartCard.tsx +86 -146
- package/src/charts/sparklines/BarSparkline.tsx +43 -5
- package/src/cli/check-coverage.test.ts +297 -0
- package/src/cli/check-theme.test.ts +38 -14
- package/src/cli/index.ts +392 -16
- package/src/cli/init-sync.test.ts +97 -0
- package/src/connectivity/connectivity-indicator.tsx +16 -9
- package/src/content/index.ts +16 -3
- package/src/content/prose.module.css +7 -28
- package/src/content/prose.module.css.test.ts +8 -24
- package/src/controls/actions.module.css +62 -0
- package/src/controls/actions.module.css.d.ts +13 -0
- package/src/controls/actions.test.tsx +252 -0
- package/src/controls/actions.tsx +655 -0
- package/src/controls/actions.type-guard.test.ts +63 -0
- package/src/controls/compare-filter.tsx +64 -0
- package/src/controls/control-group.test.tsx +229 -0
- package/src/controls/control-group.tsx +63 -0
- package/src/controls/controls.module.css +542 -0
- package/src/controls/controls.module.css.d.ts +35 -0
- package/src/controls/controls.router.test.tsx +637 -0
- package/src/controls/enum-filter.tsx +159 -0
- package/src/controls/filter-context.tsx +153 -0
- package/src/controls/filter-pill.tsx +120 -0
- package/src/controls/filter-set.test.tsx +472 -0
- package/src/controls/filter-set.tsx +225 -0
- package/src/controls/filter-sheet.tsx +285 -0
- package/src/controls/glyphs.tsx +81 -0
- package/src/controls/index.ts +48 -0
- package/src/controls/multi-select-filter.tsx +140 -0
- package/src/controls/peer-boundary.test.ts +85 -0
- package/src/controls/props-contract.test.tsx +106 -0
- package/src/controls/range-filter.tsx +184 -0
- package/src/controls/search-filter.tsx +123 -0
- package/src/controls/select-filter.tsx +89 -0
- package/src/controls/sync-button.module.css +63 -0
- package/src/controls/sync-button.module.css.d.ts +11 -0
- package/src/controls/sync-button.test.tsx +183 -0
- package/src/controls/sync-button.tsx +197 -0
- package/src/controls/toggle-filter.tsx +60 -0
- package/src/controls/view-tabs.tsx +110 -0
- package/src/controls-dates/date-range-picker.tsx +42 -0
- package/src/controls-dates/index.ts +8 -0
- package/src/dashboard/delta-badge.tsx +4 -74
- package/src/dashboard/query-state.tsx +1 -1
- package/src/dashboard/settings-section.module.css +10 -0
- package/src/dashboard/settings-section.module.css.d.ts +1 -0
- package/src/dashboard/settings-section.test.tsx +56 -0
- package/src/dashboard/settings-section.tsx +33 -30
- package/src/dashboard/stat-card.module.css +82 -0
- package/src/dashboard/stat-card.module.css.d.ts +11 -0
- package/src/dashboard/stat-card.test.tsx +67 -1
- package/src/dashboard/stat-card.tsx +209 -68
- package/src/data/data-table.test.tsx +282 -6
- package/src/data/data-table.tsx +429 -94
- package/src/data/table.ts +2 -0
- package/src/guard/check-source.test.ts +126 -13
- package/src/guard/grace.test.ts +149 -0
- package/src/guard/index.ts +172 -29
- package/src/guard/types.ts +4 -2
- package/src/index.ts +46 -5
- package/src/nav/types.ts +84 -2
- package/src/notifications/bell.tsx +6 -3
- package/src/notifications/index.ts +1 -1
- package/src/router-tanstack/field.ts +31 -0
- package/src/router-tanstack/index.ts +24 -2
- package/src/router-tanstack/multi-search-param-store.test.ts +71 -3
- package/src/router-tanstack/multi-search-param-store.ts +46 -77
- package/src/router-tanstack/search-param-store.ts +42 -159
- package/src/router-tanstack/search-store.router.test.tsx +661 -0
- package/src/router-tanstack/search-store.test.ts +418 -0
- package/src/router-tanstack/search-store.ts +380 -0
- package/src/router-tanstack/use-nav.test.tsx +187 -0
- package/src/router-tanstack/use-nav.ts +72 -14
- package/src/section/index.ts +1 -0
- package/src/section/section.module.css +104 -0
- package/src/section/section.module.css.d.ts +11 -0
- package/src/section/section.test.tsx +204 -0
- package/src/section/section.tsx +220 -0
- package/src/shell/app-breadcrumbs.tsx +44 -31
- package/src/shell/app-header.module.css +77 -37
- package/src/shell/app-header.module.css.d.ts +2 -1
- package/src/shell/app-mobile-nav.module.css +8 -0
- package/src/shell/app-mobile-nav.module.css.d.ts +1 -0
- package/src/shell/app-mobile-nav.tsx +147 -8
- package/src/shell/app-sidebar-account.tsx +2 -2
- package/src/shell/app-sidebar.module.css +303 -20
- package/src/shell/app-sidebar.module.css.d.ts +19 -1
- package/src/shell/app-sidebar.test.tsx +456 -41
- package/src/shell/app-sidebar.tsx +276 -140
- package/src/shell/index.test.tsx +291 -5
- package/src/shell/index.tsx +137 -38
- package/src/shell/mobile-nav-model.test.ts +228 -7
- package/src/shell/mobile-nav-model.ts +53 -3
- package/src/shell/nav-count-badge.tsx +1 -1
- package/src/shell/page-bar.module.css +178 -0
- package/src/shell/page-bar.module.css.d.ts +17 -0
- package/src/shell/page-bar.test.tsx +316 -0
- package/src/shell/page-bar.tsx +315 -0
- package/src/shell/sidebar-block-model.ts +83 -0
- package/src/shell/sidebar-blocks.tsx +301 -0
- package/src/shell/sidebar-search.module.css +26 -0
- package/src/shell/sidebar-search.module.css.d.ts +2 -0
- package/src/shell/sidebar-search.tsx +71 -2
- package/src/state/fields.test.tsx +258 -0
- package/src/state/fields.ts +892 -0
- package/src/state/persisted.ts +211 -0
- package/src/state.ts +46 -240
- package/src/styles.css +13 -0
- package/src/surfaces.ts +203 -36
- package/src/theme/controls.module.css +16 -1
- package/src/theme/ctl-theme.test.tsx +275 -0
- package/src/theme/ctl-theme.tsx +138 -0
- package/src/theme/ctl-tier-coverage.test.ts +170 -0
- package/src/theme/density-relations.test.ts +106 -51
- package/src/theme/divider-law.test.ts +259 -0
- package/src/theme/icon-slot.module.css +47 -0
- package/src/theme/icon-slot.module.css.d.ts +8 -0
- package/src/theme/icon-slot.test.tsx +275 -0
- package/src/theme/icon-slot.tsx +59 -0
- package/src/theme/index.ts +165 -3
- package/src/theme/layout-rhythm-css.test.ts +72 -0
- package/src/theme/radius.test.ts +11 -1
- package/src/theme/segmented-control.module.css +20 -0
- package/src/theme/shadow-surfaces.test.ts +20 -7
- package/src/theme/spacing.test.ts +75 -27
- package/src/theme/use-basalt-spacing.ts +2 -2
- package/src/tokens/density.test.ts +7 -25
- package/src/tokens/index.ts +48 -8
- package/src/tokens/palette.ts +107 -73
- package/src/widget-header/delta-badge.test.tsx +60 -0
- package/src/widget-header/delta-badge.tsx +57 -0
- package/src/widget-header/index.ts +2 -0
- package/src/widget-header/no-mantine.test.ts +33 -0
- package/src/widget-header/widget-header.module.css +335 -0
- package/src/widget-header/widget-header.module.css.d.ts +24 -0
- package/src/widget-header/widget-header.test.tsx +187 -0
- package/src/widget-header/widget-header.tsx +168 -0
- package/agent/rules/basalt-agent.md +0 -452
- package/agent/rules/basalt-app.md +0 -246
- package/agent/rules/basalt-commands.md +0 -320
- package/agent/rules/basalt-content.md +0 -409
- package/agent/rules/basalt-data.md +0 -269
- package/agent/rules/basalt-forms.md +0 -186
- package/agent/rules/basalt-notifications.md +0 -264
- package/agent/rules/basalt-query.md +0 -187
- package/agent/rules/basalt-router.md +0 -336
- package/dist/content/article-filter-bar.d.ts +0 -40
- package/dist/content/article-filter-bar.d.ts.map +0 -1
- package/dist/content/article-filter-bar.js +0 -56
- package/dist/shell/page-header.d.ts +0 -13
- package/dist/shell/page-header.d.ts.map +0 -1
- package/dist/shell/page-header.js +0 -23
- package/src/content/article-filter-bar.tsx +0 -88
- package/src/shell/page-header.tsx +0 -35
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
|
/**
|
|
@@ -171,15 +260,43 @@ export type BasaltDataTableProps<T> = {
|
|
|
171
260
|
// oxlint-disable-next-line typescript/no-explicit-any -- TanStack-idiomatic heterogeneous column array
|
|
172
261
|
columns: ColumnDef<T, any>[]
|
|
173
262
|
/**
|
|
174
|
-
* Enable column sorting via clickable headers.
|
|
263
|
+
* Enable column sorting via clickable headers. Client-side — the rows in `data` are what get
|
|
264
|
+
* reordered.
|
|
265
|
+
*
|
|
266
|
+
* Under `manualPagination` that is one page, so leaving this on without also passing
|
|
267
|
+
* `manualSorting` is a contract violation and throws in dev — see `manualPagination`.
|
|
175
268
|
* @default true
|
|
176
269
|
*/
|
|
177
270
|
enableSorting?: boolean
|
|
271
|
+
/**
|
|
272
|
+
* Hand sorting to the server: the table stops reordering `data` locally and renders it in the
|
|
273
|
+
* order given, while the headers keep toggling `SortingState` and reporting it through
|
|
274
|
+
* `onSortingChange` — which is where you re-request the sorted page.
|
|
275
|
+
*
|
|
276
|
+
* This is the required companion to `manualPagination` whenever sort headers are live; without
|
|
277
|
+
* it a server-paginated table sorts one page and the "of N" bar presents that as a sort of all N.
|
|
278
|
+
* @default false
|
|
279
|
+
* @example
|
|
280
|
+
* <BasaltDataTable
|
|
281
|
+
* data={page.rows}
|
|
282
|
+
* columns={columns}
|
|
283
|
+
* enablePagination
|
|
284
|
+
* manualPagination
|
|
285
|
+
* manualSorting
|
|
286
|
+
* rowCount={page.total}
|
|
287
|
+
* onSortingChange={(s) => refetch({ sort: s[0] })}
|
|
288
|
+
* />
|
|
289
|
+
*/
|
|
290
|
+
manualSorting?: boolean
|
|
178
291
|
/** Stripe alternate rows. Forwarded to Mantine `Table` — `'odd'`/`'even'` pick the phase. */
|
|
179
292
|
striped?: boolean | 'odd' | 'even'
|
|
180
293
|
/** Highlight hovered rows. Forwarded to Mantine `Table`. */
|
|
181
294
|
highlightOnHover?: boolean
|
|
182
|
-
/**
|
|
295
|
+
/**
|
|
296
|
+
* Rendered when no rows are VISIBLE — `data` is empty, or a filter/page matches none of it.
|
|
297
|
+
* Falls back to a simple message when omitted. (Before 1.25.0 this tracked `data.length` alone,
|
|
298
|
+
* so a search matching nothing rendered a blank `<tbody>` with no message at all.)
|
|
299
|
+
*/
|
|
183
300
|
emptyState?: ReactNode
|
|
184
301
|
/**
|
|
185
302
|
* When true, renders skeleton placeholder rows instead of the empty-state branch.
|
|
@@ -234,18 +351,31 @@ export type BasaltDataTableProps<T> = {
|
|
|
234
351
|
/** Called whenever the internal global filter value changes. */
|
|
235
352
|
onGlobalFilterChange?: (value: string) => void
|
|
236
353
|
/**
|
|
237
|
-
* Faceted column filters, rendered in the toolbar as
|
|
238
|
-
* to TanStack's per-column `columnFilters` state. The toolbar
|
|
239
|
-
* 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.
|
|
240
357
|
* @example
|
|
241
358
|
* facets={[{ columnId: 'department', label: 'Department', options: departmentOptions }]}
|
|
242
359
|
*/
|
|
243
360
|
facets?: DataTableFacet[]
|
|
361
|
+
/** Called whenever the faceted column filters change — the seam for server-side faceting. */
|
|
362
|
+
onColumnFiltersChange?: (filters: ColumnFiltersState) => void
|
|
363
|
+
/**
|
|
364
|
+
* Hand filtering to the server: the table stops narrowing `data` locally and renders every row
|
|
365
|
+
* given, while the search input and facets keep reporting through `onGlobalFilterChange` /
|
|
366
|
+
* `onColumnFiltersChange` — which is where you re-request the filtered page.
|
|
367
|
+
*
|
|
368
|
+
* Required alongside `manualPagination` whenever `enableGlobalFilter` or `facets` is set;
|
|
369
|
+
* without it the controls narrow one page while "of N" keeps counting the whole set.
|
|
370
|
+
* @default false
|
|
371
|
+
*/
|
|
372
|
+
manualFiltering?: boolean
|
|
244
373
|
/**
|
|
245
374
|
* Right-aligned toolbar slot (e.g. an "Export" button). Renders the toolbar row even when no
|
|
246
|
-
* 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.
|
|
247
377
|
*/
|
|
248
|
-
|
|
378
|
+
actions?: ReactNode
|
|
249
379
|
|
|
250
380
|
// ── Pagination ────────────────────────────────────────────────────────────────
|
|
251
381
|
|
|
@@ -272,6 +402,21 @@ export type BasaltDataTableProps<T> = {
|
|
|
272
402
|
* Disables local pagination slicing — pass the already-paginated page of `data` plus `rowCount`
|
|
273
403
|
* (and optionally `pageCount`) for server-driven pagination. `onPaginationChange` is where you
|
|
274
404
|
* fetch the next page.
|
|
405
|
+
*
|
|
406
|
+
* **It imposes a contract, enforced at render.** `data` is now one page, but the pagination bar
|
|
407
|
+
* says "Showing 1–25 of 412" — so every OTHER client-side control becomes a claim about 412 rows
|
|
408
|
+
* that it can only make about 25. Each has to be resolved explicitly, because only the call site
|
|
409
|
+
* knows whether the server does the work:
|
|
410
|
+
*
|
|
411
|
+
* - `rowCount` (or `pageCount`) must be given, or "of N" counts the page and the pager collapses
|
|
412
|
+
* to a single page nobody can leave;
|
|
413
|
+
* - sorting must be handed over with `manualSorting`, or switched off with `enableSorting={false}`;
|
|
414
|
+
* - `enableGlobalFilter` / `facets`, if used, must be handed over with `manualFiltering`;
|
|
415
|
+
* - `enablePagination` must be on, or `manualPagination` is inert and the page renders as if it
|
|
416
|
+
* were the whole table.
|
|
417
|
+
*
|
|
418
|
+
* Unresolved, each throws in dev and degrades to the honest render in production (no sort
|
|
419
|
+
* headers, no filter controls, no "of N") — never to the plausible wrong answer.
|
|
275
420
|
* @default false
|
|
276
421
|
* @example
|
|
277
422
|
* <BasaltDataTable
|
|
@@ -334,9 +479,20 @@ export type BasaltDataTableProps<T> = {
|
|
|
334
479
|
/** Row separators. Forwarded to Mantine `Table`. @default true (Mantine's own default) */
|
|
335
480
|
withRowBorders?: boolean
|
|
336
481
|
/**
|
|
337
|
-
* Outer table border
|
|
338
|
-
*
|
|
339
|
-
*
|
|
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
|
|
340
496
|
*/
|
|
341
497
|
withTableBorder?: boolean
|
|
342
498
|
}
|
|
@@ -373,6 +529,23 @@ const RANGE_LABEL_STYLE: CSSProperties = {
|
|
|
373
529
|
|
|
374
530
|
const DEFAULT_PAGE_SIZE_OPTIONS = [10, 25, 50, 100]
|
|
375
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
|
+
|
|
376
549
|
// ── Column pinning helpers ────────────────────────────────────────────────────
|
|
377
550
|
|
|
378
551
|
// Merges the left/center/right header-group triad TanStack exposes for pinning into the single
|
|
@@ -423,14 +596,110 @@ function getPinnedCellStyle<T>(
|
|
|
423
596
|
}
|
|
424
597
|
}
|
|
425
598
|
|
|
599
|
+
// ── The manual-pagination contract ────────────────────────────────────────────
|
|
600
|
+
|
|
601
|
+
/** The house dev gate (`src/provider`, `src/charts/kinds/BandStrip` use the same expression):
|
|
602
|
+
* `basaltViteConfig` defines `process.env.NODE_ENV`, so a production bundle constant-folds this to
|
|
603
|
+
* `false` and drops the throw. Read per call, never hoisted, so a test can flip it. */
|
|
604
|
+
function isDev(): boolean {
|
|
605
|
+
return process.env['NODE_ENV'] !== 'production'
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/** One client-side control that `manualPagination` turns into a claim the table cannot support. */
|
|
609
|
+
type ManualPaginationBreach = 'inert' | 'total' | 'sorting' | 'filtering'
|
|
610
|
+
|
|
611
|
+
const BREACH_REMEDY: Record<ManualPaginationBreach, string> = {
|
|
612
|
+
inert:
|
|
613
|
+
'`enablePagination` is not set, so `manualPagination` never reaches the table and the server ' +
|
|
614
|
+
'page renders as if it were the whole set, with no bar to say otherwise. Pass ' +
|
|
615
|
+
'`enablePagination`, or drop `manualPagination`.',
|
|
616
|
+
total:
|
|
617
|
+
'neither `rowCount` nor `pageCount` was given, so "of N" counts only the rows on this page and ' +
|
|
618
|
+
'the pager collapses to one page nobody can leave. Pass `rowCount={total}`.',
|
|
619
|
+
sorting:
|
|
620
|
+
'client-side sorting is still armed. It reorders only the rows in `data` — one page — while the ' +
|
|
621
|
+
'header chevron and "of N" together present it as a sort of all of them. Pass `manualSorting` ' +
|
|
622
|
+
'and sort in the request you make from `onSortingChange`, or `enableSorting={false}`.',
|
|
623
|
+
filtering:
|
|
624
|
+
'the search input / facets still filter client-side. They narrow only the rows in `data` — one ' +
|
|
625
|
+
'page — while "of N" keeps counting the whole set. Pass `manualFiltering` and filter in the ' +
|
|
626
|
+
'request you make from `onGlobalFilterChange` / `onColumnFiltersChange`, or drop ' +
|
|
627
|
+
'`enableGlobalFilter` / `facets`.',
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Every way the props on hand contradict `manualPagination`. Computed from PROPS only — never from
|
|
632
|
+
* the table instance, whose `getCanSort()` would read back the very `enableSorting` this decides.
|
|
633
|
+
*/
|
|
634
|
+
function manualPaginationBreaches(config: {
|
|
635
|
+
columns: readonly { enableSorting?: boolean }[]
|
|
636
|
+
enablePagination: boolean
|
|
637
|
+
manualPagination: boolean
|
|
638
|
+
rowCount: number | undefined
|
|
639
|
+
pageCount: number | undefined
|
|
640
|
+
enableSorting: boolean
|
|
641
|
+
manualSorting: boolean
|
|
642
|
+
enableGlobalFilter: boolean
|
|
643
|
+
hasFacets: boolean
|
|
644
|
+
manualFiltering: boolean
|
|
645
|
+
}): ManualPaginationBreach[] {
|
|
646
|
+
if (!config.manualPagination) return []
|
|
647
|
+
if (!config.enablePagination) return ['inert']
|
|
648
|
+
const breaches: ManualPaginationBreach[] = []
|
|
649
|
+
if (config.rowCount === undefined && config.pageCount === undefined) breaches.push('total')
|
|
650
|
+
const anyColumnMaySort = config.columns.some((column) => column.enableSorting !== false)
|
|
651
|
+
if (config.enableSorting && anyColumnMaySort && !config.manualSorting) breaches.push('sorting')
|
|
652
|
+
if ((config.enableGlobalFilter || config.hasFacets) && !config.manualFiltering)
|
|
653
|
+
breaches.push('filtering')
|
|
654
|
+
return breaches
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function manualPaginationMessage(breaches: ManualPaginationBreach[]): string {
|
|
658
|
+
return (
|
|
659
|
+
'BasaltDataTable: `manualPagination` is set, so `data` is one server page — but:\n · ' +
|
|
660
|
+
breaches.map((breach) => BREACH_REMEDY[breach]).join('\n · ') +
|
|
661
|
+
'\nEach has to be resolved explicitly: only the call site knows whether the server does the ' +
|
|
662
|
+
'work, so there is no default that is not a guess about the data.'
|
|
663
|
+
)
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/** Bounded by the sixteen possible breach sets, so it cannot grow with render count. */
|
|
667
|
+
const reportedBreaches = new Set<string>()
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Dev throws; production reports once and lets the caller degrade.
|
|
671
|
+
*
|
|
672
|
+
* Split deliberately from BandStrip's datum/prop rule rather than copied. That rule throws on a bad
|
|
673
|
+
* PROP because a prop is authored, deterministic, and caught the first time the component renders —
|
|
674
|
+
* all true here. What differs is that a bad `meta.align` has no honest render (every alignment we
|
|
675
|
+
* could pick might be the wrong one, and would look right in review), whereas this one does: a
|
|
676
|
+
* table with no sort headers, no filter controls and no "of N" asserts nothing false. Throwing in
|
|
677
|
+
* production would add no correctness the degradation does not already give, and would convert a
|
|
678
|
+
* misreport on a rarely-visited page into a blank one. The report is what keeps the degradation
|
|
679
|
+
* from being its own silence.
|
|
680
|
+
*/
|
|
681
|
+
function enforceManualPaginationContract(breaches: ManualPaginationBreach[]): void {
|
|
682
|
+
if (breaches.length === 0) return
|
|
683
|
+
const message = manualPaginationMessage(breaches)
|
|
684
|
+
if (isDev()) throw new Error(message)
|
|
685
|
+
if (reportedBreaches.has(message)) return
|
|
686
|
+
reportedBreaches.add(message)
|
|
687
|
+
console.error(message)
|
|
688
|
+
}
|
|
689
|
+
|
|
426
690
|
// ── Component ─────────────────────────────────────────────────────────────────
|
|
427
691
|
|
|
428
692
|
/**
|
|
429
693
|
* A sortable data table backed by TanStack Table and rendered with Mantine primitives.
|
|
430
694
|
* Generic over `T` — column types are inferred from `ColumnDef<T>`.
|
|
431
695
|
*
|
|
432
|
-
* Sorting is local/client-side (no `any`)
|
|
433
|
-
* is read-only. An `emptyState` node is rendered
|
|
696
|
+
* Sorting is local/client-side by default (no `any`); `manualSorting` hands it to the server.
|
|
697
|
+
* When `enableSorting` is `false` the table is read-only. An `emptyState` node is rendered
|
|
698
|
+
* whenever no rows are visible — empty `data`, or a filter that matches none of it.
|
|
699
|
+
*
|
|
700
|
+
* `manualPagination` imposes a contract on the rest of the props (see its docblock): every
|
|
701
|
+
* client-side control it would turn into a false claim about the unseen rows must be handed to the
|
|
702
|
+
* server or switched off, and an unresolved one throws in dev rather than misreporting.
|
|
434
703
|
*
|
|
435
704
|
* Toolbar (search + facets), pagination, and column pinning are opt-in chrome layered on top —
|
|
436
705
|
* see the corresponding props below. None of them change rendering unless explicitly enabled.
|
|
@@ -446,9 +715,13 @@ function getPinnedCellStyle<T>(
|
|
|
446
715
|
* />
|
|
447
716
|
*/
|
|
448
717
|
export function BasaltDataTable<T>({
|
|
718
|
+
title,
|
|
719
|
+
icon,
|
|
720
|
+
subtitle,
|
|
449
721
|
data,
|
|
450
722
|
columns,
|
|
451
723
|
enableSorting = true,
|
|
724
|
+
manualSorting = false,
|
|
452
725
|
striped,
|
|
453
726
|
highlightOnHover,
|
|
454
727
|
emptyState,
|
|
@@ -462,7 +735,9 @@ export function BasaltDataTable<T>({
|
|
|
462
735
|
initialGlobalFilter,
|
|
463
736
|
onGlobalFilterChange,
|
|
464
737
|
facets,
|
|
465
|
-
|
|
738
|
+
onColumnFiltersChange,
|
|
739
|
+
manualFiltering = false,
|
|
740
|
+
actions,
|
|
466
741
|
enablePagination = false,
|
|
467
742
|
pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS,
|
|
468
743
|
initialPagination,
|
|
@@ -479,7 +754,7 @@ export function BasaltDataTable<T>({
|
|
|
479
754
|
verticalSpacing,
|
|
480
755
|
horizontalSpacing,
|
|
481
756
|
withRowBorders,
|
|
482
|
-
withTableBorder =
|
|
757
|
+
withTableBorder = false,
|
|
483
758
|
}: BasaltDataTableProps<T>) {
|
|
484
759
|
const [sorting, setSorting] = useState<SortingState>(initialSorting ?? [])
|
|
485
760
|
const [globalFilter, setGlobalFilter] = useState(initialGlobalFilter ?? '')
|
|
@@ -511,6 +786,17 @@ export function BasaltDataTable<T>({
|
|
|
511
786
|
[onGlobalFilterChange],
|
|
512
787
|
)
|
|
513
788
|
|
|
789
|
+
const handleColumnFiltersChange = useCallback(
|
|
790
|
+
(updater: Updater<ColumnFiltersState>) => {
|
|
791
|
+
setColumnFilters((prev) => {
|
|
792
|
+
const next = typeof updater === 'function' ? updater(prev) : updater
|
|
793
|
+
onColumnFiltersChange?.(next)
|
|
794
|
+
return next
|
|
795
|
+
})
|
|
796
|
+
},
|
|
797
|
+
[onColumnFiltersChange],
|
|
798
|
+
)
|
|
799
|
+
|
|
514
800
|
const handlePaginationChange = useCallback(
|
|
515
801
|
(updater: Updater<PaginationState>) => {
|
|
516
802
|
setPagination((prev) => {
|
|
@@ -550,6 +836,27 @@ export function BasaltDataTable<T>({
|
|
|
550
836
|
})
|
|
551
837
|
}, [columns, facets])
|
|
552
838
|
|
|
839
|
+
// Enforced before the table is built, so the degraded options below are the ones TanStack gets —
|
|
840
|
+
// a degraded table must not merely hide a control, it must not compute the answer either.
|
|
841
|
+
const hasFacets = Boolean(facets && facets.length > 0)
|
|
842
|
+
const breaches = manualPaginationBreaches({
|
|
843
|
+
columns,
|
|
844
|
+
enablePagination,
|
|
845
|
+
manualPagination,
|
|
846
|
+
rowCount,
|
|
847
|
+
pageCount,
|
|
848
|
+
enableSorting,
|
|
849
|
+
manualSorting,
|
|
850
|
+
enableGlobalFilter,
|
|
851
|
+
hasFacets,
|
|
852
|
+
manualFiltering,
|
|
853
|
+
})
|
|
854
|
+
enforceManualPaginationContract(breaches)
|
|
855
|
+
// Unreachable in dev (the line above threw); in production these are the honest fallbacks.
|
|
856
|
+
const sortingEnabled = enableSorting && !breaches.includes('sorting')
|
|
857
|
+
const filteringEnabled = !breaches.includes('filtering')
|
|
858
|
+
const totalIsAuthoritative = !breaches.includes('total')
|
|
859
|
+
|
|
553
860
|
const table = useReactTable<T>({
|
|
554
861
|
data,
|
|
555
862
|
columns: tableColumns,
|
|
@@ -562,25 +869,36 @@ export function BasaltDataTable<T>({
|
|
|
562
869
|
},
|
|
563
870
|
onSortingChange: handleSortingChange,
|
|
564
871
|
onGlobalFilterChange: handleGlobalFilterChange,
|
|
565
|
-
onColumnFiltersChange:
|
|
872
|
+
onColumnFiltersChange: handleColumnFiltersChange,
|
|
566
873
|
onColumnPinningChange: setColumnPinning,
|
|
567
874
|
...(enablePagination && { onPaginationChange: handlePaginationChange }),
|
|
568
875
|
getCoreRowModel: getCoreRowModel(),
|
|
569
876
|
getSortedRowModel: getSortedRowModel(),
|
|
570
877
|
getFilteredRowModel: getFilteredRowModel(),
|
|
878
|
+
// Both row models stay installed and are BYPASSED by these flags, which is TanStack's own
|
|
879
|
+
// seam — so a degraded table renders `data` in the order and completeness the server sent it.
|
|
880
|
+
// Forced only by a BREACH, never by `enableSorting={false}` on its own: that pair still applies
|
|
881
|
+
// an `initialSorting` today (it disables the header control, not the row model) and a table
|
|
882
|
+
// that is not misreporting has no business losing it.
|
|
883
|
+
manualSorting: manualSorting || breaches.includes('sorting'),
|
|
884
|
+
manualFiltering: manualFiltering || breaches.includes('filtering'),
|
|
571
885
|
...(enablePagination && {
|
|
572
886
|
getPaginationRowModel: getPaginationRowModel(),
|
|
573
887
|
manualPagination,
|
|
574
888
|
...(manualPagination && rowCount !== undefined && { rowCount }),
|
|
575
889
|
...(manualPagination && pageCount !== undefined && { pageCount }),
|
|
576
890
|
}),
|
|
577
|
-
enableSorting,
|
|
891
|
+
enableSorting: sortingEnabled,
|
|
578
892
|
enableColumnPinning: enablePinning,
|
|
579
893
|
})
|
|
580
894
|
|
|
581
|
-
const
|
|
582
|
-
|
|
895
|
+
const showSearch = enableGlobalFilter && filteringEnabled
|
|
896
|
+
const showFacets = hasFacets && filteringEnabled
|
|
897
|
+
const showToolbar = showSearch || showFacets || Boolean(actions)
|
|
583
898
|
const headerGroups = enablePinning ? getOrderedHeaderGroups(table) : table.getHeaderGroups()
|
|
899
|
+
// The rows actually rendered, not `data` — a filter or a page index that matches nothing leaves
|
|
900
|
+
// `data` non-empty while the body has nothing in it, and a blank body reads as a broken table.
|
|
901
|
+
const rows = table.getRowModel().rows
|
|
584
902
|
|
|
585
903
|
const tableNode = (
|
|
586
904
|
<Table
|
|
@@ -597,7 +915,7 @@ export function BasaltDataTable<T>({
|
|
|
597
915
|
{headerGroups.map((headerGroup) => (
|
|
598
916
|
<Table.Tr key={headerGroup.id}>
|
|
599
917
|
{headerGroup.headers.map((header) => {
|
|
600
|
-
const canSort =
|
|
918
|
+
const canSort = sortingEnabled && header.column.getCanSort()
|
|
601
919
|
const sorted = header.column.getIsSorted()
|
|
602
920
|
const toggleSorting = header.column.getToggleSortingHandler()
|
|
603
921
|
const pinnedStyle = enablePinning
|
|
@@ -654,7 +972,7 @@ export function BasaltDataTable<T>({
|
|
|
654
972
|
))}
|
|
655
973
|
</Table.Tr>
|
|
656
974
|
))
|
|
657
|
-
) :
|
|
975
|
+
) : rows.length === 0 ? (
|
|
658
976
|
<Table.Tr>
|
|
659
977
|
<Table.Td colSpan={columns.length}>
|
|
660
978
|
{emptyState ?? (
|
|
@@ -665,7 +983,7 @@ export function BasaltDataTable<T>({
|
|
|
665
983
|
</Table.Td>
|
|
666
984
|
</Table.Tr>
|
|
667
985
|
) : (
|
|
668
|
-
|
|
986
|
+
rows.map((row) => {
|
|
669
987
|
const cells = enablePinning
|
|
670
988
|
? [
|
|
671
989
|
...row.getLeftVisibleCells(),
|
|
@@ -709,62 +1027,81 @@ export function BasaltDataTable<T>({
|
|
|
709
1027
|
const scrolls = maxHeight !== undefined || minWidth !== undefined
|
|
710
1028
|
|
|
711
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` }))
|
|
712
1037
|
const total = table.getRowCount()
|
|
713
1038
|
const rangeStart = total === 0 ? 0 : paginationState.pageIndex * paginationState.pageSize + 1
|
|
714
1039
|
const rangeEnd = Math.min((paginationState.pageIndex + 1) * paginationState.pageSize, total)
|
|
715
1040
|
|
|
716
1041
|
return (
|
|
717
1042
|
<>
|
|
718
|
-
{
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
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()}
|
|
730
1064
|
/>
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
<MultiSelect
|
|
739
|
-
key={facet.columnId}
|
|
740
|
-
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
|
|
741
1072
|
radius="md"
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
w={200}
|
|
1073
|
+
w={SEARCH_WIDTH}
|
|
1074
|
+
placeholder={globalFilterPlaceholder}
|
|
1075
|
+
leftSection={searchIcon}
|
|
1076
|
+
value={globalFilter}
|
|
1077
|
+
onChange={(event) => handleGlobalFilterChange(event.currentTarget.value)}
|
|
748
1078
|
/>
|
|
749
|
-
)
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
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
|
+
)}
|
|
768
1105
|
</Group>
|
|
769
1106
|
)}
|
|
770
1107
|
{scrolls ? (
|
|
@@ -783,20 +1120,18 @@ export function BasaltDataTable<T>({
|
|
|
783
1120
|
{enablePagination && (
|
|
784
1121
|
<Group justify="space-between" mt="xs" wrap="wrap" gap="xs" align="center">
|
|
785
1122
|
<Text style={RANGE_LABEL_STYLE}>
|
|
786
|
-
Showing {rangeStart}–{rangeEnd}
|
|
1123
|
+
Showing {rangeStart}–{rangeEnd}
|
|
1124
|
+
{totalIsAuthoritative ? ` of ${total}` : ''}
|
|
787
1125
|
</Text>
|
|
788
1126
|
<Group gap="xs" align="center">
|
|
789
1127
|
<Select
|
|
790
|
-
size="
|
|
1128
|
+
size="ctl"
|
|
791
1129
|
radius="md"
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
label: `${size} / page`,
|
|
795
|
-
}))}
|
|
1130
|
+
w={PAGE_SIZE_WIDTH}
|
|
1131
|
+
data={pageSizeRows}
|
|
796
1132
|
value={String(paginationState.pageSize)}
|
|
797
1133
|
onChange={(value) => value && table.setPageSize(Number(value))}
|
|
798
1134
|
allowDeselect={false}
|
|
799
|
-
w={110}
|
|
800
1135
|
/>
|
|
801
1136
|
<Pagination
|
|
802
1137
|
size="sm"
|