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
|
@@ -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.
|
|
@@ -1,264 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
source: basalt-ui
|
|
3
|
-
description: Notification conventions for basalt-ui apps — notify helpers, typed registry, persisted history store, bell/center, and BasaltNotifications overlay from the shipped ./notifications battery. @mantine/notifications is an optional peer.
|
|
4
|
-
paths:
|
|
5
|
-
- 'src/**'
|
|
6
|
-
- 'apps/**/src/**'
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# Basalt Notifications
|
|
10
|
-
|
|
11
|
-
basalt-ui ships `./notifications` — a Mantine notification adapter battery providing `notify`,
|
|
12
|
-
`notifySuccess/Error/Warning/Info`, `notifyPromise`, `defineNotifications`, `emit`,
|
|
13
|
-
`useNotificationHistory`, `NotificationBell`, `NotificationCenter`, and `BasaltNotifications`
|
|
14
|
-
on top of `@mantine/notifications`. `@mantine/notifications` is an **optional peer** — install it
|
|
15
|
-
before using this battery:
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
bun add @mantine/notifications
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
## Mount BasaltNotifications
|
|
22
|
-
|
|
23
|
-
Render **one** `<BasaltNotifications />` inside `BasaltProvider`, before the router. The overlay is
|
|
24
|
-
opt-in — it is NOT auto-mounted from `BasaltProvider` to keep the peer out of the root barrel.
|
|
25
|
-
|
|
26
|
-
```tsx
|
|
27
|
-
import { BasaltNotifications } from 'basalt-ui/notifications'
|
|
28
|
-
import { BasaltProvider } from 'basalt-ui'
|
|
29
|
-
|
|
30
|
-
createRoot(root).render(
|
|
31
|
-
<BasaltProvider>
|
|
32
|
-
<BasaltNotifications />
|
|
33
|
-
<App />
|
|
34
|
-
</BasaltProvider>,
|
|
35
|
-
)
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
`BasaltNotifications` wraps Mantine's `<Notifications />` with basalt defaults:
|
|
39
|
-
`position="bottom-right"`, `autoClose=4000`, `limit=5`. Pass props to override:
|
|
40
|
-
|
|
41
|
-
```tsx
|
|
42
|
-
<BasaltNotifications position="top-right" autoClose={3000} limit={3} />
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
NOTE: `BasaltOverlays` (from `basalt-ui/commands`) is the shipped composable mount that wires
|
|
46
|
-
notifications together with spotlight/modals in one place. A standalone `<BasaltNotifications />`
|
|
47
|
-
remains available for apps that don't use the overlays layer.
|
|
48
|
-
|
|
49
|
-
## notify
|
|
50
|
-
|
|
51
|
-
`notify(opts)` is the core helper. It maps intent → Mantine color name, sets the correct aria
|
|
52
|
-
`role` for screen reader announcement, applies per-intent `autoClose` defaults, and records the
|
|
53
|
-
toast to the persisted history store. Returns the notification id string.
|
|
54
|
-
|
|
55
|
-
```ts
|
|
56
|
-
import { notify } from 'basalt-ui/notifications'
|
|
57
|
-
|
|
58
|
-
notify({ intent: 'success', message: 'Profile saved' })
|
|
59
|
-
notify({ intent: 'error', message: 'Upload failed', title: 'Error', autoClose: false })
|
|
60
|
-
notify({ intent: 'warning', message: 'Session expires soon', dedupKey: 'session-warn' })
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
### Intent → Mantine color + aria role + autoClose
|
|
64
|
-
|
|
65
|
-
| Intent | `color=` | `role=` | Default autoClose |
|
|
66
|
-
| ------- | ---------- | --------------------- | ------------------------- |
|
|
67
|
-
| success | `'green'` | `'status'` (polite) | 2000ms |
|
|
68
|
-
| info | `'blue'` | `'status'` (polite) | 3000ms |
|
|
69
|
-
| warning | `'yellow'` | `'alert'` (assertive) | 5000ms |
|
|
70
|
-
| error | `'red'` | `'alert'` (assertive) | `false` (until dismissed) |
|
|
71
|
-
|
|
72
|
-
**Never pass raw Mantine colors directly.** Always use `notify`'s `intent` field — the mapping is
|
|
73
|
-
the canonical source so screen-reader announcement and autoClose stay consistent.
|
|
74
|
-
|
|
75
|
-
### Two-layer dedup
|
|
76
|
-
|
|
77
|
-
1. **Mantine id-dedup**: passing the same `id` to `notifications.show` is a no-op on screen
|
|
78
|
-
(Mantine's built-in dedup). Pass `id` to prevent a toast from appearing twice.
|
|
79
|
-
2. **dedupKey window**: a module-level guard skips any `notify` call using the same `dedupKey`
|
|
80
|
-
within 1500ms — useful for event-driven triggers that may fire rapidly.
|
|
81
|
-
|
|
82
|
-
```ts
|
|
83
|
-
// Fire rapidly — only the first call within 1500ms shows:
|
|
84
|
-
notify({ intent: 'warning', message: 'Rate limit', dedupKey: 'rate-limit-warn' })
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
## Convenience wrappers
|
|
88
|
-
|
|
89
|
-
```ts
|
|
90
|
-
import { notifySuccess, notifyError, notifyWarning, notifyInfo } from 'basalt-ui/notifications'
|
|
91
|
-
|
|
92
|
-
notifySuccess('Changes saved')
|
|
93
|
-
notifyError('Upload failed', { title: 'Storage error' })
|
|
94
|
-
notifyWarning('Session expires in 5 minutes')
|
|
95
|
-
notifyInfo('New version available', { autoClose: 10000 })
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
Each wrapper calls `notify` with the matching intent. The second arg takes all `NotifyOptions`
|
|
99
|
-
fields except `intent` and `message`.
|
|
100
|
-
|
|
101
|
-
## notifyPromise
|
|
102
|
-
|
|
103
|
-
For async operations — shows a loading toast, then updates to success or error on settle.
|
|
104
|
-
Uses `notifications.show` for loading, then `notifications.update` for the final state (required
|
|
105
|
-
by Mantine to replace an existing id — `show` with the same id is a no-op).
|
|
106
|
-
|
|
107
|
-
```ts
|
|
108
|
-
import { notifyPromise } from 'basalt-ui/notifications'
|
|
109
|
-
|
|
110
|
-
await notifyPromise(uploadFile(file), {
|
|
111
|
-
loading: 'Uploading…',
|
|
112
|
-
success: 'Upload complete',
|
|
113
|
-
error: 'Upload failed',
|
|
114
|
-
})
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
The promise is passed through — `await notifyPromise(...)` resolves/rejects as normal. The final
|
|
118
|
-
state is recorded to the notification history store (loading state is not recorded).
|
|
119
|
-
|
|
120
|
-
## defineNotifications + emit
|
|
121
|
-
|
|
122
|
-
`defineNotifications` is a const-generic factory (mirrors `defineSeries` from `./tokens`) for
|
|
123
|
-
typed notification kind registries. Augment `BasaltRegister.notifications` once with the spec map
|
|
124
|
-
type, then use `emit` — unknown kinds are tsc errors.
|
|
125
|
-
|
|
126
|
-
```ts
|
|
127
|
-
// notifications.ts — define and augment (app-side, once):
|
|
128
|
-
import { defineNotifications } from 'basalt-ui/notifications'
|
|
129
|
-
|
|
130
|
-
export const NOTIFICATIONS = defineNotifications({
|
|
131
|
-
'upload:success': { intent: 'success', toMessage: (p: { name: string }) => `Uploaded ${p.name}` },
|
|
132
|
-
'upload:error': { intent: 'error', toMessage: (_: unknown) => 'Upload failed' },
|
|
133
|
-
'session:warn': { intent: 'warning', toMessage: () => 'Session expires soon' },
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
declare module 'basalt-ui' {
|
|
137
|
-
interface BasaltRegister {
|
|
138
|
-
notifications: typeof NOTIFICATIONS
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
```ts
|
|
144
|
-
// usage.ts — typed emit, unknown kinds fail tsc:
|
|
145
|
-
import { emit } from 'basalt-ui/notifications'
|
|
146
|
-
|
|
147
|
-
emit('upload:success', { name: 'photo.jpg' }) // ✓ typed payload
|
|
148
|
-
emit('upload:error', null) // ✓
|
|
149
|
-
emit('nonexistent', null) // ✗ tsc error
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
`emit` resolves `intent` + calls `toMessage(payload)` from the registered spec, then calls
|
|
153
|
-
`notify()`. Pass extra `NotifyOptions` as a third argument to override spec defaults.
|
|
154
|
-
|
|
155
|
-
## defineNotification (single-spec helper)
|
|
156
|
-
|
|
157
|
-
Mirror of `defineOverlay` from `./commands` — types a single notification spec without registering
|
|
158
|
-
it. Use when splitting notification definitions across files before merging into `defineNotifications`.
|
|
159
|
-
|
|
160
|
-
```ts
|
|
161
|
-
import { defineNotification, defineNotifications } from 'basalt-ui/notifications'
|
|
162
|
-
|
|
163
|
-
// Type an isolated spec constant — useful when splitting across files:
|
|
164
|
-
const uploadSuccess = defineNotification({
|
|
165
|
-
intent: 'success',
|
|
166
|
-
toMessage: (p: { name: string }) => `Uploaded ${p.name}`,
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
// Merge into the full registry (defineNotifications does the actual registration):
|
|
170
|
-
export const NOTIFICATIONS = defineNotifications({ 'upload:success': uploadSuccess })
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
WARNING: `defineNotification` only TYPES — it does NOT register. Only `defineNotifications(map)`
|
|
174
|
-
registers. Calling `emit` without a prior `defineNotifications` is a no-op at runtime.
|
|
175
|
-
|
|
176
|
-
### `NotificationSpec` has no `title`/`icon` — by design
|
|
177
|
-
|
|
178
|
-
`NotificationSpec` is `{ intent, toMessage? }` — deliberately no `title`/`icon` field. Those stay
|
|
179
|
-
per-call `NotifyOptions` (`emit(kind, payload, { title: '…' })` or `notify({ ..., title: '…' })`),
|
|
180
|
-
so a title that varies per call site isn't forced into the registry. If every call to a given kind
|
|
181
|
-
repeats the same title, pass it at each call site — there's no registry-level default to fall back
|
|
182
|
-
on.
|
|
183
|
-
|
|
184
|
-
### Colors beyond the 4 intents live outside the registry
|
|
185
|
-
|
|
186
|
-
`notify`/`emit` only resolve `intent` to one of the four Mantine colors (§ Intent → Mantine color
|
|
187
|
-
above) — there's no `intent` override and no raw-color escape in `NotifyOptions`. A feature needing
|
|
188
|
-
per-category colored toasts beyond `success`/`info`/`warning`/`error` (e.g. achievement toasts keyed
|
|
189
|
-
by achievement type) cannot ride the typed registry. The documented stance: call Mantine's raw
|
|
190
|
-
`notifications.show({ color, ... })` directly, outside `notify`/`emit`. The cost is explicit — a
|
|
191
|
-
raw `notifications.show` toast is invisible to the persisted history store, the bell, and the
|
|
192
|
-
notification center, since only `notify`/`notifyPromise` record to history.
|
|
193
|
-
|
|
194
|
-
## Notification history store
|
|
195
|
-
|
|
196
|
-
Every `notify()` / `notifyPromise()` call records to a module-level persisted external store
|
|
197
|
-
(own `useSyncExternalStore` + versioned `{ v, value }` envelope under `basalt:notifications` in
|
|
198
|
-
localStorage). The store is **not** hook-scoped — `add`, `markRead`, `markAllRead`, `clear` are
|
|
199
|
-
module-level and callable outside React.
|
|
200
|
-
|
|
201
|
-
Ring-buffer cap: 50 items. Items store `{ id, intent, title?, message, createdAt, readAt? }` —
|
|
202
|
-
**message and title are strings** (ReactNode is not serializable). Cross-tab sync via the
|
|
203
|
-
`storage` event.
|
|
204
|
-
|
|
205
|
-
### useNotificationHistory
|
|
206
|
-
|
|
207
|
-
```ts
|
|
208
|
-
import { useNotificationHistory } from 'basalt-ui/notifications'
|
|
209
|
-
|
|
210
|
-
const { items, unreadCount, markRead, markAllRead, clear } = useNotificationHistory()
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
| Field | Type | Description |
|
|
214
|
-
| --------------- | --------------------------- | ------------------------------------------ |
|
|
215
|
-
| `items` | `NotificationHistoryItem[]` | All history (newest first, ring-buffer 50) |
|
|
216
|
-
| `unreadCount` | `number` | Items with no `readAt` |
|
|
217
|
-
| `markRead(id)` | `(id: string) => void` | Mark one item read |
|
|
218
|
-
| `markAllRead()` | `() => void` | Mark all items read |
|
|
219
|
-
| `clear()` | `() => void` | Clear all history |
|
|
220
|
-
|
|
221
|
-
## NotificationBell
|
|
222
|
-
|
|
223
|
-
A Mantine `ActionIcon` + `Indicator` showing unread count. Opens a `NotificationCenter` in a
|
|
224
|
-
`Popover`. Marks all items read when the center opens. Designed for the shell `globalActions` slot.
|
|
225
|
-
|
|
226
|
-
```tsx
|
|
227
|
-
import { NotificationBell } from 'basalt-ui/notifications'
|
|
228
|
-
import { BasaltShell } from 'basalt-ui'
|
|
229
|
-
;<BasaltShell globalActions={<NotificationBell />} {...rest} />
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
The bell renders a bell SVG icon (no `@tabler/icons` dependency — icons are ReactNode per house
|
|
233
|
-
rules). The unread indicator is capped at `9+` for counts above 9.
|
|
234
|
-
|
|
235
|
-
## NotificationCenter
|
|
236
|
-
|
|
237
|
-
Lists history items (title, message, relative time, unread dot) with "Mark all read" and "Clear"
|
|
238
|
-
actions. Consumes `useNotificationHistory`. Composable — mount it inside any Popover, Drawer, or
|
|
239
|
-
panel:
|
|
240
|
-
|
|
241
|
-
```tsx
|
|
242
|
-
import { NotificationCenter } from 'basalt-ui/notifications'
|
|
243
|
-
import { Drawer } from '@mantine/core'
|
|
244
|
-
;<Drawer opened={opened} onClose={close} title="Notifications">
|
|
245
|
-
<NotificationCenter maxHeight={500} />
|
|
246
|
-
</Drawer>
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
`NotificationBell` uses `NotificationCenter` internally. Mount `NotificationCenter` directly only
|
|
250
|
-
when you need a custom container (e.g. a full-page notifications view or a custom Drawer).
|
|
251
|
-
|
|
252
|
-
## Deferred (not yet shipped)
|
|
253
|
-
|
|
254
|
-
The following features are advisory notes for future work — they are NOT part of the current battery:
|
|
255
|
-
|
|
256
|
-
- **Web Push** (`usePushSubscription` + `sw.js` + Elysia push route): server-side push delivery
|
|
257
|
-
for background notifications. Requires a service worker, a push endpoint, and a vapid key.
|
|
258
|
-
- **Visibility-aware delivery buffering**: buffer toasts while the tab is hidden (`document.hidden`)
|
|
259
|
-
and flush on `visibilitychange` — prevents lost toasts when the tab is backgrounded.
|
|
260
|
-
- **IntersectionObserver per-item mark-seen**: replace the mark-read-on-open approach with
|
|
261
|
-
per-item IntersectionObserver so only viewed items are marked read (higher fidelity).
|
|
262
|
-
- **Cross-device read-sync**: propagate `readAt` via an Elysia endpoint so read state syncs
|
|
263
|
-
across browser sessions (requires auth and a backend store).
|
|
264
|
-
- **Quiet hours**: suppress non-error toasts during a configurable time window.
|
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
source: basalt-ui
|
|
3
|
-
description: TanStack Query conventions for basalt-ui apps — query factories, Suspense reads, and mutation/invalidation. basalt-ui ships ./query (createBasaltQueryClient + unwrap + devtools); this rule covers the advisory consumer patterns on top.
|
|
4
|
-
paths:
|
|
5
|
-
- 'src/lib/queries/**'
|
|
6
|
-
- 'apps/**/src/lib/queries/**'
|
|
7
|
-
- 'src/routes/**'
|
|
8
|
-
- 'apps/**/src/routes/**'
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
# Basalt Query — TanStack Query Conventions
|
|
12
|
-
|
|
13
|
-
basalt-ui ships `./query` — a thin TanStack Query adapter providing `createBasaltQueryClient`,
|
|
14
|
-
`unwrap`, `BasaltQueryDevtools`, and convenience re-exports (`QueryClientProvider`,
|
|
15
|
-
`QueryErrorResetBoundary`, `useQueryErrorResetBoundary`). @tanstack/react-query is a **required**
|
|
16
|
-
peer — `BasaltProvider` hard-requires it at build time.
|
|
17
|
-
|
|
18
|
-
`./query` also re-exports the primary fetch hooks so consumers never need to dual-import from both
|
|
19
|
-
`basalt-ui/query` and `@tanstack/react-query`:
|
|
20
|
-
|
|
21
|
-
```ts
|
|
22
|
-
import {
|
|
23
|
-
useQuery,
|
|
24
|
-
useSuspenseQuery,
|
|
25
|
-
useMutation,
|
|
26
|
-
useQueryClient,
|
|
27
|
-
useInfiniteQuery,
|
|
28
|
-
queryOptions,
|
|
29
|
-
} from 'basalt-ui/query'
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
This rule covers the advisory consumer patterns on top of that adapter. Nothing in the framework
|
|
33
|
-
enforces these conventions beyond the `unwrap` seam.
|
|
34
|
-
|
|
35
|
-
## Query factories
|
|
36
|
-
|
|
37
|
-
Each resource has a factory in `src/lib/queries/<resource>.ts`:
|
|
38
|
-
|
|
39
|
-
```ts
|
|
40
|
-
import { queryOptions, useSuspenseQuery } from 'basalt-ui/query'
|
|
41
|
-
import { unwrap } from 'basalt-ui/query'
|
|
42
|
-
import { api } from '../api-client'
|
|
43
|
-
|
|
44
|
-
export const resourceQueries = {
|
|
45
|
-
list: (params: ListParams) =>
|
|
46
|
-
queryOptions({
|
|
47
|
-
queryKey: ['resource', 'list', params],
|
|
48
|
-
queryFn: () => unwrap(api.resource.get({ query: params })),
|
|
49
|
-
}),
|
|
50
|
-
summary: () =>
|
|
51
|
-
queryOptions({
|
|
52
|
-
queryKey: ['resource', 'summary'],
|
|
53
|
-
queryFn: () => unwrap(api.resource.summary.get()),
|
|
54
|
-
}),
|
|
55
|
-
}
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
- Key hierarchy: `[resource, action, ...params]`.
|
|
59
|
-
- `queryOptions` and all hooks import from `basalt-ui/query` — do not dual-import from
|
|
60
|
-
`@tanstack/react-query` directly.
|
|
61
|
-
- `queryFn` unwraps typed responses via `unwrap()` — throws on the error branch so failures surface
|
|
62
|
-
to the nearest error boundary or TanStack Query's error state. Also throws when `data` is `null`
|
|
63
|
-
with no error (e.g. a 204 No Content or a silent transport failure that returns
|
|
64
|
-
`{ data: null, error: null }`). Both cases are bugs the caller must address.
|
|
65
|
-
- Never use raw strings as query keys in components — always import from the factory.
|
|
66
|
-
|
|
67
|
-
## Reading data in components
|
|
68
|
-
|
|
69
|
-
```ts
|
|
70
|
-
import { useSuspenseQuery } from 'basalt-ui/query'
|
|
71
|
-
|
|
72
|
-
const { data } = useSuspenseQuery(resourceQueries.summary())
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
- Always `useSuspenseQuery` inside loader-prefetched routes (never `useQuery`) — pairs with the
|
|
76
|
-
router's `ensureQueryData` (see basalt-router.md).
|
|
77
|
-
- Wrap pages or panels in `<Suspense fallback={…}>` at the route level.
|
|
78
|
-
|
|
79
|
-
## Rendering a query — use `QueryState`, don't write the switch
|
|
80
|
-
|
|
81
|
-
```tsx
|
|
82
|
-
import { QueryState } from 'basalt-ui' // NOT basalt-ui/query — it renders Mantine
|
|
83
|
-
|
|
84
|
-
function Library() {
|
|
85
|
-
const q = useQuery(imageQueries.list())
|
|
86
|
-
return (
|
|
87
|
-
<QueryState
|
|
88
|
-
query={q}
|
|
89
|
-
variant="page" // or "section" for a panel inside a page
|
|
90
|
-
errorTitle="Could not load images"
|
|
91
|
-
empty={{ title: 'No images yet', description: 'Upload one to get started.' }}
|
|
92
|
-
isEmpty={(d) => d.items.length === 0}
|
|
93
|
-
>
|
|
94
|
-
{(data) => <ImageGrid items={data.items} />}
|
|
95
|
-
</QueryState>
|
|
96
|
-
)
|
|
97
|
-
}
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
**Rendering only the empty branch is the bug this retires.** A consumer's library showed
|
|
101
|
-
`No images` on a **500**, and a share detail showed `Share not found` on a dropped connection —
|
|
102
|
-
"nothing here" is what an app renders when nobody wrote the other three branches. One import
|
|
103
|
-
replaced 204 hand-rolled lines across 10 call sites there.
|
|
104
|
-
|
|
105
|
-
Precedence, in evaluation order:
|
|
106
|
-
|
|
107
|
-
| Condition | Renders |
|
|
108
|
-
| ----------------------------------- | ------------------------------------------------------------------------- |
|
|
109
|
-
| `isError`, no `data` | `ErrorState` — `errorTitle` / `errorFallback` / `errorAction`, with Retry |
|
|
110
|
-
| no `data`, `fetchStatus === 'idle'` | the `empty` copy (nothing, if `empty` is omitted) |
|
|
111
|
-
| no `data` | `loading`, else `LoadingState` |
|
|
112
|
-
| `data`, `isEmpty(data)` | the `empty` copy |
|
|
113
|
-
| `data` | `children` — a node, or `(data) => node` |
|
|
114
|
-
| `data` **and** `isError` | a fixed _Showing cached data_ banner above `children` |
|
|
115
|
-
|
|
116
|
-
`errorTitle` / `errorFallback` / `errorAction` reach the no-data error branch only; the stale-data
|
|
117
|
-
banner carries its own copy. `empty` is `{ title, description?, icon?, action? }` — `description`
|
|
118
|
-
is optional, so a compact panel needs no invented second sentence.
|
|
119
|
-
|
|
120
|
-
**`useQuery`, not `useSuspenseQuery`.** A suspense read never hands you an undefined `data` or an
|
|
121
|
-
error branch — it throws to the nearest boundary — so `QueryState` has nothing to decide there.
|
|
122
|
-
Suspense + an error boundary and `QueryState` are the two ways to render a query; pick one per read.
|
|
123
|
-
|
|
124
|
-
`LoadingState` and `ErrorState` ship beside it as the escape hatch for a page that must place its
|
|
125
|
-
branches in different DOM positions.
|
|
126
|
-
|
|
127
|
-
**`query` is a structural subset, checked at runtime.** It needs `{ data, isError, error,
|
|
128
|
-
fetchStatus, refetch }` — a TanStack `UseQueryResult` satisfies it, and a hand-composed object is
|
|
129
|
-
legal. Because the subset gives up the compiler's help, a missing `isError` **throws** naming the
|
|
130
|
-
field rather than rendering a false claim about the data.
|
|
131
|
-
|
|
132
|
-
Decoding an unknown error into copy is `toErrorMessage(err, fallback?)` from `basalt-ui/query`,
|
|
133
|
-
with `errorStatus(err)` for the status; `toErrorMessage` folds the status into the fallback when the
|
|
134
|
-
body decodes to nothing readable, so an opaque envelope never renders as `{}`.
|
|
135
|
-
|
|
136
|
-
## Mutations
|
|
137
|
-
|
|
138
|
-
```ts
|
|
139
|
-
const mutation = useMutation({
|
|
140
|
-
mutationFn: (body) => unwrap(api.resource.post({ body })),
|
|
141
|
-
onSuccess: () => {
|
|
142
|
-
queryClient.invalidateQueries({ queryKey: ['resource'] })
|
|
143
|
-
notifications.show({ message: 'Saved', color: 'green' })
|
|
144
|
-
},
|
|
145
|
-
})
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
- Invalidate by resource prefix (first key segment) — catches list + summary at once.
|
|
149
|
-
- Show a notification on success; let errors bubble to the global error boundary.
|
|
150
|
-
- For the in-button success flip (loading spinner → green `IconCheck`), drive it off `mutation.isPending`
|
|
151
|
-
/ `mutation.isSuccess` — see the interaction-feedback section in basalt-mantine.md.
|
|
152
|
-
|
|
153
|
-
## DevTools
|
|
154
|
-
|
|
155
|
-
Use `BasaltQueryDevtools` from `basalt-ui/query` — it is lazy, production-excluded, and safe to
|
|
156
|
-
import unconditionally:
|
|
157
|
-
|
|
158
|
-
```tsx
|
|
159
|
-
import { BasaltQueryDevtools, QueryClientProvider } from 'basalt-ui/query'
|
|
160
|
-
|
|
161
|
-
function Root() {
|
|
162
|
-
return (
|
|
163
|
-
<QueryClientProvider client={client}>
|
|
164
|
-
{children}
|
|
165
|
-
<BasaltQueryDevtools initialIsOpen={false} />
|
|
166
|
-
</QueryClientProvider>
|
|
167
|
-
)
|
|
168
|
-
}
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
Never import `@tanstack/react-query-devtools` directly in app code — it is an optional peer and
|
|
172
|
-
the lazy wrapper handles the production guard for you.
|
|
173
|
-
|
|
174
|
-
## Eden footguns (hard authoring rules)
|
|
175
|
-
|
|
176
|
-
Two patterns silently degrade Eden Treaty inference to `any` — both must be avoided:
|
|
177
|
-
|
|
178
|
-
1. **Non-chained route definitions**: Elysia routes MUST be method-chained on the `app` instance.
|
|
179
|
-
A standalone `app.get(...)` (reassigned or called without chaining back to the root) drops the
|
|
180
|
-
`App` type to `any`, which propagates into the Treaty client and loses all type safety.
|
|
181
|
-
|
|
182
|
-
2. **Mismatched tsconfig path aliases**: The client package and the server package MUST share or
|
|
183
|
-
extend one root `tsconfig` so path aliases (`~/*`, `@/*`, etc.) resolve identically in both.
|
|
184
|
-
A mismatch causes Eden's type extraction to fail silently and degrade to `any`.
|
|
185
|
-
|
|
186
|
-
Both footguns produce no TypeScript error at the point of breakage — they only manifest as `any`
|
|
187
|
-
types on the Treaty client, making them hard to spot. Check these first when Eden types regress.
|