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
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WidgetHeader — the one heading primitive behind every section, card and table title
|
|
3
|
+
* (docs/CONTROLS-SPEC.md §2.2, law C8). Plain elements + one CSS module, no `@mantine/*` import
|
|
4
|
+
* anywhere in this folder, so it can be composed from inside `charts/` without tripping
|
|
5
|
+
* `basalt/token-layer-boundary` or `check-dist-layering.mjs`.
|
|
6
|
+
*
|
|
7
|
+
* `tier: 'section'` renders an `<h2>` at the taller section-header height; `tier: 'widget'`
|
|
8
|
+
* renders an `<h3>` at the tighter, display-only icon-tier height — both share the same head-font
|
|
9
|
+
* 88%/550 title treatment. The title row holds title/icon/info/count/actions only; `value` +
|
|
10
|
+
* `delta` render on their own hero-metric row directly beneath it, for both tiers — never inline
|
|
11
|
+
* with the title. `StatCard`, `ChartCard`, `Section`, `SettingsSection`/`DangerZone` and
|
|
12
|
+
* `BasaltDataTable` each compose this and render nothing else above their body (wave 3,
|
|
13
|
+
* docs/CONTROLS-SPEC.md §2.2's composer table).
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* import { WidgetHeader } from 'basalt-ui'
|
|
17
|
+
*
|
|
18
|
+
* <WidgetHeader
|
|
19
|
+
* tier="widget"
|
|
20
|
+
* title="Active Users"
|
|
21
|
+
* value="12,483"
|
|
22
|
+
* delta={4.2}
|
|
23
|
+
* deltaPeriod="MoM"
|
|
24
|
+
* actions={<ActionIcon variant="subtle"><IconDots /></ActionIcon>}
|
|
25
|
+
* />
|
|
26
|
+
*/
|
|
27
|
+
import { useEffect, useId, useRef, useState } from 'react'
|
|
28
|
+
import type { ReactNode } from 'react'
|
|
29
|
+
import { IconSlot } from '../theme/icon-slot'
|
|
30
|
+
import { DeltaBadge } from './delta-badge'
|
|
31
|
+
import classes from './widget-header.module.css'
|
|
32
|
+
|
|
33
|
+
export type WidgetHeaderProps = {
|
|
34
|
+
/** `section` renders an `<h2>` at the section-header height; `widget` renders an `<h3>` at the
|
|
35
|
+
* tighter icon-tier height, display-only. */
|
|
36
|
+
tier: 'section' | 'widget'
|
|
37
|
+
/** Head-font title (88% stretch, weight 550). */
|
|
38
|
+
title: string
|
|
39
|
+
/** Optional leading icon, rendered before the title. Decorative — hidden from assistive tech. */
|
|
40
|
+
icon?: ReactNode
|
|
41
|
+
/** Optional muted line rendered below the title row. */
|
|
42
|
+
subtitle?: string
|
|
43
|
+
/**
|
|
44
|
+
* Renders an info glyph BESIDE the heading (never inside it — this text would otherwise become
|
|
45
|
+
* part of the heading's accessible name, and an `info` paragraph read out in every headings list
|
|
46
|
+
* is what that cost). The glyph is named `More information`; the text itself reaches assistive
|
|
47
|
+
* tech through `aria-describedby` on a `role="tooltip"` bubble that opens on hover, focus and
|
|
48
|
+
* click — so a keyboard user gets it too. Mantine-free: a plain state bubble, not a Tooltip.
|
|
49
|
+
*/
|
|
50
|
+
info?: string
|
|
51
|
+
/** Pre-formatted metric value (mono, hero size). Renders on its own row with `delta`, directly
|
|
52
|
+
* under the title row — never inline with the title. */
|
|
53
|
+
value?: string
|
|
54
|
+
/** Signed delta rendered via `DeltaBadge`, on the same hero-metric row as `value`. */
|
|
55
|
+
delta?: number
|
|
56
|
+
/** Comparison timeframe forwarded to `DeltaBadge` (e.g. `MoM`). */
|
|
57
|
+
deltaPeriod?: string
|
|
58
|
+
/** Trend visual slot, rendered below the metric row. Carries `data-placement="right"` so a
|
|
59
|
+
* wave-3 composer's `bleed` layout can select it. */
|
|
60
|
+
sparkline?: ReactNode
|
|
61
|
+
/** Mono count tag rendered after the title (C11 — every table/list states its count). `0` is a
|
|
62
|
+
* real count and renders, distinct from omitting the prop entirely. */
|
|
63
|
+
count?: number
|
|
64
|
+
/** Right-aligned slot (`margin-inline-start: auto`) — the C1/C5 home for header actions. */
|
|
65
|
+
actions?: ReactNode
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The info affordance. A `title` attribute alone was never enough — it renders on hover only, so a
|
|
70
|
+
* keyboard user focusing the glyph saw nothing at all. This is the bubble `ChartCard` used to own
|
|
71
|
+
* before it composed `WidgetHeader`, moved down here so every tier gets the same behaviour:
|
|
72
|
+
* opens on hover/focus/click, closes on leave/blur/Escape/outside pointer-down, and is wired to the
|
|
73
|
+
* trigger with `aria-describedby` only while open.
|
|
74
|
+
*/
|
|
75
|
+
function InfoGlyph({ text }: { text: string }) {
|
|
76
|
+
const [open, setOpen] = useState(false)
|
|
77
|
+
const tipId = useId()
|
|
78
|
+
const triggerRef = useRef<HTMLButtonElement>(null)
|
|
79
|
+
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (!open) return
|
|
82
|
+
const handlePointerDown = (event: PointerEvent) => {
|
|
83
|
+
if (triggerRef.current?.contains(event.target as Node)) return
|
|
84
|
+
setOpen(false)
|
|
85
|
+
}
|
|
86
|
+
document.addEventListener('pointerdown', handlePointerDown)
|
|
87
|
+
return () => document.removeEventListener('pointerdown', handlePointerDown)
|
|
88
|
+
}, [open])
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<span className={classes.info}>
|
|
92
|
+
<button
|
|
93
|
+
ref={triggerRef}
|
|
94
|
+
type="button"
|
|
95
|
+
className={classes.infoTrigger}
|
|
96
|
+
// The GLYPH is named, not the text — `text` reaches AT through `aria-describedby` below, so
|
|
97
|
+
// it never lands in the heading's or the button's accessible name.
|
|
98
|
+
aria-label="More information"
|
|
99
|
+
aria-describedby={open ? tipId : undefined}
|
|
100
|
+
data-open={open}
|
|
101
|
+
onMouseEnter={() => setOpen(true)}
|
|
102
|
+
onMouseLeave={() => setOpen(false)}
|
|
103
|
+
onFocus={() => setOpen(true)}
|
|
104
|
+
onBlur={() => setOpen(false)}
|
|
105
|
+
onClick={() => setOpen(true)}
|
|
106
|
+
onKeyDown={(event) => {
|
|
107
|
+
if (event.key === 'Escape') setOpen(false)
|
|
108
|
+
}}
|
|
109
|
+
>
|
|
110
|
+
<svg width={11} height={11} viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
111
|
+
<path d="M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0zm0 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3zm0 5a1 1 0 0 1 1 1v7a1 1 0 1 1-2 0v-7a1 1 0 0 1 1-1z" />
|
|
112
|
+
</svg>
|
|
113
|
+
</button>
|
|
114
|
+
{open && (
|
|
115
|
+
<span id={tipId} role="tooltip" className={classes.infoBubble}>
|
|
116
|
+
{text}
|
|
117
|
+
</span>
|
|
118
|
+
)}
|
|
119
|
+
</span>
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function WidgetHeader({
|
|
124
|
+
tier,
|
|
125
|
+
title,
|
|
126
|
+
icon,
|
|
127
|
+
subtitle,
|
|
128
|
+
info,
|
|
129
|
+
value,
|
|
130
|
+
delta,
|
|
131
|
+
deltaPeriod,
|
|
132
|
+
sparkline,
|
|
133
|
+
count,
|
|
134
|
+
actions,
|
|
135
|
+
}: WidgetHeaderProps) {
|
|
136
|
+
const Heading = tier === 'section' ? 'h2' : 'h3'
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<div className={classes.root} data-tier={tier}>
|
|
140
|
+
<div className={classes.titleRow}>
|
|
141
|
+
<Heading className={classes.heading}>
|
|
142
|
+
{/* `IconSlot` owns the box; the TIER owns its size, through the
|
|
143
|
+
`--vx-space-icon-size` this module sets per `data-tier`. So a `tier="widget"`
|
|
144
|
+
heading draws 14px and a `tier="section"` heading 16px with no prop and no
|
|
145
|
+
per-component `> svg` rule — see `theme/icon-slot.tsx`. */}
|
|
146
|
+
{icon !== undefined && <IconSlot className={classes.icon}>{icon}</IconSlot>}
|
|
147
|
+
<span className={classes.titleText}>{title}</span>
|
|
148
|
+
</Heading>
|
|
149
|
+
{/* OUTSIDE the heading on purpose — see `info`'s own doc. */}
|
|
150
|
+
{info !== undefined && <InfoGlyph text={info} />}
|
|
151
|
+
{count !== undefined && <span className={classes.count}>{count}</span>}
|
|
152
|
+
{actions !== undefined && <span className={classes.actions}>{actions}</span>}
|
|
153
|
+
</div>
|
|
154
|
+
{(value !== undefined || delta !== undefined) && (
|
|
155
|
+
<div className={classes.metrics}>
|
|
156
|
+
{value !== undefined && <span className={classes.value}>{value}</span>}
|
|
157
|
+
{delta !== undefined && <DeltaBadge value={delta} period={deltaPeriod} />}
|
|
158
|
+
</div>
|
|
159
|
+
)}
|
|
160
|
+
{subtitle !== undefined && <span className={classes.subtitle}>{subtitle}</span>}
|
|
161
|
+
{sparkline !== undefined && (
|
|
162
|
+
<div className={classes.sparkline} data-placement="right">
|
|
163
|
+
{sparkline}
|
|
164
|
+
</div>
|
|
165
|
+
)}
|
|
166
|
+
</div>
|
|
167
|
+
)
|
|
168
|
+
}
|
|
@@ -1,452 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
source: basalt-ui
|
|
3
|
-
description: Streaming-agent layer for basalt-ui apps — Eden-native transport, AgentPart exhaustive handling, StickToBottom, chat history, AND the multi-thread ThreadWorkspace (concurrent runs + a distilled-outcome feed + detail panel). Headless layer in ./agent (Mantine-free); the Mantine-coupled chrome ships from its own basalt-ui/agent-chat subpath (also re-exported from the root entry) and uses basalt-ui/content's Markdown for rendering. Covers authoring doctrine, the Eden #231 footgun, and the agent-chat lint guards (agent-resume-guard, agent-no-raw-usechat, ai-sdk-major) with their basalt-agent-allow escape token.
|
|
4
|
-
paths:
|
|
5
|
-
- 'src/**/*agent*'
|
|
6
|
-
- 'src/**/chat*'
|
|
7
|
-
- 'src/**/transport*'
|
|
8
|
-
- 'apps/**/src/**/*agent*'
|
|
9
|
-
- 'apps/**/src/**/chat*'
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
# Basalt Agent — Streaming Chat Layer
|
|
13
|
-
|
|
14
|
-
basalt-ui ships `./agent` — a headless, Mantine-free streaming-chat layer with an injected
|
|
15
|
-
transport seam, exhaustive part rendering, auto-scroll, and persisted history. `./agent` ships no
|
|
16
|
-
markdown renderer of its own — `agent/** -> content` is lint-blocked by design, so `PartList` takes
|
|
17
|
-
a consumer-supplied `components.text`; basalt's own renderer is `basalt-ui/content`'s `Markdown`.
|
|
18
|
-
|
|
19
|
-
## AgentPart discriminated union
|
|
20
|
-
|
|
21
|
-
```ts
|
|
22
|
-
import type { AgentPart } from 'basalt-ui/agent'
|
|
23
|
-
// StartPart | TextPart | ReasoningPart | ToolCallPart | SourcePart | ErrorPart
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
`StartPart` is a no-op resumption signal, not renderable content — see "Stream resumption" below.
|
|
27
|
-
|
|
28
|
-
Every switch over `AgentPart` MUST end with `default: return assertNever(part)`. This is
|
|
29
|
-
enforced by tsc — adding a new variant without a case is a compile error.
|
|
30
|
-
|
|
31
|
-
## AgentTransport — the injected seam
|
|
32
|
-
|
|
33
|
-
```ts
|
|
34
|
-
import { useAgentStream, edenTransport, type AgentPart } from 'basalt-ui/agent'
|
|
35
|
-
|
|
36
|
-
// Wrap your Eden call:
|
|
37
|
-
const transport = edenTransport<AgentPart>((input, signal) =>
|
|
38
|
-
api.chat.post({ body: { message: input }, fetch: { signal } }),
|
|
39
|
-
)
|
|
40
|
-
|
|
41
|
-
// Wire into the hook:
|
|
42
|
-
const { parts, status, send, stop, regenerate } = useAgentStream({ transport })
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
`edenTransport` is zero-dep: it awaits the Eden `{data, error}` envelope, throws on error,
|
|
46
|
-
and yields from the `AsyncGenerator<TPart>` data value.
|
|
47
|
-
|
|
48
|
-
The transport seam is generic (`AgentTransport<TPart, TInput>`) — consumers can extend AgentPart
|
|
49
|
-
with domain variants and pass a custom input type. For a mock (tests/playground), implement the
|
|
50
|
-
interface directly without `edenTransport`:
|
|
51
|
-
|
|
52
|
-
```ts
|
|
53
|
-
const mockTransport: AgentTransport = {
|
|
54
|
-
async *stream(input) {
|
|
55
|
-
yield { type: 'text', text: `Echo: ${input}` }
|
|
56
|
-
},
|
|
57
|
-
}
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
## Eden #231 — the critical stream footgun
|
|
61
|
-
|
|
62
|
-
**Issue (STILL OPEN):** applying a `t.Object` or `t.Union` response schema to an Elysia
|
|
63
|
-
`async function*` stream route collapses the streamed union to `any` in Eden Treaty.
|
|
64
|
-
|
|
65
|
-
**Mitigation (required doctrine):**
|
|
66
|
-
|
|
67
|
-
1. **NO response schema on the stream route.** Leave the `.post(handler)` call schema-free for
|
|
68
|
-
the streaming generator.
|
|
69
|
-
2. **Validate at yield-time.** Check part shape before yielding, not in the Elysia schema layer.
|
|
70
|
-
Use `parseAgentPart` from `basalt-ui/agent` to narrow untrusted values arriving over the wire:
|
|
71
|
-
|
|
72
|
-
```ts
|
|
73
|
-
import { parseAgentPart } from 'basalt-ui/agent'
|
|
74
|
-
|
|
75
|
-
// On the client — e.g. parsing raw NDJSON lines from a custom transport:
|
|
76
|
-
const raw: unknown = JSON.parse(line)
|
|
77
|
-
const part = parseAgentPart(raw) // AgentPart | null
|
|
78
|
-
if (part !== null) handlePart(part)
|
|
79
|
-
|
|
80
|
-
// On the server — guard before yielding from external tool output:
|
|
81
|
-
const candidate = buildPartFromTool(toolResult)
|
|
82
|
-
const part = parseAgentPart(candidate)
|
|
83
|
-
if (part !== null) yield part
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
`parseAgentPart` performs structural narrowing against every variant in the discriminated union
|
|
87
|
-
and returns `null` for unknown types or malformed shapes — no exceptions thrown.
|
|
88
|
-
|
|
89
|
-
3. **Explicit return type annotation.** The handler MUST declare `: AsyncGenerator<AgentPart>`.
|
|
90
|
-
|
|
91
|
-
```ts
|
|
92
|
-
// ✓ Correct — explicit return type, no response schema:
|
|
93
|
-
app.post('/chat', async function* ({ body }): AsyncGenerator<AgentPart> {
|
|
94
|
-
// validate body.message here, not via t.Object schema
|
|
95
|
-
yield { type: 'text', text: 'Hello' }
|
|
96
|
-
yield { type: 'text', text: ' world' }
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
// ✗ Wrong — response schema collapses the union to `any`:
|
|
100
|
-
app.post(
|
|
101
|
-
'/chat',
|
|
102
|
-
async function* ({ body }): AsyncGenerator<AgentPart> {
|
|
103
|
-
yield { type: 'text', text: 'Hello' }
|
|
104
|
-
},
|
|
105
|
-
{
|
|
106
|
-
response: t.Object({ type: t.Literal('text'), text: t.String() }), // drops to any
|
|
107
|
-
},
|
|
108
|
-
)
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
## Two additional Eden silent-`any` footguns
|
|
112
|
-
|
|
113
|
-
These apply to ALL Eden Treaty usage (not just streams):
|
|
114
|
-
|
|
115
|
-
1. **Non-chained routes.** Elysia routes MUST be method-chained (`app.get(...).post(...)`). A
|
|
116
|
-
standalone `app.get(...)` that is not chained back drops the `App` type to `any`, losing all
|
|
117
|
-
Treaty type safety.
|
|
118
|
-
|
|
119
|
-
2. **Mismatched tsconfig path aliases.** Client and server packages MUST share or extend one root
|
|
120
|
-
`tsconfig` so path aliases (`~/*`, `@/*`) resolve identically. A mismatch causes Eden's type
|
|
121
|
-
extraction to fail silently and degrade to `any`.
|
|
122
|
-
|
|
123
|
-
Both footguns produce no TypeScript error at the point of breakage — they only manifest as `any`
|
|
124
|
-
types on the Treaty client.
|
|
125
|
-
|
|
126
|
-
## useAgentStream
|
|
127
|
-
|
|
128
|
-
```ts
|
|
129
|
-
const { parts, status, send, stop, regenerate } = useAgentStream({ transport })
|
|
130
|
-
// status: 'idle' | 'streaming' | 'done' | 'error'
|
|
131
|
-
// send(input): starts a new stream, resets parts
|
|
132
|
-
// stop(): aborts the in-flight stream via AbortController
|
|
133
|
-
// regenerate(): re-runs the last input
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
## PartList — exhaustive rendering
|
|
137
|
-
|
|
138
|
-
```tsx
|
|
139
|
-
import { PartList } from 'basalt-ui/agent'
|
|
140
|
-
import { Markdown } from 'basalt-ui/content'
|
|
141
|
-
|
|
142
|
-
// Default headless renderers (plain HTML elements):
|
|
143
|
-
<PartList parts={parts} />
|
|
144
|
-
|
|
145
|
-
// With basalt's own Markdown for the text renderer:
|
|
146
|
-
<PartList
|
|
147
|
-
parts={parts}
|
|
148
|
-
components={{
|
|
149
|
-
text: ({ part }) => <Markdown streaming density="chat">{part.text}</Markdown>,
|
|
150
|
-
}}
|
|
151
|
-
/>
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
`PartList` ships five headless default renderers (zero Mantine):
|
|
155
|
-
|
|
156
|
-
- `text` → `<div className="basalt-agent-text">`
|
|
157
|
-
- `reasoning` → `<details className="basalt-agent-reasoning">`
|
|
158
|
-
- `tool` → `<div className="basalt-agent-tool">` with labelled input/output `<pre>`
|
|
159
|
-
- `source` → `<a className="basalt-agent-source">`
|
|
160
|
-
- `error` → `<div className="basalt-agent-error" role="alert">`
|
|
161
|
-
|
|
162
|
-
`./agent` deliberately ships no markdown renderer of its own — `agent/** -> content` is
|
|
163
|
-
lint-blocked by design (the Mantine-free boundary). The package's only markdown renderer is
|
|
164
|
-
`basalt-ui/content`'s `Markdown`; `threadPartRenderers` (re-exported from the root `basalt-ui`
|
|
165
|
-
entry) already wires it in as the `text` renderer for `ThreadTranscript`/`ThreadWorkspace`.
|
|
166
|
-
|
|
167
|
-
## BasaltStickToBottom (optional peer)
|
|
168
|
-
|
|
169
|
-
```tsx
|
|
170
|
-
import { BasaltStickToBottom } from 'basalt-ui/agent'
|
|
171
|
-
;<BasaltStickToBottom style={{ height: '400px', overflow: 'auto' }}>
|
|
172
|
-
<PartList parts={parts} />
|
|
173
|
-
</BasaltStickToBottom>
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
**Install:** `bun add use-stick-to-bottom`
|
|
177
|
-
|
|
178
|
-
Lazily loaded — `use-stick-to-bottom` is an optional peer. Falls back to a plain scrollable div
|
|
179
|
-
while loading. Surfaces a scroll-to-bottom button (`.basalt-agent-scroll-to-bottom`) when the user
|
|
180
|
-
scrolls up.
|
|
181
|
-
|
|
182
|
-
## createChatHistoryStore
|
|
183
|
-
|
|
184
|
-
```ts
|
|
185
|
-
import { createChatHistoryStore } from 'basalt-ui/agent'
|
|
186
|
-
|
|
187
|
-
// Call once at module scope with a stable key:
|
|
188
|
-
const useChatHistory = createChatHistoryStore({ key: 'main-chat', version: 1, max: 100 })
|
|
189
|
-
|
|
190
|
-
// In a component:
|
|
191
|
-
const { messages, append, clear } = useChatHistory()
|
|
192
|
-
append({ id: crypto.randomUUID(), role: 'user', parts: [...], createdAt: Date.now() })
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
Built on `createPersistedState` — SSR-safe, cross-tab via the `storage` event. Ring-buffered to
|
|
196
|
-
`max` (default 100). Increment `version` when the `ChatMessage` shape changes to clear stale data.
|
|
197
|
-
|
|
198
|
-
## Multi-thread workspace (shipped)
|
|
199
|
-
|
|
200
|
-
For the "many short chats" pattern — each prompt a short-lived thread, the feed showing only a
|
|
201
|
-
distilled outcome (title + summary), a right-hand detail panel to open and continue — basalt-ui
|
|
202
|
-
ships a full workspace. The headless multi-thread layer lives in `./agent` (Mantine-free); the
|
|
203
|
-
ready-built Mantine chrome ships from `basalt-ui/agent-chat` (also re-exported from the root
|
|
204
|
-
entry).
|
|
205
|
-
|
|
206
|
-
### Headless (`basalt-ui/agent`)
|
|
207
|
-
|
|
208
|
-
```ts
|
|
209
|
-
import { createThreadsStore, useAgentThreadRuns, heuristicOutcome } from 'basalt-ui/agent'
|
|
210
|
-
import type { AgentThread, AgentOutcome, OutcomeResolver, ThreadStatus } from 'basalt-ui/agent'
|
|
211
|
-
|
|
212
|
-
// Call ONCE at module scope — same doctrine as createChatHistoryStore:
|
|
213
|
-
const useThreads = createThreadsStore({ key: 'main-threads', version: 1 })
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
- `createThreadsStore({ key, version, maxThreads?, maxMessagesPerThread? })` — the multi-thread
|
|
217
|
-
analog of `createChatHistoryStore`: a persisted, ring-buffered registry of `AgentThread`s (each
|
|
218
|
-
carrying its own `ChatMessage[]`, a distilled `AgentOutcome | null`, a `ThreadStatus`, and a read
|
|
219
|
-
flag). Threads are newest-first; `create()` returns the new id and never touches `activeId`.
|
|
220
|
-
- `useAgentThreadRuns({ transport, store, resolveOutcome })` — the CONCURRENT run manager. Unlike
|
|
221
|
-
`useAgentStream` (one in-flight turn), it runs N streams keyed by thread id, so many short chats
|
|
222
|
-
stream and resolve in the background independently. `start(threadId, input)` / `stop(threadId)` /
|
|
223
|
-
`stopAll()`; `runs` is the live per-thread `{ status, parts }` map for the detail view.
|
|
224
|
-
- `AgentOutcome = { title; summary; status: 'done' | 'attention' | 'error' }` — the distilled feed
|
|
225
|
-
projection, deliberately a DIFFERENT shape from the transcript so raw prompt/thinking can never
|
|
226
|
-
leak into the feed (the enforced boundary).
|
|
227
|
-
|
|
228
|
-
### Outcome resolver — the summarize-to-outcome seam
|
|
229
|
-
|
|
230
|
-
basalt ships ONLY the seam, never an LLM call. `OutcomeResolver = (thread) => AgentOutcome |
|
|
231
|
-
Promise<AgentOutcome>` — the app supplies it. In production derive `{ title, summary }` from the
|
|
232
|
-
finished run (e.g. a structured final step of your own model) and return it. `heuristicOutcome` is
|
|
233
|
-
a demo-only fallback that truncates the last assistant text — never the production path.
|
|
234
|
-
|
|
235
|
-
### Ready-built UI (`basalt-ui/agent-chat`, Mantine)
|
|
236
|
-
|
|
237
|
-
The Mantine-coupled chrome (`ThreadWorkspace` and the lower-level pieces below it) ships its own
|
|
238
|
-
subpath, `basalt-ui/agent-chat` — added in 1.10.0. The root `basalt-ui` entry still re-exports the
|
|
239
|
-
same components (nothing that imported from `basalt-ui` before 1.10.0 needs to change), but a
|
|
240
|
-
consumer who only needs the thread-chat UI can import from `./agent-chat` directly, without pulling
|
|
241
|
-
in `BasaltProvider`, `BasaltShell`, the dashboard composites, or `basalt-ui/connectivity`:
|
|
242
|
-
|
|
243
|
-
```tsx
|
|
244
|
-
import { ThreadWorkspace } from 'basalt-ui/agent-chat'
|
|
245
|
-
;<ThreadWorkspace
|
|
246
|
-
useThreads={useThreads}
|
|
247
|
-
transport={transport}
|
|
248
|
-
resolveOutcome={resolveOutcome}
|
|
249
|
-
newThreadPlaceholder="Ask anything…"
|
|
250
|
-
/>
|
|
251
|
-
```
|
|
252
|
-
|
|
253
|
-
`ThreadWorkspace` is the flagship composite: a `ThreadFeed` of distilled `ThreadOutcomeCard`s + an
|
|
254
|
-
anchored new-thread `Composer` on the left, a `ThreadDetailPanel` (full transcript + a continue
|
|
255
|
-
composer) on the right, collapsing to a single pane below 768px. The lower-level pieces
|
|
256
|
-
(`ThreadFeed`, `ThreadOutcomeCard`, `ThreadDetailPanel`, `Composer`, `ThreadTranscript`,
|
|
257
|
-
`threadPartRenderers`) are exported too for bespoke layouts. Motion (feed insert, panel slide) runs
|
|
258
|
-
on the shared `MOTION_*` tokens and honours `useReducedMotion`.
|
|
259
|
-
|
|
260
|
-
`ThreadFeed` also takes a `variant` (`'outcome'`, the default above, or `'inline'` for
|
|
261
|
-
`ThreadFeedRow`, the Slack-style row that expands in place instead of opening a separate detail
|
|
262
|
-
panel — lazily mounted on first expand, then kept mounted and hidden via CSS on every collapse
|
|
263
|
-
after that) and a `renderRow` override that takes priority over `variant` entirely, for full control
|
|
264
|
-
over live-run wiring (`onSend`/`onStop`/`liveParts`/`liveStatus`) the built-in `'inline'` row
|
|
265
|
-
doesn't expose on its own; wire a real `onSend` on `ThreadFeed` itself to make the built-in row's
|
|
266
|
-
composer usable, or omit it and the row's composer renders disabled rather than a live control that
|
|
267
|
-
silently discards input. `ThreadTranscript` also takes `groupConsecutive` (suppresses role
|
|
268
|
-
label/chrome on same-speaker runs), a per-message `affordances` contract (timestamp/copy/regenerate/
|
|
269
|
-
custom actions), and an optional `virtualize`/`height` windowing mode for very long threads. A
|
|
270
|
-
windowed transcript owns its own scroll node (never nest it in `BasaltStickToBottom`) and scrolls
|
|
271
|
-
itself to the newest message once on mount — `virtualize={{ initialScroll: 'start' }}` opts out, and
|
|
272
|
-
is what a consumer restoring its own saved scroll position wants so the two don't fight.
|
|
273
|
-
|
|
274
|
-
**Boundary:** the headless layer (`createThreadsStore`, `useAgentThreadRuns`, the outcome types)
|
|
275
|
-
stays Mantine-free in `./agent`; the components are Mantine-coupled and ship from `./agent-chat`
|
|
276
|
-
(also re-exported from the root entry). Never add `@mantine/*` under `src/agent/**` — it is
|
|
277
|
-
oxlint-enforced Mantine-free.
|
|
278
|
-
|
|
279
|
-
**No custom part types at the workspace level.** `ThreadWorkspace`, `ThreadDetailPanel`, and
|
|
280
|
-
`ThreadTranscript` render the framework's `AgentPart` union only — they are not generic over a
|
|
281
|
-
consumer-extended part type. If you need custom rendering for the existing part shapes, drop to
|
|
282
|
-
the headless layer instead: compose your own transcript from `PartList` (`basalt-ui/agent`) with a
|
|
283
|
-
custom `components` renderer map, rather than passing a wider type through `ThreadWorkspace`.
|
|
284
|
-
|
|
285
|
-
## Lint guards (agent-chat) — a SEPARATE escape token from `theme-allow`
|
|
286
|
-
|
|
287
|
-
Three oxlint rules (1.10.0) protect the streaming/resume discipline above. All three share one
|
|
288
|
-
escape comment, `basalt-agent-allow` — deliberately NOT `theme-allow`. The two tokens are unrelated:
|
|
289
|
-
a `theme-allow` comment on a color/spacing exception never suppresses one of these, and a
|
|
290
|
-
`basalt-agent-allow` comment never suppresses a `theme-allow` finding. Don't reach for the color
|
|
291
|
-
exemption to quiet a streaming guard — mark the correct token, on the correct line, and only when
|
|
292
|
-
you actually own the guard you're bypassing.
|
|
293
|
-
|
|
294
|
-
- **`basalt/agent-resume-guard`** — flags an unguarded `useChat({ resume: true })` or a bare
|
|
295
|
-
`resumeStream()` call outside `useAgentThreadRuns`. `useAgentThreadRuns` owns single-consumer
|
|
296
|
-
discipline and StrictMode-safe reconnection; a raw resume call re-fires on every effect re-run
|
|
297
|
-
(vercel/ai#7891, no merged fix upstream). Mark `basalt-agent-allow` only if you own the guard
|
|
298
|
-
yourself.
|
|
299
|
-
- **`basalt/agent-no-raw-usechat`** — flags importing `useChat`/`useCompletion` directly from
|
|
300
|
-
`@ai-sdk/react` or `ai/react`. Use `useAgentStream`/`useAgentThreadRuns` over `aiSdkTransport`
|
|
301
|
-
instead — they add unmount abort, supersede guards, and single-consumer resume that the raw hook
|
|
302
|
-
doesn't. Type-only imports (`import type { useChat }`) are exempt; nothing to guard against at
|
|
303
|
-
runtime.
|
|
304
|
-
- **`basalt/ai-sdk-major`** — flags a file whose nearest `package.json` declares a different `ai`
|
|
305
|
-
major than the one basalt-ui itself declares as a peer. A producer and consumer on different `ai`
|
|
306
|
-
majors can throw `'Unknown chunk type'` at runtime — this is the exact defect one production
|
|
307
|
-
consumer hit (one app package on `ai@5`, a sibling on `ai@7`). This rule is PER-FILE — it only
|
|
308
|
-
sees the nearest `package.json` to the linted file, so a lint run scoped to one workspace package
|
|
309
|
-
can't catch a skew against a _different_ package. `bunx basalt-ui doctor`'s `ai-major-parity`
|
|
310
|
-
check is the cross-package counterpart: it walks every workspace manifest and is a HARD failure,
|
|
311
|
-
not a lint warning — run both, they check different things. Like its two siblings above, it
|
|
312
|
-
honours `basalt-agent-allow` on the flagged import (or the line above it) — mark it on an
|
|
313
|
-
intentional producer/consumer file. The repo-wide equivalent for `doctor`'s check is
|
|
314
|
-
`basalt.aiMajorSkewReason` in package.json (a mandatory written reason, not a line comment) — see
|
|
315
|
-
the README and the `BasaltConfig` JSDoc.
|
|
316
|
-
|
|
317
|
-
None of these three rules honour `theme-allow` — only `basalt-agent-allow`, on the flagged line or
|
|
318
|
-
the line above it.
|
|
319
|
-
|
|
320
|
-
## Stream resumption — the reconnect seam
|
|
321
|
-
|
|
322
|
-
basalt-ui ships the CLIENT-SIDE contract for resuming a run after a disconnect (reload, network
|
|
323
|
-
drop) — NOT a concrete backend. A real resumable transport (e.g. Redis-backed run state) is the
|
|
324
|
-
consumer's responsibility; this section documents the seam a consumer transport plugs into.
|
|
325
|
-
|
|
326
|
-
### StartPart
|
|
327
|
-
|
|
328
|
-
A sixth `AgentPart` variant, emitted once at the top of a run:
|
|
329
|
-
|
|
330
|
-
```ts
|
|
331
|
-
export type StartPart = {
|
|
332
|
-
readonly type: 'start'
|
|
333
|
-
readonly runId: string
|
|
334
|
-
readonly resumeToken?: string
|
|
335
|
-
}
|
|
336
|
-
```
|
|
337
|
-
|
|
338
|
-
Not renderable content — `PartList`'s exhaustive switch returns `null` for it, and it's stripped
|
|
339
|
-
before the consumer ever sees it: `useAgentStream` skips it in its accumulation loop; `useAgentThreadRuns`
|
|
340
|
-
skips it too but additionally reads `resumeToken` off it and stashes it on the persisted
|
|
341
|
-
`AgentThread` via `store.setResumeToken`. `parseAgentPart` validates it like every other variant
|
|
342
|
-
(`runId` required string, `resumeToken` optional string).
|
|
343
|
-
|
|
344
|
-
### AgentTransport.resume
|
|
345
|
-
|
|
346
|
-
```ts
|
|
347
|
-
export type AgentTransport<TPart = AgentPart, TInput = string> = {
|
|
348
|
-
stream: (input: TInput, signal?: AbortSignal) => AsyncGenerator<TPart>
|
|
349
|
-
resume?: (resumeToken: string, signal?: AbortSignal) => AsyncGenerator<TPart>
|
|
350
|
-
}
|
|
351
|
-
```
|
|
352
|
-
|
|
353
|
-
Optional — a transport that doesn't support resumption simply omits `resume`. `edenTransport`
|
|
354
|
-
takes an optional second `resumeCall` parameter (same shape as `call`, but keyed on `resumeToken`
|
|
355
|
-
instead of the original input) and only adds a `resume` method to the returned transport object
|
|
356
|
-
when `resumeCall` is provided (no `resume: undefined` key otherwise):
|
|
357
|
-
|
|
358
|
-
```ts
|
|
359
|
-
const transport = edenTransport<AgentPart>(
|
|
360
|
-
(input, signal) => api.chat.post({ body: { message: input }, fetch: { signal } }),
|
|
361
|
-
(resumeToken, signal) => api.chat.resume.post({ body: { resumeToken }, fetch: { signal } }),
|
|
362
|
-
)
|
|
363
|
-
```
|
|
364
|
-
|
|
365
|
-
### ThreadsStore.resumeToken / setResumeToken
|
|
366
|
-
|
|
367
|
-
`AgentThread` carries an optional `resumeToken?: string`, set from the run's most recent
|
|
368
|
-
`StartPart` and cleared once the run finalizes. `ThreadsStore.setResumeToken(id, token)` is the
|
|
369
|
-
setter — same shape/pattern as `setStatus`/`setOutcome`.
|
|
370
|
-
|
|
371
|
-
### Mount-time resume-before-interrupted
|
|
372
|
-
|
|
373
|
-
`useAgentThreadRuns`'s mount-reconciliation effect previously marked every orphaned thread
|
|
374
|
-
(`'pending'`/`'streaming'` with no live controller — e.g. after a reload mid-stream) straight to
|
|
375
|
-
`'interrupted'`. It now attempts a resume first:
|
|
376
|
-
|
|
377
|
-
1. Find the thread's last `role === 'user'` message (no new user message is created on resume — it
|
|
378
|
-
was already appended before the disconnect).
|
|
379
|
-
2. If `transport.resume` is defined AND the thread has a `resumeToken` AND that last user message
|
|
380
|
-
exists, call `transport.resume(resumeToken, signal)` and feed the resulting generator through
|
|
381
|
-
the SAME accumulate/finalize logic as a fresh `start()` call (both share one internal
|
|
382
|
-
`consumeAndFinalize` helper). The resumed run occupies the `runs` map exactly like a live turn —
|
|
383
|
-
reusing the existing `'streaming'` status; there is no separate `'resuming'` ThreadStatus.
|
|
384
|
-
3. Any failure — no `resume()`, no `resumeToken`, no user message, or the resume itself throwing —
|
|
385
|
-
falls back to `'interrupted'`, exactly as before.
|
|
386
|
-
|
|
387
|
-
Resumption is strictly additive: a transport that never emits `StartPart` and never implements
|
|
388
|
-
`resume` behaves identically to today (every orphaned thread still lands on `'interrupted'`).
|
|
389
|
-
|
|
390
|
-
## aiSdkTransport — the RECOMMENDED DEFAULT transport
|
|
391
|
-
|
|
392
|
-
For LLM chat use cases, `aiSdkTransport` (backed by the `ai` npm package, an OPTIONAL peer) is the
|
|
393
|
-
recommended default — `edenTransport` remains fully supported as the zero-extra-dependency
|
|
394
|
-
alternative (see above); neither is deprecated.
|
|
395
|
-
|
|
396
|
-
```ts
|
|
397
|
-
import { useAgentStream, aiSdkTransport } from 'basalt-ui/agent'
|
|
398
|
-
|
|
399
|
-
const transport = aiSdkTransport({ api: '/api/chat' })
|
|
400
|
-
const { parts, status, send } = useAgentStream({ transport })
|
|
401
|
-
```
|
|
402
|
-
|
|
403
|
-
**Install:** `bun add ai`. Like `BasaltStickToBottom`, `ai` is lazily resolved via a memoized
|
|
404
|
-
dynamic `import()` on first `stream()`/`resume()` — importing `basalt-ui/agent` (or calling
|
|
405
|
-
`aiSdkTransport(...)`) never eagerly resolves it. Unlike that, there is no "plain text" degrade
|
|
406
|
-
path if `ai` is missing: the rejected import propagates through the async generator
|
|
407
|
-
into the consumer's existing error handling (`useAgentStream` → `status: 'error'`;
|
|
408
|
-
`useAgentThreadRuns` → its `onFailureStatus`).
|
|
409
|
-
|
|
410
|
-
**Why diffing is needed.** AI SDK's `readUIMessageStream` yields the FULL accumulated `UIMessage`
|
|
411
|
-
snapshot on every update (a growing `parts` array; existing parts' content grows in place) — it is
|
|
412
|
-
not itself a delta stream. basalt-ui's whole part-accumulation model is delta-based, so
|
|
413
|
-
`aiSdkTransport` diffs consecutive snapshots internally and yields only the new `AgentPart` deltas.
|
|
414
|
-
One consequence: `ToolCallPart` gained an optional `toolCallId` field, since AI SDK's tool parts
|
|
415
|
-
carry one and re-emit on every state transition (`input-available` → `output-available`) at the
|
|
416
|
-
SAME array index — a consumer's own coalescing can key on `toolCallId` to update a tool block in
|
|
417
|
-
place instead of rendering a near-duplicate second block.
|
|
418
|
-
|
|
419
|
-
**Chat id binding.** `aiSdkTransport(options)` mints one stable chat id at construction — the
|
|
420
|
-
returned object is immediately usable with `useAgentStream`. Call `.forThread(threadId)` to bind a
|
|
421
|
-
transport to a caller-supplied id instead — the form `useAgentThreadRuns` expects when `transport`
|
|
422
|
-
is a per-thread factory (see "Multi-thread workspace" above):
|
|
423
|
-
|
|
424
|
-
```ts
|
|
425
|
-
const transport = aiSdkTransport({ api: '/api/chat' })
|
|
426
|
-
useAgentThreadRuns({
|
|
427
|
-
transport: (threadId) => transport.forThread(threadId),
|
|
428
|
-
store,
|
|
429
|
-
resolveOutcome,
|
|
430
|
-
})
|
|
431
|
-
```
|
|
432
|
-
|
|
433
|
-
**Scope gaps (v1):** `source-document`, `file`, `reasoning-file`, `data-*`, `step-start`, `custom`,
|
|
434
|
-
and `dynamic-tool` UIMessage parts have no `AgentPart` equivalent yet and are silently skipped by
|
|
435
|
-
the diff — not an oversight, a deliberate v1 boundary (see the code comment in
|
|
436
|
-
`ai-sdk-transport.ts`). Tool parts in the `'input-streaming'` state (partial/`DeepPartial` input)
|
|
437
|
-
are also skipped — only `'input-available'` and later states emit, to avoid flooding the consumer
|
|
438
|
-
with incomplete fragments.
|
|
439
|
-
|
|
440
|
-
## Deferred (advisory — not shipped)
|
|
441
|
-
|
|
442
|
-
The following are explicitly deferred and MUST NOT be scaffolded:
|
|
443
|
-
|
|
444
|
-
- Voice/audio streaming
|
|
445
|
-
- `streamdown` integration (Tailwind-only)
|
|
446
|
-
- Elysia stream route scaffold in app code (consumer's responsibility)
|
|
447
|
-
- `agent-parts.ts` helper files beyond the basalt surface
|
|
448
|
-
|
|
449
|
-
> The full thread-chat composite (`ThreadWorkspace`) is now **shipped** — see "Multi-thread
|
|
450
|
-
> workspace" above. It was previously on this list; a real consumer drove it, so it graduated per
|
|
451
|
-
> the "build-when-driven" rule rather than being scaffolded speculatively. `aiSdkTransport` graduated
|
|
452
|
-
> the same way — see the section above; it is no longer deferred.
|