basalt-ui 1.25.0 → 1.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +8 -6
- package/CHANGELOG.md +26 -0
- package/MIGRATING.md +815 -23
- package/README.md +45 -32
- package/agent/rules/basalt-batteries.md +205 -0
- package/agent/rules/basalt-charts.md +125 -555
- package/agent/rules/basalt-controls.md +160 -0
- package/agent/rules/basalt-mantine.md +146 -439
- package/agent/rules/basalt-state.md +118 -96
- package/agent/rules/basalt-tokens.md +126 -288
- package/agent/skills/basalt-app/SKILL.md +70 -195
- package/agent/skills/basalt-charts/SKILL.md +54 -234
- package/agent/skills/basalt-design/SKILL.md +56 -248
- package/agent/templates/CLAUDE-block.md.tpl +31 -40
- package/agent/templates/DESIGN.md.tpl +22 -51
- package/configs/oxlint-plugin.js +1465 -36
- package/configs/oxlint-plugin.test.ts +1190 -11
- package/configs/oxlint.json +11 -3
- package/dist/charts/cursor/resolve.d.ts +18 -0
- package/dist/charts/cursor/resolve.d.ts.map +1 -1
- package/dist/charts/cursor/resolve.js +15 -6
- package/dist/charts/cursor/scope.d.ts +6 -3
- package/dist/charts/cursor/scope.d.ts.map +1 -1
- package/dist/charts/cursor/store.d.ts +5 -1
- package/dist/charts/cursor/store.d.ts.map +1 -1
- package/dist/charts/cursor/store.js +4 -4
- package/dist/charts/hooks/useChartCursor.d.ts.map +1 -1
- package/dist/charts/hooks/useChartCursor.js +9 -8
- package/dist/charts/index.d.ts +2 -2
- package/dist/charts/index.d.ts.map +1 -1
- package/dist/charts/primitives/ChartCard.d.ts +34 -9
- package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
- package/dist/charts/primitives/ChartCard.js +29 -114
- package/dist/charts/primitives/ChartCard.test.d.ts +2 -0
- package/dist/charts/primitives/ChartCard.test.d.ts.map +1 -0
- package/dist/charts/sparklines/BarSparkline.d.ts +26 -2
- package/dist/charts/sparklines/BarSparkline.d.ts.map +1 -1
- package/dist/charts/sparklines/BarSparkline.js +18 -4
- package/dist/cli/check-coverage.test.d.ts +2 -0
- package/dist/cli/check-coverage.test.d.ts.map +1 -0
- package/dist/cli/index.d.ts +67 -4
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +225 -7
- package/dist/connectivity/connectivity-indicator.d.ts +4 -1
- package/dist/connectivity/connectivity-indicator.d.ts.map +1 -1
- package/dist/connectivity/connectivity-indicator.js +1 -1
- package/dist/content/index.d.ts +5 -3
- package/dist/content/index.d.ts.map +1 -1
- package/dist/content/index.js +4 -2
- package/dist/content/prose.module.css +7 -28
- package/dist/controls/actions.d.ts +169 -0
- package/dist/controls/actions.d.ts.map +1 -0
- package/dist/controls/actions.js +287 -0
- package/dist/controls/actions.module.css +62 -0
- package/dist/controls/actions.module.css.d.ts +13 -0
- package/dist/controls/actions.test.d.ts +2 -0
- package/dist/controls/actions.test.d.ts.map +1 -0
- package/dist/controls/actions.type-guard.test.d.ts +2 -0
- package/dist/controls/actions.type-guard.test.d.ts.map +1 -0
- package/dist/controls/compare-filter.d.ts +41 -0
- package/dist/controls/compare-filter.d.ts.map +1 -0
- package/dist/controls/compare-filter.js +23 -0
- package/dist/controls/control-group.d.ts +19 -0
- package/dist/controls/control-group.d.ts.map +1 -0
- package/dist/controls/control-group.js +20 -0
- package/dist/controls/control-group.test.d.ts +2 -0
- package/dist/controls/control-group.test.d.ts.map +1 -0
- package/dist/controls/controls.module.css +542 -0
- package/dist/controls/controls.module.css.d.ts +35 -0
- package/dist/controls/controls.router.test.d.ts +2 -0
- package/dist/controls/controls.router.test.d.ts.map +1 -0
- package/dist/controls/enum-filter.d.ts +39 -0
- package/dist/controls/enum-filter.d.ts.map +1 -0
- package/dist/controls/enum-filter.js +93 -0
- package/dist/controls/filter-context.d.ts +60 -0
- package/dist/controls/filter-context.d.ts.map +1 -0
- package/dist/controls/filter-context.js +69 -0
- package/dist/controls/filter-pill.d.ts +28 -0
- package/dist/controls/filter-pill.d.ts.map +1 -0
- package/dist/controls/filter-pill.js +70 -0
- package/dist/controls/filter-set.d.ts +13 -0
- package/dist/controls/filter-set.d.ts.map +1 -0
- package/dist/controls/filter-set.js +125 -0
- package/dist/controls/filter-set.test.d.ts +2 -0
- package/dist/controls/filter-set.test.d.ts.map +1 -0
- package/dist/controls/filter-sheet.d.ts +117 -0
- package/dist/controls/filter-sheet.d.ts.map +1 -0
- package/dist/controls/filter-sheet.js +146 -0
- package/dist/controls/glyphs.d.ts +27 -0
- package/dist/controls/glyphs.d.ts.map +1 -0
- package/dist/controls/glyphs.js +49 -0
- package/dist/controls/index.d.ts +20 -0
- package/dist/controls/index.d.ts.map +1 -0
- package/dist/controls/index.js +46 -0
- package/dist/controls/multi-select-filter.d.ts +23 -0
- package/dist/controls/multi-select-filter.d.ts.map +1 -0
- package/dist/controls/multi-select-filter.js +89 -0
- package/dist/controls/number-filter.d.ts +26 -0
- package/dist/controls/number-filter.d.ts.map +1 -0
- package/dist/controls/number-filter.js +143 -0
- package/dist/controls/peer-boundary.test.d.ts +2 -0
- package/dist/controls/peer-boundary.test.d.ts.map +1 -0
- package/dist/controls/props-contract.test.d.ts +2 -0
- package/dist/controls/props-contract.test.d.ts.map +1 -0
- package/dist/controls/range-filter.d.ts +43 -0
- package/dist/controls/range-filter.d.ts.map +1 -0
- package/dist/controls/range-filter.js +135 -0
- package/dist/controls/range-filter.type-guard.test.d.ts +2 -0
- package/dist/controls/range-filter.type-guard.test.d.ts.map +1 -0
- package/dist/controls/search-filter.d.ts +10 -0
- package/dist/controls/search-filter.d.ts.map +1 -0
- package/dist/controls/search-filter.js +81 -0
- package/dist/controls/select-filter.d.ts +74 -0
- package/dist/controls/select-filter.d.ts.map +1 -0
- package/dist/controls/select-filter.js +10 -0
- package/dist/controls/sync-button.d.ts +38 -0
- package/dist/controls/sync-button.d.ts.map +1 -0
- package/dist/controls/sync-button.js +119 -0
- package/dist/controls/sync-button.module.css +63 -0
- package/dist/controls/sync-button.module.css.d.ts +11 -0
- package/dist/controls/sync-button.test.d.ts +2 -0
- package/dist/controls/sync-button.test.d.ts.map +1 -0
- package/dist/controls/toggle-filter.d.ts +9 -0
- package/dist/controls/toggle-filter.d.ts.map +1 -0
- package/dist/controls/toggle-filter.js +42 -0
- package/dist/controls/view-tabs.d.ts +23 -0
- package/dist/controls/view-tabs.d.ts.map +1 -0
- package/dist/controls/view-tabs.js +59 -0
- package/dist/controls-dates/date-range-picker.d.ts +4 -0
- package/dist/controls-dates/date-range-picker.d.ts.map +1 -0
- package/dist/controls-dates/date-range-picker.js +18 -0
- package/dist/controls-dates/index.d.ts +9 -0
- package/dist/controls-dates/index.d.ts.map +1 -0
- package/dist/controls-dates/index.js +4 -0
- package/dist/dashboard/delta-badge.d.ts +6 -12
- package/dist/dashboard/delta-badge.d.ts.map +1 -1
- package/dist/dashboard/delta-badge.js +1 -50
- package/dist/dashboard/index.d.ts +1 -1
- package/dist/dashboard/index.d.ts.map +1 -1
- package/dist/dashboard/index.js +3 -1
- package/dist/dashboard/settings-section.d.ts +6 -4
- package/dist/dashboard/settings-section.d.ts.map +1 -1
- package/dist/dashboard/settings-section.js +23 -16
- package/dist/dashboard/settings-section.module.css +10 -0
- package/dist/dashboard/settings-section.module.css.d.ts +1 -0
- package/dist/dashboard/settings-section.test.d.ts +2 -0
- package/dist/dashboard/settings-section.test.d.ts.map +1 -0
- package/dist/dashboard/stat-card.d.ts +95 -9
- package/dist/dashboard/stat-card.d.ts.map +1 -1
- package/dist/dashboard/stat-card.js +108 -49
- package/dist/dashboard/stat-card.module.css +153 -0
- package/dist/dashboard/stat-card.module.css.d.ts +15 -0
- package/dist/data/data-table.d.ts +37 -15
- package/dist/data/data-table.d.ts.map +1 -1
- package/dist/data/data-table.js +96 -58
- package/dist/guard/grace.test.d.ts +20 -0
- package/dist/guard/grace.test.d.ts.map +1 -0
- package/dist/guard/index.d.ts +66 -3
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +78 -7
- package/dist/guard/types.d.ts +3 -3
- package/dist/guard/types.d.ts.map +1 -1
- package/dist/index.d.ts +13 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -10
- package/dist/nav/types.d.ts +78 -2
- package/dist/nav/types.d.ts.map +1 -1
- package/dist/notifications/bell.d.ts +4 -1
- package/dist/notifications/bell.d.ts.map +1 -1
- package/dist/notifications/index.d.ts +1 -1
- package/dist/router-tanstack/field.d.ts +14 -0
- package/dist/router-tanstack/field.d.ts.map +1 -0
- package/dist/router-tanstack/field.js +4 -0
- package/dist/router-tanstack/index.d.ts +6 -2
- package/dist/router-tanstack/index.d.ts.map +1 -1
- package/dist/router-tanstack/index.js +4 -0
- package/dist/router-tanstack/multi-search-param-store.d.ts +4 -52
- package/dist/router-tanstack/multi-search-param-store.d.ts.map +1 -1
- package/dist/router-tanstack/multi-search-param-store.js +19 -27
- package/dist/router-tanstack/search-param-store.d.ts +3 -87
- package/dist/router-tanstack/search-param-store.d.ts.map +1 -1
- package/dist/router-tanstack/search-param-store.js +16 -45
- package/dist/router-tanstack/search-store.d.ts +84 -0
- package/dist/router-tanstack/search-store.d.ts.map +1 -0
- package/dist/router-tanstack/search-store.js +271 -0
- package/dist/router-tanstack/search-store.router.test.d.ts +2 -0
- package/dist/router-tanstack/search-store.router.test.d.ts.map +1 -0
- package/dist/router-tanstack/search-store.test.d.ts +2 -0
- package/dist/router-tanstack/search-store.test.d.ts.map +1 -0
- package/dist/router-tanstack/use-nav.d.ts +0 -2
- package/dist/router-tanstack/use-nav.d.ts.map +1 -1
- package/dist/router-tanstack/use-nav.js +34 -12
- package/dist/router-tanstack/use-nav.test.d.ts +2 -0
- package/dist/router-tanstack/use-nav.test.d.ts.map +1 -0
- package/dist/section/index.d.ts +2 -0
- package/dist/section/index.d.ts.map +1 -0
- package/dist/section/index.js +4 -0
- package/dist/section/section.d.ts +24 -0
- package/dist/section/section.d.ts.map +1 -0
- package/dist/section/section.js +126 -0
- package/dist/section/section.module.css +104 -0
- package/dist/section/section.module.css.d.ts +11 -0
- package/dist/section/section.test.d.ts +2 -0
- package/dist/section/section.test.d.ts.map +1 -0
- package/dist/shell/app-breadcrumbs.d.ts.map +1 -1
- package/dist/shell/app-breadcrumbs.js +18 -15
- package/dist/shell/app-header.module.css +77 -37
- package/dist/shell/app-header.module.css.d.ts +2 -1
- package/dist/shell/app-mobile-nav.d.ts +9 -2
- package/dist/shell/app-mobile-nav.d.ts.map +1 -1
- package/dist/shell/app-mobile-nav.js +98 -11
- package/dist/shell/app-mobile-nav.module.css +8 -0
- package/dist/shell/app-mobile-nav.module.css.d.ts +1 -0
- package/dist/shell/app-sidebar.d.ts +55 -28
- package/dist/shell/app-sidebar.d.ts.map +1 -1
- package/dist/shell/app-sidebar.js +116 -67
- package/dist/shell/app-sidebar.module.css +303 -20
- package/dist/shell/app-sidebar.module.css.d.ts +19 -1
- package/dist/shell/index.d.ts +55 -21
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +40 -18
- package/dist/shell/mobile-nav-model.d.ts +13 -2
- package/dist/shell/mobile-nav-model.d.ts.map +1 -1
- package/dist/shell/mobile-nav-model.js +15 -1
- package/dist/shell/nav-count-badge.d.ts +1 -1
- package/dist/shell/page-bar.d.ts +51 -0
- package/dist/shell/page-bar.d.ts.map +1 -0
- package/dist/shell/page-bar.js +154 -0
- package/dist/shell/page-bar.module.css +178 -0
- package/dist/shell/page-bar.module.css.d.ts +17 -0
- package/dist/shell/page-bar.test.d.ts +2 -0
- package/dist/shell/page-bar.test.d.ts.map +1 -0
- package/dist/shell/sidebar-block-model.d.ts +48 -0
- package/dist/shell/sidebar-block-model.d.ts.map +1 -0
- package/dist/shell/sidebar-block-model.js +39 -0
- package/dist/shell/sidebar-blocks.d.ts +32 -0
- package/dist/shell/sidebar-blocks.d.ts.map +1 -0
- package/dist/shell/sidebar-blocks.js +169 -0
- package/dist/shell/sidebar-search.d.ts +13 -1
- package/dist/shell/sidebar-search.d.ts.map +1 -1
- package/dist/shell/sidebar-search.js +34 -1
- package/dist/shell/sidebar-search.module.css +26 -0
- package/dist/shell/sidebar-search.module.css.d.ts +2 -0
- package/dist/state/fields.d.ts +565 -0
- package/dist/state/fields.d.ts.map +1 -0
- package/dist/state/fields.js +405 -0
- package/dist/state/fields.test.d.ts +2 -0
- package/dist/state/fields.test.d.ts.map +1 -0
- package/dist/state/persisted.d.ts +60 -0
- package/dist/state/persisted.d.ts.map +1 -0
- package/dist/state/persisted.js +123 -0
- package/dist/state/persisted.test.d.ts +2 -0
- package/dist/state/persisted.test.d.ts.map +1 -0
- package/dist/state.d.ts +11 -56
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +14 -120
- package/dist/styles.css +13 -0
- package/dist/surfaces.d.ts +106 -24
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +156 -19
- package/dist/theme/controls.module.css +16 -1
- package/dist/theme/ctl-theme.d.ts +32 -0
- package/dist/theme/ctl-theme.d.ts.map +1 -0
- package/dist/theme/ctl-theme.js +54 -0
- package/dist/theme/ctl-theme.test.d.ts +2 -0
- package/dist/theme/ctl-theme.test.d.ts.map +1 -0
- package/dist/theme/ctl-tier-coverage.test.d.ts +2 -0
- package/dist/theme/ctl-tier-coverage.test.d.ts.map +1 -0
- package/dist/theme/divider-law.test.d.ts +2 -0
- package/dist/theme/divider-law.test.d.ts.map +1 -0
- package/dist/theme/icon-slot.d.ts +44 -0
- package/dist/theme/icon-slot.d.ts.map +1 -0
- package/dist/theme/icon-slot.js +21 -0
- package/dist/theme/icon-slot.module.css +47 -0
- package/dist/theme/icon-slot.module.css.d.ts +8 -0
- package/dist/theme/icon-slot.test.d.ts +2 -0
- package/dist/theme/icon-slot.test.d.ts.map +1 -0
- package/dist/theme/index.d.ts +1 -0
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/index.js +82 -5
- package/dist/theme/layout-rhythm-css.test.d.ts +2 -0
- package/dist/theme/layout-rhythm-css.test.d.ts.map +1 -0
- package/dist/theme/segmented-control.module.css +20 -0
- package/dist/theme/shadow-surfaces.test.d.ts.map +1 -1
- package/dist/tokens/index.d.ts +12 -0
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +38 -8
- package/dist/tokens/palette.d.ts +97 -61
- package/dist/tokens/palette.d.ts.map +1 -1
- package/dist/tokens/palette.js +76 -38
- package/dist/tokens.css +10 -2
- package/dist/widget-header/delta-badge.d.ts +13 -0
- package/dist/widget-header/delta-badge.d.ts.map +1 -0
- package/dist/widget-header/delta-badge.js +29 -0
- package/dist/widget-header/delta-badge.test.d.ts +2 -0
- package/dist/widget-header/delta-badge.test.d.ts.map +1 -0
- package/dist/widget-header/index.d.ts +3 -0
- package/dist/widget-header/index.d.ts.map +1 -0
- package/dist/widget-header/index.js +6 -0
- package/dist/widget-header/no-mantine.test.d.ts +2 -0
- package/dist/widget-header/no-mantine.test.d.ts.map +1 -0
- package/dist/widget-header/widget-header.d.ts +67 -0
- package/dist/widget-header/widget-header.d.ts.map +1 -0
- package/dist/widget-header/widget-header.js +89 -0
- package/dist/widget-header/widget-header.module.css +350 -0
- package/dist/widget-header/widget-header.module.css.d.ts +25 -0
- package/dist/widget-header/widget-header.test.d.ts +2 -0
- package/dist/widget-header/widget-header.test.d.ts.map +1 -0
- package/llms.txt +30 -17
- package/package.json +14 -1
- package/src/charts/cursor/resolve.test.ts +29 -1
- package/src/charts/cursor/resolve.ts +38 -6
- package/src/charts/cursor/scope.tsx +6 -3
- package/src/charts/cursor/store.test.ts +19 -1
- package/src/charts/cursor/store.ts +10 -5
- package/src/charts/hooks/useChartCursor.ts +16 -8
- package/src/charts/index.ts +2 -2
- package/src/charts/primitives/CartesianChart.test.tsx +124 -0
- package/src/charts/primitives/ChartCard.test.tsx +66 -0
- package/src/charts/primitives/ChartCard.tsx +86 -146
- package/src/charts/sparklines/BarSparkline.tsx +43 -5
- package/src/cli/check-coverage.test.ts +297 -0
- package/src/cli/check-theme.test.ts +38 -14
- package/src/cli/index.ts +392 -16
- package/src/cli/init-sync.test.ts +97 -0
- package/src/connectivity/connectivity-indicator.tsx +16 -9
- package/src/content/index.ts +16 -3
- package/src/content/prose.module.css +7 -28
- package/src/content/prose.module.css.test.ts +8 -24
- package/src/controls/actions.module.css +62 -0
- package/src/controls/actions.module.css.d.ts +13 -0
- package/src/controls/actions.test.tsx +252 -0
- package/src/controls/actions.tsx +655 -0
- package/src/controls/actions.type-guard.test.ts +63 -0
- package/src/controls/compare-filter.tsx +64 -0
- package/src/controls/control-group.test.tsx +229 -0
- package/src/controls/control-group.tsx +63 -0
- package/src/controls/controls.module.css +542 -0
- package/src/controls/controls.module.css.d.ts +35 -0
- package/src/controls/controls.router.test.tsx +1053 -0
- package/src/controls/enum-filter.tsx +170 -0
- package/src/controls/filter-context.tsx +158 -0
- package/src/controls/filter-pill.tsx +120 -0
- package/src/controls/filter-set.test.tsx +472 -0
- package/src/controls/filter-set.tsx +225 -0
- package/src/controls/filter-sheet.tsx +285 -0
- package/src/controls/glyphs.tsx +81 -0
- package/src/controls/index.ts +49 -0
- package/src/controls/multi-select-filter.tsx +140 -0
- package/src/controls/number-filter.tsx +291 -0
- package/src/controls/peer-boundary.test.ts +85 -0
- package/src/controls/props-contract.test.tsx +137 -0
- package/src/controls/range-filter.tsx +237 -0
- package/src/controls/range-filter.type-guard.test.tsx +122 -0
- package/src/controls/search-filter.tsx +123 -0
- package/src/controls/select-filter.tsx +89 -0
- package/src/controls/sync-button.module.css +63 -0
- package/src/controls/sync-button.module.css.d.ts +11 -0
- package/src/controls/sync-button.test.tsx +183 -0
- package/src/controls/sync-button.tsx +197 -0
- package/src/controls/toggle-filter.tsx +60 -0
- package/src/controls/view-tabs.tsx +110 -0
- package/src/controls-dates/date-range-picker.tsx +42 -0
- package/src/controls-dates/index.ts +8 -0
- package/src/dashboard/delta-badge.tsx +4 -74
- package/src/dashboard/index.ts +6 -1
- package/src/dashboard/settings-section.module.css +10 -0
- package/src/dashboard/settings-section.module.css.d.ts +1 -0
- package/src/dashboard/settings-section.test.tsx +56 -0
- package/src/dashboard/settings-section.tsx +33 -30
- package/src/dashboard/stat-card.module.css +153 -0
- package/src/dashboard/stat-card.module.css.d.ts +15 -0
- package/src/dashboard/stat-card.test.tsx +298 -1
- package/src/dashboard/stat-card.tsx +289 -67
- package/src/data/data-table.test.tsx +96 -11
- package/src/data/data-table.tsx +228 -84
- package/src/guard/check-source.test.ts +159 -13
- package/src/guard/grace.test.ts +158 -0
- package/src/guard/index.ts +185 -29
- package/src/guard/types.ts +4 -2
- package/src/index.ts +51 -5
- package/src/nav/types.ts +84 -2
- package/src/notifications/bell.tsx +6 -3
- package/src/notifications/index.ts +1 -1
- package/src/router-tanstack/field.ts +35 -0
- package/src/router-tanstack/index.ts +28 -2
- package/src/router-tanstack/multi-search-param-store.test.ts +71 -3
- package/src/router-tanstack/multi-search-param-store.ts +46 -77
- package/src/router-tanstack/search-param-store.ts +42 -159
- package/src/router-tanstack/search-store.router.test.tsx +882 -0
- package/src/router-tanstack/search-store.test.ts +578 -0
- package/src/router-tanstack/search-store.ts +546 -0
- package/src/router-tanstack/use-nav.test.tsx +187 -0
- package/src/router-tanstack/use-nav.ts +72 -14
- package/src/section/index.ts +1 -0
- package/src/section/section.module.css +104 -0
- package/src/section/section.module.css.d.ts +11 -0
- package/src/section/section.test.tsx +204 -0
- package/src/section/section.tsx +220 -0
- package/src/shell/app-breadcrumbs.tsx +44 -31
- package/src/shell/app-header.module.css +77 -37
- package/src/shell/app-header.module.css.d.ts +2 -1
- package/src/shell/app-mobile-nav.module.css +8 -0
- package/src/shell/app-mobile-nav.module.css.d.ts +1 -0
- package/src/shell/app-mobile-nav.tsx +147 -8
- package/src/shell/app-sidebar-account.tsx +2 -2
- package/src/shell/app-sidebar.module.css +303 -20
- package/src/shell/app-sidebar.module.css.d.ts +19 -1
- package/src/shell/app-sidebar.test.tsx +456 -41
- package/src/shell/app-sidebar.tsx +276 -140
- package/src/shell/index.test.tsx +291 -5
- package/src/shell/index.tsx +137 -38
- package/src/shell/mobile-nav-model.test.ts +228 -7
- package/src/shell/mobile-nav-model.ts +53 -3
- package/src/shell/nav-count-badge.tsx +1 -1
- package/src/shell/page-bar.module.css +178 -0
- package/src/shell/page-bar.module.css.d.ts +17 -0
- package/src/shell/page-bar.test.tsx +316 -0
- package/src/shell/page-bar.tsx +315 -0
- package/src/shell/sidebar-block-model.ts +83 -0
- package/src/shell/sidebar-blocks.tsx +301 -0
- package/src/shell/sidebar-search.module.css +26 -0
- package/src/shell/sidebar-search.module.css.d.ts +2 -0
- package/src/shell/sidebar-search.tsx +71 -2
- package/src/state/fields.test.tsx +512 -0
- package/src/state/fields.ts +1150 -0
- package/src/state/persisted.test.ts +91 -0
- package/src/state/persisted.ts +261 -0
- package/src/state.ts +52 -240
- package/src/styles.css +13 -0
- package/src/surfaces.ts +204 -36
- package/src/theme/controls.module.css +16 -1
- package/src/theme/ctl-theme.test.tsx +275 -0
- package/src/theme/ctl-theme.tsx +138 -0
- package/src/theme/ctl-tier-coverage.test.ts +170 -0
- package/src/theme/density-relations.test.ts +106 -51
- package/src/theme/divider-law.test.ts +259 -0
- package/src/theme/icon-slot.module.css +47 -0
- package/src/theme/icon-slot.module.css.d.ts +8 -0
- package/src/theme/icon-slot.test.tsx +275 -0
- package/src/theme/icon-slot.tsx +59 -0
- package/src/theme/index.ts +165 -3
- package/src/theme/layout-rhythm-css.test.ts +72 -0
- package/src/theme/radius.test.ts +11 -1
- package/src/theme/segmented-control.module.css +20 -0
- package/src/theme/shadow-surfaces.test.ts +20 -7
- package/src/theme/spacing.test.ts +75 -27
- package/src/theme/use-basalt-spacing.ts +2 -2
- package/src/tokens/density.test.ts +7 -25
- package/src/tokens/index.ts +48 -8
- package/src/tokens/palette.ts +107 -73
- package/src/widget-header/delta-badge.test.tsx +60 -0
- package/src/widget-header/delta-badge.tsx +57 -0
- package/src/widget-header/index.ts +2 -0
- package/src/widget-header/no-mantine.test.ts +33 -0
- package/src/widget-header/widget-header.module.css +350 -0
- package/src/widget-header/widget-header.module.css.d.ts +25 -0
- package/src/widget-header/widget-header.test.tsx +187 -0
- package/src/widget-header/widget-header.tsx +213 -0
- package/agent/rules/basalt-agent.md +0 -452
- package/agent/rules/basalt-app.md +0 -246
- package/agent/rules/basalt-commands.md +0 -320
- package/agent/rules/basalt-content.md +0 -409
- package/agent/rules/basalt-data.md +0 -329
- package/agent/rules/basalt-forms.md +0 -186
- package/agent/rules/basalt-notifications.md +0 -264
- package/agent/rules/basalt-query.md +0 -187
- package/agent/rules/basalt-router.md +0 -336
- package/dist/content/article-filter-bar.d.ts +0 -40
- package/dist/content/article-filter-bar.d.ts.map +0 -1
- package/dist/content/article-filter-bar.js +0 -56
- package/dist/shell/page-header.d.ts +0 -13
- package/dist/shell/page-header.d.ts.map +0 -1
- package/dist/shell/page-header.js +0 -23
- package/src/content/article-filter-bar.tsx +0 -88
- package/src/shell/page-header.tsx +0 -35
|
@@ -1,219 +1,94 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: basalt-app
|
|
3
|
-
description: Scaffold or refresh a basalt-ui app —
|
|
4
|
-
when_to_use: User is adding basalt-ui to a new or existing app, asks how to set it up
|
|
3
|
+
description: Scaffold or refresh a basalt-ui app — the procedure around `basalt-ui init` (first-time scaffold) and `basalt-ui sync` (drift-managed refresh), plus the runtime wiring neither of them does for you (provider, shell, vite, CSS order). Use when setting up basalt-ui in a new or existing app, after upgrading it, or when the shipped doctrine seems missing.
|
|
4
|
+
when_to_use: User is adding basalt-ui to a new or existing app, asks how to set it up or scaffold it, wants to wire BasaltProvider / BasaltShell / the vite preset, refresh the shipped rules after an upgrade, or wire a freshness check in CI. Also when /basalt-design reports no consumer DESIGN.md.
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
`/basalt-app`
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
`/basalt-app` gets a consumer scaffolded and keeps its shipped doctrine fresh. Everything — the
|
|
8
|
+
components, the toolchain presets, the rules, the skills, the templates — ships in the one npm
|
|
9
|
+
package; `init` places it and `sync` refreshes it.
|
|
10
10
|
|
|
11
|
-
##
|
|
12
|
-
|
|
13
|
-
Everything ships in the `basalt-ui` npm package — components, toolchain presets, rules, skills,
|
|
14
|
-
templates. Becoming a consumer is exactly two steps:
|
|
11
|
+
## 1. Install and scaffold
|
|
15
12
|
|
|
16
13
|
```bash
|
|
17
14
|
bun add basalt-ui
|
|
18
|
-
bunx basalt-ui init
|
|
19
|
-
./node_modules/.bin/basalt-ui --version
|
|
15
|
+
bunx basalt-ui init # the ONE command that legitimately predates the install
|
|
16
|
+
./node_modules/.bin/basalt-ui --version # every command after it: the LOCAL bin
|
|
20
17
|
```
|
|
21
18
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
reason anything is copied. Everything a machine reads is a **reference** instead: the seeded
|
|
39
|
-
`.oxlintrc.json` and `lefthook.yml` just `extends` the presets inside
|
|
40
|
-
`node_modules/basalt-ui/configs/`, so the toolchain auto-updates with the package while you still
|
|
41
|
-
own the files.
|
|
42
|
-
|
|
43
|
-
## First-time scaffold: `bunx basalt-ui init`
|
|
44
|
-
|
|
45
|
-
`init` writes the repo-side doctrine and toolchain into a consumer (monorepo globs supported):
|
|
46
|
-
|
|
47
|
-
- `.claude/rules/basalt-*.md` — the thirteen shipped rules (`basalt-tokens`, `basalt-charts`,
|
|
48
|
-
`basalt-mantine`, `basalt-router`, `basalt-query`, `basalt-state`, `basalt-forms`,
|
|
49
|
-
`basalt-notifications`, `basalt-commands`, `basalt-data`, `basalt-agent`, `basalt-content`,
|
|
50
|
-
`basalt-app`), managed verbatim.
|
|
51
|
-
- `.claude/skills/basalt-{app,charts,design}/SKILL.md` — the three skills (`/basalt-app`,
|
|
52
|
-
`/basalt-charts`, `/basalt-design`), managed verbatim, same path the rules take.
|
|
53
|
-
- A managed `<!-- basalt:begin --> ... <!-- basalt:end -->` block in `CLAUDE.md`: stack facts, the
|
|
54
|
-
**DESIGN.md-is-law** pointer, and the **frontend-design restraint override** (in a dashboard, the
|
|
55
|
-
bold move is calm — defer to `/basalt-design`, not generic "make it striking" instincts).
|
|
56
|
-
- A **thin** `DESIGN.md` seed (deltas only on top of the `basalt-*` rules): identity confirmation,
|
|
57
|
-
the series dictionary, app deviations. The CLAUDE block `@`-imports it, so it auto-loads.
|
|
58
|
-
- Toolchain seeds: `.oxlintrc.json` and `lefthook.yml` are `extends` stubs pointing into
|
|
59
|
-
`node_modules/basalt-ui/configs/`; `.oxfmtrc.json` is a starting copy (oxfmt has no `extends`);
|
|
60
|
-
`.github/workflows/check.yml` is a starting workflow. All four are yours after the first write.
|
|
61
|
-
- `.basalt/manifest.json` — a sha256 per managed file + the basalt-ui version that wrote it, so
|
|
62
|
-
`sync` can three-way diff and `doctor` can spot a stale install.
|
|
63
|
-
|
|
64
|
-
It also patches `package.json`: **`basalt.roots`** inferred from the real layout (workspace packages
|
|
65
|
-
depending on basalt or Mantine, else every workspace `src/`, else `src`) and a **`lint:basalt`**
|
|
66
|
-
script (`oxlint . && basalt-ui check-theme`). Everything derives from `roots` — the guard's scan, the
|
|
67
|
-
seeded CI `oxfmt` globs, the default exemption — so a workspace repo without it scaffolds a guard
|
|
68
|
-
that scans zero files while every gate reports green. Correct it if your sources live elsewhere. A
|
|
69
|
-
Mantine-free consumer adds `"basalt": { "profile": "tokens-only" }` by hand; `check-theme` never
|
|
70
|
-
infers that, because inferring it from a missing `@mantine/core` would silence 17 kinds on a repo
|
|
71
|
-
that simply keeps Mantine in another workspace package.
|
|
72
|
-
|
|
73
|
-
**`init` on an EXISTING app is a lint-debt event, not a no-op.** Adopting the shipped preset turns
|
|
74
|
-
on whole oxlint plugins the repo was never linted against, so previously-clean code lands with real
|
|
75
|
-
findings on the first run. `init` names the plugins and every file it kept — plus what keeping it
|
|
76
|
-
costs (an existing `.oxlintrc.json` that does not `extends` the preset means the entire basalt lint
|
|
77
|
-
half is off, and nothing else says so; `--merge-lint` splices it in). Run `oxlint .` and triage
|
|
78
|
-
before the next commit; turn a rule off with a written reason rather than disabling a plugin.
|
|
79
|
-
|
|
80
|
-
Then `basalt-ui doctor`. `SKIPPED` is a third outcome beside pass/warn/fail and exits non-zero on
|
|
81
|
-
its own — a check that could not run is not a check that passed — and the hard checks now include
|
|
82
|
-
`basalt-resolves`, `guard-scan` (would `check-theme` cover more than zero files?) and
|
|
83
|
-
`oxlint-preset` (does `.oxlintrc.json` really extend the preset? JSONC is parsed, not rejected).
|
|
84
|
-
Expect doctor to go red where it was green on an older basalt; that is the point.
|
|
85
|
-
|
|
86
|
-
After `init`, do the runtime wiring (the CLI scaffolds files, not your app's composition):
|
|
19
|
+
`init` writes the managed doctrine (`.claude/rules/basalt-*.md`, `.claude/skills/basalt-*/SKILL.md`,
|
|
20
|
+
the `CLAUDE.md` block), the seeds you then own (`DESIGN.md`, `.oxlintrc.json`, `.oxfmtrc.json`,
|
|
21
|
+
`lefthook.yml`, CI, optional scaffolds), and `.basalt/manifest.json` — a sha256 per managed unit so
|
|
22
|
+
`sync` can three-way diff. It also patches `basalt.roots` and a `lint:basalt` script.
|
|
23
|
+
|
|
24
|
+
Two modes, decided by one question — **does Claude read this file?** Managed files are
|
|
25
|
+
framework-owned and meant to be overwritten (the sync diff is the review gate); seeds are written
|
|
26
|
+
once and yours forever. Claude Code cannot load rules or skills out of `node_modules`, which is the
|
|
27
|
+
only reason anything is copied at all; everything a MACHINE reads is an `extends` reference into
|
|
28
|
+
`node_modules/basalt-ui/configs/`, so the toolchain auto-updates with the package.
|
|
29
|
+
|
|
30
|
+
**`init` on an existing app is a lint-debt event, not a no-op** — read
|
|
31
|
+
`.claude/rules/basalt-batteries.md` § "App bootstrapping" for what it turns on and what keeping your
|
|
32
|
+
own `.oxlintrc.json` costs.
|
|
33
|
+
|
|
34
|
+
## 2. Wire the runtime (the CLI scaffolds files, not your app's composition)
|
|
87
35
|
|
|
88
36
|
```tsx
|
|
89
|
-
//
|
|
90
|
-
import
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
// Theme — start from the base, override deltas only
|
|
99
|
-
import { createBasaltTheme } from 'basalt-ui'
|
|
100
|
-
const theme = createBasaltTheme({
|
|
101
|
-
/* app deltas */
|
|
102
|
-
})
|
|
37
|
+
// main.tsx — CSS order is load-bearing: every @mantine/*/styles.layer.css, THEN basalt's
|
|
38
|
+
import '@mantine/core/styles.layer.css'
|
|
39
|
+
import 'basalt-ui/styles.css'
|
|
40
|
+
import { BasaltProvider, createBasaltTheme } from 'basalt-ui'
|
|
41
|
+
import { BasaltOverlays } from 'basalt-ui/commands'
|
|
42
|
+
;<BasaltProvider theme={createBasaltTheme(/* app deltas only */)} defaultColorScheme="dark">
|
|
43
|
+
<BasaltOverlays>{/* data layer, then the router */}</BasaltOverlays>
|
|
44
|
+
</BasaltProvider>
|
|
103
45
|
```
|
|
104
46
|
|
|
105
47
|
```ts
|
|
106
|
-
// vite.config.ts —
|
|
107
|
-
// BASALT_LOCAL alias) composed with basaltAppPlugin (PWA head, manifest, icon metadata)
|
|
108
|
-
import react from '@vitejs/plugin-react'
|
|
109
|
-
import { basaltAppPlugin, basaltViteConfig } from 'basalt-ui/vite'
|
|
110
|
-
import { defineConfig } from 'vite'
|
|
111
|
-
|
|
48
|
+
// vite.config.ts — the preset is config-only; the plugin half is spread into your own plugins
|
|
112
49
|
export default defineConfig({
|
|
113
50
|
...basaltViteConfig({ port: 5173, apiTarget: 'http://localhost:3000' }),
|
|
114
51
|
plugins: [react(), ...basaltAppPlugin({ name: 'MyApp', description: '…' })],
|
|
115
52
|
})
|
|
116
53
|
```
|
|
117
54
|
|
|
118
|
-
`
|
|
119
|
-
|
|
120
|
-
|
|
55
|
+
Then `BasaltShell` with a `useNav(NAV)` spread (basalt-state.md), `PageBar` per page
|
|
56
|
+
(basalt-controls.md), and `<first basalt.root>/lib/series.ts` as the app's one guard-exempt series
|
|
57
|
+
source (`/basalt-charts`). There is no Tailwind.
|
|
58
|
+
|
|
59
|
+
## 3. Verify the wiring took
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
./node_modules/.bin/basalt-ui doctor # read it to the LAST line
|
|
63
|
+
bun run lint:basalt # oxlint . && basalt-ui check-theme
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`SKIPPED` is a third outcome beside pass/warn/fail and **exits non-zero on its own** — a check that
|
|
67
|
+
could not run is not a check that passed. Expect doctor to go red where an older basalt was green;
|
|
68
|
+
that is the point. Then triage `oxlint .` before the next commit.
|
|
69
|
+
|
|
70
|
+
## 4. Refresh after every upgrade
|
|
121
71
|
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
|
|
72
|
+
```bash
|
|
73
|
+
./node_modules/.bin/basalt-ui sync # --check in CI, --force to overwrite local edits
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
- Unchanged since basalt wrote it → overwritten. Locally edited → diffed and SKIPPED. Missing →
|
|
77
|
+
recreated. **Retired upstream → deleted**, and named in the output; a rule file basalt stopped
|
|
78
|
+
shipping is doctrine your agent would otherwise keep reading.
|
|
79
|
+
- `sync --check` makes no writes and exits non-zero on any of that — wire it as the freshness gate.
|
|
80
|
+
- Run it anywhere in the repo: it resolves the project the way `check-theme` and `doctor` do,
|
|
81
|
+
announces a relocation, refreshes a PARENT install from a sub-package, and still refuses to
|
|
82
|
+
scaffold a second consumer (that stays `init`'s decision).
|
|
83
|
+
- Seeds are never reconciled or reported. They are yours.
|
|
84
|
+
|
|
85
|
+
## 5. Not a React app?
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
bunx basalt-ui tokens:css --out src/tokens.css # no install to resolve, so bunx is right here
|
|
89
|
+
bunx basalt-ui fonts:css --out src/fonts.css
|
|
125
90
|
```
|
|
126
91
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
passing every gate, because a fork by a token-fluent author uses exactly the right tokens.
|
|
131
|
-
`basalt/shadow-basalt-export` reads all nine published barrels since 1.21.0 (the charts layer
|
|
132
|
-
included), but it warns only on an **exact** name collision, in a basalt-scoped file, on a
|
|
133
|
-
component-shaped declaration — round 5 found the renamed forks (`Cell`, `Box`, `Stat`) sail straight
|
|
134
|
-
past it. It is a tripwire, not coverage; nothing catches the
|
|
135
|
-
rest. Ask before you build; do not wait to be told.
|
|
136
|
-
|
|
137
|
-
The consumer series file (`<first basalt.root>/lib/series.ts` by default — so `src/lib/series.ts` on
|
|
138
|
-
a plain app, `apps/web/src/lib/series.ts` on a monorepo; override via `basalt.seriesModulePath`) is
|
|
139
|
-
the single guard-exempt palette source — see `/basalt-charts`.
|
|
140
|
-
|
|
141
|
-
## Refresh: `basalt-ui sync`
|
|
142
|
-
|
|
143
|
-
`sync` re-applies shipped doctrine after a basalt-ui upgrade, three-way against
|
|
144
|
-
`.basalt/manifest.json`:
|
|
145
|
-
|
|
146
|
-
- **Unchanged since last write** → overwrite with the new version.
|
|
147
|
-
- **Locally edited** → show a diff and **skip** (preserves your edits) unless `--force`.
|
|
148
|
-
- **Missing** → recreate.
|
|
149
|
-
- `sync --check` makes no changes and exits non-zero on drift — wire it as a CI freshness gate so a
|
|
150
|
-
consumer can't silently fall behind the shipped rules.
|
|
151
|
-
|
|
152
|
-
**Run it anywhere in the repo.** `sync`, `check-theme` and `doctor` share one resolver:
|
|
153
|
-
`BASALT_CWD` → the cwd → declared workspace packages → a two-level descend → an **ascend** to the
|
|
154
|
-
nearest ancestor carrying a basalt project, bounded by the repo root. Both directions announce the
|
|
155
|
-
relocation in one sentence. From a sub-package `sync` now refreshes the parent install rather than
|
|
156
|
-
refusing — but it still **exits 1 rather than scaffolding a second consumer** where the resolved
|
|
157
|
-
project has no `.basalt/manifest.json`. Creating an install stays `basalt-ui init`'s decision.
|
|
158
|
-
|
|
159
|
-
Seeds (`DESIGN.md`, the toolchain files, the scaffolds) are never reconciled or drift-reported —
|
|
160
|
-
they are yours. The managed files (rules, skills, the CLAUDE block) are framework-owned and meant
|
|
161
|
-
to be overwritten; the sync diff is where you review a doctrine change before committing it.
|
|
162
|
-
|
|
163
|
-
## Precedence the scaffold establishes
|
|
164
|
-
|
|
165
|
-
After scaffolding, the doctrine resolves in this order (highest wins):
|
|
166
|
-
|
|
167
|
-
> consumer `DESIGN.md` (deltas) > shipped `basalt-*` rules > skills
|
|
168
|
-
|
|
169
|
-
So an app deviation goes in `DESIGN.md`; the universal law and its enforcement live in the
|
|
170
|
-
`basalt-*` rules; and the skills (`/basalt-design`, `/basalt-charts`) are the method that obeys
|
|
171
|
-
both.
|
|
172
|
-
|
|
173
|
-
## Checklist
|
|
174
|
-
|
|
175
|
-
- Ran `bun add basalt-ui && bunx basalt-ui init` — rules, skills, CLAUDE block, DESIGN.md all
|
|
176
|
-
present from the one package. Every command after it runs the local bin, not `bunx`.
|
|
177
|
-
- `BasaltProvider` wraps the app; `createBasaltTheme` used for theme deltas; `basalt-ui/styles.css`
|
|
178
|
-
imported.
|
|
179
|
-
- `basaltViteConfig` adopted; `oxlint . && basalt-ui check-theme` is the lint command.
|
|
180
|
-
- `basaltAppPlugin` composed into `plugins` (see the vite.config.ts snippet above); either the six
|
|
181
|
-
default icon files (`favicon.ico`, `favicon.svg`, `favicon-96x96.png`, `apple-touch-icon.png`,
|
|
182
|
-
`web-app-manifest-192x192.png`, `web-app-manifest-512x512.png`) under `public/`, or an `icons`
|
|
183
|
-
array naming the ones you actually have. `doctor` reads the option out of your vite config, so an
|
|
184
|
-
app that names one SVG is not told to generate five PNGs it does not want.
|
|
185
|
-
- `site.webmanifest` is emitted by `basaltAppPlugin` — no manual manifest file needed unless
|
|
186
|
-
`manifest: false` was passed.
|
|
187
|
-
- A thin `DESIGN.md` (deltas) + the thirteen `basalt-*` rules + the managed CLAUDE block are present.
|
|
188
|
-
- `<first basalt.root>/lib/series.ts` exists as the one guard-exempt series source (for any app metric colors).
|
|
189
|
-
- `basalt-ui sync --check` wired in CI (recommended) to catch drift on future upgrades.
|
|
190
|
-
- `basalt-ui doctor` run after `init` and read to the last line — a `SKIPPED` check exits non-zero
|
|
191
|
-
and is not a pass.
|
|
192
|
-
|
|
193
|
-
## Notes
|
|
194
|
-
|
|
195
|
-
- `init`, `sync`, and `check-theme` are all **real**. `sync` reconciles via a sha256 three-way diff
|
|
196
|
-
(`--check` is a CI drift gate, `--force` overwrites local edits); `check-theme` is the palette
|
|
197
|
-
guard; `doctor` checks resolution, version, guard scan, oxlint preset, spacing-scale drift and
|
|
198
|
-
cross-package `ai` major parity. All via the local bin. An unrecognized flag exits 1 naming it
|
|
199
|
-
(`doctor --json` used to run doctor and exit 0), and an unknown command says so above the usage
|
|
200
|
-
block.
|
|
201
|
-
- **Not a React app?** `bunx basalt-ui tokens:css --out src/tokens.css` (no install to resolve, so
|
|
202
|
-
`bunx` is right here) emits the `--vx-*` layer with
|
|
203
|
-
no package in your dependency tree, `--selector-class dark` for the Tailwind `<html class="dark">`
|
|
204
|
-
convention, and `fonts:css` emits the shipped `--basalt-font-*` stacks. Both carry the two-line
|
|
205
|
-
`@generated basalt-ui` header the guard skips on — that header verbatim, and then only the LINES
|
|
206
|
-
that are basalt custom properties, selectors, `}` or self-closing comments, so the marker pasted
|
|
207
|
-
into a `.tsx` suppresses nothing and a declaration added to such a file is still reported — and
|
|
208
|
-
both take `--check` as a CI drift gate — which compares everything but the header's provenance
|
|
209
|
-
line, so a basalt-ui version bump alone never forces a no-op commit. See <https://github.com/jkrumm/basalt-ui/blob/master/docs/FRAMEWORK-FREE.md> (not in the package).
|
|
210
|
-
- The framework ships **no** icon or notification dep — pass icons as `ReactNode` and wire toasts
|
|
211
|
-
yourself (e.g. `ThemeLabControls`' `copyIcon` / `onCopy`).
|
|
212
|
-
- The framework DOES ship `motion` (motion.dev, formerly framer-motion) as a bundled dependency —
|
|
213
|
-
animated chrome (`ThemeToggle` today) comes for free, no consumer dependency needed. Reach for
|
|
214
|
-
the shared `MOTION_DURATION` / `MOTION_SPRING` tokens for any new animated interaction rather
|
|
215
|
-
than inventing ad hoc durations/easings or a competing animation library.
|
|
216
|
-
- `vite-plugin-pwa` (`^1.3.0`) is an OPTIONAL peer, only needed when `basaltAppPlugin`'s
|
|
217
|
-
`serviceWorker` option is truthy — omit it entirely for apps that don't want an installable/
|
|
218
|
-
offline-capable app. See `agent/rules/basalt-app.md` for the full head/PWA/manifest/icons
|
|
219
|
-
surface `basaltAppPlugin` owns.
|
|
92
|
+
Both carry the two-line `@generated basalt-ui` header the guard skips, and both take `--check` as a
|
|
93
|
+
CI drift gate that ignores the provenance version — so a basalt bump alone never forces a no-op
|
|
94
|
+
commit. Declare `"basalt": { "profile": "tokens-only" }` by hand; `check-theme` never infers it.
|
|
@@ -1,254 +1,74 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: basalt-charts
|
|
3
|
-
description: Add or extend a chart in a basalt-ui app — pick a kind vs
|
|
4
|
-
when_to_use: User wants to add a chart, restyle
|
|
3
|
+
description: Add or extend a chart in a basalt-ui app — the procedure: pick a shipped kind vs extract one vs stay bespoke (Rule of Three), register the consumer series with defineSeries/groupTokens, compose CartesianChart (or ChartFrame for a non-single-plot shape), then verify against the guards. Use when adding a chart, a new metric/series color, or extracting a repeated chart shape.
|
|
4
|
+
when_to_use: User wants to add a chart, restyle one structurally, introduce a new metric/series and its color, extract a recurring chart shape into a reusable kind, or asks where chart series colors live and how to register them. The how-to companion to /basalt-design, which owns the aesthetic law.
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
`/basalt-charts` is the
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
`./tokens` surfaces. `.claude/rules/basalt-charts.md` — which you have — is the chart-primitive
|
|
11
|
-
contract everything below assumes (the repo's `docs/CHARTS-SPEC.md` is the long form and is not in
|
|
12
|
-
the package:
|
|
13
|
-
<https://github.com/jkrumm/basalt-ui/blob/master/docs/CHARTS-SPEC.md>) — composing `CartesianChart` (single-plot) or `ChartFrame` directly
|
|
14
|
-
(multi-pane/radial/matrix) is mandatory, mechanically enforced (see "Respect the boundary" below).
|
|
7
|
+
`/basalt-charts` is the HOW. `.claude/rules/basalt-charts.md` is the contract everything below
|
|
8
|
+
assumes, and `/basalt-design` owns the aesthetic law — read both before writing chart code. The
|
|
9
|
+
props themselves are in the shipped types and `llms.txt` at the install directory.
|
|
15
10
|
|
|
16
|
-
##
|
|
11
|
+
## 1. Pick the path (Rule of Three)
|
|
17
12
|
|
|
18
|
-
|
|
13
|
+
1. **A shipped kind fits** → use it. `ZonedLine`, `Bars`, `StackedArea`, `MultiLine`, `DualPanel`,
|
|
14
|
+
`Heatmap`, `Donut`, `BandStrip`, `MirroredBars`. Read their props before assuming a gap: dual
|
|
15
|
+
axis, zones, thresholds, refLines, per-point markers, folding, per-series formatting and
|
|
16
|
+
`isPending` are all declarative already.
|
|
17
|
+
2. **This is the SECOND instance of a shape with no kind** → extract a kind and migrate both call
|
|
18
|
+
sites. Not on the first, not past the third.
|
|
19
|
+
3. **Genuinely unique** → stay bespoke in the page's own chart file, composing the primitive
|
|
20
|
+
directly. Not in a shared kinds directory.
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
axis, zones/thresholds/refLines as plain arrays). Done.
|
|
24
|
-
- **`ZonedLine`** — a single-series line with zone bands, thresholds, x-zones, refLines.
|
|
25
|
-
- **`Bars`** — 1+ stacked positive/negative bar series, optional line overlays, dual-axis config.
|
|
26
|
-
- **`StackedArea`** — opaque stacked bands, auto cumulative-top y-domain.
|
|
27
|
-
- **`MultiLine`** — N series on a shared y-axis (or two, via `y2`): legend-hover dimming,
|
|
28
|
-
per-series synced dots, dashed companion (MA) lines, per-point markers (PR stars / status
|
|
29
|
-
dots), zones + refLines, fixed or auto domain (also covers z-score/σ charts via a fixed
|
|
30
|
-
symmetric domain + zero refLine). Replaces bespoke multi-line / relative-progression /
|
|
31
|
-
training-load / strength-composite / fitness-trends.
|
|
32
|
-
- **`DualPanel`** — top line-pane + bottom signed-histogram pane sharing ONE x-scale and ONE
|
|
33
|
-
cursor; optional fill-between two top lines, zones, refLines. Replaces bespoke divergence /
|
|
34
|
-
momentum.
|
|
35
|
-
- **`Heatmap`** — category×category intensity grid (`color-mix` alpha), per-cell tooltip,
|
|
36
|
-
optional gradient legend strip. Replaces bespoke time-of-day.
|
|
37
|
-
- **`Donut`** — proportional donut, optional `centerContent` overlay.
|
|
38
|
-
- **`BandStrip`** — 1-D categorical bands, no y dimension: one rect per slot,
|
|
39
|
-
`getBand(d) => { state, fill?, absentFraction?, marker? }`. `series` is the state set and
|
|
40
|
-
drives legend, fill and the one derived tooltip row. Replaces a bespoke state/availability
|
|
41
|
-
strip.
|
|
42
|
-
- **`MirroredBars`** — two BAR panes over one x scale and one baseline, each in its own domain
|
|
43
|
-
(`up`/`down` take `{ key, max?, autoMaxFloor?, ticks?, format }`). Not `DualPanel`, whose top
|
|
44
|
-
pane is a LINE and whose bottom takes one SIGNED accessor.
|
|
45
|
-
2. **Extract a new kind (second instance of a pattern).** When you are about to build the _second_
|
|
46
|
-
chart of a shape that has no shipped kind, extract a kind. Don't extract on the first; don't
|
|
47
|
-
wait past the third. A new kind lives in the consumer's chart kinds dir (or, if generic enough
|
|
48
|
-
to promote, gets contributed to basalt-ui `src/charts/kinds/`). Migrate both call sites onto it.
|
|
49
|
-
A single-plot kind composes `CartesianChart`, drawing only marks; a multi-pane or non-cartesian
|
|
50
|
-
kind composes `ChartFrame` directly (see "Compose the primitives" below).
|
|
51
|
-
3. **Stay bespoke (genuinely unique).** A one-off shape (e.g. a dual-panel MACD, or two series on
|
|
52
|
-
genuinely different scales that don't fit any kind's config surface) composes `CartesianChart`
|
|
53
|
-
or `ChartFrame` directly in the page's chart file — not in `kinds/`.
|
|
22
|
+
**Extracting a kind? Prove it by porting.** Move real call sites onto it and ship the list of what
|
|
23
|
+
it could NOT express. A demo page is written against the API that already exists, so it proves
|
|
24
|
+
nothing.
|
|
54
25
|
|
|
55
|
-
|
|
56
|
-
sites onto it and reporting what it could NOT express — not by a demo page, which is written
|
|
57
|
-
against the API that already exists. The two band kinds were built that way (linewatch, 1884 → 957
|
|
58
|
-
source lines, 11 `hand-rolled-plot` waivers retired, two live `NaN`-geometry bugs found). Ship the
|
|
59
|
-
"still cannot express" list with the kind.
|
|
26
|
+
## 2. Register the series (this is consumer data)
|
|
60
27
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
> kind — don't loosen the primitives.**
|
|
64
|
-
|
|
65
|
-
A good kind's props: `data`, `chartId`, generic `getX` accessor, `height`/`aspectRatio`/`fill`,
|
|
66
|
-
zones / thresholds / refLines arrays, one `AxisConfig` object per axis, `series: ChartSeries<T>[]`
|
|
67
|
-
as the single source of truth for color/label/legend/tooltip. Bespoke escape hatches
|
|
68
|
-
(`tooltip.extraRows`, `tooltip.prependRows`) are fine; they must not grow into a god-object config.
|
|
69
|
-
|
|
70
|
-
`ZonedLine` and `MultiLine` also take `xZones?: XZoneSpec[]` — vertical time-window bands, the
|
|
71
|
-
counterpart to the horizontal zones above. `{ from?, to?, fill }` bounds are `getX` **domain keys**
|
|
72
|
-
(the label string, not a date/timestamp); an omitted bound is the plot edge, a key missing from the
|
|
73
|
-
domain skips the band rather than clamping to one.
|
|
74
|
-
|
|
75
|
-
## Register a consumer series (VX.series is app-side)
|
|
76
|
-
|
|
77
|
-
The framework ships generic primitives and the framework palette (semantic / status / neutral /
|
|
78
|
-
surface) — **but not a domain series tree.** `VX.series` does not exist in basalt-ui; argo's
|
|
79
|
-
`SERIES`/`ACTIVITY`/`USAGE_*` maps stayed app-side. Each consumer rebuilds its own series in **one
|
|
80
|
-
guard-exempt file** (e.g. `src/lib/series.ts`), using the shipped extensibility helpers from
|
|
81
|
-
`basalt-ui/tokens`:
|
|
28
|
+
`VX.series` does not exist in the framework — which metric owns which hue is domain data, declared
|
|
29
|
+
in ONE guard-exempt file per app:
|
|
82
30
|
|
|
83
31
|
```ts
|
|
84
|
-
// src/lib/series.ts
|
|
85
|
-
import { defineSeries, groupTokens, seriesTokens
|
|
86
|
-
|
|
87
|
-
// 1. Author the per-theme pairs (hue keeps identity, shifts shade across schemes).
|
|
88
|
-
const HEALTH = defineSeries({
|
|
89
|
-
hrv: { light: '#634DBF', dark: '#7C6BD6' }, // violet — lighter on dark to avoid glow
|
|
90
|
-
restingHr: { light: '#C22762', dark: '#E0639A' },
|
|
91
|
-
})
|
|
92
|
-
const WALKING = defineSeries({
|
|
93
|
-
intensityMin: { light: '#3F7D4F', dark: '#5FA372' }, // forest green — muted status hue
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
// 2. Turn the maps into namespaced token refs (var(--vx-health-hrv), ...).
|
|
97
|
-
// seriesTokens(map, prefix?) for a flat namespace; groupTokens(name, map) namespaces under name.
|
|
98
|
-
export const SERIES = {
|
|
99
|
-
...seriesTokens(HEALTH), // → { hrv: 'var(--vx-hrv)', restingHr: 'var(--vx-restingHr)' }
|
|
100
|
-
walking: groupTokens('walking', WALKING), // → { intensityMin: 'var(--vx-walking-intensityMin)', ... }
|
|
101
|
-
}
|
|
32
|
+
// src/lib/series.ts — the one file exempt from check-theme, because it IS the palette source
|
|
33
|
+
import { buildPaletteCss, defineSeries, groupTokens, seriesTokens } from 'basalt-ui/tokens'
|
|
102
34
|
|
|
103
|
-
|
|
104
|
-
export const
|
|
105
|
-
|
|
106
|
-
derived: ['--vx-hrv-area: 12%'], // optional scheme-independent derived vars
|
|
107
|
-
})
|
|
35
|
+
const HEALTH = defineSeries({ hrv: { light: '…', dark: '…' } }) // lighter on dark, deeper on light
|
|
36
|
+
export const SERIES = { ...seriesTokens(HEALTH), walking: groupTokens('walking', WALKING) }
|
|
37
|
+
export const PALETTE_CSS = buildPaletteCss({ groups: { '': HEALTH, 'walking-': WALKING } })
|
|
108
38
|
```
|
|
109
39
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
and the tokens).
|
|
114
|
-
- Inject `PALETTE_CSS` once (BasaltProvider injects the framework palette; the consumer appends its
|
|
115
|
-
series CSS — or passes `injectPalette={false}` and head-injects both).
|
|
116
|
-
- Then charts read `SERIES.hrv`, never a hex. This is what keeps `basalt-ui check-theme` green.
|
|
117
|
-
- A series with nothing visible to point at (flat at the domain floor, all-zero) can still carry a
|
|
118
|
-
qualifier: pass `note` on the series' `SeriesStyle` (flows to `LegendEntry.note` via
|
|
119
|
-
`deriveLegend`) and `ChartLegend` renders it, muted, after the label.
|
|
40
|
+
`seriesTokens`/`groupTokens` are exact-keyed, so a stale or typo'd key fails `tsc`. Feed the SAME
|
|
41
|
+
maps to `buildPaletteCss` (or `BasaltProvider`'s `paletteOptions.groups`, keyed by var PREFIX — the
|
|
42
|
+
trailing dash matters) or the refs point at variables nothing declares.
|
|
120
43
|
|
|
121
|
-
|
|
122
|
-
`ThemeLabControls` (see `/basalt-design`).
|
|
44
|
+
## 3. Compose the primitive
|
|
123
45
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
Every chart — kind or bespoke — composes `CartesianChart` for a single plot rect, or `ChartFrame`
|
|
127
|
-
directly for a multi-pane / non-cartesian shape — five of them ship: `DualPanel`'s two panes,
|
|
128
|
-
`MirroredBars`' two bar panes, `BandStrip`'s missing y dimension, `Heatmap`'s grid, `Donut`'s ring.
|
|
129
|
-
Either way the contract is the same: the primitive owns margin, scales, grid,
|
|
130
|
-
axes, legend, the shared cursor, and the tooltip; you supply `series` and draw ONLY marks. This is
|
|
131
|
-
mandatory, not a convention — `basalt/hand-rolled-plot` fails the build on a bypass (see "Respect
|
|
132
|
-
the boundary" below); it is easier to compose it than to work around it.
|
|
133
|
-
|
|
134
|
-
**The common case — a single-plot chart:**
|
|
46
|
+
A single-plot chart composes `CartesianChart` and draws only marks in the render prop — margins,
|
|
47
|
+
scales, axes, grid, the shared cursor, crosshair, dots and tooltip are already done:
|
|
135
48
|
|
|
136
49
|
```tsx
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const HRV_SERIES: ChartSeries<HrvPoint>[] = [
|
|
144
|
-
{ key: 'hrv', label: 'HRV', color: SERIES.hrv, mark: 'line', getValue: (d) => d.hrv },
|
|
145
|
-
]
|
|
146
|
-
|
|
147
|
-
function HrvChart({ data }: { data: HrvPoint[] }) {
|
|
148
|
-
return (
|
|
149
|
-
<CartesianChart
|
|
150
|
-
data={data}
|
|
151
|
-
chartId="hrv"
|
|
152
|
-
getX={(d) => d.date}
|
|
153
|
-
series={HRV_SERIES}
|
|
154
|
-
y={{ domain: 'auto', format: (v) => `${Math.round(v)} ms` }}
|
|
155
|
-
height={240}
|
|
156
|
-
>
|
|
157
|
-
{({ xScale, yScale, visible }) =>
|
|
158
|
-
visible.map((s) => (
|
|
159
|
-
<LinePath<HrvPoint>
|
|
160
|
-
key={s.key}
|
|
161
|
-
data={data}
|
|
162
|
-
x={(d) => xScale(d.date) ?? 0}
|
|
163
|
-
y={(d) => yScale(s.getValue(d) ?? 0)}
|
|
164
|
-
stroke={s.color}
|
|
165
|
-
strokeWidth={VX.lineWidth}
|
|
166
|
-
curve={curveMonotoneX}
|
|
167
|
-
/>
|
|
168
|
-
))
|
|
169
|
-
}
|
|
170
|
-
</CartesianChart>
|
|
171
|
-
)
|
|
172
|
-
}
|
|
50
|
+
<CartesianChart data={data} chartId="hrv" getX={(d) => d.date} series={HRV} y={{ domain: 'auto' }} height={240}>
|
|
51
|
+
{({ xScale, yScale, visible }) =>
|
|
52
|
+
visible.map((s) => <LinePath key={s.key} data={data} x={…} y={…} stroke={s.color} />)
|
|
53
|
+
}
|
|
54
|
+
</CartesianChart>
|
|
173
55
|
```
|
|
174
56
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
resolution is **domain-aware** (nearest parsed date/number within a chart's own step, not exact
|
|
194
|
-
string match), so a chart that folds its calendar into weekly buckets still tracks a hover from an
|
|
195
|
-
unfolded daily sibling.
|
|
196
|
-
|
|
197
|
-
Wrap a subtree in `ChartCursorScope` to **isolate** it onto a private cursor instead — the inverse
|
|
198
|
-
of the old opt-in pattern, for e.g. two independent dashboards rendered side by side over the same
|
|
199
|
-
calendar that must NOT sync with each other:
|
|
200
|
-
|
|
201
|
-
```tsx
|
|
202
|
-
import { ChartCursorScope } from 'basalt-ui/charts'
|
|
203
|
-
;<ChartCursorScope>
|
|
204
|
-
<ChartCard>…</ChartCard>
|
|
205
|
-
<ChartCard>…</ChartCard>
|
|
206
|
-
</ChartCursorScope>
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
## Respect the boundary (oxlint-enforced)
|
|
210
|
-
|
|
211
|
-
- **`@visx/*` may only be imported inside chart files.** The shipped consumer oxlint preset bans
|
|
212
|
-
direct `@visx/*` outside `**/charts/**`. Need a raw visx primitive for a bespoke chart? Pull it
|
|
213
|
-
from basalt-ui's curated re-export in `basalt-ui/charts`, or keep the chart under a charts dir.
|
|
214
|
-
- **basalt-ui's own `./charts` and `./tokens` are Mantine-free** — zero `@mantine/*` imports,
|
|
215
|
-
keeping the token layer upstream of Mantine inside the framework itself (`cssVariablesResolver`
|
|
216
|
-
reads `--vx-*` tokens to bind Mantine's surfaces to them; a chart importing `@mantine/*` directly
|
|
217
|
-
would fork chrome and charts apart), AND letting `basalt-ui/charts`/`basalt-ui/tokens` resolve and
|
|
218
|
-
render with no `@mantine/*` installed (real, CI-tested — `scripts/pack-test.sh`'s
|
|
219
|
-
"charts/tokens-only (no-Mantine) resolution + render" step). This is a basalt-internal invariant,
|
|
220
|
-
not a rule enforced on your own app code — compose the shipped primitives from
|
|
221
|
-
`basalt-ui/charts`/`basalt-ui/tokens` and you inherit it for free.
|
|
222
|
-
- **No raw color literals** anywhere except the one guard-exempt series file (and a deliberate
|
|
223
|
-
`theme-allow` line). `basalt-ui check-theme` is the teeth; run it before committing.
|
|
224
|
-
- **No raw visx axes.** `basalt-ui check-theme`'s `raw-visx-axis` guard fails the build on a raw
|
|
225
|
-
`<AxisLeft>`/`<AxisBottom>`/`<AxisRight>` inside any `/charts/` file — use the tokenized
|
|
226
|
-
`AxisLeftNumeric` / `AxisRightNumeric` / `AxisBottomDate` (escape via `theme-allow`).
|
|
227
|
-
- **No hand-rolled plot assembly.** `basalt/hand-rolled-plot` fails the build on a chart-assembly
|
|
228
|
-
primitive (`AxisLeftNumeric`, `AxisRightNumeric`, `AxisBottomDate`, `HoverOverlay`, `Crosshair`)
|
|
229
|
-
rendered in a file that doesn't compose `CartesianChart`, per NODE. Escape:
|
|
230
|
-
`theme-allow hand-rolled-plot — <why>` on the one node, or `theme-allow-file hand-rolled-plot —
|
|
231
|
-
<why>` anywhere in the file — how a genuinely non-single-plot shape (`DualPanel`, `MirroredBars`,
|
|
232
|
-
`BandStrip`; `Heatmap` and `Donut` render no assembly primitive and need none) declares itself.
|
|
233
|
-
`-file` is the 1.21.0 spelling and it is required. The file that defines `CartesianChart` is
|
|
234
|
-
exempt definitionally.
|
|
235
|
-
- **No hand-authored legends.** `basalt/chart-legend-literal` fails the build on a hand-written
|
|
236
|
-
array literal passed to `ChartLegend`'s `items` — derive it from the same `series` the chart draws
|
|
237
|
-
(`deriveLegend`, or let `ChartFrame`/`CartesianChart` do it) so it can't name a series the plot no
|
|
238
|
-
longer draws.
|
|
239
|
-
- Both ship at `warn` in the consumer preset (`configs/oxlint.json`) for one minor and `error`
|
|
240
|
-
repo-local (the "grace minor" doctrine in `packages/basalt-ui/CLAUDE.md`) — promoting to `error`
|
|
241
|
-
in the next minor.
|
|
242
|
-
|
|
243
|
-
## Checklist for a new chart
|
|
244
|
-
|
|
245
|
-
- Picked the right path: reused a kind / extracted on the second instance / stayed bespoke for a
|
|
246
|
-
true one-off.
|
|
247
|
-
- Composed `CartesianChart` (single plot) or `ChartFrame` directly (multi-pane / non-cartesian) —
|
|
248
|
-
never hand-assembled margin/scales/axes/cursor/tooltip.
|
|
249
|
-
- Any new metric color is a `{ light, dark }` pair in the consumer series file, exposed via
|
|
250
|
-
`seriesTokens`/`groupTokens`, and wired into `PALETTE_CSS` via `buildPaletteCss`.
|
|
251
|
-
- Charts read `SERIES.*` / `VX.*` — `basalt-ui check-theme` green, zero raw hex.
|
|
252
|
-
- Marks draw `ctx.visible`, never the `series` prop, so a legend toggle actually removes them.
|
|
253
|
-
- `@visx/*` stays inside chart files; chart code imports no `@mantine/*`.
|
|
254
|
-
- Tuned both schemes via the theme lab where the hue needed it (defer to `/basalt-design`).
|
|
57
|
+
- **Draw `visible`, never the `series` prop** — otherwise a legend toggle hides nothing.
|
|
58
|
+
- **Dual axis is `y2`**, not a flag; a series opts in with `axis: 'right'`.
|
|
59
|
+
- **A multi-pane, radial or matrix shape composes `ChartFrame`** + `useChartCursor` + `autoMargin` +
|
|
60
|
+
`ChartTooltipFloat` and declares itself with `theme-allow-file hand-rolled-plot — <why>`.
|
|
61
|
+
- **Size is `height` / `aspectRatio` / `fill`** — one of the three, nothing else. Charts measure
|
|
62
|
+
themselves.
|
|
63
|
+
- Never substitute a raw visx axis, a hand-written legend array, a `@visx/tooltip` import, an
|
|
64
|
+
`rgba()` fill or a hex.
|
|
65
|
+
|
|
66
|
+
## 4. Verify
|
|
67
|
+
|
|
68
|
+
- `oxlint .` green — no `basalt/hand-rolled-plot`, no `basalt/chart-legend-literal`, no
|
|
69
|
+
`basalt/chart-in-raw-surface`, no `@visx/*` import outside a `charts/` segment.
|
|
70
|
+
- `basalt-ui check-theme` green — no raw hex, no raw axis, every chart entry point has an
|
|
71
|
+
`ariaLabel`.
|
|
72
|
+
- Every new color is a `{ light, dark }` pair in the series file and wired into the palette CSS.
|
|
73
|
+
- Tooltip rows and the legend are DERIVED from `series`; the plot draws `visible`.
|
|
74
|
+
- Checked in both schemes, and with a pending query (`isPending`, not `data ?? []`).
|