basalt-ui 1.25.0 → 1.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +8 -6
- package/CHANGELOG.md +26 -0
- package/MIGRATING.md +815 -23
- package/README.md +45 -32
- package/agent/rules/basalt-batteries.md +205 -0
- package/agent/rules/basalt-charts.md +125 -555
- package/agent/rules/basalt-controls.md +160 -0
- package/agent/rules/basalt-mantine.md +146 -439
- package/agent/rules/basalt-state.md +118 -96
- package/agent/rules/basalt-tokens.md +126 -288
- package/agent/skills/basalt-app/SKILL.md +70 -195
- package/agent/skills/basalt-charts/SKILL.md +54 -234
- package/agent/skills/basalt-design/SKILL.md +56 -248
- package/agent/templates/CLAUDE-block.md.tpl +31 -40
- package/agent/templates/DESIGN.md.tpl +22 -51
- package/configs/oxlint-plugin.js +1465 -36
- package/configs/oxlint-plugin.test.ts +1190 -11
- package/configs/oxlint.json +11 -3
- package/dist/charts/cursor/resolve.d.ts +18 -0
- package/dist/charts/cursor/resolve.d.ts.map +1 -1
- package/dist/charts/cursor/resolve.js +15 -6
- package/dist/charts/cursor/scope.d.ts +6 -3
- package/dist/charts/cursor/scope.d.ts.map +1 -1
- package/dist/charts/cursor/store.d.ts +5 -1
- package/dist/charts/cursor/store.d.ts.map +1 -1
- package/dist/charts/cursor/store.js +4 -4
- package/dist/charts/hooks/useChartCursor.d.ts.map +1 -1
- package/dist/charts/hooks/useChartCursor.js +9 -8
- package/dist/charts/index.d.ts +2 -2
- package/dist/charts/index.d.ts.map +1 -1
- package/dist/charts/primitives/ChartCard.d.ts +34 -9
- package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
- package/dist/charts/primitives/ChartCard.js +29 -114
- package/dist/charts/primitives/ChartCard.test.d.ts +2 -0
- package/dist/charts/primitives/ChartCard.test.d.ts.map +1 -0
- package/dist/charts/sparklines/BarSparkline.d.ts +26 -2
- package/dist/charts/sparklines/BarSparkline.d.ts.map +1 -1
- package/dist/charts/sparklines/BarSparkline.js +18 -4
- package/dist/cli/check-coverage.test.d.ts +2 -0
- package/dist/cli/check-coverage.test.d.ts.map +1 -0
- package/dist/cli/index.d.ts +67 -4
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +225 -7
- package/dist/connectivity/connectivity-indicator.d.ts +4 -1
- package/dist/connectivity/connectivity-indicator.d.ts.map +1 -1
- package/dist/connectivity/connectivity-indicator.js +1 -1
- package/dist/content/index.d.ts +5 -3
- package/dist/content/index.d.ts.map +1 -1
- package/dist/content/index.js +4 -2
- package/dist/content/prose.module.css +7 -28
- package/dist/controls/actions.d.ts +169 -0
- package/dist/controls/actions.d.ts.map +1 -0
- package/dist/controls/actions.js +287 -0
- package/dist/controls/actions.module.css +62 -0
- package/dist/controls/actions.module.css.d.ts +13 -0
- package/dist/controls/actions.test.d.ts +2 -0
- package/dist/controls/actions.test.d.ts.map +1 -0
- package/dist/controls/actions.type-guard.test.d.ts +2 -0
- package/dist/controls/actions.type-guard.test.d.ts.map +1 -0
- package/dist/controls/compare-filter.d.ts +41 -0
- package/dist/controls/compare-filter.d.ts.map +1 -0
- package/dist/controls/compare-filter.js +23 -0
- package/dist/controls/control-group.d.ts +19 -0
- package/dist/controls/control-group.d.ts.map +1 -0
- package/dist/controls/control-group.js +20 -0
- package/dist/controls/control-group.test.d.ts +2 -0
- package/dist/controls/control-group.test.d.ts.map +1 -0
- package/dist/controls/controls.module.css +542 -0
- package/dist/controls/controls.module.css.d.ts +35 -0
- package/dist/controls/controls.router.test.d.ts +2 -0
- package/dist/controls/controls.router.test.d.ts.map +1 -0
- package/dist/controls/enum-filter.d.ts +39 -0
- package/dist/controls/enum-filter.d.ts.map +1 -0
- package/dist/controls/enum-filter.js +93 -0
- package/dist/controls/filter-context.d.ts +60 -0
- package/dist/controls/filter-context.d.ts.map +1 -0
- package/dist/controls/filter-context.js +69 -0
- package/dist/controls/filter-pill.d.ts +28 -0
- package/dist/controls/filter-pill.d.ts.map +1 -0
- package/dist/controls/filter-pill.js +70 -0
- package/dist/controls/filter-set.d.ts +13 -0
- package/dist/controls/filter-set.d.ts.map +1 -0
- package/dist/controls/filter-set.js +125 -0
- package/dist/controls/filter-set.test.d.ts +2 -0
- package/dist/controls/filter-set.test.d.ts.map +1 -0
- package/dist/controls/filter-sheet.d.ts +117 -0
- package/dist/controls/filter-sheet.d.ts.map +1 -0
- package/dist/controls/filter-sheet.js +146 -0
- package/dist/controls/glyphs.d.ts +27 -0
- package/dist/controls/glyphs.d.ts.map +1 -0
- package/dist/controls/glyphs.js +49 -0
- package/dist/controls/index.d.ts +20 -0
- package/dist/controls/index.d.ts.map +1 -0
- package/dist/controls/index.js +46 -0
- package/dist/controls/multi-select-filter.d.ts +23 -0
- package/dist/controls/multi-select-filter.d.ts.map +1 -0
- package/dist/controls/multi-select-filter.js +89 -0
- package/dist/controls/number-filter.d.ts +26 -0
- package/dist/controls/number-filter.d.ts.map +1 -0
- package/dist/controls/number-filter.js +143 -0
- package/dist/controls/peer-boundary.test.d.ts +2 -0
- package/dist/controls/peer-boundary.test.d.ts.map +1 -0
- package/dist/controls/props-contract.test.d.ts +2 -0
- package/dist/controls/props-contract.test.d.ts.map +1 -0
- package/dist/controls/range-filter.d.ts +43 -0
- package/dist/controls/range-filter.d.ts.map +1 -0
- package/dist/controls/range-filter.js +135 -0
- package/dist/controls/range-filter.type-guard.test.d.ts +2 -0
- package/dist/controls/range-filter.type-guard.test.d.ts.map +1 -0
- package/dist/controls/search-filter.d.ts +10 -0
- package/dist/controls/search-filter.d.ts.map +1 -0
- package/dist/controls/search-filter.js +81 -0
- package/dist/controls/select-filter.d.ts +74 -0
- package/dist/controls/select-filter.d.ts.map +1 -0
- package/dist/controls/select-filter.js +10 -0
- package/dist/controls/sync-button.d.ts +38 -0
- package/dist/controls/sync-button.d.ts.map +1 -0
- package/dist/controls/sync-button.js +119 -0
- package/dist/controls/sync-button.module.css +63 -0
- package/dist/controls/sync-button.module.css.d.ts +11 -0
- package/dist/controls/sync-button.test.d.ts +2 -0
- package/dist/controls/sync-button.test.d.ts.map +1 -0
- package/dist/controls/toggle-filter.d.ts +9 -0
- package/dist/controls/toggle-filter.d.ts.map +1 -0
- package/dist/controls/toggle-filter.js +42 -0
- package/dist/controls/view-tabs.d.ts +23 -0
- package/dist/controls/view-tabs.d.ts.map +1 -0
- package/dist/controls/view-tabs.js +59 -0
- package/dist/controls-dates/date-range-picker.d.ts +4 -0
- package/dist/controls-dates/date-range-picker.d.ts.map +1 -0
- package/dist/controls-dates/date-range-picker.js +18 -0
- package/dist/controls-dates/index.d.ts +9 -0
- package/dist/controls-dates/index.d.ts.map +1 -0
- package/dist/controls-dates/index.js +4 -0
- package/dist/dashboard/delta-badge.d.ts +6 -12
- package/dist/dashboard/delta-badge.d.ts.map +1 -1
- package/dist/dashboard/delta-badge.js +1 -50
- package/dist/dashboard/index.d.ts +1 -1
- package/dist/dashboard/index.d.ts.map +1 -1
- package/dist/dashboard/index.js +3 -1
- package/dist/dashboard/settings-section.d.ts +6 -4
- package/dist/dashboard/settings-section.d.ts.map +1 -1
- package/dist/dashboard/settings-section.js +23 -16
- package/dist/dashboard/settings-section.module.css +10 -0
- package/dist/dashboard/settings-section.module.css.d.ts +1 -0
- package/dist/dashboard/settings-section.test.d.ts +2 -0
- package/dist/dashboard/settings-section.test.d.ts.map +1 -0
- package/dist/dashboard/stat-card.d.ts +95 -9
- package/dist/dashboard/stat-card.d.ts.map +1 -1
- package/dist/dashboard/stat-card.js +108 -49
- package/dist/dashboard/stat-card.module.css +153 -0
- package/dist/dashboard/stat-card.module.css.d.ts +15 -0
- package/dist/data/data-table.d.ts +37 -15
- package/dist/data/data-table.d.ts.map +1 -1
- package/dist/data/data-table.js +96 -58
- package/dist/guard/grace.test.d.ts +20 -0
- package/dist/guard/grace.test.d.ts.map +1 -0
- package/dist/guard/index.d.ts +66 -3
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +78 -7
- package/dist/guard/types.d.ts +3 -3
- package/dist/guard/types.d.ts.map +1 -1
- package/dist/index.d.ts +13 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -10
- package/dist/nav/types.d.ts +78 -2
- package/dist/nav/types.d.ts.map +1 -1
- package/dist/notifications/bell.d.ts +4 -1
- package/dist/notifications/bell.d.ts.map +1 -1
- package/dist/notifications/index.d.ts +1 -1
- package/dist/router-tanstack/field.d.ts +14 -0
- package/dist/router-tanstack/field.d.ts.map +1 -0
- package/dist/router-tanstack/field.js +4 -0
- package/dist/router-tanstack/index.d.ts +6 -2
- package/dist/router-tanstack/index.d.ts.map +1 -1
- package/dist/router-tanstack/index.js +4 -0
- package/dist/router-tanstack/multi-search-param-store.d.ts +4 -52
- package/dist/router-tanstack/multi-search-param-store.d.ts.map +1 -1
- package/dist/router-tanstack/multi-search-param-store.js +19 -27
- package/dist/router-tanstack/search-param-store.d.ts +3 -87
- package/dist/router-tanstack/search-param-store.d.ts.map +1 -1
- package/dist/router-tanstack/search-param-store.js +16 -45
- package/dist/router-tanstack/search-store.d.ts +84 -0
- package/dist/router-tanstack/search-store.d.ts.map +1 -0
- package/dist/router-tanstack/search-store.js +271 -0
- package/dist/router-tanstack/search-store.router.test.d.ts +2 -0
- package/dist/router-tanstack/search-store.router.test.d.ts.map +1 -0
- package/dist/router-tanstack/search-store.test.d.ts +2 -0
- package/dist/router-tanstack/search-store.test.d.ts.map +1 -0
- package/dist/router-tanstack/use-nav.d.ts +0 -2
- package/dist/router-tanstack/use-nav.d.ts.map +1 -1
- package/dist/router-tanstack/use-nav.js +34 -12
- package/dist/router-tanstack/use-nav.test.d.ts +2 -0
- package/dist/router-tanstack/use-nav.test.d.ts.map +1 -0
- package/dist/section/index.d.ts +2 -0
- package/dist/section/index.d.ts.map +1 -0
- package/dist/section/index.js +4 -0
- package/dist/section/section.d.ts +24 -0
- package/dist/section/section.d.ts.map +1 -0
- package/dist/section/section.js +126 -0
- package/dist/section/section.module.css +104 -0
- package/dist/section/section.module.css.d.ts +11 -0
- package/dist/section/section.test.d.ts +2 -0
- package/dist/section/section.test.d.ts.map +1 -0
- package/dist/shell/app-breadcrumbs.d.ts.map +1 -1
- package/dist/shell/app-breadcrumbs.js +18 -15
- package/dist/shell/app-header.module.css +77 -37
- package/dist/shell/app-header.module.css.d.ts +2 -1
- package/dist/shell/app-mobile-nav.d.ts +9 -2
- package/dist/shell/app-mobile-nav.d.ts.map +1 -1
- package/dist/shell/app-mobile-nav.js +98 -11
- package/dist/shell/app-mobile-nav.module.css +8 -0
- package/dist/shell/app-mobile-nav.module.css.d.ts +1 -0
- package/dist/shell/app-sidebar.d.ts +55 -28
- package/dist/shell/app-sidebar.d.ts.map +1 -1
- package/dist/shell/app-sidebar.js +116 -67
- package/dist/shell/app-sidebar.module.css +303 -20
- package/dist/shell/app-sidebar.module.css.d.ts +19 -1
- package/dist/shell/index.d.ts +55 -21
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +40 -18
- package/dist/shell/mobile-nav-model.d.ts +13 -2
- package/dist/shell/mobile-nav-model.d.ts.map +1 -1
- package/dist/shell/mobile-nav-model.js +15 -1
- package/dist/shell/nav-count-badge.d.ts +1 -1
- package/dist/shell/page-bar.d.ts +51 -0
- package/dist/shell/page-bar.d.ts.map +1 -0
- package/dist/shell/page-bar.js +154 -0
- package/dist/shell/page-bar.module.css +178 -0
- package/dist/shell/page-bar.module.css.d.ts +17 -0
- package/dist/shell/page-bar.test.d.ts +2 -0
- package/dist/shell/page-bar.test.d.ts.map +1 -0
- package/dist/shell/sidebar-block-model.d.ts +48 -0
- package/dist/shell/sidebar-block-model.d.ts.map +1 -0
- package/dist/shell/sidebar-block-model.js +39 -0
- package/dist/shell/sidebar-blocks.d.ts +32 -0
- package/dist/shell/sidebar-blocks.d.ts.map +1 -0
- package/dist/shell/sidebar-blocks.js +169 -0
- package/dist/shell/sidebar-search.d.ts +13 -1
- package/dist/shell/sidebar-search.d.ts.map +1 -1
- package/dist/shell/sidebar-search.js +34 -1
- package/dist/shell/sidebar-search.module.css +26 -0
- package/dist/shell/sidebar-search.module.css.d.ts +2 -0
- package/dist/state/fields.d.ts +565 -0
- package/dist/state/fields.d.ts.map +1 -0
- package/dist/state/fields.js +405 -0
- package/dist/state/fields.test.d.ts +2 -0
- package/dist/state/fields.test.d.ts.map +1 -0
- package/dist/state/persisted.d.ts +60 -0
- package/dist/state/persisted.d.ts.map +1 -0
- package/dist/state/persisted.js +123 -0
- package/dist/state/persisted.test.d.ts +2 -0
- package/dist/state/persisted.test.d.ts.map +1 -0
- package/dist/state.d.ts +11 -56
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +14 -120
- package/dist/styles.css +13 -0
- package/dist/surfaces.d.ts +106 -24
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +156 -19
- package/dist/theme/controls.module.css +16 -1
- package/dist/theme/ctl-theme.d.ts +32 -0
- package/dist/theme/ctl-theme.d.ts.map +1 -0
- package/dist/theme/ctl-theme.js +54 -0
- package/dist/theme/ctl-theme.test.d.ts +2 -0
- package/dist/theme/ctl-theme.test.d.ts.map +1 -0
- package/dist/theme/ctl-tier-coverage.test.d.ts +2 -0
- package/dist/theme/ctl-tier-coverage.test.d.ts.map +1 -0
- package/dist/theme/divider-law.test.d.ts +2 -0
- package/dist/theme/divider-law.test.d.ts.map +1 -0
- package/dist/theme/icon-slot.d.ts +44 -0
- package/dist/theme/icon-slot.d.ts.map +1 -0
- package/dist/theme/icon-slot.js +21 -0
- package/dist/theme/icon-slot.module.css +47 -0
- package/dist/theme/icon-slot.module.css.d.ts +8 -0
- package/dist/theme/icon-slot.test.d.ts +2 -0
- package/dist/theme/icon-slot.test.d.ts.map +1 -0
- package/dist/theme/index.d.ts +1 -0
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/index.js +82 -5
- package/dist/theme/layout-rhythm-css.test.d.ts +2 -0
- package/dist/theme/layout-rhythm-css.test.d.ts.map +1 -0
- package/dist/theme/segmented-control.module.css +20 -0
- package/dist/theme/shadow-surfaces.test.d.ts.map +1 -1
- package/dist/tokens/index.d.ts +12 -0
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +38 -8
- package/dist/tokens/palette.d.ts +97 -61
- package/dist/tokens/palette.d.ts.map +1 -1
- package/dist/tokens/palette.js +76 -38
- package/dist/tokens.css +10 -2
- package/dist/widget-header/delta-badge.d.ts +13 -0
- package/dist/widget-header/delta-badge.d.ts.map +1 -0
- package/dist/widget-header/delta-badge.js +29 -0
- package/dist/widget-header/delta-badge.test.d.ts +2 -0
- package/dist/widget-header/delta-badge.test.d.ts.map +1 -0
- package/dist/widget-header/index.d.ts +3 -0
- package/dist/widget-header/index.d.ts.map +1 -0
- package/dist/widget-header/index.js +6 -0
- package/dist/widget-header/no-mantine.test.d.ts +2 -0
- package/dist/widget-header/no-mantine.test.d.ts.map +1 -0
- package/dist/widget-header/widget-header.d.ts +67 -0
- package/dist/widget-header/widget-header.d.ts.map +1 -0
- package/dist/widget-header/widget-header.js +89 -0
- package/dist/widget-header/widget-header.module.css +350 -0
- package/dist/widget-header/widget-header.module.css.d.ts +25 -0
- package/dist/widget-header/widget-header.test.d.ts +2 -0
- package/dist/widget-header/widget-header.test.d.ts.map +1 -0
- package/llms.txt +30 -17
- package/package.json +14 -1
- package/src/charts/cursor/resolve.test.ts +29 -1
- package/src/charts/cursor/resolve.ts +38 -6
- package/src/charts/cursor/scope.tsx +6 -3
- package/src/charts/cursor/store.test.ts +19 -1
- package/src/charts/cursor/store.ts +10 -5
- package/src/charts/hooks/useChartCursor.ts +16 -8
- package/src/charts/index.ts +2 -2
- package/src/charts/primitives/CartesianChart.test.tsx +124 -0
- package/src/charts/primitives/ChartCard.test.tsx +66 -0
- package/src/charts/primitives/ChartCard.tsx +86 -146
- package/src/charts/sparklines/BarSparkline.tsx +43 -5
- package/src/cli/check-coverage.test.ts +297 -0
- package/src/cli/check-theme.test.ts +38 -14
- package/src/cli/index.ts +392 -16
- package/src/cli/init-sync.test.ts +97 -0
- package/src/connectivity/connectivity-indicator.tsx +16 -9
- package/src/content/index.ts +16 -3
- package/src/content/prose.module.css +7 -28
- package/src/content/prose.module.css.test.ts +8 -24
- package/src/controls/actions.module.css +62 -0
- package/src/controls/actions.module.css.d.ts +13 -0
- package/src/controls/actions.test.tsx +252 -0
- package/src/controls/actions.tsx +655 -0
- package/src/controls/actions.type-guard.test.ts +63 -0
- package/src/controls/compare-filter.tsx +64 -0
- package/src/controls/control-group.test.tsx +229 -0
- package/src/controls/control-group.tsx +63 -0
- package/src/controls/controls.module.css +542 -0
- package/src/controls/controls.module.css.d.ts +35 -0
- package/src/controls/controls.router.test.tsx +1053 -0
- package/src/controls/enum-filter.tsx +170 -0
- package/src/controls/filter-context.tsx +158 -0
- package/src/controls/filter-pill.tsx +120 -0
- package/src/controls/filter-set.test.tsx +472 -0
- package/src/controls/filter-set.tsx +225 -0
- package/src/controls/filter-sheet.tsx +285 -0
- package/src/controls/glyphs.tsx +81 -0
- package/src/controls/index.ts +49 -0
- package/src/controls/multi-select-filter.tsx +140 -0
- package/src/controls/number-filter.tsx +291 -0
- package/src/controls/peer-boundary.test.ts +85 -0
- package/src/controls/props-contract.test.tsx +137 -0
- package/src/controls/range-filter.tsx +237 -0
- package/src/controls/range-filter.type-guard.test.tsx +122 -0
- package/src/controls/search-filter.tsx +123 -0
- package/src/controls/select-filter.tsx +89 -0
- package/src/controls/sync-button.module.css +63 -0
- package/src/controls/sync-button.module.css.d.ts +11 -0
- package/src/controls/sync-button.test.tsx +183 -0
- package/src/controls/sync-button.tsx +197 -0
- package/src/controls/toggle-filter.tsx +60 -0
- package/src/controls/view-tabs.tsx +110 -0
- package/src/controls-dates/date-range-picker.tsx +42 -0
- package/src/controls-dates/index.ts +8 -0
- package/src/dashboard/delta-badge.tsx +4 -74
- package/src/dashboard/index.ts +6 -1
- package/src/dashboard/settings-section.module.css +10 -0
- package/src/dashboard/settings-section.module.css.d.ts +1 -0
- package/src/dashboard/settings-section.test.tsx +56 -0
- package/src/dashboard/settings-section.tsx +33 -30
- package/src/dashboard/stat-card.module.css +153 -0
- package/src/dashboard/stat-card.module.css.d.ts +15 -0
- package/src/dashboard/stat-card.test.tsx +298 -1
- package/src/dashboard/stat-card.tsx +289 -67
- package/src/data/data-table.test.tsx +96 -11
- package/src/data/data-table.tsx +228 -84
- package/src/guard/check-source.test.ts +159 -13
- package/src/guard/grace.test.ts +158 -0
- package/src/guard/index.ts +185 -29
- package/src/guard/types.ts +4 -2
- package/src/index.ts +51 -5
- package/src/nav/types.ts +84 -2
- package/src/notifications/bell.tsx +6 -3
- package/src/notifications/index.ts +1 -1
- package/src/router-tanstack/field.ts +35 -0
- package/src/router-tanstack/index.ts +28 -2
- package/src/router-tanstack/multi-search-param-store.test.ts +71 -3
- package/src/router-tanstack/multi-search-param-store.ts +46 -77
- package/src/router-tanstack/search-param-store.ts +42 -159
- package/src/router-tanstack/search-store.router.test.tsx +882 -0
- package/src/router-tanstack/search-store.test.ts +578 -0
- package/src/router-tanstack/search-store.ts +546 -0
- package/src/router-tanstack/use-nav.test.tsx +187 -0
- package/src/router-tanstack/use-nav.ts +72 -14
- package/src/section/index.ts +1 -0
- package/src/section/section.module.css +104 -0
- package/src/section/section.module.css.d.ts +11 -0
- package/src/section/section.test.tsx +204 -0
- package/src/section/section.tsx +220 -0
- package/src/shell/app-breadcrumbs.tsx +44 -31
- package/src/shell/app-header.module.css +77 -37
- package/src/shell/app-header.module.css.d.ts +2 -1
- package/src/shell/app-mobile-nav.module.css +8 -0
- package/src/shell/app-mobile-nav.module.css.d.ts +1 -0
- package/src/shell/app-mobile-nav.tsx +147 -8
- package/src/shell/app-sidebar-account.tsx +2 -2
- package/src/shell/app-sidebar.module.css +303 -20
- package/src/shell/app-sidebar.module.css.d.ts +19 -1
- package/src/shell/app-sidebar.test.tsx +456 -41
- package/src/shell/app-sidebar.tsx +276 -140
- package/src/shell/index.test.tsx +291 -5
- package/src/shell/index.tsx +137 -38
- package/src/shell/mobile-nav-model.test.ts +228 -7
- package/src/shell/mobile-nav-model.ts +53 -3
- package/src/shell/nav-count-badge.tsx +1 -1
- package/src/shell/page-bar.module.css +178 -0
- package/src/shell/page-bar.module.css.d.ts +17 -0
- package/src/shell/page-bar.test.tsx +316 -0
- package/src/shell/page-bar.tsx +315 -0
- package/src/shell/sidebar-block-model.ts +83 -0
- package/src/shell/sidebar-blocks.tsx +301 -0
- package/src/shell/sidebar-search.module.css +26 -0
- package/src/shell/sidebar-search.module.css.d.ts +2 -0
- package/src/shell/sidebar-search.tsx +71 -2
- package/src/state/fields.test.tsx +512 -0
- package/src/state/fields.ts +1150 -0
- package/src/state/persisted.test.ts +91 -0
- package/src/state/persisted.ts +261 -0
- package/src/state.ts +52 -240
- package/src/styles.css +13 -0
- package/src/surfaces.ts +204 -36
- package/src/theme/controls.module.css +16 -1
- package/src/theme/ctl-theme.test.tsx +275 -0
- package/src/theme/ctl-theme.tsx +138 -0
- package/src/theme/ctl-tier-coverage.test.ts +170 -0
- package/src/theme/density-relations.test.ts +106 -51
- package/src/theme/divider-law.test.ts +259 -0
- package/src/theme/icon-slot.module.css +47 -0
- package/src/theme/icon-slot.module.css.d.ts +8 -0
- package/src/theme/icon-slot.test.tsx +275 -0
- package/src/theme/icon-slot.tsx +59 -0
- package/src/theme/index.ts +165 -3
- package/src/theme/layout-rhythm-css.test.ts +72 -0
- package/src/theme/radius.test.ts +11 -1
- package/src/theme/segmented-control.module.css +20 -0
- package/src/theme/shadow-surfaces.test.ts +20 -7
- package/src/theme/spacing.test.ts +75 -27
- package/src/theme/use-basalt-spacing.ts +2 -2
- package/src/tokens/density.test.ts +7 -25
- package/src/tokens/index.ts +48 -8
- package/src/tokens/palette.ts +107 -73
- package/src/widget-header/delta-badge.test.tsx +60 -0
- package/src/widget-header/delta-badge.tsx +57 -0
- package/src/widget-header/index.ts +2 -0
- package/src/widget-header/no-mantine.test.ts +33 -0
- package/src/widget-header/widget-header.module.css +350 -0
- package/src/widget-header/widget-header.module.css.d.ts +25 -0
- package/src/widget-header/widget-header.test.tsx +187 -0
- package/src/widget-header/widget-header.tsx +213 -0
- package/agent/rules/basalt-agent.md +0 -452
- package/agent/rules/basalt-app.md +0 -246
- package/agent/rules/basalt-commands.md +0 -320
- package/agent/rules/basalt-content.md +0 -409
- package/agent/rules/basalt-data.md +0 -329
- package/agent/rules/basalt-forms.md +0 -186
- package/agent/rules/basalt-notifications.md +0 -264
- package/agent/rules/basalt-query.md +0 -187
- package/agent/rules/basalt-router.md +0 -336
- package/dist/content/article-filter-bar.d.ts +0 -40
- package/dist/content/article-filter-bar.d.ts.map +0 -1
- package/dist/content/article-filter-bar.js +0 -56
- package/dist/shell/page-header.d.ts +0 -13
- package/dist/shell/page-header.d.ts.map +0 -1
- package/dist/shell/page-header.js +0 -23
- package/src/content/article-filter-bar.tsx +0 -88
- package/src/shell/page-header.tsx +0 -35
|
@@ -1,329 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
source: basalt-ui
|
|
3
|
-
description: Data table and virtual list adapter battery from basalt-ui/data — BasaltDataTable (sortable TanStack Table + Mantine Table) and BasaltVirtualList (TanStack Virtual + Mantine Box). Both packages are optional peers.
|
|
4
|
-
paths:
|
|
5
|
-
- 'src/**'
|
|
6
|
-
- 'apps/**/src/**'
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# Basalt Data
|
|
10
|
-
|
|
11
|
-
basalt-ui ships `./data` — a Mantine-coupled data adapter battery providing `BasaltDataTable` and
|
|
12
|
-
`BasaltVirtualList` backed by headless TanStack Table and TanStack Virtual respectively. Both
|
|
13
|
-
optional peers must be installed before use:
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
bun add @tanstack/react-table @tanstack/react-virtual
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
## BasaltDataTable
|
|
20
|
-
|
|
21
|
-
A generic sortable data table over `@tanstack/react-table`, rendered with Mantine `Table`
|
|
22
|
-
primitives (`Table.Thead/Tbody/Tr/Th/Td`). Client-side sorting is built in via `useState`; no
|
|
23
|
-
server-side wiring required.
|
|
24
|
-
|
|
25
|
-
```tsx
|
|
26
|
-
import { BasaltDataTable, createColumnHelper } from 'basalt-ui/data'
|
|
27
|
-
|
|
28
|
-
type User = { name: string; email: string; age: number }
|
|
29
|
-
|
|
30
|
-
const col = createColumnHelper<User>()
|
|
31
|
-
const columns = [
|
|
32
|
-
col.accessor('name', { header: 'Name' }),
|
|
33
|
-
col.accessor('email', { header: 'Email' }),
|
|
34
|
-
col.accessor('age', { header: 'Age' }),
|
|
35
|
-
]
|
|
36
|
-
|
|
37
|
-
<BasaltDataTable
|
|
38
|
-
data={users}
|
|
39
|
-
columns={columns}
|
|
40
|
-
enableSorting // default true — clickable sort headers with ↑/↓ indicators
|
|
41
|
-
striped
|
|
42
|
-
highlightOnHover
|
|
43
|
-
emptyState={<Text c="dimmed">No users found.</Text>}
|
|
44
|
-
/>
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
### Props
|
|
48
|
-
|
|
49
|
-
| Prop | Type | Default | Notes |
|
|
50
|
-
| ------------------ | --------------------------- | -------- | ------------------------------------------------------ |
|
|
51
|
-
| `data` | `T[]` | required | Row data array |
|
|
52
|
-
| `columns` | `ColumnDef<T>[]` | required | TanStack column definitions |
|
|
53
|
-
| `enableSorting` | `boolean` | `true` | Clickable headers, asc/desc toggle |
|
|
54
|
-
| `striped` | `boolean` | — | Forwarded to Mantine `Table` |
|
|
55
|
-
| `highlightOnHover` | `boolean` | — | Forwarded to Mantine `Table` |
|
|
56
|
-
| `emptyState` | `ReactNode` | — | Shown when `data` is empty and not loading |
|
|
57
|
-
| `isLoading` | `boolean` | `false` | Shows skeleton rows instead of empty-state/data |
|
|
58
|
-
| `skeletonRows` | `number` | `5` | Number of skeleton rows when `isLoading` is true |
|
|
59
|
-
| `initialSorting` | `SortingState` | `[]` | Initial sort state; drives `useState` initializer |
|
|
60
|
-
| `onSortingChange` | `(s: SortingState) => void` | — | Called whenever sorting changes; omit for uncontrolled |
|
|
61
|
-
|
|
62
|
-
Body chrome — all conditional pass-throughs to Mantine `Table` except `withTableBorder`, which
|
|
63
|
-
basalt defaults to `true`:
|
|
64
|
-
|
|
65
|
-
| Prop | Type | Notes |
|
|
66
|
-
| --------------------------------------- | ------------------------------- | -------------------------------------------------------------------- |
|
|
67
|
-
| `maxHeight` | `number \| string` | Caps the body height — renders `Table.ScrollContainer type="native"` |
|
|
68
|
-
| `minWidth` | `number \| string` | Same container; `maxHeight` alone passes `minWidth={0}` |
|
|
69
|
-
| `stickyHeader` / `stickyHeaderOffset` | `boolean` / `number \| string` | Forwarded to Mantine; pair with `maxHeight` |
|
|
70
|
-
| `verticalSpacing` / `horizontalSpacing` | `MantineSpacing` | Cell padding |
|
|
71
|
-
| `withRowBorders` / `withTableBorder` | `boolean` | `withTableBorder` defaults `true` here, overriding Mantine's `false` |
|
|
72
|
-
| `meta: { align }` (per column) | `'left' \| 'center' \| 'right'` | Sets `textAlign` on BOTH the `th` and the `td` |
|
|
73
|
-
| `meta: { numeral: false }` (per column) | `boolean` | Opts a numeric cell OUT of the mono-numeral style. Opt-out only |
|
|
74
|
-
|
|
75
|
-
`meta.align` is a `ColumnMeta` module augmentation, so a typo'd key is a tsc error and a value
|
|
76
|
-
outside the union **throws** naming the column — a money column never silently left-aligns. Set it
|
|
77
|
-
once on the column def instead of repeating `textAlign: 'right'` on the header and the cell.
|
|
78
|
-
|
|
79
|
-
**`meta.numeral: false` is needed less often than it looks.** The auto style sets `fontFamily`,
|
|
80
|
-
`fontSize`, `fontWeight`, `fontVariantNumeric` and `color` on the `<td>` whenever the raw value is a
|
|
81
|
-
number — but a child `Text` sets its own `color` and `fontWeight` and wins those, and never sets a
|
|
82
|
-
`fontFamily`. **Only `fontFamily` leaks.** So `c="dimmed"` is not a reason to opt out (and on a
|
|
83
|
-
percentage column the mono figures are what you want); `fw={600}` on an accent figure is, because
|
|
84
|
-
the `<td>`'s monospace still overrides the family the `Text` inherited. One consumer measured this
|
|
85
|
-
across three tables: **3 columns** genuinely needed the opt-out, not the 4 that were assumed. Reach
|
|
86
|
-
for it when the cell renders its own chrome — a coloured `Text`, a `Badge`, a sparkline — and check
|
|
87
|
-
what actually leaked before adding it. `null` is worth a thought too: `typeof null === 'object'`, so
|
|
88
|
-
a `number | null` column goes mono only on its non-null rows, which is worse than uniformly wrong.
|
|
89
|
-
|
|
90
|
-
### Controlled sorting and URL sync
|
|
91
|
-
|
|
92
|
-
`initialSorting` seeds the internal `useState` and `onSortingChange` is called on every sort
|
|
93
|
-
change — the table stays internally managed (uncontrolled) when `onSortingChange` is omitted.
|
|
94
|
-
|
|
95
|
-
To sync sort state with TanStack Router search params:
|
|
96
|
-
|
|
97
|
-
```tsx
|
|
98
|
-
import { type SortingState } from 'basalt-ui/data'
|
|
99
|
-
|
|
100
|
-
// In the route definition (validated search params):
|
|
101
|
-
// sortBy: z.string().optional(), sortDir: z.enum(['asc','desc']).optional()
|
|
102
|
-
|
|
103
|
-
function UsersTable() {
|
|
104
|
-
const { sortBy, sortDir } = Route.useSearch()
|
|
105
|
-
const navigate = Route.useNavigate()
|
|
106
|
-
|
|
107
|
-
const initialSorting: SortingState = sortBy ? [{ id: sortBy, desc: sortDir === 'desc' }] : []
|
|
108
|
-
|
|
109
|
-
return (
|
|
110
|
-
<BasaltDataTable
|
|
111
|
-
data={users}
|
|
112
|
-
columns={columns}
|
|
113
|
-
initialSorting={initialSorting}
|
|
114
|
-
onSortingChange={(s) => {
|
|
115
|
-
const col = s[0]
|
|
116
|
-
navigate({
|
|
117
|
-
search: (prev) => ({
|
|
118
|
-
...prev,
|
|
119
|
-
sortBy: col?.id,
|
|
120
|
-
sortDir: col?.desc ? 'desc' : 'asc',
|
|
121
|
-
}),
|
|
122
|
-
})
|
|
123
|
-
}}
|
|
124
|
-
/>
|
|
125
|
-
)
|
|
126
|
-
}
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
### Column definitions
|
|
130
|
-
|
|
131
|
-
Use `createColumnHelper<T>()` (re-exported from `basalt-ui/data`) for a typed accessor builder.
|
|
132
|
-
`ColumnDef<T>` is generic over the row type only — `TValue` defaults to `unknown`, so no `any`
|
|
133
|
-
is needed.
|
|
134
|
-
|
|
135
|
-
```ts
|
|
136
|
-
import { createColumnHelper, type ColumnDef } from 'basalt-ui/data'
|
|
137
|
-
|
|
138
|
-
type Row = { id: number; label: string }
|
|
139
|
-
const col = createColumnHelper<Row>()
|
|
140
|
-
|
|
141
|
-
// Accessor column — infers the value type from the key
|
|
142
|
-
col.accessor('label', { header: 'Label' })
|
|
143
|
-
|
|
144
|
-
// Display column — custom renderer, no accessor
|
|
145
|
-
col.display({ id: 'actions', header: 'Actions', cell: (ctx) => <Button>Delete {ctx.row.original.id}</Button> })
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
### No `any`
|
|
149
|
-
|
|
150
|
-
`ColumnDef<T>` defaults `TValue` to `unknown`, not `any`. Never cast column defs to `any` — use
|
|
151
|
-
the correct `ColumnDef<T>` type or `ColumnDef<T, unknown>`.
|
|
152
|
-
|
|
153
|
-
### Also shipped, and easy to miss
|
|
154
|
-
|
|
155
|
-
Global search (`enableGlobalFilter`, `globalFilterPlaceholder`, `searchIcon`,
|
|
156
|
-
`initialGlobalFilter`, `onGlobalFilterChange`), faceted filters (`facets`), a toolbar slot
|
|
157
|
-
(`toolbarActions`), pagination client- or server-side (`enablePagination`, `pageSizeOptions`,
|
|
158
|
-
`initialPagination`, `onPaginationChange`, `manualPagination`, `rowCount`, `pageCount`) and column
|
|
159
|
-
pinning (`enablePinning`, `initialColumnPinning`). **Don't hand-roll any of these** — read the props
|
|
160
|
-
on the type, they are all there.
|
|
161
|
-
|
|
162
|
-
### `manualPagination` imposes a contract on the rest of the props
|
|
163
|
-
|
|
164
|
-
Adopting it makes `data` **one server page** while the bar still reads `Showing 1–25 of 412`. Every
|
|
165
|
-
other client-side control then becomes a claim about 412 rows it can only make about 25 — a sort
|
|
166
|
-
that reorders one page under a header chevron that says it sorted all of them is a plausible, wrong
|
|
167
|
-
answer with nothing on screen to give it away. So each control has to be resolved explicitly; there
|
|
168
|
-
is no safe default, because only the call site knows whether the server does the work.
|
|
169
|
-
|
|
170
|
-
| With `manualPagination` | Resolve it with |
|
|
171
|
-
| ------------------------- | ----------------------------------------------------------------------------------- |
|
|
172
|
-
| the pagination bar itself | `enablePagination` (without it `manualPagination` is inert) + `rowCount` |
|
|
173
|
-
| sorting | `manualSorting` + sort in the `onSortingChange` request, or `enableSorting={false}` |
|
|
174
|
-
| `enableGlobalFilter` | `manualFiltering` + filter in the `onGlobalFilterChange` request |
|
|
175
|
-
| `facets` | `manualFiltering` + `onColumnFiltersChange` |
|
|
176
|
-
|
|
177
|
-
```tsx
|
|
178
|
-
<BasaltDataTable
|
|
179
|
-
data={page.rows}
|
|
180
|
-
columns={columns}
|
|
181
|
-
enablePagination
|
|
182
|
-
manualPagination
|
|
183
|
-
rowCount={page.total}
|
|
184
|
-
manualSorting
|
|
185
|
-
onSortingChange={(s) => refetch({ sort: s[0] })}
|
|
186
|
-
onPaginationChange={(p) => refetch({ page: p })}
|
|
187
|
-
/>
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
`manualSorting` / `manualFiltering` don't switch the controls off — the headers and inputs stay
|
|
191
|
-
live and keep reporting through their `on*Change` callbacks. They stop the table from _recomputing_
|
|
192
|
-
the answer locally, so what renders is the order and the selection the server sent.
|
|
193
|
-
|
|
194
|
-
**Unresolved, it is not silent.** A contradiction throws in dev naming every breach at once, and a
|
|
195
|
-
production bundle (where the throw is constant-folded away) degrades to the honest table instead:
|
|
196
|
-
no sort headers, no filter controls, no `of N` it cannot stand behind, plus one `console.error`.
|
|
197
|
-
You will never get the plausible wrong answer, but you also won't get sorting you thought you had —
|
|
198
|
-
so read the throw, don't work around it.
|
|
199
|
-
|
|
200
|
-
Still deferred: row selection, row expansion / sub-rows, and fully controlled (external) sorting
|
|
201
|
-
STATE — `manualSorting` hands the _work_ to the server, but the table still owns the `SortingState`
|
|
202
|
-
and reports it through `onSortingChange`. Add them when a concrete consumer need arises; keep the API
|
|
203
|
-
additive.
|
|
204
|
-
|
|
205
|
-
### The blessed lane vs the raw escape hatch
|
|
206
|
-
|
|
207
|
-
`BasaltDataTable` (from `basalt-ui/data/table`) is the **blessed, opinionated grid** — batteries
|
|
208
|
-
included (sorting, loading skeletons, empty state, Mantine `Table` chrome) and the default choice
|
|
209
|
-
for a data grid in a basalt-ui app.
|
|
210
|
-
|
|
211
|
-
For a bespoke table shape `BasaltDataTable` doesn't cover, `basalt-ui/data/table` also re-exports
|
|
212
|
-
the raw TanStack Table primitives as the documented **escape hatch**: `useReactTable`, `flexRender`,
|
|
213
|
-
`getCoreRowModel`, `getSortedRowModel`, `createColumnHelper`, and the `ColumnDef` type — the full
|
|
214
|
-
surface for constructing and rendering a fully custom table, with no direct `@tanstack/react-table`
|
|
215
|
-
import in consumer code:
|
|
216
|
-
|
|
217
|
-
```tsx
|
|
218
|
-
import {
|
|
219
|
-
createColumnHelper,
|
|
220
|
-
flexRender,
|
|
221
|
-
getCoreRowModel,
|
|
222
|
-
useReactTable,
|
|
223
|
-
} from 'basalt-ui/data/table'
|
|
224
|
-
|
|
225
|
-
type Row = { name: string; score: number }
|
|
226
|
-
const col = createColumnHelper<Row>()
|
|
227
|
-
const columns = [
|
|
228
|
-
col.accessor('name', { header: 'Name' }),
|
|
229
|
-
col.accessor('score', { header: 'Score' }),
|
|
230
|
-
]
|
|
231
|
-
|
|
232
|
-
const table = useReactTable({ data: rows, columns, getCoreRowModel: getCoreRowModel() })
|
|
233
|
-
// table.getHeaderGroups() / table.getRowModel().rows, rendered with flexRender(...) and your own markup
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
Reach for the escape hatch only when `BasaltDataTable`'s props genuinely can't express the shape.
|
|
237
|
-
Pagination, server-side data and filtering are **not** among those cases any more — see above.
|
|
238
|
-
|
|
239
|
-
**Both lanes scroll through the same node.** An escape-hatch table that needs a capped body wraps
|
|
240
|
-
it in `<Table.ScrollContainer type="native" minWidth={0} maxHeight={320}>` — and that is byte for
|
|
241
|
-
byte what `BasaltDataTable`'s `maxHeight`/`minWidth` render. The blessed lane and the escape cannot
|
|
242
|
-
contradict each other on scrolling, and neither should reach for `type="scrollarea"`: `ScrollArea`'s
|
|
243
|
-
custom viewport is the positioning context a sticky `<thead>` resolves against, so the default type
|
|
244
|
-
pins the header to the page viewport instead of the table's box. (`basalt/raw-scroll-container`
|
|
245
|
-
takes no view here — it steers raw `overflow: auto`, and `Table.ScrollContainer` is a Mantine
|
|
246
|
-
component, not a raw scroll box.)
|
|
247
|
-
|
|
248
|
-
**Adopt the blessed lane for ownership, not for a line count.** Porting one consumer's three tables
|
|
249
|
-
onto these props made them 341 → **370–379** lines, **29–38 longer**: a scratch port measured the
|
|
250
|
-
low end, the consumer's own shipped port the high end, because it kept the comment blocks
|
|
251
|
-
justifying each table's `numeral` opt-outs and its `enableSorting` decision. Accessor blocks at
|
|
252
|
-
4–6 lines each cost more than an eight-`<Table.Td>` row at ~3 when every cell is bespoke, and the
|
|
253
|
-
opt-outs are lines the hand-rolled version never had to write. Budget the high end. What the port
|
|
254
|
-
bought was the `type="native"` footgun, alignment stated once instead of on both `th` and `td` —
|
|
255
|
-
**4 literals in one table alone, 6 across the two that had alignment to state** — and
|
|
256
|
-
sorting/filtering/pagination no longer being yours to maintain. Expect that trade, not a shrink.
|
|
257
|
-
|
|
258
|
-
That consumer deliberately left the third table hand-rolled, and the reason generalises: four of
|
|
259
|
-
its six columns would have needed `enableSorting: false` (three sparkline cells over `number[]`
|
|
260
|
-
accessors, one status enum), and a numeric column rendering its own coloured `Text` would have
|
|
261
|
-
needed a `numeral: false` it did not need before. A table with no sorting, no pagination, no filter
|
|
262
|
-
and no alignment to declare is a layout grid that happens to use `<table>` — adopting the component
|
|
263
|
-
there buys nothing and taxes you to switch off things you never asked for.
|
|
264
|
-
|
|
265
|
-
## BasaltVirtualList
|
|
266
|
-
|
|
267
|
-
A windowed virtual list backed by TanStack Virtual, rendered inside a Mantine `Box` scroll
|
|
268
|
-
container. Only the visible rows plus `overscan` are in the DOM at any time — suitable for lists
|
|
269
|
-
of 1 000+ items.
|
|
270
|
-
|
|
271
|
-
```tsx
|
|
272
|
-
import { Box } from '@mantine/core'
|
|
273
|
-
import { VX } from 'basalt-ui/tokens'
|
|
274
|
-
import { BasaltVirtualList } from 'basalt-ui/data'
|
|
275
|
-
|
|
276
|
-
const items = Array.from({ length: 10_000 }, (_, i) => ({ id: i, name: `Row ${i}` }))
|
|
277
|
-
|
|
278
|
-
<BasaltVirtualList
|
|
279
|
-
items={items}
|
|
280
|
-
height={400}
|
|
281
|
-
estimateSize={40}
|
|
282
|
-
overscan={5}
|
|
283
|
-
renderItem={(item, index) => (
|
|
284
|
-
<Box px="sm" py={8} style={{ borderBottom: `1px solid ${VX.divider}` }}>
|
|
285
|
-
{index + 1}. {item.name}
|
|
286
|
-
</Box>
|
|
287
|
-
)}
|
|
288
|
-
getItemKey={(item) => item.id}
|
|
289
|
-
/>
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
### Props
|
|
293
|
-
|
|
294
|
-
| Prop | Type | Default | Notes |
|
|
295
|
-
| -------------- | ---------------------------------------------- | -------- | ------------------------------------------------- |
|
|
296
|
-
| `items` | `T[]` | required | Full unsliced item list |
|
|
297
|
-
| `height` | `number \| string` | required | Scroll container height (px number or CSS string) |
|
|
298
|
-
| `estimateSize` | `number` | `40` | Estimated row height for layout (px) |
|
|
299
|
-
| `overscan` | `number` | `5` | Extra rows rendered beyond visible viewport |
|
|
300
|
-
| `renderItem` | `(item: T, index: number) => ReactNode` | required | Row render function |
|
|
301
|
-
| `getItemKey` | `(item: T, index: number) => string \| number` | — | Stable key (defaults to index) |
|
|
302
|
-
| `isLoading` | `boolean` | `false` | Shows skeleton rows at `estimateSize` height |
|
|
303
|
-
| `skeletonRows` | `number` | `5` | Number of skeleton rows when `isLoading` is true |
|
|
304
|
-
|
|
305
|
-
### useFlushSync: false (React 19)
|
|
306
|
-
|
|
307
|
-
Always pass `useFlushSync: false` to `useVirtualizer`. TanStack Virtual internally calls the
|
|
308
|
-
deprecated `flushSync` on scroll events; this opt-out disables that path and is required for React
|
|
309
|
-
19+ apps (silences a runtime warning). `BasaltVirtualList` sets this automatically — do not
|
|
310
|
-
override it.
|
|
311
|
-
|
|
312
|
-
### Absolute-position transform pattern
|
|
313
|
-
|
|
314
|
-
The virtual list uses the canonical TanStack Virtual render pattern:
|
|
315
|
-
|
|
316
|
-
1. **Scroll container** — fixed height, `overflow: auto`, holds a `ref` passed to `getScrollElement`.
|
|
317
|
-
2. **Inner sizer div** — height equals `getTotalSize()`, position `relative` — defines the total
|
|
318
|
-
scrollable area without rendering all rows.
|
|
319
|
-
3. **Virtual rows** — absolutely positioned (`position: absolute`, `top: 0`, `left: 0`) with
|
|
320
|
-
`transform: translateY(vi.start)` — each row "jumps" to its virtual position without relayout.
|
|
321
|
-
|
|
322
|
-
Never use `margin-top` or `top: vi.start` for positioning — the `transform` approach avoids
|
|
323
|
-
layout reflow and is the canonical pattern for TanStack Virtual.
|
|
324
|
-
|
|
325
|
-
### When to virtualize
|
|
326
|
-
|
|
327
|
-
Use `BasaltVirtualList` when the list exceeds ~200 rows. Below that, a plain Mantine `Stack` or
|
|
328
|
-
`Table` is cheaper (no virtualizer overhead, simpler DOM, easier accessibility). Above ~1 000
|
|
329
|
-
rows, virtualization is essential for scroll performance.
|
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
source: basalt-ui
|
|
3
|
-
description: Mantine form conventions for basalt-ui apps — useBasaltForm, field, FormErrorSummary, useFormDraft from the shipped ./forms battery. @mantine/form is an optional peer.
|
|
4
|
-
paths:
|
|
5
|
-
- 'src/**'
|
|
6
|
-
- 'apps/**/src/**'
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# Basalt Forms
|
|
10
|
-
|
|
11
|
-
basalt-ui ships `./forms` — a Mantine form adapter battery providing `useBasaltForm`, `field`,
|
|
12
|
-
`FormErrorSummary`, and `useFormDraft` on top of `@mantine/form`. `@mantine/form` is an **optional
|
|
13
|
-
peer** — install it explicitly before using this battery:
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
bun add @mantine/form
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
Valibot is the default validation library (install it too: `bun add valibot`). Zod 4 works if
|
|
20
|
-
already standardised in the project.
|
|
21
|
-
|
|
22
|
-
## useBasaltForm
|
|
23
|
-
|
|
24
|
-
`useBasaltForm` is a typed `useForm` wrapper that applies basalt defaults — `mode: 'uncontrolled'`
|
|
25
|
-
and `schemaResolver(schema, { sync: true })` when a schema is provided.
|
|
26
|
-
|
|
27
|
-
```ts
|
|
28
|
-
import * as v from 'valibot'
|
|
29
|
-
import { useBasaltForm, field } from 'basalt-ui/forms'
|
|
30
|
-
import { TextInput, NumberInput, Button } from '@mantine/core'
|
|
31
|
-
|
|
32
|
-
const Schema = v.object({
|
|
33
|
-
name: v.pipe(v.string(), v.minLength(2, 'Name must be at least 2 characters')),
|
|
34
|
-
email: v.pipe(v.string(), v.email('Enter a valid email')),
|
|
35
|
-
amount: v.pipe(v.number(), v.minValue(0, 'Amount must be non-negative')),
|
|
36
|
-
})
|
|
37
|
-
type Values = v.InferOutput<typeof Schema>
|
|
38
|
-
const INITIAL: Values = { name: '', email: '', amount: 0 }
|
|
39
|
-
|
|
40
|
-
function MyForm() {
|
|
41
|
-
const form = useBasaltForm({ initialValues: INITIAL, schema: Schema })
|
|
42
|
-
|
|
43
|
-
return (
|
|
44
|
-
<form onSubmit={form.onSubmit((values) => console.log(values))}>
|
|
45
|
-
<TextInput {...field(form, 'name')} label="Name" />
|
|
46
|
-
<TextInput {...field(form, 'email')} label="Email" />
|
|
47
|
-
<NumberInput {...field(form, 'amount')} label="Amount" />
|
|
48
|
-
<Button type="submit">Submit</Button>
|
|
49
|
-
</form>
|
|
50
|
-
)
|
|
51
|
-
}
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
- **`mode: 'uncontrolled'`** is the default — avoids per-keystroke re-renders. Pass
|
|
55
|
-
`mode: 'controlled'` if real-time derived state is needed.
|
|
56
|
-
- **Schema param** types against `StandardSchemaV1` from `basalt-ui` (vendored spec, not a package).
|
|
57
|
-
A real Valibot or Zod 4 schema is structurally assignable — no wrapper needed.
|
|
58
|
-
- **`{ sync: true }`** keeps `form.validate()` / `form.isValid()` synchronous for sync schemas.
|
|
59
|
-
Omit only when the schema has async refinements.
|
|
60
|
-
|
|
61
|
-
## field
|
|
62
|
-
|
|
63
|
-
`field(form, path)` bundles `getInputProps(path)` + `key(path)` into one spread — the two things
|
|
64
|
-
every Mantine v9 uncontrolled field requires:
|
|
65
|
-
|
|
66
|
-
```ts
|
|
67
|
-
<TextInput {...field(form, 'email')} label="Email" />
|
|
68
|
-
// equivalent to:
|
|
69
|
-
<TextInput {...form.getInputProps('email')} key={form.key('email')} label="Email" />
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
## schemaResolver options
|
|
73
|
-
|
|
74
|
-
- **`{ sync: true }`** — required for sync schemas (Zod 4, Valibot, ArkType). Keeps
|
|
75
|
-
`form.validate()` and `form.isValid()` synchronous.
|
|
76
|
-
- **Zod 4** — `import { z } from 'zod'`. Zod 4's root export implements Standard Schema natively.
|
|
77
|
-
- **Valibot** — `import * as v from 'valibot'`. No separate resolver needed; the object schema
|
|
78
|
-
is directly assignable to `StandardSchemaV1`.
|
|
79
|
-
- **ArkType**, **Effect/Schema** — also implement Standard Schema natively.
|
|
80
|
-
- Type schema params against `StandardSchemaV1` from `basalt-ui` (never `ZodSchema`).
|
|
81
|
-
|
|
82
|
-
## FormErrorSummary
|
|
83
|
-
|
|
84
|
-
An accessible error-summary component for long forms and screen readers. Renders a Mantine `Alert`
|
|
85
|
-
with `role="alert"` listing all field errors. Returns `null` when the form is clean.
|
|
86
|
-
|
|
87
|
-
```tsx
|
|
88
|
-
import { FormErrorSummary } from 'basalt-ui/forms'
|
|
89
|
-
|
|
90
|
-
// Place at the top of the form so screen readers announce errors on submit:
|
|
91
|
-
;<form onSubmit={form.onSubmit(handleSubmit)}>
|
|
92
|
-
<FormErrorSummary form={form} title="Fix these before submitting" />
|
|
93
|
-
{/* fields */}
|
|
94
|
-
</form>
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
- Place at the **top of the form** so AT focus moves to the summary on submit failure.
|
|
98
|
-
- `title` defaults to `"Please fix the following errors"`.
|
|
99
|
-
- Renders `null` (no DOM node) when `form.errors` is empty — safe to include unconditionally.
|
|
100
|
-
|
|
101
|
-
## useFormDraft
|
|
102
|
-
|
|
103
|
-
`useFormDraft` persists form values to `localStorage` via `createPersistedState` (versioned,
|
|
104
|
-
SSR-safe, cross-tab). It restores the draft on mount and exposes `clearDraft` + `saveDraft`.
|
|
105
|
-
|
|
106
|
-
```ts
|
|
107
|
-
import { useBasaltForm, field, FormErrorSummary, useFormDraft } from 'basalt-ui/forms'
|
|
108
|
-
import { Button, TextInput } from '@mantine/core'
|
|
109
|
-
|
|
110
|
-
const INITIAL: Values = { name: '', email: '' }
|
|
111
|
-
|
|
112
|
-
function EditForm() {
|
|
113
|
-
const { clearDraft, saveDraft } = useFormDraft(form, { key: 'edit-form', version: 1 })
|
|
114
|
-
|
|
115
|
-
// Wire saveDraft into useBasaltForm's onValuesChange for automatic autosave:
|
|
116
|
-
const form = useBasaltForm({
|
|
117
|
-
initialValues: INITIAL,
|
|
118
|
-
schema: Schema,
|
|
119
|
-
onValuesChange: saveDraft, // ← automatic autosave on every field change
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
return (
|
|
123
|
-
<form
|
|
124
|
-
onSubmit={form.onSubmit((values) => {
|
|
125
|
-
submit(values)
|
|
126
|
-
clearDraft() // ← clear on success
|
|
127
|
-
})}
|
|
128
|
-
>
|
|
129
|
-
<FormErrorSummary form={form} />
|
|
130
|
-
<TextInput {...field(form, 'name')} label="Name" />
|
|
131
|
-
<Button type="submit">Submit</Button>
|
|
132
|
-
<Button variant="subtle" onClick={clearDraft}>Discard draft</Button>
|
|
133
|
-
</form>
|
|
134
|
-
)
|
|
135
|
-
}
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
### Autosave mechanism
|
|
139
|
-
|
|
140
|
-
`@mantine/form` v9 has no built-in whole-form change subscription on an existing form instance —
|
|
141
|
-
`form.watch` is per-field only. The correct autosave paths are:
|
|
142
|
-
|
|
143
|
-
1. **`useBasaltForm({ onValuesChange: saveDraft })`** (recommended) — `onValuesChange` fires on every
|
|
144
|
-
value change with `(values, previous)`. Pass `saveDraft` directly (it snapshots `form.getValues()`
|
|
145
|
-
internally).
|
|
146
|
-
2. **`<TextInput {...field(form, 'name')} onBlur={saveDraft} />`** — explicit blur-based autosave.
|
|
147
|
-
Use this for externally-created forms or when keystroke-level persistence is too aggressive.
|
|
148
|
-
|
|
149
|
-
### Key must be stable
|
|
150
|
-
|
|
151
|
-
The `key` option must not change between renders. Changing it recreates the storage instance and
|
|
152
|
-
loses the current draft. Use a module-level constant or a prop passed down from the route.
|
|
153
|
-
|
|
154
|
-
### Version bump
|
|
155
|
-
|
|
156
|
-
Increment `version` when the form values shape changes (added/removed fields, renamed keys). The
|
|
157
|
-
draft is automatically discarded and falls back to `initialValues` — no migration needed for simple
|
|
158
|
-
shape changes.
|
|
159
|
-
|
|
160
|
-
### clearDraft on success
|
|
161
|
-
|
|
162
|
-
Always call `clearDraft()` in `onSubmit`'s success path so stale form data doesn't reappear after
|
|
163
|
-
a successful save:
|
|
164
|
-
|
|
165
|
-
```ts
|
|
166
|
-
form.onSubmit(async (values) => {
|
|
167
|
-
await mutation.mutateAsync(values)
|
|
168
|
-
clearDraft()
|
|
169
|
-
})
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
## List helpers
|
|
173
|
-
|
|
174
|
-
For dynamic lists: `form.insertListItem('items', {...})` / `form.removeListItem('items', index)`.
|
|
175
|
-
Access via `form.getValues().items[index]` (uncontrolled) and bind with
|
|
176
|
-
`form.getInputProps('items.0.field')` / `field(form, 'items.0.field')`.
|
|
177
|
-
|
|
178
|
-
## Submission
|
|
179
|
-
|
|
180
|
-
```ts
|
|
181
|
-
form.onSubmit((values) => mutation.mutate(values))
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
Reset after success with `form.reset()` — never mutate `form.values` directly. Surface validation
|
|
185
|
-
as inline field errors via `getInputProps`; toast only on submit-level failures.
|
|
186
|
-
See basalt-query.md for the mutation/invalidation pattern.
|