basalt-ui 1.26.0 → 1.28.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 +23 -23
- package/CHANGELOG.md +67 -0
- package/MIGRATING.md +896 -37
- package/README.md +33 -3
- package/agent/rules/basalt-batteries.md +25 -10
- package/agent/rules/basalt-controls.md +73 -43
- package/agent/rules/basalt-mantine.md +17 -6
- package/agent/rules/basalt-state.md +5 -0
- package/agent/rules/basalt-tokens.md +1 -1
- package/configs/oxlint-plugin.js +1286 -73
- package/configs/oxlint-plugin.test.ts +1338 -29
- package/configs/oxlint.json +12 -5
- package/dist/agent/coalesce.d.ts.map +1 -1
- package/dist/agent/coalesce.js +2 -1
- package/dist/agent/merge.d.ts.map +1 -1
- package/dist/agent/merge.js +2 -1
- package/dist/agent/part-list.d.ts.map +1 -1
- package/dist/agent/part-list.js +2 -0
- package/dist/agent/stick-to-bottom.d.ts +3 -6
- package/dist/agent/stick-to-bottom.d.ts.map +1 -1
- package/dist/agent/stick-to-bottom.js +6 -3
- package/dist/agent-chat/composer.d.ts +6 -2
- package/dist/agent-chat/composer.d.ts.map +1 -1
- package/dist/agent-chat/composer.js +49 -33
- package/dist/agent-chat/relative-time.d.ts +0 -9
- package/dist/agent-chat/relative-time.d.ts.map +1 -1
- package/dist/agent-chat/relative-time.js +2 -1
- package/dist/agent-chat/thread-detail-panel.d.ts +3 -2
- package/dist/agent-chat/thread-detail-panel.d.ts.map +1 -1
- package/dist/agent-chat/thread-detail-panel.js +8 -5
- package/dist/agent-chat/thread-feed-row.d.ts +2 -1
- package/dist/agent-chat/thread-feed-row.d.ts.map +1 -1
- package/dist/agent-chat/thread-feed-row.js +92 -92
- package/dist/agent-chat/thread-feed.d.ts +3 -2
- package/dist/agent-chat/thread-feed.d.ts.map +1 -1
- package/dist/agent-chat/thread-feed.js +8 -3
- package/dist/agent-chat/thread-message.d.ts +4 -3
- package/dist/agent-chat/thread-message.d.ts.map +1 -1
- package/dist/agent-chat/thread-message.js +70 -31
- package/dist/agent-chat/thread-outcome-card.d.ts +3 -2
- package/dist/agent-chat/thread-outcome-card.d.ts.map +1 -1
- package/dist/agent-chat/thread-outcome-card.js +9 -2
- package/dist/agent-chat/thread-workspace.d.ts +7 -2
- package/dist/agent-chat/thread-workspace.d.ts.map +1 -1
- package/dist/agent-chat/thread-workspace.js +62 -29
- package/dist/agent-chat/tool-chip.d.ts +3 -2
- package/dist/agent-chat/tool-chip.d.ts.map +1 -1
- package/dist/agent-chat/tool-chip.js +7 -2
- package/dist/charts/hooks/useBandPlot.d.ts.map +1 -1
- package/dist/charts/hooks/useBandPlot.js +15 -8
- package/dist/charts/index.d.ts +8 -7
- package/dist/charts/index.d.ts.map +1 -1
- package/dist/charts/index.js +45 -5
- package/dist/charts/kinds/BandStrip.d.ts +10 -7
- package/dist/charts/kinds/BandStrip.d.ts.map +1 -1
- package/dist/charts/kinds/BandStrip.js +12 -7
- package/dist/charts/kinds/Bars.d.ts +12 -6
- package/dist/charts/kinds/Bars.d.ts.map +1 -1
- package/dist/charts/kinds/Bars.js +37 -11
- package/dist/charts/kinds/Donut.d.ts +6 -5
- package/dist/charts/kinds/Donut.d.ts.map +1 -1
- package/dist/charts/kinds/Donut.js +19 -2
- package/dist/charts/kinds/DualPanel.d.ts +6 -5
- package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
- package/dist/charts/kinds/DualPanel.js +36 -8
- package/dist/charts/kinds/Heatmap.d.ts +10 -6
- package/dist/charts/kinds/Heatmap.d.ts.map +1 -1
- package/dist/charts/kinds/Heatmap.js +58 -31
- package/dist/charts/kinds/MirroredBars.d.ts +6 -5
- package/dist/charts/kinds/MirroredBars.d.ts.map +1 -1
- package/dist/charts/kinds/MirroredBars.js +9 -2
- package/dist/charts/kinds/MultiLine.d.ts +11 -6
- package/dist/charts/kinds/MultiLine.d.ts.map +1 -1
- package/dist/charts/kinds/MultiLine.js +24 -15
- package/dist/charts/kinds/StackedArea.d.ts +18 -8
- package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
- package/dist/charts/kinds/StackedArea.js +29 -4
- package/dist/charts/kinds/ZonedLine.d.ts +12 -8
- package/dist/charts/kinds/ZonedLine.d.ts.map +1 -1
- package/dist/charts/kinds/ZonedLine.js +33 -23
- package/dist/charts/layout/auto-margin.d.ts +65 -8
- package/dist/charts/layout/auto-margin.d.ts.map +1 -1
- package/dist/charts/layout/auto-margin.js +45 -10
- package/dist/charts/layout/log-ticks.d.ts +27 -0
- package/dist/charts/layout/log-ticks.d.ts.map +1 -0
- package/dist/charts/layout/log-ticks.js +42 -0
- package/dist/charts/primitives/Axes.d.ts +11 -2
- package/dist/charts/primitives/Axes.d.ts.map +1 -1
- package/dist/charts/primitives/Axes.js +15 -5
- package/dist/charts/primitives/CartesianChart.d.ts +45 -7
- package/dist/charts/primitives/CartesianChart.d.ts.map +1 -1
- package/dist/charts/primitives/CartesianChart.js +86 -28
- package/dist/charts/primitives/ChartCard.d.ts +27 -2
- package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
- package/dist/charts/primitives/ChartCard.js +37 -17
- package/dist/charts/primitives/ChartFrame.d.ts +30 -4
- package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
- package/dist/charts/primitives/ChartFrame.js +45 -15
- package/dist/charts/primitives/ChartLegend.d.ts +13 -2
- package/dist/charts/primitives/ChartLegend.d.ts.map +1 -1
- package/dist/charts/primitives/ChartLegend.js +33 -13
- package/dist/charts/primitives/ChartPending.d.ts +77 -4
- package/dist/charts/primitives/ChartPending.d.ts.map +1 -1
- package/dist/charts/primitives/ChartPending.js +100 -6
- package/dist/charts/primitives/ChartTooltip.d.ts +7 -0
- package/dist/charts/primitives/ChartTooltip.d.ts.map +1 -1
- package/dist/charts/primitives/ChartTooltip.js +18 -5
- package/dist/charts/primitives/Crosshair.d.ts +3 -0
- package/dist/charts/primitives/Crosshair.d.ts.map +1 -1
- package/dist/charts/primitives/Crosshair.js +3 -1
- package/dist/charts/primitives/chart-frame-layout.d.ts +95 -0
- package/dist/charts/primitives/chart-frame-layout.d.ts.map +1 -0
- package/dist/charts/primitives/chart-frame-layout.js +89 -0
- package/dist/charts/primitives/chart-tier.d.ts +16 -0
- package/dist/charts/primitives/chart-tier.d.ts.map +1 -0
- package/dist/charts/primitives/chart-tier.js +20 -0
- package/dist/charts/series.d.ts +55 -1
- package/dist/charts/series.d.ts.map +1 -1
- package/dist/charts/series.js +27 -1
- package/dist/charts/sparklines/BarSparkline.d.ts +3 -3
- package/dist/charts/sparklines/BarSparkline.d.ts.map +1 -1
- package/dist/charts/sparklines/BarSparkline.js +9 -3
- package/dist/charts/sparklines/LineSparkline.d.ts +3 -3
- package/dist/charts/sparklines/LineSparkline.d.ts.map +1 -1
- package/dist/charts/sparklines/LineSparkline.js +15 -3
- package/dist/charts/sparklines/index.d.ts +2 -2
- package/dist/charts/sparklines/index.d.ts.map +1 -1
- package/dist/charts/utils/format.d.ts +92 -2
- package/dist/charts/utils/format.d.ts.map +1 -1
- package/dist/charts/utils/format.js +64 -1
- package/dist/charts/utils/ticks.d.ts +81 -2
- package/dist/charts/utils/ticks.d.ts.map +1 -1
- package/dist/charts/utils/ticks.js +54 -4
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2 -2
- package/dist/commands/ShortcutsHelp.d.ts +3 -2
- package/dist/commands/ShortcutsHelp.d.ts.map +1 -1
- package/dist/commands/ShortcutsHelp.js +8 -2
- package/dist/commands/define-commands.d.ts.map +1 -1
- package/dist/commands/define-commands.js +2 -2
- package/dist/commands/define-overlays.d.ts +102 -0
- package/dist/commands/define-overlays.d.ts.map +1 -1
- package/dist/commands/define-overlays.js +102 -2
- package/dist/commands/index.d.ts +10 -1
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +3 -1
- package/dist/commands/overlays-mount.d.ts +13 -6
- package/dist/commands/overlays-mount.d.ts.map +1 -1
- package/dist/commands/overlays-mount.js +23 -14
- package/dist/common/errors.d.ts +34 -0
- package/dist/common/errors.d.ts.map +1 -0
- package/dist/common/errors.js +36 -0
- package/dist/common/index.d.ts +15 -0
- package/dist/common/index.d.ts.map +1 -0
- package/dist/common/index.js +29 -0
- package/dist/common/props.d.ts +63 -0
- package/dist/common/props.d.ts.map +1 -0
- package/dist/common/props.js +6 -0
- package/dist/common/query-state-like.d.ts +43 -0
- package/dist/common/query-state-like.d.ts.map +1 -0
- package/dist/common/query-state-like.js +20 -0
- package/dist/common/refs.d.ts +29 -0
- package/dist/common/refs.d.ts.map +1 -0
- package/dist/common/refs.js +29 -0
- package/dist/common/scroll-parent.d.ts +23 -0
- package/dist/common/scroll-parent.d.ts.map +1 -0
- package/dist/common/scroll-parent.js +21 -0
- package/dist/common/validate.d.ts +38 -0
- package/dist/common/validate.d.ts.map +1 -0
- package/dist/common/validate.js +36 -0
- package/dist/connectivity/connectivity-indicator.d.ts +3 -1
- package/dist/connectivity/connectivity-indicator.d.ts.map +1 -1
- package/dist/connectivity/connectivity-indicator.js +3 -1
- package/dist/connectivity/connectivity-provider.d.ts.map +1 -1
- package/dist/connectivity/connectivity-provider.js +4 -1
- package/dist/connectivity/index.d.ts +1 -0
- package/dist/connectivity/index.d.ts.map +1 -1
- package/dist/connectivity/types.d.ts +2 -2
- package/dist/connectivity/types.d.ts.map +1 -1
- package/dist/connectivity/use-connectivity.d.ts.map +1 -1
- package/dist/content/article-layout.module.css +11 -4
- package/dist/content/callout.d.ts +8 -4
- package/dist/content/callout.d.ts.map +1 -1
- package/dist/content/callout.js +15 -8
- package/dist/content/guide.d.ts +5 -4
- package/dist/content/guide.d.ts.map +1 -1
- package/dist/content/guide.js +13 -4
- package/dist/content/heading-anchor.d.ts +24 -2
- package/dist/content/heading-anchor.d.ts.map +1 -1
- package/dist/content/heading-anchor.js +23 -9
- package/dist/content/index.d.ts.map +1 -1
- package/dist/content/markdown.d.ts.map +1 -1
- package/dist/content/markdown.js +2 -0
- package/dist/content/prose.module.css +9 -7
- package/dist/content/reading-progress.d.ts +10 -4
- package/dist/content/reading-progress.d.ts.map +1 -1
- package/dist/content/reading-progress.js +26 -10
- package/dist/content/slug.d.ts +4 -0
- package/dist/content/slug.d.ts.map +1 -1
- package/dist/content/toc.d.ts +11 -0
- package/dist/content/toc.d.ts.map +1 -1
- package/dist/content/toc.js +23 -10
- package/dist/controls/actions.d.ts +49 -6
- package/dist/controls/actions.d.ts.map +1 -1
- package/dist/controls/actions.js +39 -8
- package/dist/controls/compare-filter.d.ts +3 -2
- package/dist/controls/compare-filter.d.ts.map +1 -1
- package/dist/controls/compare-filter.js +17 -2
- package/dist/controls/control-group.d.ts +3 -3
- package/dist/controls/control-group.d.ts.map +1 -1
- package/dist/controls/control-group.js +9 -2
- package/dist/controls/controls.module.css +169 -186
- package/dist/controls/controls.module.css.d.ts +9 -11
- package/dist/controls/enum-filter.d.ts +13 -3
- package/dist/controls/enum-filter.d.ts.map +1 -1
- package/dist/controls/enum-filter.js +32 -15
- package/dist/controls/filter-context.d.ts +15 -6
- package/dist/controls/filter-context.d.ts.map +1 -1
- package/dist/controls/filter-pill.d.ts +3 -3
- package/dist/controls/filter-pill.d.ts.map +1 -1
- package/dist/controls/filter-pill.js +4 -1
- package/dist/controls/filter-set.d.ts +3 -2
- package/dist/controls/filter-set.d.ts.map +1 -1
- package/dist/controls/filter-set.js +66 -41
- package/dist/controls/filter-sheet.d.ts +20 -80
- package/dist/controls/filter-sheet.d.ts.map +1 -1
- package/dist/controls/filter-sheet.js +9 -95
- package/dist/controls/index.d.ts +3 -0
- package/dist/controls/index.d.ts.map +1 -1
- package/dist/controls/index.js +6 -0
- package/dist/controls/multi-select-filter.d.ts +18 -2
- package/dist/controls/multi-select-filter.d.ts.map +1 -1
- package/dist/controls/multi-select-filter.js +124 -22
- package/dist/controls/number-filter.d.ts +28 -0
- package/dist/controls/number-filter.d.ts.map +1 -0
- package/dist/controls/number-filter.js +172 -0
- package/dist/controls/panel-row.d.ts +132 -0
- package/dist/controls/panel-row.d.ts.map +1 -0
- package/dist/controls/panel-row.js +173 -0
- package/dist/controls/panel-row.module.css +86 -0
- package/dist/controls/panel-row.module.css.d.ts +13 -0
- package/dist/controls/range-filter.d.ts +20 -5
- package/dist/controls/range-filter.d.ts.map +1 -1
- package/dist/controls/range-filter.js +75 -38
- package/dist/controls/search-filter.d.ts +3 -2
- package/dist/controls/search-filter.d.ts.map +1 -1
- package/dist/controls/search-filter.js +26 -11
- package/dist/controls/select-filter.d.ts +2 -1
- package/dist/controls/select-filter.d.ts.map +1 -1
- package/dist/controls/select-filter.js +4 -0
- package/dist/controls/slider-control.d.ts +36 -0
- package/dist/controls/slider-control.d.ts.map +1 -0
- package/dist/controls/slider-control.js +64 -0
- package/dist/controls/sync-button.d.ts +3 -2
- package/dist/controls/sync-button.d.ts.map +1 -1
- package/dist/controls/sync-button.js +16 -3
- package/dist/controls/toggle-filter.d.ts +3 -2
- package/dist/controls/toggle-filter.d.ts.map +1 -1
- package/dist/controls/toggle-filter.js +29 -12
- package/dist/controls/view-tabs.d.ts +3 -2
- package/dist/controls/view-tabs.d.ts.map +1 -1
- package/dist/controls/view-tabs.js +43 -11
- package/dist/controls-dates/date-range-picker.d.ts +1 -1
- package/dist/controls-dates/date-range-picker.d.ts.map +1 -1
- package/dist/controls-dates/date-range-picker.js +6 -1
- package/dist/dashboard/delta-badge.d.ts +5 -1
- package/dist/dashboard/delta-badge.d.ts.map +1 -1
- package/dist/dashboard/empty-state.d.ts +10 -3
- package/dist/dashboard/empty-state.d.ts.map +1 -1
- package/dist/dashboard/empty-state.js +37 -6
- package/dist/dashboard/index.d.ts +6 -4
- package/dist/dashboard/index.d.ts.map +1 -1
- package/dist/dashboard/index.js +10 -2
- package/dist/dashboard/query-state.d.ts +48 -27
- package/dist/dashboard/query-state.d.ts.map +1 -1
- package/dist/dashboard/query-state.js +111 -57
- package/dist/dashboard/settings-section.d.ts +19 -9
- package/dist/dashboard/settings-section.d.ts.map +1 -1
- package/dist/dashboard/settings-section.js +74 -25
- package/dist/dashboard/stat-card.d.ts +76 -19
- package/dist/dashboard/stat-card.d.ts.map +1 -1
- package/dist/dashboard/stat-card.js +78 -38
- package/dist/dashboard/stat-card.module.css +112 -26
- package/dist/dashboard/stat-card.module.css.d.ts +5 -0
- package/dist/dashboard/stat-group.d.ts +41 -0
- package/dist/dashboard/stat-group.d.ts.map +1 -0
- package/dist/dashboard/stat-group.js +30 -0
- package/dist/dashboard/stat-group.module.css +66 -0
- package/dist/dashboard/stat-group.module.css.d.ts +8 -0
- package/dist/dashboard/widget-grid.d.ts +27 -0
- package/dist/dashboard/widget-grid.d.ts.map +1 -0
- package/dist/dashboard/widget-grid.js +41 -0
- package/dist/dashboard/widget-grid.module.css +53 -0
- package/dist/dashboard/widget-grid.module.css.d.ts +9 -0
- package/dist/data/data-table.d.ts +151 -12
- package/dist/data/data-table.d.ts.map +1 -1
- package/dist/data/data-table.js +392 -138
- package/dist/data/data-table.module.css +64 -0
- package/dist/data/data-table.module.css.d.ts +11 -0
- package/dist/data/index.d.ts +2 -2
- package/dist/data/index.d.ts.map +1 -1
- package/dist/data/index.js +24 -2
- package/dist/data/query-branch.d.ts +28 -0
- package/dist/data/query-branch.d.ts.map +1 -0
- package/dist/data/query-branch.js +10 -0
- package/dist/data/table.d.ts +2 -2
- package/dist/data/table.d.ts.map +1 -1
- package/dist/data/virtual-list.d.ts +65 -6
- package/dist/data/virtual-list.d.ts.map +1 -1
- package/dist/data/virtual-list.js +89 -38
- package/dist/data/virtual.d.ts +1 -1
- package/dist/data/virtual.d.ts.map +1 -1
- package/dist/forms/FormErrorSummary.d.ts +4 -3
- package/dist/forms/FormErrorSummary.d.ts.map +1 -1
- package/dist/forms/FormErrorSummary.js +6 -4
- package/dist/forms/create-form.d.ts +2 -2
- package/dist/forms/field.d.ts +46 -6
- package/dist/forms/field.d.ts.map +1 -1
- package/dist/forms/field.js +10 -5
- package/dist/forms/form-layout.d.ts +142 -0
- package/dist/forms/form-layout.d.ts.map +1 -0
- package/dist/forms/form-layout.js +110 -0
- package/dist/forms/form-layout.module.css +145 -0
- package/dist/forms/form-layout.module.css.d.ts +20 -0
- package/dist/forms/form-state.d.ts +42 -0
- package/dist/forms/form-state.d.ts.map +1 -0
- package/dist/forms/form-state.js +26 -0
- package/dist/forms/index.d.ts +22 -6
- package/dist/forms/index.d.ts.map +1 -1
- package/dist/forms/index.js +19 -3
- package/dist/forms/use-field-array.d.ts +60 -0
- package/dist/forms/use-field-array.d.ts.map +1 -0
- package/dist/forms/use-field-array.js +20 -0
- package/dist/forms/use-form-draft.d.ts +25 -10
- package/dist/forms/use-form-draft.d.ts.map +1 -1
- package/dist/forms/use-form-draft.js +55 -18
- package/dist/forms/use-form-submit.d.ts +88 -0
- package/dist/forms/use-form-submit.d.ts.map +1 -0
- package/dist/forms/use-form-submit.js +63 -0
- package/dist/guard/index.d.ts +1 -1
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +27 -11
- package/dist/index.d.ts +10 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +44 -4
- package/dist/nav/types.d.ts +15 -6
- package/dist/nav/types.d.ts.map +1 -1
- package/dist/notifications/bell.d.ts +3 -2
- package/dist/notifications/bell.d.ts.map +1 -1
- package/dist/notifications/bell.js +73 -56
- package/dist/notifications/center.d.ts +15 -4
- package/dist/notifications/center.d.ts.map +1 -1
- package/dist/notifications/center.js +5 -2
- package/dist/notifications/define-notifications.d.ts +56 -14
- package/dist/notifications/define-notifications.d.ts.map +1 -1
- package/dist/notifications/define-notifications.js +2 -1
- package/dist/notifications/index.d.ts +6 -2
- package/dist/notifications/index.d.ts.map +1 -1
- package/dist/notifications/index.js +5 -1
- package/dist/notifications/mount-guard.d.ts +8 -0
- package/dist/notifications/mount-guard.d.ts.map +1 -0
- package/dist/notifications/mount-guard.js +19 -0
- package/dist/notifications/notify.d.ts +58 -1
- package/dist/notifications/notify.d.ts.map +1 -1
- package/dist/notifications/notify.js +46 -2
- package/dist/notifications/overlay.d.ts.map +1 -1
- package/dist/notifications/overlay.js +2 -0
- package/dist/notifications/store.d.ts +7 -0
- package/dist/notifications/store.d.ts.map +1 -1
- package/dist/provider/index.d.ts +37 -5
- package/dist/provider/index.d.ts.map +1 -1
- package/dist/provider/index.js +66 -28
- package/dist/provider/lab-theme.d.ts.map +1 -1
- package/dist/provider/lab-theme.js +2 -1
- package/dist/query/devtools.d.ts.map +1 -1
- package/dist/query/devtools.js +1 -0
- package/dist/router-tanstack/field.d.ts +1 -1
- package/dist/router-tanstack/field.d.ts.map +1 -1
- package/dist/router-tanstack/index.d.ts +1 -1
- package/dist/router-tanstack/index.d.ts.map +1 -1
- package/dist/router-tanstack/search-store.d.ts +4 -1
- package/dist/router-tanstack/search-store.d.ts.map +1 -1
- package/dist/router-tanstack/search-store.js +92 -8
- package/dist/router-tanstack/use-nav.d.ts.map +1 -1
- package/dist/router-tanstack/use-nav.js +1 -1
- package/dist/section/index.d.ts +1 -1
- package/dist/section/index.d.ts.map +1 -1
- package/dist/section/section.d.ts +36 -2
- package/dist/section/section.d.ts.map +1 -1
- package/dist/section/section.js +49 -57
- package/dist/section/section.module.css +11 -0
- package/dist/shell/account-types.d.ts +5 -2
- package/dist/shell/account-types.d.ts.map +1 -1
- package/dist/shell/app-brand.d.ts +13 -0
- package/dist/shell/app-brand.d.ts.map +1 -0
- package/dist/shell/app-brand.js +94 -0
- package/dist/shell/app-brand.module.css +109 -0
- package/dist/shell/app-brand.module.css.d.ts +14 -0
- package/dist/shell/app-breadcrumbs.d.ts +4 -2
- package/dist/shell/app-breadcrumbs.d.ts.map +1 -1
- package/dist/shell/app-breadcrumbs.js +33 -22
- package/dist/shell/app-header.module.css +10 -5
- package/dist/shell/app-main.module.css +107 -0
- package/dist/shell/app-main.module.css.d.ts +11 -0
- package/dist/shell/app-mobile-nav.d.ts +3 -2
- package/dist/shell/app-mobile-nav.d.ts.map +1 -1
- package/dist/shell/app-mobile-nav.js +29 -15
- package/dist/shell/app-mobile-nav.module.css +48 -27
- package/dist/shell/app-mobile-nav.module.css.d.ts +0 -1
- package/dist/shell/app-sidebar-account.d.ts +2 -1
- package/dist/shell/app-sidebar-account.d.ts.map +1 -1
- package/dist/shell/app-sidebar-account.js +57 -22
- package/dist/shell/app-sidebar.d.ts +9 -3
- package/dist/shell/app-sidebar.d.ts.map +1 -1
- package/dist/shell/app-sidebar.js +55 -109
- package/dist/shell/app-sidebar.module.css +53 -96
- package/dist/shell/app-sidebar.module.css.d.ts +0 -5
- package/dist/shell/index.d.ts +14 -6
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +47 -12
- package/dist/shell/isomorphic-layout-effect.d.ts +14 -0
- package/dist/shell/isomorphic-layout-effect.d.ts.map +1 -0
- package/dist/shell/isomorphic-layout-effect.js +5 -0
- package/dist/shell/mobile-nav-model.d.ts.map +1 -1
- package/dist/shell/mobile-nav-model.js +27 -6
- package/dist/shell/nav-count-badge.d.ts +5 -3
- package/dist/shell/nav-count-badge.d.ts.map +1 -1
- package/dist/shell/nav-count-badge.js +3 -1
- package/dist/shell/page-aside.d.ts +43 -0
- package/dist/shell/page-aside.d.ts.map +1 -0
- package/dist/shell/page-aside.js +202 -0
- package/dist/shell/page-aside.module.css +174 -0
- package/dist/shell/page-aside.module.css.d.ts +14 -0
- package/dist/shell/page-bar.d.ts +55 -15
- package/dist/shell/page-bar.d.ts.map +1 -1
- package/dist/shell/page-bar.js +130 -41
- package/dist/shell/page-bar.module.css +107 -13
- package/dist/shell/page-bar.module.css.d.ts +4 -1
- package/dist/shell/sidebar-search.d.ts +4 -2
- package/dist/shell/sidebar-search.d.ts.map +1 -1
- package/dist/shell/sidebar-search.js +9 -4
- package/dist/shell/sidebar-search.module.css +1 -1
- package/dist/state/fields.d.ts +227 -34
- package/dist/state/fields.d.ts.map +1 -1
- package/dist/state/fields.js +77 -30
- package/dist/state/persisted-or-local.d.ts +9 -0
- package/dist/state/persisted-or-local.d.ts.map +1 -0
- package/dist/state/persisted-or-local.js +24 -0
- package/dist/state/persisted.d.ts +12 -0
- package/dist/state/persisted.d.ts.map +1 -1
- package/dist/state/persisted.js +36 -18
- package/dist/state.d.ts +1 -1
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +2 -6
- package/dist/surfaces.d.ts +12 -11
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +43 -12
- package/dist/theme/ctl-theme.d.ts.map +1 -1
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/index.js +102 -17
- package/dist/theme/nav-link.module.css +37 -26
- package/dist/theme/segmented-control.module.css +22 -0
- package/dist/theme/segmented-control.module.css.d.ts +1 -0
- package/dist/theme-lab/boot.d.ts +66 -0
- package/dist/theme-lab/boot.d.ts.map +1 -0
- package/dist/theme-lab/boot.js +82 -0
- package/dist/theme-lab/index.d.ts +4 -43
- package/dist/theme-lab/index.d.ts.map +1 -1
- package/dist/theme-lab/index.js +9 -69
- package/dist/theme-toggle/index.d.ts +3 -2
- package/dist/theme-toggle/index.d.ts.map +1 -1
- package/dist/theme-toggle/index.js +8 -2
- package/dist/tokens/index.d.ts +42 -0
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +35 -6
- package/dist/tokens/palette.d.ts +45 -25
- package/dist/tokens/palette.d.ts.map +1 -1
- package/dist/tokens/palette.js +31 -14
- package/dist/tokens.css +4 -5
- package/dist/utils/is-dev.d.ts +3 -0
- package/dist/utils/is-dev.d.ts.map +1 -0
- package/dist/utils/is-dev.js +6 -0
- package/dist/widget-header/delta-badge.d.ts +7 -1
- package/dist/widget-header/delta-badge.d.ts.map +1 -1
- package/dist/widget-header/delta-badge.js +8 -1
- package/dist/widget-header/index.d.ts +2 -2
- package/dist/widget-header/index.d.ts.map +1 -1
- package/dist/widget-header/index.js +3 -1
- package/dist/widget-header/widget-header.d.ts +99 -4
- package/dist/widget-header/widget-header.d.ts.map +1 -1
- package/dist/widget-header/widget-header.js +45 -18
- package/dist/widget-header/widget-header.module.css +87 -7
- package/dist/widget-header/widget-header.module.css.d.ts +1 -0
- package/llms.txt +6 -6
- package/package.json +49 -25
- package/src/agent/coalesce.ts +2 -1
- package/src/agent/merge.ts +2 -1
- package/src/agent/part-list.tsx +2 -0
- package/src/agent/stick-to-bottom.tsx +9 -11
- package/src/agent-chat/composer.tsx +124 -103
- package/src/agent-chat/relative-time.ts +2 -1
- package/src/agent-chat/thread-detail-panel.tsx +9 -5
- package/src/agent-chat/thread-feed-row.tsx +17 -9
- package/src/agent-chat/thread-feed.tsx +9 -3
- package/src/agent-chat/thread-message.tsx +48 -13
- package/src/agent-chat/thread-outcome-card.tsx +9 -1
- package/src/agent-chat/thread-workspace.tsx +72 -33
- package/src/agent-chat/tool-chip.tsx +8 -2
- package/src/charts/hooks/useBandPlot.ts +26 -8
- package/src/charts/index.ts +39 -4
- package/src/charts/kinds/BandStrip.tsx +36 -14
- package/src/charts/kinds/Bars.tsx +67 -16
- package/src/charts/kinds/Donut.tsx +30 -3
- package/src/charts/kinds/DualPanel.tsx +56 -10
- package/src/charts/kinds/Heatmap.tsx +90 -33
- package/src/charts/kinds/MirroredBars.tsx +21 -3
- package/src/charts/kinds/MultiLine.tsx +61 -18
- package/src/charts/kinds/StackedArea.tsx +77 -6
- package/src/charts/kinds/ZonedLine.tsx +71 -31
- package/src/charts/layout/auto-margin.ts +134 -21
- package/src/charts/layout/log-ticks.ts +90 -0
- package/src/charts/primitives/Axes.tsx +33 -5
- package/src/charts/primitives/CartesianChart.tsx +194 -41
- package/src/charts/primitives/ChartCard.tsx +66 -25
- package/src/charts/primitives/ChartFrame.tsx +107 -26
- package/src/charts/primitives/ChartLegend.tsx +43 -13
- package/src/charts/primitives/ChartPending.tsx +185 -7
- package/src/charts/primitives/ChartTooltip.tsx +37 -5
- package/src/charts/primitives/Crosshair.tsx +6 -3
- package/src/charts/primitives/chart-frame-layout.ts +212 -0
- package/src/charts/primitives/chart-tier.tsx +37 -0
- package/src/charts/series.ts +77 -1
- package/src/charts/sparklines/BarSparkline.tsx +11 -3
- package/src/charts/sparklines/LineSparkline.tsx +18 -4
- package/src/charts/sparklines/index.ts +2 -2
- package/src/charts/utils/format.ts +163 -2
- package/src/charts/utils/ticks.ts +149 -4
- package/src/cli/index.ts +8 -3
- package/src/commands/ShortcutsHelp.tsx +10 -3
- package/src/commands/define-commands.ts +2 -2
- package/src/commands/define-overlays.ts +216 -1
- package/src/commands/index.ts +12 -0
- package/src/commands/overlays-mount.tsx +60 -26
- package/src/common/errors.ts +73 -0
- package/src/common/index.ts +21 -0
- package/src/common/props.ts +71 -0
- package/src/common/query-state-like.ts +64 -0
- package/src/common/refs.ts +58 -0
- package/src/common/scroll-parent.ts +42 -0
- package/src/common/validate.ts +94 -0
- package/src/connectivity/connectivity-indicator.tsx +6 -1
- package/src/connectivity/connectivity-provider.tsx +4 -1
- package/src/connectivity/index.ts +1 -0
- package/src/connectivity/types.ts +5 -2
- package/src/connectivity/use-connectivity.ts +3 -2
- package/src/content/article-layout.module.css +11 -4
- package/src/content/callout.tsx +13 -7
- package/src/content/guide.tsx +14 -4
- package/src/content/heading-anchor.tsx +14 -3
- package/src/content/index.ts +2 -0
- package/src/content/markdown.tsx +2 -0
- package/src/content/prose.module.css +9 -7
- package/src/content/reading-progress.tsx +40 -15
- package/src/content/slug.ts +4 -0
- package/src/content/toc.tsx +47 -10
- package/src/controls/actions.tsx +89 -9
- package/src/controls/compare-filter.tsx +20 -3
- package/src/controls/control-group.tsx +11 -4
- package/src/controls/controls.module.css +169 -186
- package/src/controls/controls.module.css.d.ts +9 -11
- package/src/controls/enum-filter.tsx +46 -19
- package/src/controls/filter-context.tsx +21 -8
- package/src/controls/filter-pill.tsx +6 -3
- package/src/controls/filter-set.tsx +32 -13
- package/src/controls/filter-sheet.tsx +44 -194
- package/src/controls/index.ts +8 -0
- package/src/controls/multi-select-filter.tsx +179 -29
- package/src/controls/number-filter.tsx +329 -0
- package/src/controls/panel-row.module.css +86 -0
- package/src/controls/panel-row.module.css.d.ts +13 -0
- package/src/controls/panel-row.tsx +392 -0
- package/src/controls/range-filter.tsx +133 -43
- package/src/controls/search-filter.tsx +31 -13
- package/src/controls/select-filter.tsx +11 -1
- package/src/controls/slider-control.tsx +159 -0
- package/src/controls/sync-button.tsx +13 -3
- package/src/controls/toggle-filter.tsx +37 -17
- package/src/controls/view-tabs.tsx +63 -11
- package/src/controls-dates/date-range-picker.tsx +8 -1
- package/src/dashboard/delta-badge.tsx +5 -1
- package/src/dashboard/empty-state.tsx +72 -25
- package/src/dashboard/index.ts +19 -2
- package/src/dashboard/query-state.tsx +154 -84
- package/src/dashboard/settings-section.tsx +87 -31
- package/src/dashboard/stat-card.module.css +112 -26
- package/src/dashboard/stat-card.module.css.d.ts +5 -0
- package/src/dashboard/stat-card.tsx +207 -68
- package/src/dashboard/stat-group.module.css +66 -0
- package/src/dashboard/stat-group.module.css.d.ts +8 -0
- package/src/dashboard/stat-group.tsx +74 -0
- package/src/dashboard/widget-grid.module.css +53 -0
- package/src/dashboard/widget-grid.module.css.d.ts +9 -0
- package/src/dashboard/widget-grid.tsx +127 -0
- package/src/data/data-table.module.css +64 -0
- package/src/data/data-table.module.css.d.ts +11 -0
- package/src/data/data-table.tsx +927 -346
- package/src/data/index.ts +30 -2
- package/src/data/query-branch.ts +53 -0
- package/src/data/table.ts +5 -0
- package/src/data/virtual-list.tsx +189 -55
- package/src/data/virtual.ts +5 -1
- package/src/forms/FormErrorSummary.tsx +13 -6
- package/src/forms/create-form.ts +2 -2
- package/src/forms/field.ts +60 -11
- package/src/forms/form-layout.module.css +145 -0
- package/src/forms/form-layout.module.css.d.ts +20 -0
- package/src/forms/form-layout.tsx +305 -0
- package/src/forms/form-state.tsx +94 -0
- package/src/forms/index.ts +38 -7
- package/src/forms/use-field-array.ts +94 -0
- package/src/forms/use-form-draft.ts +145 -53
- package/src/forms/use-form-submit.ts +199 -0
- package/src/guard/index.ts +91 -23
- package/src/index.ts +79 -10
- package/src/nav/types.ts +15 -6
- package/src/notifications/bell.tsx +18 -4
- package/src/notifications/center.tsx +18 -5
- package/src/notifications/define-notifications.ts +64 -18
- package/src/notifications/index.ts +12 -1
- package/src/notifications/mount-guard.ts +41 -0
- package/src/notifications/notify.tsx +122 -3
- package/src/notifications/overlay.tsx +2 -0
- package/src/notifications/store.ts +7 -0
- package/src/provider/index.tsx +101 -13
- package/src/provider/lab-theme.ts +2 -1
- package/src/query/devtools.tsx +1 -0
- package/src/router-tanstack/field.ts +5 -0
- package/src/router-tanstack/index.ts +4 -0
- package/src/router-tanstack/search-store.ts +171 -12
- package/src/router-tanstack/use-nav.ts +18 -13
- package/src/section/index.ts +1 -1
- package/src/section/section.module.css +11 -0
- package/src/section/section.tsx +163 -96
- package/src/shell/account-types.ts +5 -2
- package/src/shell/app-brand.module.css +109 -0
- package/src/shell/app-brand.module.css.d.ts +14 -0
- package/src/shell/app-brand.tsx +140 -0
- package/src/shell/app-breadcrumbs.tsx +19 -9
- package/src/shell/app-header.module.css +10 -5
- package/src/shell/app-main.module.css +107 -0
- package/src/shell/app-main.module.css.d.ts +11 -0
- package/src/shell/app-mobile-nav.module.css +48 -27
- package/src/shell/app-mobile-nav.module.css.d.ts +0 -1
- package/src/shell/app-mobile-nav.tsx +43 -11
- package/src/shell/app-sidebar-account.tsx +29 -6
- package/src/shell/app-sidebar.module.css +53 -96
- package/src/shell/app-sidebar.module.css.d.ts +0 -5
- package/src/shell/app-sidebar.tsx +40 -113
- package/src/shell/index.tsx +156 -65
- package/src/shell/isomorphic-layout-effect.ts +14 -0
- package/src/shell/mobile-nav-model.ts +66 -10
- package/src/shell/nav-count-badge.tsx +6 -1
- package/src/shell/page-aside.module.css +174 -0
- package/src/shell/page-aside.module.css.d.ts +14 -0
- package/src/shell/page-aside.tsx +377 -0
- package/src/shell/page-bar.module.css +107 -13
- package/src/shell/page-bar.module.css.d.ts +4 -1
- package/src/shell/page-bar.tsx +294 -94
- package/src/shell/sidebar-search.module.css +1 -1
- package/src/shell/sidebar-search.tsx +13 -4
- package/src/state/fields.ts +360 -69
- package/src/state/persisted-or-local.ts +50 -0
- package/src/state/persisted.ts +76 -26
- package/src/state.ts +4 -7
- package/src/surfaces.ts +43 -12
- package/src/theme/ctl-theme.tsx +10 -0
- package/src/theme/index.ts +129 -16
- package/src/theme/nav-link.module.css +37 -26
- package/src/theme/segmented-control.module.css +22 -0
- package/src/theme/segmented-control.module.css.d.ts +1 -0
- package/src/theme-lab/boot.ts +140 -0
- package/src/theme-lab/index.tsx +21 -112
- package/src/theme-toggle/index.tsx +10 -3
- package/src/tokens/index.ts +50 -6
- package/src/tokens/palette.ts +58 -30
- package/src/utils/is-dev.ts +4 -0
- package/src/widget-header/delta-badge.tsx +25 -5
- package/src/widget-header/index.ts +10 -2
- package/src/widget-header/widget-header.module.css +87 -7
- package/src/widget-header/widget-header.module.css.d.ts +1 -0
- package/src/widget-header/widget-header.tsx +166 -42
- package/dist/agent/adapter.test.d.ts +0 -2
- package/dist/agent/adapter.test.d.ts.map +0 -1
- package/dist/agent/ai-sdk-transport.test.d.ts +0 -2
- package/dist/agent/ai-sdk-transport.test.d.ts.map +0 -1
- package/dist/agent/coalesce.test.d.ts +0 -2
- package/dist/agent/coalesce.test.d.ts.map +0 -1
- package/dist/agent/foreign.test.d.ts +0 -2
- package/dist/agent/foreign.test.d.ts.map +0 -1
- package/dist/agent/id.test.d.ts +0 -2
- package/dist/agent/id.test.d.ts.map +0 -1
- package/dist/agent/merge.test.d.ts +0 -2
- package/dist/agent/merge.test.d.ts.map +0 -1
- package/dist/agent/part-list.test.d.ts +0 -2
- package/dist/agent/part-list.test.d.ts.map +0 -1
- package/dist/agent/parts.test.d.ts +0 -2
- package/dist/agent/parts.test.d.ts.map +0 -1
- package/dist/agent/thread.test.d.ts +0 -2
- package/dist/agent/thread.test.d.ts.map +0 -1
- package/dist/agent/transport.test.d.ts +0 -2
- package/dist/agent/transport.test.d.ts.map +0 -1
- package/dist/agent/use-agent-stream.test.d.ts +0 -2
- package/dist/agent/use-agent-stream.test.d.ts.map +0 -1
- package/dist/agent/use-agent-thread-runs.foreign.test.d.ts +0 -2
- package/dist/agent/use-agent-thread-runs.foreign.test.d.ts.map +0 -1
- package/dist/agent/use-agent-thread-runs.resume.test.d.ts +0 -2
- package/dist/agent/use-agent-thread-runs.resume.test.d.ts.map +0 -1
- package/dist/agent/use-agent-thread-runs.test.d.ts +0 -2
- package/dist/agent/use-agent-thread-runs.test.d.ts.map +0 -1
- package/dist/agent/use-agent-thread-runs.wedge.test.d.ts +0 -2
- package/dist/agent/use-agent-thread-runs.wedge.test.d.ts.map +0 -1
- package/dist/agent-chat/composer.test.d.ts +0 -2
- package/dist/agent-chat/composer.test.d.ts.map +0 -1
- package/dist/agent-chat/relative-time.test.d.ts +0 -2
- package/dist/agent-chat/relative-time.test.d.ts.map +0 -1
- package/dist/agent-chat/thread-feed-row.test.d.ts +0 -2
- package/dist/agent-chat/thread-feed-row.test.d.ts.map +0 -1
- package/dist/agent-chat/thread-feed.test.d.ts +0 -2
- package/dist/agent-chat/thread-feed.test.d.ts.map +0 -1
- package/dist/agent-chat/thread-message.test.d.ts +0 -2
- package/dist/agent-chat/thread-message.test.d.ts.map +0 -1
- package/dist/agent-chat/thread-workspace.test.d.ts +0 -2
- package/dist/agent-chat/thread-workspace.test.d.ts.map +0 -1
- package/dist/agent-chat/tool-chip.test.d.ts +0 -2
- package/dist/agent-chat/tool-chip.test.d.ts.map +0 -1
- package/dist/agent-chat/virtualize.type-guard.test.d.ts +0 -2
- package/dist/agent-chat/virtualize.type-guard.test.d.ts.map +0 -1
- package/dist/charts/cursor/resolve.test.d.ts +0 -2
- package/dist/charts/cursor/resolve.test.d.ts.map +0 -1
- package/dist/charts/cursor/store.test.d.ts +0 -2
- package/dist/charts/cursor/store.test.d.ts.map +0 -1
- package/dist/charts/kinds/BandStrip.test.d.ts +0 -2
- package/dist/charts/kinds/BandStrip.test.d.ts.map +0 -1
- package/dist/charts/kinds/Bars.test.d.ts +0 -2
- package/dist/charts/kinds/Bars.test.d.ts.map +0 -1
- package/dist/charts/kinds/DualPanel.test.d.ts +0 -2
- package/dist/charts/kinds/DualPanel.test.d.ts.map +0 -1
- package/dist/charts/kinds/MirroredBars.test.d.ts +0 -2
- package/dist/charts/kinds/MirroredBars.test.d.ts.map +0 -1
- package/dist/charts/kinds/MultiLine.test.d.ts +0 -2
- package/dist/charts/kinds/MultiLine.test.d.ts.map +0 -1
- package/dist/charts/kinds/StackedArea.test.d.ts +0 -2
- package/dist/charts/kinds/StackedArea.test.d.ts.map +0 -1
- package/dist/charts/kinds/ZonedLine.test.d.ts +0 -2
- package/dist/charts/kinds/ZonedLine.test.d.ts.map +0 -1
- package/dist/charts/layout/auto-margin.test.d.ts +0 -2
- package/dist/charts/layout/auto-margin.test.d.ts.map +0 -1
- package/dist/charts/primitives/CartesianChart.test.d.ts +0 -2
- package/dist/charts/primitives/CartesianChart.test.d.ts.map +0 -1
- package/dist/charts/primitives/ChartCard.test.d.ts +0 -2
- package/dist/charts/primitives/ChartCard.test.d.ts.map +0 -1
- package/dist/charts/primitives/ChartFrame.test.d.ts +0 -2
- package/dist/charts/primitives/ChartFrame.test.d.ts.map +0 -1
- package/dist/charts/primitives/ChartLegend.test.d.ts +0 -2
- package/dist/charts/primitives/ChartLegend.test.d.ts.map +0 -1
- package/dist/charts/primitives/ChartTooltip.test.d.ts +0 -2
- package/dist/charts/primitives/ChartTooltip.test.d.ts.map +0 -1
- package/dist/charts/primitives/XZoneRects.test.d.ts +0 -2
- package/dist/charts/primitives/XZoneRects.test.d.ts.map +0 -1
- package/dist/charts/series.test.d.ts +0 -2
- package/dist/charts/series.test.d.ts.map +0 -1
- package/dist/charts/utils/domain.test.d.ts +0 -2
- package/dist/charts/utils/domain.test.d.ts.map +0 -1
- package/dist/charts/utils/measure-text.test.d.ts +0 -2
- package/dist/charts/utils/measure-text.test.d.ts.map +0 -1
- package/dist/cli/check-coverage.test.d.ts +0 -2
- package/dist/cli/check-coverage.test.d.ts.map +0 -1
- package/dist/cli/check-theme.test.d.ts +0 -2
- package/dist/cli/check-theme.test.d.ts.map +0 -1
- package/dist/cli/help.test.d.ts +0 -2
- package/dist/cli/help.test.d.ts.map +0 -1
- package/dist/cli/init-sync.test.d.ts +0 -2
- package/dist/cli/init-sync.test.d.ts.map +0 -1
- package/dist/cli/placement-engine.test.d.ts +0 -2
- package/dist/cli/placement-engine.test.d.ts.map +0 -1
- package/dist/cli/shipped-versions.test.d.ts +0 -2
- package/dist/cli/shipped-versions.test.d.ts.map +0 -1
- package/dist/cli/tarball-parity.test.d.ts +0 -2
- package/dist/cli/tarball-parity.test.d.ts.map +0 -1
- package/dist/cli/template-format.test.d.ts +0 -2
- package/dist/cli/template-format.test.d.ts.map +0 -1
- package/dist/cli/template-ledger.test.d.ts +0 -2
- package/dist/cli/template-ledger.test.d.ts.map +0 -1
- package/dist/cli/tokens-css.test.d.ts +0 -2
- package/dist/cli/tokens-css.test.d.ts.map +0 -1
- package/dist/cli/toolchain-wiring.test.d.ts +0 -2
- package/dist/cli/toolchain-wiring.test.d.ts.map +0 -1
- package/dist/cli/waiver-audit.test.d.ts +0 -2
- package/dist/cli/waiver-audit.test.d.ts.map +0 -1
- package/dist/content/article-actions.test.d.ts +0 -2
- package/dist/content/article-actions.test.d.ts.map +0 -1
- package/dist/content/article-model.test.d.ts +0 -2
- package/dist/content/article-model.test.d.ts.map +0 -1
- package/dist/content/block-split.test.d.ts +0 -2
- package/dist/content/block-split.test.d.ts.map +0 -1
- package/dist/content/heading-components.test.d.ts +0 -2
- package/dist/content/heading-components.test.d.ts.map +0 -1
- package/dist/content/markdown.test.d.ts +0 -2
- package/dist/content/markdown.test.d.ts.map +0 -1
- package/dist/content/prose.module.css.test.d.ts +0 -2
- package/dist/content/prose.module.css.test.d.ts.map +0 -1
- package/dist/content/sanitize.test.d.ts +0 -2
- package/dist/content/sanitize.test.d.ts.map +0 -1
- package/dist/content/slug.test.d.ts +0 -2
- package/dist/content/slug.test.d.ts.map +0 -1
- package/dist/controls/actions.test.d.ts +0 -2
- package/dist/controls/actions.test.d.ts.map +0 -1
- package/dist/controls/actions.type-guard.test.d.ts +0 -2
- package/dist/controls/actions.type-guard.test.d.ts.map +0 -1
- package/dist/controls/control-group.test.d.ts +0 -2
- package/dist/controls/control-group.test.d.ts.map +0 -1
- package/dist/controls/controls.router.test.d.ts +0 -2
- package/dist/controls/controls.router.test.d.ts.map +0 -1
- package/dist/controls/filter-set.test.d.ts +0 -2
- package/dist/controls/filter-set.test.d.ts.map +0 -1
- package/dist/controls/peer-boundary.test.d.ts +0 -2
- package/dist/controls/peer-boundary.test.d.ts.map +0 -1
- package/dist/controls/props-contract.test.d.ts +0 -2
- package/dist/controls/props-contract.test.d.ts.map +0 -1
- package/dist/controls/sync-button.test.d.ts +0 -2
- package/dist/controls/sync-button.test.d.ts.map +0 -1
- package/dist/dashboard/query-state.test.d.ts +0 -2
- package/dist/dashboard/query-state.test.d.ts.map +0 -1
- package/dist/dashboard/settings-section.test.d.ts +0 -2
- package/dist/dashboard/settings-section.test.d.ts.map +0 -1
- package/dist/dashboard/stat-card.test.d.ts +0 -2
- package/dist/dashboard/stat-card.test.d.ts.map +0 -1
- package/dist/data/data-table.test.d.ts +0 -2
- package/dist/data/data-table.test.d.ts.map +0 -1
- package/dist/guard/check-source.test.d.ts +0 -2
- package/dist/guard/check-source.test.d.ts.map +0 -1
- package/dist/guard/grace.test.d.ts +0 -20
- package/dist/guard/grace.test.d.ts.map +0 -1
- package/dist/guard/guard-hook.test.d.ts +0 -2
- package/dist/guard/guard-hook.test.d.ts.map +0 -1
- package/dist/provider/index.test.d.ts +0 -2
- package/dist/provider/index.test.d.ts.map +0 -1
- package/dist/provider/lab-theme.test.d.ts +0 -2
- package/dist/provider/lab-theme.test.d.ts.map +0 -1
- package/dist/query/error-message.test.d.ts +0 -2
- package/dist/query/error-message.test.d.ts.map +0 -1
- package/dist/router-tanstack/multi-search-param-store.test.d.ts +0 -2
- package/dist/router-tanstack/multi-search-param-store.test.d.ts.map +0 -1
- package/dist/router-tanstack/nav.test.d.ts +0 -2
- package/dist/router-tanstack/nav.test.d.ts.map +0 -1
- package/dist/router-tanstack/search-param-store.test.d.ts +0 -2
- package/dist/router-tanstack/search-param-store.test.d.ts.map +0 -1
- package/dist/router-tanstack/search-store.router.test.d.ts +0 -2
- package/dist/router-tanstack/search-store.router.test.d.ts.map +0 -1
- package/dist/router-tanstack/search-store.test.d.ts +0 -2
- package/dist/router-tanstack/search-store.test.d.ts.map +0 -1
- package/dist/router-tanstack/use-nav.test.d.ts +0 -2
- package/dist/router-tanstack/use-nav.test.d.ts.map +0 -1
- package/dist/section/section.test.d.ts +0 -2
- package/dist/section/section.test.d.ts.map +0 -1
- package/dist/shell/app-mobile-nav.test.d.ts +0 -2
- package/dist/shell/app-mobile-nav.test.d.ts.map +0 -1
- package/dist/shell/app-sidebar.test.d.ts +0 -2
- package/dist/shell/app-sidebar.test.d.ts.map +0 -1
- package/dist/shell/index.test.d.ts +0 -2
- package/dist/shell/index.test.d.ts.map +0 -1
- package/dist/shell/mobile-nav-model.test.d.ts +0 -2
- package/dist/shell/mobile-nav-model.test.d.ts.map +0 -1
- package/dist/shell/page-bar.test.d.ts +0 -2
- package/dist/shell/page-bar.test.d.ts.map +0 -1
- package/dist/state/fields.test.d.ts +0 -2
- package/dist/state/fields.test.d.ts.map +0 -1
- package/dist/styles.floor.test.d.ts +0 -2
- package/dist/styles.floor.test.d.ts.map +0 -1
- package/dist/theme/border-coverage.test.d.ts +0 -18
- package/dist/theme/border-coverage.test.d.ts.map +0 -1
- package/dist/theme/contrast.test.d.ts +0 -2
- package/dist/theme/contrast.test.d.ts.map +0 -1
- package/dist/theme/ctl-theme.test.d.ts +0 -2
- package/dist/theme/ctl-theme.test.d.ts.map +0 -1
- package/dist/theme/ctl-tier-coverage.test.d.ts +0 -2
- package/dist/theme/ctl-tier-coverage.test.d.ts.map +0 -1
- package/dist/theme/density-option.test.d.ts +0 -2
- package/dist/theme/density-option.test.d.ts.map +0 -1
- package/dist/theme/density-relations.test.d.ts +0 -2
- package/dist/theme/density-relations.test.d.ts.map +0 -1
- package/dist/theme/derive-theme.test.d.ts +0 -2
- package/dist/theme/derive-theme.test.d.ts.map +0 -1
- package/dist/theme/divider-law.test.d.ts +0 -2
- package/dist/theme/divider-law.test.d.ts.map +0 -1
- package/dist/theme/fonts-option.test.d.ts +0 -2
- package/dist/theme/fonts-option.test.d.ts.map +0 -1
- package/dist/theme/icon-slot.test.d.ts +0 -2
- package/dist/theme/icon-slot.test.d.ts.map +0 -1
- package/dist/theme/layout-rhythm-css.test.d.ts +0 -2
- package/dist/theme/layout-rhythm-css.test.d.ts.map +0 -1
- package/dist/theme/radius-option.test.d.ts +0 -2
- package/dist/theme/radius-option.test.d.ts.map +0 -1
- package/dist/theme/radius.test.d.ts +0 -2
- package/dist/theme/radius.test.d.ts.map +0 -1
- package/dist/theme/shadow-surfaces.test.d.ts +0 -26
- package/dist/theme/shadow-surfaces.test.d.ts.map +0 -1
- package/dist/theme/spacing.test.d.ts +0 -2
- package/dist/theme/spacing.test.d.ts.map +0 -1
- package/dist/theme/use-basalt-spacing.test.d.ts +0 -2
- package/dist/theme/use-basalt-spacing.test.d.ts.map +0 -1
- package/dist/theme-lab/derive-controls.test.d.ts +0 -2
- package/dist/theme-lab/derive-controls.test.d.ts.map +0 -1
- package/dist/theme-lab/derive-state.test.d.ts +0 -2
- package/dist/theme-lab/derive-state.test.d.ts.map +0 -1
- package/dist/theme-lab/theme-lab.test.d.ts +0 -2
- package/dist/theme-lab/theme-lab.test.d.ts.map +0 -1
- package/dist/tokens/build-fonts-css.test.d.ts +0 -2
- package/dist/tokens/build-fonts-css.test.d.ts.map +0 -1
- package/dist/tokens/build-radius-css.test.d.ts +0 -2
- package/dist/tokens/build-radius-css.test.d.ts.map +0 -1
- package/dist/tokens/chart-margin.test.d.ts +0 -2
- package/dist/tokens/chart-margin.test.d.ts.map +0 -1
- package/dist/tokens/density.test.d.ts +0 -2
- package/dist/tokens/density.test.d.ts.map +0 -1
- package/dist/tokens/derive-radius.test.d.ts +0 -2
- package/dist/tokens/derive-radius.test.d.ts.map +0 -1
- package/dist/tokens/derive.test.d.ts +0 -2
- package/dist/tokens/derive.test.d.ts.map +0 -1
- package/dist/tokens/palette.test.d.ts +0 -2
- package/dist/tokens/palette.test.d.ts.map +0 -1
- package/dist/tokens/text-scale.test.d.ts +0 -2
- package/dist/tokens/text-scale.test.d.ts.map +0 -1
- package/dist/vite-color-mix.test.d.ts +0 -2
- package/dist/vite-color-mix.test.d.ts.map +0 -1
- package/dist/vite.test.d.ts +0 -2
- package/dist/vite.test.d.ts.map +0 -1
- package/dist/widget-header/delta-badge.test.d.ts +0 -2
- package/dist/widget-header/delta-badge.test.d.ts.map +0 -1
- package/dist/widget-header/no-mantine.test.d.ts +0 -2
- package/dist/widget-header/no-mantine.test.d.ts.map +0 -1
- package/dist/widget-header/widget-header.test.d.ts +0 -2
- package/dist/widget-header/widget-header.test.d.ts.map +0 -1
- package/src/agent/adapter.test.ts +0 -1640
- package/src/agent/ai-sdk-transport.test.ts +0 -658
- package/src/agent/coalesce.test.ts +0 -387
- package/src/agent/foreign.test.ts +0 -51
- package/src/agent/id.test.ts +0 -169
- package/src/agent/merge.test.ts +0 -206
- package/src/agent/part-list.test.tsx +0 -112
- package/src/agent/parts.test.ts +0 -720
- package/src/agent/thread.test.ts +0 -220
- package/src/agent/transport.test.ts +0 -70
- package/src/agent/use-agent-stream.test.tsx +0 -287
- package/src/agent/use-agent-thread-runs.foreign.test.tsx +0 -138
- package/src/agent/use-agent-thread-runs.resume.test.tsx +0 -294
- package/src/agent/use-agent-thread-runs.test.tsx +0 -1120
- package/src/agent/use-agent-thread-runs.wedge.test.tsx +0 -387
- package/src/agent-chat/composer.test.tsx +0 -924
- package/src/agent-chat/relative-time.test.ts +0 -95
- package/src/agent-chat/thread-feed-row.test.tsx +0 -510
- package/src/agent-chat/thread-feed.test.tsx +0 -241
- package/src/agent-chat/thread-message.test.tsx +0 -1285
- package/src/agent-chat/thread-workspace.test.tsx +0 -169
- package/src/agent-chat/tool-chip.test.tsx +0 -139
- package/src/agent-chat/virtualize.type-guard.test.ts +0 -49
- package/src/charts/cursor/resolve.test.ts +0 -151
- package/src/charts/cursor/store.test.ts +0 -64
- package/src/charts/kinds/BandStrip.test.tsx +0 -282
- package/src/charts/kinds/Bars.test.tsx +0 -183
- package/src/charts/kinds/DualPanel.test.tsx +0 -527
- package/src/charts/kinds/MirroredBars.test.tsx +0 -232
- package/src/charts/kinds/MultiLine.test.tsx +0 -218
- package/src/charts/kinds/StackedArea.test.tsx +0 -114
- package/src/charts/kinds/ZonedLine.test.tsx +0 -147
- package/src/charts/layout/auto-margin.test.ts +0 -42
- package/src/charts/primitives/CartesianChart.test.tsx +0 -647
- package/src/charts/primitives/ChartCard.test.tsx +0 -66
- package/src/charts/primitives/ChartFrame.test.tsx +0 -142
- package/src/charts/primitives/ChartLegend.test.tsx +0 -78
- package/src/charts/primitives/ChartTooltip.test.tsx +0 -44
- package/src/charts/primitives/XZoneRects.test.tsx +0 -123
- package/src/charts/series.test.ts +0 -112
- package/src/charts/utils/domain.test.ts +0 -54
- package/src/charts/utils/measure-text.test.ts +0 -36
- package/src/cli/check-coverage.test.ts +0 -297
- package/src/cli/check-theme.test.ts +0 -837
- package/src/cli/help.test.ts +0 -170
- package/src/cli/init-sync.test.ts +0 -343
- package/src/cli/placement-engine.test.ts +0 -298
- package/src/cli/shipped-versions.test.ts +0 -83
- package/src/cli/tarball-parity.test.ts +0 -44
- package/src/cli/template-format.test.ts +0 -64
- package/src/cli/template-ledger.test.ts +0 -219
- package/src/cli/tokens-css.test.ts +0 -249
- package/src/cli/toolchain-wiring.test.ts +0 -1358
- package/src/cli/waiver-audit.test.ts +0 -215
- package/src/content/article-actions.test.ts +0 -78
- package/src/content/article-model.test.ts +0 -119
- package/src/content/block-split.test.ts +0 -75
- package/src/content/heading-components.test.tsx +0 -65
- package/src/content/markdown.test.tsx +0 -603
- package/src/content/prose.module.css.test.ts +0 -39
- package/src/content/sanitize.test.ts +0 -659
- package/src/content/slug.test.ts +0 -62
- package/src/controls/actions.test.tsx +0 -252
- package/src/controls/actions.type-guard.test.ts +0 -63
- package/src/controls/control-group.test.tsx +0 -229
- package/src/controls/controls.router.test.tsx +0 -637
- package/src/controls/filter-set.test.tsx +0 -472
- package/src/controls/peer-boundary.test.ts +0 -85
- package/src/controls/props-contract.test.tsx +0 -106
- package/src/controls/sync-button.test.tsx +0 -183
- package/src/dashboard/query-state.test.tsx +0 -152
- package/src/dashboard/settings-section.test.tsx +0 -56
- package/src/dashboard/stat-card.test.tsx +0 -154
- package/src/data/data-table.test.tsx +0 -434
- package/src/guard/check-source.test.ts +0 -3355
- package/src/guard/grace.test.ts +0 -149
- package/src/guard/guard-hook.test.ts +0 -203
- package/src/provider/index.test.tsx +0 -101
- package/src/provider/lab-theme.test.ts +0 -147
- package/src/query/error-message.test.ts +0 -67
- package/src/router-tanstack/multi-search-param-store.test.ts +0 -124
- package/src/router-tanstack/nav.test.ts +0 -111
- package/src/router-tanstack/search-param-store.test.ts +0 -135
- package/src/router-tanstack/search-store.router.test.tsx +0 -661
- package/src/router-tanstack/search-store.test.ts +0 -418
- package/src/router-tanstack/use-nav.test.tsx +0 -187
- package/src/section/section.test.tsx +0 -204
- package/src/shell/app-mobile-nav.test.tsx +0 -403
- package/src/shell/app-sidebar.test.tsx +0 -509
- package/src/shell/index.test.tsx +0 -428
- package/src/shell/mobile-nav-model.test.ts +0 -517
- package/src/shell/page-bar.test.tsx +0 -316
- package/src/state/fields.test.tsx +0 -258
- package/src/styles.floor.test.ts +0 -36
- package/src/theme/border-coverage.test.ts +0 -272
- package/src/theme/contrast.test.ts +0 -354
- package/src/theme/ctl-theme.test.tsx +0 -275
- package/src/theme/ctl-tier-coverage.test.ts +0 -170
- package/src/theme/density-option.test.ts +0 -96
- package/src/theme/density-relations.test.ts +0 -506
- package/src/theme/derive-theme.test.ts +0 -101
- package/src/theme/divider-law.test.ts +0 -259
- package/src/theme/fonts-option.test.ts +0 -75
- package/src/theme/icon-slot.test.tsx +0 -275
- package/src/theme/layout-rhythm-css.test.ts +0 -72
- package/src/theme/radius-option.test.ts +0 -86
- package/src/theme/radius.test.ts +0 -142
- package/src/theme/shadow-surfaces.test.ts +0 -488
- package/src/theme/spacing.test.ts +0 -495
- package/src/theme/use-basalt-spacing.test.tsx +0 -55
- package/src/theme-lab/derive-controls.test.ts +0 -94
- package/src/theme-lab/derive-state.test.ts +0 -71
- package/src/theme-lab/theme-lab.test.ts +0 -46
- package/src/tokens/build-fonts-css.test.ts +0 -52
- package/src/tokens/build-radius-css.test.ts +0 -25
- package/src/tokens/chart-margin.test.ts +0 -39
- package/src/tokens/density.test.ts +0 -259
- package/src/tokens/derive-radius.test.ts +0 -62
- package/src/tokens/derive.test.ts +0 -30
- package/src/tokens/palette.test.ts +0 -114
- package/src/tokens/text-scale.test.ts +0 -51
- package/src/vite-color-mix.test.ts +0 -46
- package/src/vite.test.ts +0 -423
- package/src/widget-header/delta-badge.test.tsx +0 -60
- package/src/widget-header/no-mantine.test.ts +0 -33
- package/src/widget-header/widget-header.test.tsx +0 -187
|
@@ -14,6 +14,7 @@ import { dirname, resolve } from 'node:path'
|
|
|
14
14
|
// eslint-disable-next-line -- the plugin is plain JS; the ledger + id set are named exports beside it
|
|
15
15
|
import basaltPlugin, {
|
|
16
16
|
CTL_THEME_TAGS,
|
|
17
|
+
DEPRECATED_EXPORTS,
|
|
17
18
|
KNOWN_RULE_IDS,
|
|
18
19
|
PLUGIN_RULE_ADVISORY,
|
|
19
20
|
PLUGIN_RULE_GRACE,
|
|
@@ -35,7 +36,14 @@ beforeEach(() => {
|
|
|
35
36
|
plugins: [],
|
|
36
37
|
jsPlugins: [PLUGIN_PATH],
|
|
37
38
|
rules: {
|
|
39
|
+
'basalt/provider-above-router': 'error',
|
|
40
|
+
'basalt/duplicate-notifications-mount': 'error',
|
|
41
|
+
'basalt/query-dual-import': 'error',
|
|
42
|
+
'basalt/query-fn-unwrap': 'error',
|
|
43
|
+
'basalt/deprecated-export': 'error',
|
|
44
|
+
'basalt/forms-field-key': 'error',
|
|
38
45
|
'basalt/no-raw-font-size': 'error',
|
|
46
|
+
'basalt/raw-size-literal': 'error',
|
|
39
47
|
'basalt/card-inset': 'error',
|
|
40
48
|
'basalt/chart-in-raw-surface': 'error',
|
|
41
49
|
'basalt/hand-rolled-plot': 'error',
|
|
@@ -45,6 +53,7 @@ beforeEach(() => {
|
|
|
45
53
|
'basalt/raw-scroll-container': 'error',
|
|
46
54
|
'basalt/hand-rolled-filter': 'error',
|
|
47
55
|
'basalt/control-outside-home': 'error',
|
|
56
|
+
'basalt/bound-control-outside-home': 'error',
|
|
48
57
|
'basalt/control-size-literal': 'error',
|
|
49
58
|
'basalt/page-bar-budget': 'error',
|
|
50
59
|
'basalt/in-body-page-title': 'error',
|
|
@@ -111,6 +120,76 @@ describe('basalt/no-raw-font-size', () => {
|
|
|
111
120
|
expect(code).toBe(0)
|
|
112
121
|
expect(rules).not.toContain('no-raw-font-size')
|
|
113
122
|
})
|
|
123
|
+
|
|
124
|
+
// F25: symmetric with raw-size-literal's own test-file skip below — the doctrine is about
|
|
125
|
+
// shipped UI, not fixtures.
|
|
126
|
+
it('does NOT flag a numeric fz inside a *.test.tsx file', () => {
|
|
127
|
+
const { code, rules } = run(`export const C = () => <Text fz={10}>a</Text>\n`, 'lib.test.tsx')
|
|
128
|
+
expect(code).toBe(0)
|
|
129
|
+
expect(rules).not.toContain('no-raw-font-size')
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
// ── raw-size-literal ─────────────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
describe('basalt/raw-size-literal', () => {
|
|
136
|
+
it('flags a CSS-length string on size', () => {
|
|
137
|
+
const { code, rules } = run(`export const C = () => <ThemeIcon size="2rem" />\n`)
|
|
138
|
+
expect(code).toBe(1)
|
|
139
|
+
expect(rules).toContain('raw-size-literal')
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('flags a CSS-length string on fz', () => {
|
|
143
|
+
const { code, rules } = run(`export const C = () => <Text fz="0.8rem">a</Text>\n`)
|
|
144
|
+
expect(code).toBe(1)
|
|
145
|
+
expect(rules).toContain('raw-size-literal')
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('does NOT flag a scale token on size', () => {
|
|
149
|
+
const { code, rules } = run(`export const C = () => <ThemeIcon size="md" />\n`)
|
|
150
|
+
expect(code).toBe(0)
|
|
151
|
+
expect(rules).not.toContain('raw-size-literal')
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('does NOT flag a numeric size (the documented icon-dimension idiom)', () => {
|
|
155
|
+
const { code, rules } = run(`export const C = () => <ThemeIcon size={32} />\n`)
|
|
156
|
+
expect(code).toBe(0)
|
|
157
|
+
expect(rules).not.toContain('raw-size-literal')
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('does NOT flag with a same-line theme-allow comment', () => {
|
|
161
|
+
const { code, rules } = run(
|
|
162
|
+
`export const C = () => <Text fz="0.8rem" /* theme-allow: legacy */>a</Text>\n`,
|
|
163
|
+
)
|
|
164
|
+
expect(code).toBe(0)
|
|
165
|
+
expect(rules).not.toContain('raw-size-literal')
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
// F25: was the asymmetric half — no-raw-font-size already skipped test files, this rule did not.
|
|
169
|
+
it('does NOT flag a CSS-length string inside a *.test.tsx file', () => {
|
|
170
|
+
const { code, rules } = run(
|
|
171
|
+
`export const C = () => <Text fz="0.8rem">a</Text>\n`,
|
|
172
|
+
'lib.test.tsx',
|
|
173
|
+
)
|
|
174
|
+
expect(code).toBe(0)
|
|
175
|
+
expect(rules).not.toContain('raw-size-literal')
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
// A3: the /src/tokens/ exemption used to be a bare path-substring check, then a check gated on
|
|
179
|
+
// `isBasaltScopedFile` — but that predicate tests "does the nearest package.json depend on
|
|
180
|
+
// basalt-ui", not "is this file basalt's own", so a CONSUMER package that merely depends on
|
|
181
|
+
// basalt-ui could still put raw literals under its own `src/tokens/` and silence the rule. The
|
|
182
|
+
// exemption is deleted outright rather than re-gated on package identity: both rules only ever
|
|
183
|
+
// visit JSX, and basalt's real `src/tokens/**` is pure `.ts` data with none, so no gate was ever
|
|
184
|
+
// load-bearing there. A `src/tokens/` path — basalt's own or a consumer's — now always fires.
|
|
185
|
+
it('DOES flag a CSS-length string inside a src/tokens/ file (no path exemption)', () => {
|
|
186
|
+
const { code, rules } = run(
|
|
187
|
+
`export const C = () => <ThemeIcon size="2rem" />\n`,
|
|
188
|
+
'src/tokens/palette.tsx',
|
|
189
|
+
)
|
|
190
|
+
expect(code).toBe(1)
|
|
191
|
+
expect(rules).toContain('raw-size-literal')
|
|
192
|
+
})
|
|
114
193
|
})
|
|
115
194
|
|
|
116
195
|
// ── card-inset ───────────────────────────────────────────────────────────────
|
|
@@ -1296,6 +1375,30 @@ describe('PLUGIN_RULE_GRACE', () => {
|
|
|
1296
1375
|
it('passes the C16 gate against the real ledger and package version', () => {
|
|
1297
1376
|
expect(() => assertGraceLedger(Object.fromEntries(graceEntries), pkgVersion)).not.toThrow()
|
|
1298
1377
|
})
|
|
1378
|
+
|
|
1379
|
+
// F27: a grace entry is written for a rule that lands in the NEXT release, so its `since` can sit
|
|
1380
|
+
// one minor ahead of `package.json`'s current version (`bound-control-outside-home` at 1.28.0
|
|
1381
|
+
// while the published version still reads 1.27.0, exactly like `control-outside-home` before it).
|
|
1382
|
+
// What this catches is a `since` that is malformed or planted further out than that — a value
|
|
1383
|
+
// static analysis has no way to verify is "the version this actually ships in", so the bound this
|
|
1384
|
+
// test enforces is the one thing that IS checkable: well-formed semver, no more than one minor
|
|
1385
|
+
// past what is currently published.
|
|
1386
|
+
it("every grace entry's since is valid semver, at most one minor past the published version", () => {
|
|
1387
|
+
const SEMVER = /^\d+\.\d+\.\d+$/
|
|
1388
|
+
const [major, minor] = pkgVersion.split('.').map(Number)
|
|
1389
|
+
const nextMinorCeiling = `${major}.${minor + 1}.0`
|
|
1390
|
+
for (const [id, entry] of graceEntries) {
|
|
1391
|
+
if (!SEMVER.test(entry.since)) {
|
|
1392
|
+
throw new Error(`${id}: \`since\` (${entry.since}) is not valid semver.`)
|
|
1393
|
+
}
|
|
1394
|
+
if (compareSemver(entry.since, nextMinorCeiling) > 0) {
|
|
1395
|
+
throw new Error(
|
|
1396
|
+
`${id}: \`since\` (${entry.since}) is more than one minor past the published version ` +
|
|
1397
|
+
`${pkgVersion}.`,
|
|
1398
|
+
)
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
})
|
|
1299
1402
|
})
|
|
1300
1403
|
|
|
1301
1404
|
// ── chart-legend-literal: the .map() half ────────────────────────────────────
|
|
@@ -1681,6 +1784,28 @@ describe('basalt/control-outside-home', () => {
|
|
|
1681
1784
|
},
|
|
1682
1785
|
)
|
|
1683
1786
|
|
|
1787
|
+
// FormRow/FormGroup (`basalt-ui/forms`) are law C1's third home written for a `<form>` rather
|
|
1788
|
+
// than a settings page — the same non-home treatment SettingsRow gets, via the same
|
|
1789
|
+
// CONTROL_HOST_TAGS ancestry walk.
|
|
1790
|
+
it.each(['FormRow', 'FormGroup'])(
|
|
1791
|
+
'does NOT flag a raw Select nested inside a %s imported from basalt-ui/forms',
|
|
1792
|
+
(host) => {
|
|
1793
|
+
const { code, rules } = run(
|
|
1794
|
+
`import { ${host} } from 'basalt-ui/forms'\n${MANTINE_IMPORT}` +
|
|
1795
|
+
`export const C = () => <${host} label="L"><Select data={[]} /></${host}>\n`,
|
|
1796
|
+
)
|
|
1797
|
+
expect(code).toBe(0)
|
|
1798
|
+
expect(rules).not.toContain('control-outside-home')
|
|
1799
|
+
},
|
|
1800
|
+
)
|
|
1801
|
+
|
|
1802
|
+
it('still flags a bare Select in a page body with no FormRow/FormGroup around it', () => {
|
|
1803
|
+
const { rules } = run(
|
|
1804
|
+
`${MANTINE_IMPORT}export const C = () => <div><Select data={[]} /></div>\n`,
|
|
1805
|
+
)
|
|
1806
|
+
expect(rules).toContain('control-outside-home')
|
|
1807
|
+
})
|
|
1808
|
+
|
|
1684
1809
|
it('does NOT flag one under a member-expression non-home (Menu.Dropdown)', () => {
|
|
1685
1810
|
const { rules } = run(
|
|
1686
1811
|
`${MANTINE_IMPORT}export const C = () => <Menu.Dropdown><Select data={[]} /></Menu.Dropdown>\n`,
|
|
@@ -1708,6 +1833,58 @@ describe('basalt/control-outside-home', () => {
|
|
|
1708
1833
|
expect(rules).not.toContain('control-outside-home')
|
|
1709
1834
|
})
|
|
1710
1835
|
|
|
1836
|
+
/**
|
|
1837
|
+
* The exemption's SECOND half. `CONTROL_OWNER_NAMES` carries generic names (`PanelRow`,
|
|
1838
|
+
* `EnumFilter`, `SliderControl`), so a bare-name match let one local helper switch this rule —
|
|
1839
|
+
* and `bound-control-outside-home` and `responsive-twin` with it — off for a whole consumer file.
|
|
1840
|
+
* A file that imports `basalt-ui*` is CONSUMING basalt; basalt's own control sources import each
|
|
1841
|
+
* other relatively and never name the package.
|
|
1842
|
+
*/
|
|
1843
|
+
it('DOES flag a file that declares its own PanelRow but imports basalt-ui', () => {
|
|
1844
|
+
const { rules } = run(
|
|
1845
|
+
`import { Section } from 'basalt-ui'\n${MANTINE_IMPORT}` +
|
|
1846
|
+
`function PanelRow({ children }) {\n return <div>{children}</div>\n}\n` +
|
|
1847
|
+
`export const C = () => (\n <Section title="x">\n <Select value={v} onChange={set} data={[]} />\n </Section>\n)\n`,
|
|
1848
|
+
)
|
|
1849
|
+
expect(rules).toContain('control-outside-home')
|
|
1850
|
+
})
|
|
1851
|
+
|
|
1852
|
+
/**
|
|
1853
|
+
* The basalt SUBTREE homes, which this rule did not know: `bound-control-outside-home` has read
|
|
1854
|
+
* `FilterSet` / `PageAside` / `PanelRow` as homes since it shipped, so a `<Select>` inside a
|
|
1855
|
+
* `<PageAside><PanelRow>` reported while the bound control beside it did not — two rules
|
|
1856
|
+
* answering one question two ways.
|
|
1857
|
+
*/
|
|
1858
|
+
it('does NOT flag one inside a PageAside > PanelRow subtree', () => {
|
|
1859
|
+
const { code, rules } = run(
|
|
1860
|
+
`import { PageAside } from 'basalt-ui'\nimport { PanelRow } from 'basalt-ui/controls'\n${MANTINE_IMPORT}` +
|
|
1861
|
+
`export const C = () => (\n <PageAside title="Filters">\n <PanelRow label="Scale">\n <Select data={[]} />\n </PanelRow>\n </PageAside>\n)\n`,
|
|
1862
|
+
'src/scale-page.tsx',
|
|
1863
|
+
)
|
|
1864
|
+
expect(code).toBe(0)
|
|
1865
|
+
expect(rules).not.toContain('control-outside-home')
|
|
1866
|
+
})
|
|
1867
|
+
|
|
1868
|
+
it('does NOT flag one inside a FilterSet subtree', () => {
|
|
1869
|
+
const { rules } = run(
|
|
1870
|
+
`import { FilterSet } from 'basalt-ui/controls'\n${MANTINE_IMPORT}` +
|
|
1871
|
+
`export const C = () => (\n <FilterSet>\n <Select data={[]} />\n </FilterSet>\n)\n`,
|
|
1872
|
+
'src/scale-page.tsx',
|
|
1873
|
+
)
|
|
1874
|
+
expect(rules).not.toContain('control-outside-home')
|
|
1875
|
+
})
|
|
1876
|
+
|
|
1877
|
+
// Provenance, the test `SLOT_OWNER_TAGS` already applies: a consumer's own `PanelRow` is a layout
|
|
1878
|
+
// helper, not a basalt home, so it exempts nothing.
|
|
1879
|
+
it("still flags one inside a consumer's OWN PanelRow", () => {
|
|
1880
|
+
const { rules } = run(
|
|
1881
|
+
`import { PanelRow } from './panel-row'\n${MANTINE_IMPORT}` +
|
|
1882
|
+
`export const C = () => (\n <PanelRow label="Scale">\n <Select data={[]} />\n </PanelRow>\n)\n`,
|
|
1883
|
+
'src/scale-page.tsx',
|
|
1884
|
+
)
|
|
1885
|
+
expect(rules).toContain('control-outside-home')
|
|
1886
|
+
})
|
|
1887
|
+
|
|
1711
1888
|
it('flags an ALIASED Mantine import with no home', () => {
|
|
1712
1889
|
const { code, rules } = run(
|
|
1713
1890
|
`import { Select as MantineSelect } from '@mantine/core'\n` +
|
|
@@ -1731,6 +1908,292 @@ describe('basalt/control-outside-home', () => {
|
|
|
1731
1908
|
)
|
|
1732
1909
|
expect(rules).not.toContain('control-outside-home')
|
|
1733
1910
|
})
|
|
1911
|
+
|
|
1912
|
+
/**
|
|
1913
|
+
* The CROSS-FILE case, exempted by a naming convention rather than by ancestry — argo carried 9 of
|
|
1914
|
+
* these, each a control in a modal/form module whose `<Modal>` is rendered by the parent route.
|
|
1915
|
+
* Nothing in the flagged file can see the host, so the ancestry walk never could have.
|
|
1916
|
+
*/
|
|
1917
|
+
describe('the overlay filename convention', () => {
|
|
1918
|
+
const SOURCE = `${MANTINE_IMPORT}export const C = () => <div><Select data={[]} /></div>\n`
|
|
1919
|
+
|
|
1920
|
+
it.each([
|
|
1921
|
+
'src/edit-session-modal.tsx',
|
|
1922
|
+
'src/filters-drawer.tsx',
|
|
1923
|
+
'src/column-popover.tsx',
|
|
1924
|
+
'src/detail-panel.tsx',
|
|
1925
|
+
'src/booking-form.tsx',
|
|
1926
|
+
])('does NOT flag %s — its host lives in the parent', (filename) => {
|
|
1927
|
+
const { code, rules } = run(SOURCE, filename)
|
|
1928
|
+
expect(code).toBe(0)
|
|
1929
|
+
expect(rules).not.toContain('control-outside-home')
|
|
1930
|
+
})
|
|
1931
|
+
|
|
1932
|
+
it('still flags a plain page module beside them', () => {
|
|
1933
|
+
const { rules } = run(SOURCE, 'src/bookings-page.tsx')
|
|
1934
|
+
expect(rules).toContain('control-outside-home')
|
|
1935
|
+
})
|
|
1936
|
+
|
|
1937
|
+
// Basename only: a directory of modals holds the page pieces around them too, so a whole
|
|
1938
|
+
// `modal/` tree going silent is the version of this exemption that WOULD switch the rule off.
|
|
1939
|
+
it('is a BASENAME convention, not a directory one', () => {
|
|
1940
|
+
const { rules } = run(SOURCE, 'src/modal/session.tsx')
|
|
1941
|
+
expect(rules).toContain('control-outside-home')
|
|
1942
|
+
})
|
|
1943
|
+
|
|
1944
|
+
// `modal.tsx` with no leading segment is a page module in every consumer that has one; the
|
|
1945
|
+
// convention is `<subject>-modal.tsx`, and the regex says so.
|
|
1946
|
+
it('needs the leading subject — a bare modal.tsx is not the convention', () => {
|
|
1947
|
+
const { rules } = run(SOURCE, 'src/modal.tsx')
|
|
1948
|
+
expect(rules).toContain('control-outside-home')
|
|
1949
|
+
})
|
|
1950
|
+
|
|
1951
|
+
// The SECOND dialect: a repo mandating `PascalCase.tsx` for component files (basalt's own root
|
|
1952
|
+
// CLAUDE.md included) can never write `foo-panel.tsx`, so the kebab form alone exempted nothing
|
|
1953
|
+
// there — which is how the CBBI panel collected four warns for a `<PageAside>` in its parent.
|
|
1954
|
+
it.each(['src/EditSessionModal.tsx', 'src/FiltersDrawer.tsx', 'src/CbbiPanel.tsx'])(
|
|
1955
|
+
'does NOT flag %s — the PascalCase dialect of the same convention',
|
|
1956
|
+
(filename) => {
|
|
1957
|
+
const { code, rules } = run(SOURCE, filename)
|
|
1958
|
+
expect(code).toBe(0)
|
|
1959
|
+
expect(rules).not.toContain('control-outside-home')
|
|
1960
|
+
},
|
|
1961
|
+
)
|
|
1962
|
+
|
|
1963
|
+
/**
|
|
1964
|
+
* The trade, stated as a test rather than only as prose: a `FooPanel.tsx` that renders NO page
|
|
1965
|
+
* bar is exempt WHOLE-FILE, so a control that genuinely belongs in a bar goes unreported there.
|
|
1966
|
+
* Measured before keeping it — across argo, linewatch, image-share, rb and image-gen the
|
|
1967
|
+
* PascalCase dialect matches 9 files, all in image-gen, and NONE of them renders a `<PageBar>`
|
|
1968
|
+
* or sits under a `routes/` directory. Nine overlay bodies, zero pages, so the dialect stays
|
|
1969
|
+
* whole-file rather than growing a "does it render a page bar" predicate (CONTROLS-SPEC §6).
|
|
1970
|
+
*/
|
|
1971
|
+
it('exempts a FooPanel.tsx that renders no page bar — the measured trade', () => {
|
|
1972
|
+
const { code, rules } = run(SOURCE, 'src/components/refine/CropPanel.tsx')
|
|
1973
|
+
expect(code).toBe(0)
|
|
1974
|
+
expect(rules).not.toContain('control-outside-home')
|
|
1975
|
+
})
|
|
1976
|
+
|
|
1977
|
+
it('needs the leading subject in the PascalCase dialect too', () => {
|
|
1978
|
+
const { rules } = run(SOURCE, 'src/Panel.tsx')
|
|
1979
|
+
expect(rules).toContain('control-outside-home')
|
|
1980
|
+
})
|
|
1981
|
+
|
|
1982
|
+
it('is CASE-sensitive — foopanel.tsx is neither dialect', () => {
|
|
1983
|
+
const { rules } = run(SOURCE, 'src/foopanel.tsx')
|
|
1984
|
+
expect(rules).toContain('control-outside-home')
|
|
1985
|
+
})
|
|
1986
|
+
})
|
|
1987
|
+
})
|
|
1988
|
+
|
|
1989
|
+
/**
|
|
1990
|
+
* Ledger G5 (`docs/ASIDE-SPEC.md` §2) — the basalt half of law C1. Mirrors the
|
|
1991
|
+
* `control-outside-home` block above case for case, because the two rules answer the same question
|
|
1992
|
+
* about a different tag set: that one matches raw Mantine, this one matches a bound basalt control.
|
|
1993
|
+
*/
|
|
1994
|
+
describe('basalt/bound-control-outside-home', () => {
|
|
1995
|
+
const CONTROLS_IMPORT = `import { FilterSet, PanelRow, SelectFilter, SliderControl } from 'basalt-ui/controls'\n`
|
|
1996
|
+
const ASIDE_IMPORT = `import { PageAside, PageBar, Section } from 'basalt-ui'\n`
|
|
1997
|
+
|
|
1998
|
+
it("flags a bound control in a Section's BODY — the stray pill G5 named", () => {
|
|
1999
|
+
const { code, rules } = run(
|
|
2000
|
+
`${ASIDE_IMPORT}${CONTROLS_IMPORT}export const C = () => (\n <Section title="Composition">\n <SelectFilter field={f} />\n </Section>\n)\n`,
|
|
2001
|
+
)
|
|
2002
|
+
expect(code).toBe(1)
|
|
2003
|
+
expect(rules).toContain('bound-control-outside-home')
|
|
2004
|
+
})
|
|
2005
|
+
|
|
2006
|
+
it('flags one in a bare page Stack', () => {
|
|
2007
|
+
const { rules } = run(
|
|
2008
|
+
`import { Stack } from '@mantine/core'\n${CONTROLS_IMPORT}` +
|
|
2009
|
+
`export const C = () => (\n <Stack>\n <SelectFilter field={f} />\n </Stack>\n)\n`,
|
|
2010
|
+
)
|
|
2011
|
+
expect(rules).toContain('bound-control-outside-home')
|
|
2012
|
+
})
|
|
2013
|
+
|
|
2014
|
+
it('does NOT flag one in a PageBar filters slot', () => {
|
|
2015
|
+
const { code, rules } = run(
|
|
2016
|
+
`${ASIDE_IMPORT}${CONTROLS_IMPORT}export const C = () => <PageBar filters={<SelectFilter field={f} />} />\n`,
|
|
2017
|
+
)
|
|
2018
|
+
expect(code).toBe(0)
|
|
2019
|
+
expect(rules).not.toContain('bound-control-outside-home')
|
|
2020
|
+
})
|
|
2021
|
+
|
|
2022
|
+
it('does NOT flag one inside a FilterSet subtree', () => {
|
|
2023
|
+
const { code, rules } = run(
|
|
2024
|
+
`${CONTROLS_IMPORT}export const C = () => (\n <FilterSet>\n <SelectFilter field={f} />\n </FilterSet>\n)\n`,
|
|
2025
|
+
)
|
|
2026
|
+
expect(code).toBe(0)
|
|
2027
|
+
expect(rules).not.toContain('bound-control-outside-home')
|
|
2028
|
+
})
|
|
2029
|
+
|
|
2030
|
+
// The aside is a shell REGION whose body IS a home — it scopes its children to the `panel`
|
|
2031
|
+
// surface, where the same control renders as a row rather than a pill (docs/ASIDE-SPEC.md §3).
|
|
2032
|
+
it('does NOT flag one inside a PageAside subtree, Section and all', () => {
|
|
2033
|
+
const { code, rules } = run(
|
|
2034
|
+
`${ASIDE_IMPORT}${CONTROLS_IMPORT}export const C = () => (\n <PageAside title="Filters">\n <Section title="Origin">\n <SelectFilter field={f} />\n </Section>\n </PageAside>\n)\n`,
|
|
2035
|
+
)
|
|
2036
|
+
expect(code).toBe(0)
|
|
2037
|
+
expect(rules).not.toContain('bound-control-outside-home')
|
|
2038
|
+
})
|
|
2039
|
+
|
|
2040
|
+
it("does NOT flag one as a PanelRow's `end` — the attribute hangs off PanelRow itself", () => {
|
|
2041
|
+
const { code, rules } = run(
|
|
2042
|
+
`${CONTROLS_IMPORT}export const C = () => <PanelRow label="Weight" end={<SelectFilter field={f} />} />\n`,
|
|
2043
|
+
)
|
|
2044
|
+
expect(code).toBe(0)
|
|
2045
|
+
expect(rules).not.toContain('bound-control-outside-home')
|
|
2046
|
+
})
|
|
2047
|
+
|
|
2048
|
+
it("does NOT flag one in a PanelRow's children — the row IS the home", () => {
|
|
2049
|
+
const { rules } = run(
|
|
2050
|
+
`${CONTROLS_IMPORT}export const C = () => (\n <PanelRow label="Origin">\n <SelectFilter field={f} />\n </PanelRow>\n)\n`,
|
|
2051
|
+
)
|
|
2052
|
+
expect(rules).not.toContain('bound-control-outside-home')
|
|
2053
|
+
})
|
|
2054
|
+
|
|
2055
|
+
it('does NOT flag one inside an overlay host (Modal)', () => {
|
|
2056
|
+
const { rules } = run(
|
|
2057
|
+
`import { Modal } from '@mantine/core'\n${CONTROLS_IMPORT}` +
|
|
2058
|
+
`export const C = () => (\n <Modal opened>\n <SelectFilter field={f} />\n </Modal>\n)\n`,
|
|
2059
|
+
)
|
|
2060
|
+
expect(rules).not.toContain('bound-control-outside-home')
|
|
2061
|
+
})
|
|
2062
|
+
|
|
2063
|
+
// FormRow/FormGroup are the same declared non-home as SettingsRow — shared CONTROL_HOST_TAGS walk.
|
|
2064
|
+
it.each(['FormRow', 'FormGroup'])('does NOT flag one inside a %s', (host) => {
|
|
2065
|
+
const { code, rules } = run(
|
|
2066
|
+
`import { ${host} } from 'basalt-ui/forms'\n${CONTROLS_IMPORT}` +
|
|
2067
|
+
`export const C = () => <${host} label="L"><SelectFilter field={f} /></${host}>\n`,
|
|
2068
|
+
)
|
|
2069
|
+
expect(code).toBe(0)
|
|
2070
|
+
expect(rules).not.toContain('bound-control-outside-home')
|
|
2071
|
+
})
|
|
2072
|
+
|
|
2073
|
+
it.each(['src/cbbi-panel.tsx', 'src/CbbiPanel.tsx'])(
|
|
2074
|
+
'does NOT flag %s — a panel body whose host lives in the parent, in either dialect',
|
|
2075
|
+
(filename) => {
|
|
2076
|
+
const { code, rules } = run(
|
|
2077
|
+
`${CONTROLS_IMPORT}export const C = () => <div><SelectFilter field={f} /></div>\n`,
|
|
2078
|
+
filename,
|
|
2079
|
+
)
|
|
2080
|
+
expect(code).toBe(0)
|
|
2081
|
+
expect(rules).not.toContain('bound-control-outside-home')
|
|
2082
|
+
},
|
|
2083
|
+
)
|
|
2084
|
+
|
|
2085
|
+
// Both dialects need the leading SUBJECT, and neither is case-insensitive — `foopanel.tsx` is a
|
|
2086
|
+
// page module spelled without a separator, not a declaration.
|
|
2087
|
+
it.each(['src/foopanel.tsx', 'src/Panel.tsx'])('still flags %s', (filename) => {
|
|
2088
|
+
const { rules } = run(
|
|
2089
|
+
`${CONTROLS_IMPORT}export const C = () => <div><SelectFilter field={f} /></div>\n`,
|
|
2090
|
+
filename,
|
|
2091
|
+
)
|
|
2092
|
+
expect(rules).toContain('bound-control-outside-home')
|
|
2093
|
+
})
|
|
2094
|
+
|
|
2095
|
+
it('honors a scoped theme-allow', () => {
|
|
2096
|
+
const { rules } = run(
|
|
2097
|
+
`${CONTROLS_IMPORT}export const C = () => (\n <div>\n {/* theme-allow bound-control-outside-home — a one-off embedded picker */}\n <SelectFilter field={f} />\n </div>\n)\n`,
|
|
2098
|
+
)
|
|
2099
|
+
expect(rules).not.toContain('bound-control-outside-home')
|
|
2100
|
+
})
|
|
2101
|
+
|
|
2102
|
+
it('honors a theme-allow-file declaration', () => {
|
|
2103
|
+
const { rules } = run(
|
|
2104
|
+
`// theme-allow-file bound-control-outside-home — a legacy page, migrating next sprint\n` +
|
|
2105
|
+
`${CONTROLS_IMPORT}export const C = () => <div><SelectFilter field={f} /></div>\n`,
|
|
2106
|
+
)
|
|
2107
|
+
expect(rules).not.toContain('bound-control-outside-home')
|
|
2108
|
+
})
|
|
2109
|
+
|
|
2110
|
+
// Provenance, the same test the raw-filter rules apply the other way round: a consumer's own
|
|
2111
|
+
// `SelectFilter` is not basalt's, and basalt has no claim over where it is written.
|
|
2112
|
+
it('does NOT flag a locally-defined SelectFilter', () => {
|
|
2113
|
+
const { code, rules } = run(
|
|
2114
|
+
`import { SelectFilter } from './my-filters'\n` +
|
|
2115
|
+
`export const C = () => <div><SelectFilter field={f} /></div>\n`,
|
|
2116
|
+
)
|
|
2117
|
+
expect(code).toBe(0)
|
|
2118
|
+
expect(rules).not.toContain('bound-control-outside-home')
|
|
2119
|
+
})
|
|
2120
|
+
|
|
2121
|
+
it('flags an ALIASED basalt import with no home', () => {
|
|
2122
|
+
const { rules } = run(
|
|
2123
|
+
`import { SelectFilter as Picker } from 'basalt-ui/controls'\n` +
|
|
2124
|
+
`export const C = () => <div><Picker field={f} /></div>\n`,
|
|
2125
|
+
)
|
|
2126
|
+
expect(rules).toContain('bound-control-outside-home')
|
|
2127
|
+
})
|
|
2128
|
+
|
|
2129
|
+
/**
|
|
2130
|
+
* The owner exemption needs the file to import NOTHING from `basalt-ui*` — see
|
|
2131
|
+
* `control-outside-home`'s copy for why. For THIS rule that makes the exemption reachable only
|
|
2132
|
+
* inside basalt's own `src/`, which is exactly where it is meant to apply: provenance already
|
|
2133
|
+
* requires a basalt import (or a relative one inside that tree), so a consumer file can never be
|
|
2134
|
+
* both the owner and a place this rule could fire.
|
|
2135
|
+
*/
|
|
2136
|
+
it('DOES flag a consumer file that declares its own PanelRow', () => {
|
|
2137
|
+
const { rules } = run(
|
|
2138
|
+
`${CONTROLS_IMPORT}function PanelRow({ children }) {\n return <div>{children}</div>\n}\n` +
|
|
2139
|
+
`export const C = () => <div><SelectFilter field={f} /></div>\n`,
|
|
2140
|
+
)
|
|
2141
|
+
expect(rules).toContain('bound-control-outside-home')
|
|
2142
|
+
})
|
|
2143
|
+
|
|
2144
|
+
// Provenance on the HOME too, the same test `control-outside-home` applies: a consumer's own
|
|
2145
|
+
// `PanelRow` is a layout helper, not the aside's row primitive.
|
|
2146
|
+
it("still flags one inside a consumer's OWN PanelRow", () => {
|
|
2147
|
+
const { rules } = run(
|
|
2148
|
+
`import { SelectFilter } from 'basalt-ui/controls'\nimport { PanelRow } from './panel-row'\n` +
|
|
2149
|
+
`export const C = () => (\n <PanelRow label="Scale">\n <SelectFilter field={f} />\n </PanelRow>\n)\n`,
|
|
2150
|
+
)
|
|
2151
|
+
expect(rules).toContain('bound-control-outside-home')
|
|
2152
|
+
})
|
|
2153
|
+
|
|
2154
|
+
/**
|
|
2155
|
+
* `SliderControl` is NOT policed: it renders its own `PanelRow` and has no pill form at all
|
|
2156
|
+
* (`docs/ASIDE-SPEC.md` §3), so "this renders as a stray pill" is false for it and a `Section`
|
|
2157
|
+
* body is a legitimate place to write one. The plugin has no per-tag home set, so the honest fix
|
|
2158
|
+
* is dropping it from the policed set rather than giving it a private exemption.
|
|
2159
|
+
*/
|
|
2160
|
+
it('does NOT flag a SliderControl in a Section body — it has no pill form', () => {
|
|
2161
|
+
const { code, rules } = run(
|
|
2162
|
+
`${ASIDE_IMPORT}${CONTROLS_IMPORT}export const C = () => (\n <Section title="Weights">\n <SliderControl field={f} label="Pi" />\n </Section>\n)\n`,
|
|
2163
|
+
)
|
|
2164
|
+
expect(code).toBe(0)
|
|
2165
|
+
expect(rules).not.toContain('bound-control-outside-home')
|
|
2166
|
+
})
|
|
2167
|
+
|
|
2168
|
+
it('resolves a hoisted binding handed to a slot, like its siblings do', () => {
|
|
2169
|
+
const { rules } = run(
|
|
2170
|
+
`${ASIDE_IMPORT}${CONTROLS_IMPORT}const pills = <SelectFilter field={f} />\n` +
|
|
2171
|
+
`export const C = () => <PageBar filters={pills} />\n`,
|
|
2172
|
+
)
|
|
2173
|
+
expect(rules).not.toContain('bound-control-outside-home')
|
|
2174
|
+
})
|
|
2175
|
+
|
|
2176
|
+
/**
|
|
2177
|
+
* …and one handed to a subtree home as its CHILDREN, which the ancestry walk cannot see: at the
|
|
2178
|
+
* `const` there is no `PageAside` above the node at all. The same `Program:exit` deferral the
|
|
2179
|
+
* slot lane uses, over the home's `{expr}` children instead of its attributes.
|
|
2180
|
+
*/
|
|
2181
|
+
it('resolves a hoisted binding rendered as a PageAside CHILD', () => {
|
|
2182
|
+
const { code, rules } = run(
|
|
2183
|
+
`${ASIDE_IMPORT}${CONTROLS_IMPORT}const rows = <SelectFilter field={f} />\n` +
|
|
2184
|
+
`export const C = () => <PageAside title="Filters">{rows}</PageAside>\n`,
|
|
2185
|
+
)
|
|
2186
|
+
expect(code).toBe(0)
|
|
2187
|
+
expect(rules).not.toContain('bound-control-outside-home')
|
|
2188
|
+
})
|
|
2189
|
+
|
|
2190
|
+
it('still flags the same hoisted binding under a bare Stack', () => {
|
|
2191
|
+
const { rules } = run(
|
|
2192
|
+
`import { Stack } from '@mantine/core'\n${CONTROLS_IMPORT}const rows = <SelectFilter field={f} />\n` +
|
|
2193
|
+
`export const C = () => <Stack>{rows}</Stack>\n`,
|
|
2194
|
+
)
|
|
2195
|
+
expect(rules).toContain('bound-control-outside-home')
|
|
2196
|
+
})
|
|
1734
2197
|
})
|
|
1735
2198
|
|
|
1736
2199
|
describe('basalt/control-size-literal', () => {
|
|
@@ -1761,6 +2224,82 @@ describe('basalt/control-size-literal', () => {
|
|
|
1761
2224
|
expect(rules).not.toContain('control-size-literal')
|
|
1762
2225
|
})
|
|
1763
2226
|
|
|
2227
|
+
/**
|
|
2228
|
+
* `ChartCard` is the one slot owner whose slot cannot mount the tier theme — it lives inside the
|
|
2229
|
+
* Mantine-free chart layer, so it writes `data-basalt-tier` by hand and the control there really
|
|
2230
|
+
* does have to state its own size. The rule fires on the `size` ATTRIBUTE and cannot tell a
|
|
2231
|
+
* correct `ctl` from the `xs` it exists to catch, so this is an exemption, not a waiver.
|
|
2232
|
+
*/
|
|
2233
|
+
describe('ChartCard.actions — the one slot with no tier theme', () => {
|
|
2234
|
+
it('does NOT flag a size prop there', () => {
|
|
2235
|
+
const { code, rules } = run(
|
|
2236
|
+
`${BASALT_IMPORT}${MANTINE_IMPORT}export const C = () => <ChartCard title="T" actions={<Button size="ctl">Go</Button>} />
|
|
2237
|
+
`,
|
|
2238
|
+
)
|
|
2239
|
+
expect(code).toBe(0)
|
|
2240
|
+
expect(rules).not.toContain('control-size-literal')
|
|
2241
|
+
})
|
|
2242
|
+
|
|
2243
|
+
// The owner test reads the IMPORTED name, so the alias every wrapper writes still resolves.
|
|
2244
|
+
it('does NOT flag it under an aliased ChartCard import', () => {
|
|
2245
|
+
const { rules } = run(
|
|
2246
|
+
`import { ChartCard as Card } from 'basalt-ui'
|
|
2247
|
+
${MANTINE_IMPORT}` +
|
|
2248
|
+
`export const C = () => <Card title="T" actions={<Button size="xs">Go</Button>} />
|
|
2249
|
+
`,
|
|
2250
|
+
)
|
|
2251
|
+
expect(rules).not.toContain('control-size-literal')
|
|
2252
|
+
})
|
|
2253
|
+
|
|
2254
|
+
// The positive control: every OTHER home does mount the theme, so the rule still fires there.
|
|
2255
|
+
it('STILL flags the same prop in a Section slot', () => {
|
|
2256
|
+
const { code, rules } = run(
|
|
2257
|
+
`${BASALT_IMPORT}${MANTINE_IMPORT}export const C = () => <Section title="T" actions={<Button size="xs">Go</Button>} />
|
|
2258
|
+
`,
|
|
2259
|
+
)
|
|
2260
|
+
expect(code).toBe(1)
|
|
2261
|
+
expect(rules).toContain('control-size-literal')
|
|
2262
|
+
})
|
|
2263
|
+
|
|
2264
|
+
// The exemption is keyed on the OWNER, and one hoisted binding can be handed to several — so it
|
|
2265
|
+
// holds only when EVERY home it reached is tierless. A last-writer-wins owner map made the
|
|
2266
|
+
// verdict depend on which attribute came later in the file: same code, opposite answer.
|
|
2267
|
+
it('STILL flags a hoisted binding shared with a Section slot — ChartCard last', () => {
|
|
2268
|
+
const { rules } = run(
|
|
2269
|
+
`${BASALT_IMPORT}${MANTINE_IMPORT}const acts = <Button size="xs">Go</Button>\n` +
|
|
2270
|
+
`export const C = () => (\n <>\n <Section title="S" actions={acts} />\n <ChartCard title="T" actions={acts} />\n </>\n)\n`,
|
|
2271
|
+
)
|
|
2272
|
+
expect(rules).toContain('control-size-literal')
|
|
2273
|
+
})
|
|
2274
|
+
|
|
2275
|
+
it('STILL flags a hoisted binding shared with a Section slot — ChartCard first', () => {
|
|
2276
|
+
const { rules } = run(
|
|
2277
|
+
`${BASALT_IMPORT}${MANTINE_IMPORT}const acts = <Button size="xs">Go</Button>\n` +
|
|
2278
|
+
`export const C = () => (\n <>\n <ChartCard title="T" actions={acts} />\n <Section title="S" actions={acts} />\n </>\n)\n`,
|
|
2279
|
+
)
|
|
2280
|
+
expect(rules).toContain('control-size-literal')
|
|
2281
|
+
})
|
|
2282
|
+
|
|
2283
|
+
it('does NOT flag a hoisted binding handed only to ChartCard', () => {
|
|
2284
|
+
const { code, rules } = run(
|
|
2285
|
+
`${BASALT_IMPORT}${MANTINE_IMPORT}const acts = <Button size="ctl">Go</Button>\n` +
|
|
2286
|
+
`export const C = () => <ChartCard title="T" actions={acts} />\n`,
|
|
2287
|
+
)
|
|
2288
|
+
expect(code).toBe(0)
|
|
2289
|
+
expect(rules).not.toContain('control-size-literal')
|
|
2290
|
+
})
|
|
2291
|
+
|
|
2292
|
+
// A consumer's OWN `ChartCard` is not basalt's, so it is not a home at all and nothing fires —
|
|
2293
|
+
// the same provenance rule every other home tag follows.
|
|
2294
|
+
it('hand-rolled-filter is NOT exempted there — the tier is not what makes a filter wrong', () => {
|
|
2295
|
+
const { rules } = run(
|
|
2296
|
+
`${BASALT_IMPORT}${MANTINE_IMPORT}export const C = () => <ChartCard title="T" actions={<Select data={[]} />} />
|
|
2297
|
+
`,
|
|
2298
|
+
)
|
|
2299
|
+
expect(rules).toContain('hand-rolled-filter')
|
|
2300
|
+
})
|
|
2301
|
+
})
|
|
2302
|
+
|
|
1764
2303
|
it('does NOT flag a size prop on the HOME itself', () => {
|
|
1765
2304
|
const { code, rules } = run(
|
|
1766
2305
|
`${BASALT_IMPORT}${MANTINE_IMPORT}export const C = () => <PageBar w={200} actions={<Button>Go</Button>} />\n`,
|
|
@@ -2043,6 +2582,17 @@ describe('basalt/responsive-twin', () => {
|
|
|
2043
2582
|
expect(rules).not.toContain('responsive-twin')
|
|
2044
2583
|
})
|
|
2045
2584
|
|
|
2585
|
+
// The owner exemption's second half, the same predicate its two siblings apply: a consumer file
|
|
2586
|
+
// that names a helper `PanelRow` and imports basalt is consuming basalt, not defining a control.
|
|
2587
|
+
it('DOES flag a file that declares its own PanelRow but imports basalt-ui', () => {
|
|
2588
|
+
const { rules } = run(
|
|
2589
|
+
`import { Section } from 'basalt-ui'\n` +
|
|
2590
|
+
`function PanelRow({ children }) {\n return <div>{children}</div>\n}\n` +
|
|
2591
|
+
`export const C = () => (\n <div>\n <SegmentedControl visibleFrom="sm" data={[]} />\n <SegmentedControl hiddenFrom="sm" data={[]} />\n </div>\n)\n`,
|
|
2592
|
+
)
|
|
2593
|
+
expect(rules).toContain('responsive-twin')
|
|
2594
|
+
})
|
|
2595
|
+
|
|
2046
2596
|
it('honors a scoped theme-allow', () => {
|
|
2047
2597
|
const { rules } = run(
|
|
2048
2598
|
`export const C = () => (\n <div>\n <SegmentedControl visibleFrom="sm" data={[]} />\n {/* theme-allow responsive-twin — two genuinely different data sets */}\n <SegmentedControl hiddenFrom="sm" data={[]} />\n </div>\n)\n`,
|
|
@@ -2232,42 +2782,801 @@ describe('basalt/shadow-basalt-export — SHADOW_ALIASES', () => {
|
|
|
2232
2782
|
})
|
|
2233
2783
|
})
|
|
2234
2784
|
|
|
2235
|
-
|
|
2785
|
+
/**
|
|
2786
|
+
* The provenance half of the alias table: a file that COMPOSES the export it renames is a wrapper,
|
|
2787
|
+
* not a fork. argo's three hero cards each imported `StatCard`, wrapped it, and each took a warn
|
|
2788
|
+
* telling them to import the thing they were already importing.
|
|
2789
|
+
*
|
|
2790
|
+
* Both directions are asserted, because the exemption is only worth having if the rule still fires
|
|
2791
|
+
* WITHOUT the import — a wrapper is evidence, and silence on every `HeroCard` in the repo would be
|
|
2792
|
+
* the rule switching itself off.
|
|
2793
|
+
*/
|
|
2794
|
+
describe('basalt/shadow-basalt-export — an alias that composes what it renames', () => {
|
|
2795
|
+
it('fires on a HeroCard that does not import StatCard', () => {
|
|
2796
|
+
const { rules, output } = run(
|
|
2797
|
+
`export function HeroCard() {\n return null\n}\n`,
|
|
2798
|
+
'src/hero-card.tsx',
|
|
2799
|
+
)
|
|
2800
|
+
expect(rules).toContain('shadow-basalt-export')
|
|
2801
|
+
expect(output).toContain('StatCard')
|
|
2802
|
+
})
|
|
2236
2803
|
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2804
|
+
it('is silent on a HeroCard that composes the imported StatCard', () => {
|
|
2805
|
+
const { code, rules } = run(
|
|
2806
|
+
`import { StatCard } from 'basalt-ui'\n` +
|
|
2807
|
+
`export function HeroCard() {\n return <StatCard title="x" value="1" />\n}\n`,
|
|
2808
|
+
'src/hero-card.tsx',
|
|
2809
|
+
)
|
|
2810
|
+
expect(code).toBe(0)
|
|
2811
|
+
expect(rules).not.toContain('shadow-basalt-export')
|
|
2243
2812
|
})
|
|
2244
2813
|
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
// catches a list entry deleted without its surface).
|
|
2256
|
-
for (const id of owners.keys()) {
|
|
2257
|
-
expect([id, (PLUGIN_RULE_ID_LIST as readonly string[]).includes(id)]).toEqual([id, true])
|
|
2258
|
-
}
|
|
2814
|
+
// The provenance test reads the IMPORTED name, not the local one — `StatCard as BaseStatCard` is
|
|
2815
|
+
// the canonical way a wrapper avoids shadowing its own import, and a local-name set would answer
|
|
2816
|
+
// `BaseStatCard` and miss it.
|
|
2817
|
+
it('is silent when the composed export is imported under an alias', () => {
|
|
2818
|
+
const { rules } = run(
|
|
2819
|
+
`import { StatCard as BaseStatCard } from 'basalt-ui'\n` +
|
|
2820
|
+
`export function HeroCard() {\n return <BaseStatCard title="x" value="1" />\n}\n`,
|
|
2821
|
+
'src/hero-card.tsx',
|
|
2822
|
+
)
|
|
2823
|
+
expect(rules).not.toContain('shadow-basalt-export')
|
|
2259
2824
|
})
|
|
2260
2825
|
|
|
2261
|
-
it(
|
|
2262
|
-
|
|
2826
|
+
it('is silent from a basalt SUBPATH too, not just the root barrel', () => {
|
|
2827
|
+
const { rules } = run(
|
|
2828
|
+
`import { RangeFilter } from 'basalt-ui/controls'\n` +
|
|
2829
|
+
`export function WindowSelector() {\n return <RangeFilter field={{}} />\n}\n`,
|
|
2830
|
+
'src/window-selector.tsx',
|
|
2831
|
+
)
|
|
2832
|
+
expect(rules).not.toContain('shadow-basalt-export')
|
|
2263
2833
|
})
|
|
2264
2834
|
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
'
|
|
2269
|
-
|
|
2270
|
-
'
|
|
2835
|
+
// Importing SOMETHING from basalt is not the exemption — importing the thing you renamed is.
|
|
2836
|
+
it('still fires when the file imports a DIFFERENT basalt export', () => {
|
|
2837
|
+
const { rules } = run(
|
|
2838
|
+
`import { Section } from 'basalt-ui'\n` +
|
|
2839
|
+
`export function HeroCard() {\n return <Section title="x" />\n}\n`,
|
|
2840
|
+
'src/hero-card.tsx',
|
|
2841
|
+
)
|
|
2842
|
+
expect(rules).toContain('shadow-basalt-export')
|
|
2843
|
+
})
|
|
2844
|
+
|
|
2845
|
+
// The COLLISION half is deliberately not exempted: a local `StatCard` beside an
|
|
2846
|
+
// `import { StatCard as Base }` kept the name AND a piece of the original, which is the fork
|
|
2847
|
+
// shape this rule most wants to see.
|
|
2848
|
+
it('still fires on a name COLLISION even when the export is imported under an alias', () => {
|
|
2849
|
+
const { rules } = run(
|
|
2850
|
+
`import { StatCard as Base } from 'basalt-ui'\n` +
|
|
2851
|
+
`export function StatCard() {\n return <Base title="x" value="1" />\n}\n`,
|
|
2852
|
+
'src/stat-card.tsx',
|
|
2853
|
+
)
|
|
2854
|
+
expect(rules).toContain('shadow-basalt-export')
|
|
2855
|
+
})
|
|
2856
|
+
|
|
2857
|
+
// COMPOSITION, not import. A type-only import is erased at compile time, so the shape a
|
|
2858
|
+
// props-copying fork actually writes — `ComponentProps<typeof StatCard>` off an
|
|
2859
|
+
// `import type` — composes nothing and used to exempt the whole file.
|
|
2860
|
+
it('still fires on a type-only import of the export it renames', () => {
|
|
2861
|
+
const { rules } = run(
|
|
2862
|
+
`import type { StatCard } from 'basalt-ui'\n` +
|
|
2863
|
+
`export function HeroCard() {\n return <div />\n}\n`,
|
|
2864
|
+
'src/hero-card.tsx',
|
|
2865
|
+
)
|
|
2866
|
+
expect(rules).toContain('shadow-basalt-export')
|
|
2867
|
+
})
|
|
2868
|
+
|
|
2869
|
+
it('still fires on an inline `type` specifier', () => {
|
|
2870
|
+
const { rules } = run(
|
|
2871
|
+
`import { type StatCard } from 'basalt-ui'\n` +
|
|
2872
|
+
`export function HeroCard() {\n return <div />\n}\n`,
|
|
2873
|
+
'src/hero-card.tsx',
|
|
2874
|
+
)
|
|
2875
|
+
expect(rules).toContain('shadow-basalt-export')
|
|
2876
|
+
})
|
|
2877
|
+
|
|
2878
|
+
// `typeof StatCard` is a `TSTypeQuery` around a plain Identifier, so a VALUE import used only in
|
|
2879
|
+
// a type position has to be rejected too — otherwise the props-copying fork just drops `type`.
|
|
2880
|
+
it('still fires when a value import is only used in a type position', () => {
|
|
2881
|
+
const { rules } = run(
|
|
2882
|
+
`import { StatCard } from 'basalt-ui'\n` +
|
|
2883
|
+
`type P = { card: typeof StatCard }\n` +
|
|
2884
|
+
`export function HeroCard(props: P) {\n return <div>{String(props)}</div>\n}\n`,
|
|
2885
|
+
'src/hero-card.tsx',
|
|
2886
|
+
)
|
|
2887
|
+
expect(rules).toContain('shadow-basalt-export')
|
|
2888
|
+
})
|
|
2889
|
+
|
|
2890
|
+
it('still fires on a dead import left behind after the body was re-rolled', () => {
|
|
2891
|
+
const { rules } = run(
|
|
2892
|
+
`import { StatCard } from 'basalt-ui'\n` +
|
|
2893
|
+
`export function HeroCard() {\n return <div />\n}\n`,
|
|
2894
|
+
'src/hero-card.tsx',
|
|
2895
|
+
)
|
|
2896
|
+
expect(rules).toContain('shadow-basalt-export')
|
|
2897
|
+
})
|
|
2898
|
+
|
|
2899
|
+
// A JSX tag is not the only way to compose: a wrapper that hands the component on as a prop or to
|
|
2900
|
+
// `createElement` composes it just as much, and a JSX-only test would report both.
|
|
2901
|
+
it('is silent when the import is passed on as a prop rather than rendered', () => {
|
|
2902
|
+
const { code, rules } = run(
|
|
2903
|
+
`import { StatCard } from 'basalt-ui'\n` +
|
|
2904
|
+
`export function HeroCard() {\n return <Slot component={StatCard} />\n}\n`,
|
|
2905
|
+
'src/hero-card.tsx',
|
|
2906
|
+
)
|
|
2907
|
+
expect(code).toBe(0)
|
|
2908
|
+
expect(rules).not.toContain('shadow-basalt-export')
|
|
2909
|
+
})
|
|
2910
|
+
})
|
|
2911
|
+
|
|
2912
|
+
// ── SURFACES ↔ the plugin registry ───────────────────────────────────────────
|
|
2913
|
+
|
|
2914
|
+
describe('PLUGIN_RULE_ID_LIST ↔ the plugin', () => {
|
|
2915
|
+
// surfaces.ts cannot import the plugin (it is dependency-free by contract, and the plugin is a
|
|
2916
|
+
// standalone .js loaded out of a consumer's node_modules), so the list is hand-maintained and
|
|
2917
|
+
// THIS is the gate that keeps it honest.
|
|
2918
|
+
it('is exactly the plugin s registered rule ids', () => {
|
|
2919
|
+
expect([...PLUGIN_RULE_ID_LIST].toSorted()).toEqual(Object.keys(basaltPlugin.rules).toSorted())
|
|
2920
|
+
})
|
|
2921
|
+
|
|
2922
|
+
it('maps every plugin rule to exactly one surface', () => {
|
|
2923
|
+
const owners = new Map<string, string[]>()
|
|
2924
|
+
for (const [key, spec] of Object.entries(SURFACES)) {
|
|
2925
|
+
if (spec.kind !== 'doctrine') continue
|
|
2926
|
+
for (const id of spec.pluginRules) owners.set(id, [...(owners.get(id) ?? []), key])
|
|
2927
|
+
}
|
|
2928
|
+
for (const id of PLUGIN_RULE_ID_LIST) {
|
|
2929
|
+
expect([id, owners.get(id)?.length ?? 0]).toEqual([id, 1])
|
|
2930
|
+
}
|
|
2931
|
+
// …and no surface claims an id the plugin does not register (the type already forbids it; this
|
|
2932
|
+
// catches a list entry deleted without its surface).
|
|
2933
|
+
for (const id of owners.keys()) {
|
|
2934
|
+
expect([id, (PLUGIN_RULE_ID_LIST as readonly string[]).includes(id)]).toEqual([id, true])
|
|
2935
|
+
}
|
|
2936
|
+
})
|
|
2937
|
+
|
|
2938
|
+
it("puts the shell rule on the root barrel's surface (wave 6)", () => {
|
|
2939
|
+
expect(SURFACES['.'].pluginRules).toContain('hand-rolled-shell')
|
|
2940
|
+
})
|
|
2941
|
+
|
|
2942
|
+
it('gives ./controls the control-tier rules', () => {
|
|
2943
|
+
expect([...SURFACES['./controls'].pluginRules].toSorted()).toEqual([
|
|
2944
|
+
'bound-control-outside-home',
|
|
2945
|
+
'control-outside-home',
|
|
2946
|
+
'control-size-literal',
|
|
2947
|
+
'hand-rolled-filter',
|
|
2948
|
+
'responsive-twin',
|
|
2271
2949
|
])
|
|
2272
2950
|
})
|
|
2273
2951
|
})
|
|
2952
|
+
|
|
2953
|
+
// ── provider-above-router (F5) ───────────────────────────────────────────────
|
|
2954
|
+
|
|
2955
|
+
describe('basalt/provider-above-router', () => {
|
|
2956
|
+
const IMPORTS =
|
|
2957
|
+
`import { BasaltProvider } from 'basalt-ui'\n` +
|
|
2958
|
+
`import { RouterProvider } from '@tanstack/react-router'\n`
|
|
2959
|
+
|
|
2960
|
+
it('flags a BasaltProvider rendered inside a RouterProvider', () => {
|
|
2961
|
+
const { code, rules } = run(
|
|
2962
|
+
`${IMPORTS}export const App = () => (\n` +
|
|
2963
|
+
` <RouterProvider router={r}>\n <BasaltProvider>{null}</BasaltProvider>\n` +
|
|
2964
|
+
` </RouterProvider>\n)\n`,
|
|
2965
|
+
)
|
|
2966
|
+
expect(code).toBe(1)
|
|
2967
|
+
expect(rules).toContain('provider-above-router')
|
|
2968
|
+
})
|
|
2969
|
+
|
|
2970
|
+
it('does NOT flag the correct order — provider above router', () => {
|
|
2971
|
+
const { code, rules } = run(
|
|
2972
|
+
`${IMPORTS}export const App = () => (\n` +
|
|
2973
|
+
` <BasaltProvider>\n <RouterProvider router={r} />\n </BasaltProvider>\n)\n`,
|
|
2974
|
+
)
|
|
2975
|
+
expect(code).toBe(0)
|
|
2976
|
+
expect(rules).not.toContain('provider-above-router')
|
|
2977
|
+
})
|
|
2978
|
+
|
|
2979
|
+
// Provenance both ways: the tag names alone are not the finding.
|
|
2980
|
+
it("does NOT flag a consumer's own BasaltProvider (no basalt import)", () => {
|
|
2981
|
+
const { code, rules } = run(
|
|
2982
|
+
`import { RouterProvider } from '@tanstack/react-router'\n` +
|
|
2983
|
+
`import { BasaltProvider } from './my-provider'\n` +
|
|
2984
|
+
`export const App = () => (\n <RouterProvider router={r}>\n` +
|
|
2985
|
+
` <BasaltProvider>{null}</BasaltProvider>\n </RouterProvider>\n)\n`,
|
|
2986
|
+
)
|
|
2987
|
+
expect(code).toBe(0)
|
|
2988
|
+
expect(rules).not.toContain('provider-above-router')
|
|
2989
|
+
})
|
|
2990
|
+
|
|
2991
|
+
it('does NOT flag a RouterProvider that is not TanStack s', () => {
|
|
2992
|
+
const { code, rules } = run(
|
|
2993
|
+
`import { BasaltProvider } from 'basalt-ui'\n` +
|
|
2994
|
+
`import { RouterProvider } from 'other-router'\n` +
|
|
2995
|
+
`export const App = () => (\n <RouterProvider router={r}>\n` +
|
|
2996
|
+
` <BasaltProvider>{null}</BasaltProvider>\n </RouterProvider>\n)\n`,
|
|
2997
|
+
)
|
|
2998
|
+
expect(code).toBe(0)
|
|
2999
|
+
expect(rules).not.toContain('provider-above-router')
|
|
3000
|
+
})
|
|
3001
|
+
|
|
3002
|
+
// The alias is why the ancestry is read by BINDING and not by written tag name.
|
|
3003
|
+
it('flags through an aliased RouterProvider import', () => {
|
|
3004
|
+
const { code, rules } = run(
|
|
3005
|
+
`import { BasaltProvider } from 'basalt-ui'\n` +
|
|
3006
|
+
`import { RouterProvider as Router } from '@tanstack/react-router'\n` +
|
|
3007
|
+
`export const App = () => (\n <Router router={r}>\n` +
|
|
3008
|
+
` <BasaltProvider>{null}</BasaltProvider>\n </Router>\n)\n`,
|
|
3009
|
+
)
|
|
3010
|
+
expect(code).toBe(1)
|
|
3011
|
+
expect(rules).toContain('provider-above-router')
|
|
3012
|
+
})
|
|
3013
|
+
|
|
3014
|
+
it('honours a theme-allow naming the rule', () => {
|
|
3015
|
+
const { code, rules } = run(
|
|
3016
|
+
`${IMPORTS}export const App = () => (\n <RouterProvider router={r}>\n` +
|
|
3017
|
+
` {/* theme-allow provider-above-router — legacy bootstrap, ported next minor */}\n` +
|
|
3018
|
+
` <BasaltProvider>{null}</BasaltProvider>\n </RouterProvider>\n)\n`,
|
|
3019
|
+
)
|
|
3020
|
+
expect(code).toBe(0)
|
|
3021
|
+
expect(rules).not.toContain('provider-above-router')
|
|
3022
|
+
})
|
|
3023
|
+
|
|
3024
|
+
// The documented limit of the heuristic — stated in the rule's own JSDoc, pinned here so a
|
|
3025
|
+
// future widening has to move this test rather than discover the gap.
|
|
3026
|
+
it('does NOT see a cross-file composition (static JSX ancestry only)', () => {
|
|
3027
|
+
const { code, rules } = run(
|
|
3028
|
+
`import { BasaltProvider } from 'basalt-ui'\n` +
|
|
3029
|
+
`import { Routes } from './routes'\n` +
|
|
3030
|
+
`export const App = () => (\n <Routes>\n <BasaltProvider>{null}</BasaltProvider>\n` +
|
|
3031
|
+
` </Routes>\n)\n`,
|
|
3032
|
+
)
|
|
3033
|
+
expect(code).toBe(0)
|
|
3034
|
+
expect(rules).not.toContain('provider-above-router')
|
|
3035
|
+
})
|
|
3036
|
+
})
|
|
3037
|
+
|
|
3038
|
+
// ── duplicate-notifications-mount (F5) ───────────────────────────────────────
|
|
3039
|
+
|
|
3040
|
+
describe('basalt/duplicate-notifications-mount', () => {
|
|
3041
|
+
const IMPORTS =
|
|
3042
|
+
`import { BasaltOverlays } from 'basalt-ui/commands'\n` +
|
|
3043
|
+
`import { BasaltNotifications } from 'basalt-ui/notifications'\n`
|
|
3044
|
+
|
|
3045
|
+
it('flags both mounts in one file', () => {
|
|
3046
|
+
const { code, rules } = run(
|
|
3047
|
+
`${IMPORTS}export const App = () => (\n <BasaltOverlays notifications>\n` +
|
|
3048
|
+
` <BasaltNotifications />\n </BasaltOverlays>\n)\n`,
|
|
3049
|
+
)
|
|
3050
|
+
expect(code).toBe(1)
|
|
3051
|
+
expect(rules).toContain('duplicate-notifications-mount')
|
|
3052
|
+
})
|
|
3053
|
+
|
|
3054
|
+
// `notifications` defaults to TRUE, so the attribute being absent is the same defect.
|
|
3055
|
+
it('flags when BasaltOverlays writes no notifications prop at all', () => {
|
|
3056
|
+
const { code, rules } = run(
|
|
3057
|
+
`${IMPORTS}export const App = () => (\n <BasaltOverlays>\n` +
|
|
3058
|
+
` <BasaltNotifications />\n </BasaltOverlays>\n)\n`,
|
|
3059
|
+
)
|
|
3060
|
+
expect(code).toBe(1)
|
|
3061
|
+
expect(rules).toContain('duplicate-notifications-mount')
|
|
3062
|
+
})
|
|
3063
|
+
|
|
3064
|
+
it('does NOT flag when BasaltOverlays disables its notifications layer', () => {
|
|
3065
|
+
const { code, rules } = run(
|
|
3066
|
+
`${IMPORTS}export const App = () => (\n <BasaltOverlays notifications={false}>\n` +
|
|
3067
|
+
` <BasaltNotifications />\n </BasaltOverlays>\n)\n`,
|
|
3068
|
+
)
|
|
3069
|
+
expect(code).toBe(0)
|
|
3070
|
+
expect(rules).not.toContain('duplicate-notifications-mount')
|
|
3071
|
+
})
|
|
3072
|
+
|
|
3073
|
+
it('does NOT flag a standalone BasaltNotifications on its own', () => {
|
|
3074
|
+
const { code, rules } = run(
|
|
3075
|
+
`import { BasaltNotifications } from 'basalt-ui/notifications'\n` +
|
|
3076
|
+
`export const App = () => <BasaltNotifications />\n`,
|
|
3077
|
+
)
|
|
3078
|
+
expect(code).toBe(0)
|
|
3079
|
+
expect(rules).not.toContain('duplicate-notifications-mount')
|
|
3080
|
+
})
|
|
3081
|
+
|
|
3082
|
+
it("does NOT flag a consumer's own components of the same names", () => {
|
|
3083
|
+
const { code, rules } = run(
|
|
3084
|
+
`import { BasaltOverlays } from './overlays'\n` +
|
|
3085
|
+
`import { BasaltNotifications } from './notifications'\n` +
|
|
3086
|
+
`export const App = () => (\n <BasaltOverlays>\n <BasaltNotifications />\n` +
|
|
3087
|
+
` </BasaltOverlays>\n)\n`,
|
|
3088
|
+
)
|
|
3089
|
+
expect(code).toBe(0)
|
|
3090
|
+
expect(rules).not.toContain('duplicate-notifications-mount')
|
|
3091
|
+
})
|
|
3092
|
+
|
|
3093
|
+
it('honours a theme-allow naming the rule', () => {
|
|
3094
|
+
const { code, rules } = run(
|
|
3095
|
+
`${IMPORTS}export const App = () => (\n <BasaltOverlays notifications>\n` +
|
|
3096
|
+
` {/* theme-allow duplicate-notifications-mount — two roots, never both mounted */}\n` +
|
|
3097
|
+
` <BasaltNotifications />\n </BasaltOverlays>\n)\n`,
|
|
3098
|
+
)
|
|
3099
|
+
expect(code).toBe(0)
|
|
3100
|
+
expect(rules).not.toContain('duplicate-notifications-mount')
|
|
3101
|
+
})
|
|
3102
|
+
|
|
3103
|
+
// Two mounts WRITTEN, one mount RENDERED — the ternary is the shape a consumer reaches for while
|
|
3104
|
+
// migrating from the standalone layer to the composed one, and it was never a double mount.
|
|
3105
|
+
it('does NOT flag the two branches of one ternary', () => {
|
|
3106
|
+
const { code, rules } = run(
|
|
3107
|
+
`${IMPORTS}export const App = ({ composed }) => (\n` +
|
|
3108
|
+
` <div>{composed ? <BasaltOverlays /> : <BasaltNotifications />}</div>\n)\n`,
|
|
3109
|
+
)
|
|
3110
|
+
expect(code).toBe(0)
|
|
3111
|
+
expect(rules).not.toContain('duplicate-notifications-mount')
|
|
3112
|
+
})
|
|
3113
|
+
|
|
3114
|
+
it('does NOT flag the two operands of one logical expression', () => {
|
|
3115
|
+
const { code, rules } = run(
|
|
3116
|
+
`${IMPORTS}export const App = ({ composed }) => (\n` +
|
|
3117
|
+
` <div>{(composed && <BasaltOverlays />) || <BasaltNotifications />}</div>\n)\n`,
|
|
3118
|
+
)
|
|
3119
|
+
expect(code).toBe(0)
|
|
3120
|
+
expect(rules).not.toContain('duplicate-notifications-mount')
|
|
3121
|
+
})
|
|
3122
|
+
|
|
3123
|
+
// The exemption is the NEAREST common ancestor only. Two separate `&&` guards can both hold, so
|
|
3124
|
+
// this stays a finding — the pair is conditional, not exclusive.
|
|
3125
|
+
it('DOES flag two independently-guarded mounts under one parent', () => {
|
|
3126
|
+
const { code, rules } = run(
|
|
3127
|
+
`${IMPORTS}export const App = ({ a, b }) => (\n` +
|
|
3128
|
+
` <div>{a && <BasaltOverlays />}{b && <BasaltNotifications />}</div>\n)\n`,
|
|
3129
|
+
)
|
|
3130
|
+
expect(code).toBe(1)
|
|
3131
|
+
expect(rules).toContain('duplicate-notifications-mount')
|
|
3132
|
+
})
|
|
3133
|
+
|
|
3134
|
+
// One exclusive pair does not excuse a second, unconditional overlays mount.
|
|
3135
|
+
it('DOES flag when a NON-exclusive overlays mount also exists', () => {
|
|
3136
|
+
const { code, rules } = run(
|
|
3137
|
+
`${IMPORTS}export const App = ({ composed }) => (\n` +
|
|
3138
|
+
` <BasaltOverlays>\n` +
|
|
3139
|
+
` {composed ? <BasaltOverlays /> : <BasaltNotifications />}\n` +
|
|
3140
|
+
` </BasaltOverlays>\n)\n`,
|
|
3141
|
+
)
|
|
3142
|
+
expect(code).toBe(1)
|
|
3143
|
+
expect(rules).toContain('duplicate-notifications-mount')
|
|
3144
|
+
})
|
|
3145
|
+
})
|
|
3146
|
+
|
|
3147
|
+
// ── query-dual-import (F5) ───────────────────────────────────────────────────
|
|
3148
|
+
|
|
3149
|
+
describe('basalt/query-dual-import', () => {
|
|
3150
|
+
it('flags a file importing from both @tanstack/react-query and basalt-ui/query', () => {
|
|
3151
|
+
const { code, rules, output } = run(
|
|
3152
|
+
`import { useQuery } from '@tanstack/react-query'\n` +
|
|
3153
|
+
`import { unwrap } from 'basalt-ui/query'\n` +
|
|
3154
|
+
`export const useThing = () => useQuery({ queryKey: ['t'], queryFn: () => unwrap(get()) })\n`,
|
|
3155
|
+
)
|
|
3156
|
+
expect(code).toBe(1)
|
|
3157
|
+
expect(rules).toContain('query-dual-import')
|
|
3158
|
+
expect(output).toContain('Dual query import')
|
|
3159
|
+
})
|
|
3160
|
+
|
|
3161
|
+
// The softer half — the one that actually catches the drift, and why the rule is `warn`.
|
|
3162
|
+
it('flags a raw @tanstack/react-query import beside a basalt root import', () => {
|
|
3163
|
+
const { code, rules, output } = run(
|
|
3164
|
+
`import { useQuery } from '@tanstack/react-query'\n` +
|
|
3165
|
+
`import { QueryState } from 'basalt-ui'\n` +
|
|
3166
|
+
`export const useThing = () => useQuery({ queryKey: ['t'], queryFn: get })\n`,
|
|
3167
|
+
)
|
|
3168
|
+
expect(code).toBe(1)
|
|
3169
|
+
expect(rules).toContain('query-dual-import')
|
|
3170
|
+
expect(output).toContain('Raw @tanstack/react-query import')
|
|
3171
|
+
})
|
|
3172
|
+
|
|
3173
|
+
it('does NOT flag basalt-ui/query on its own', () => {
|
|
3174
|
+
const { code, rules } = run(
|
|
3175
|
+
`import { useQuery, unwrap } from 'basalt-ui/query'\n` +
|
|
3176
|
+
`export const useThing = () => useQuery({ queryKey: ['t'], queryFn: () => unwrap(get()) })\n`,
|
|
3177
|
+
)
|
|
3178
|
+
expect(code).toBe(0)
|
|
3179
|
+
expect(rules).not.toContain('query-dual-import')
|
|
3180
|
+
})
|
|
3181
|
+
|
|
3182
|
+
// `import type` is erased at compile time — there is no runtime import to route through the
|
|
3183
|
+
// seam, and typing against the library's own result type is what `QueryStateLike` documents as
|
|
3184
|
+
// legitimate. Same skip `agent-no-raw-usechat` and `ai-sdk-major` take.
|
|
3185
|
+
it('does NOT flag a type-only @tanstack/react-query import beside basalt-ui/query', () => {
|
|
3186
|
+
const { code, rules } = run(
|
|
3187
|
+
`import type { UseQueryResult } from '@tanstack/react-query'\n` +
|
|
3188
|
+
`import { unwrap } from 'basalt-ui/query'\n` +
|
|
3189
|
+
`export const pick = (q: UseQueryResult<number>) => unwrap(q.data)\n`,
|
|
3190
|
+
)
|
|
3191
|
+
expect(code).toBe(0)
|
|
3192
|
+
expect(rules).not.toContain('query-dual-import')
|
|
3193
|
+
})
|
|
3194
|
+
|
|
3195
|
+
it('does NOT flag @tanstack/react-query in a file with no basalt import', () => {
|
|
3196
|
+
const { code, rules } = run(
|
|
3197
|
+
`import { useQuery } from '@tanstack/react-query'\n` +
|
|
3198
|
+
`export const useThing = () => useQuery({ queryKey: ['t'], queryFn: get })\n`,
|
|
3199
|
+
)
|
|
3200
|
+
expect(code).toBe(0)
|
|
3201
|
+
expect(rules).not.toContain('query-dual-import')
|
|
3202
|
+
})
|
|
3203
|
+
|
|
3204
|
+
// The devtools package is a different package, not a subpath.
|
|
3205
|
+
it('does NOT flag @tanstack/react-query-devtools', () => {
|
|
3206
|
+
const { code, rules } = run(
|
|
3207
|
+
`import { ReactQueryDevtools } from '@tanstack/react-query-devtools'\n` +
|
|
3208
|
+
`import { unwrap } from 'basalt-ui/query'\n` +
|
|
3209
|
+
`export const D = () => <ReactQueryDevtools />\n`,
|
|
3210
|
+
)
|
|
3211
|
+
expect(code).toBe(0)
|
|
3212
|
+
expect(rules).not.toContain('query-dual-import')
|
|
3213
|
+
})
|
|
3214
|
+
|
|
3215
|
+
it('honours a theme-allow naming the rule', () => {
|
|
3216
|
+
const { code, rules } = run(
|
|
3217
|
+
`// theme-allow query-dual-import — the persister API has no basalt re-export yet\n` +
|
|
3218
|
+
`import { useQuery } from '@tanstack/react-query'\n` +
|
|
3219
|
+
`import { unwrap } from 'basalt-ui/query'\n` +
|
|
3220
|
+
`export const useThing = () => useQuery({ queryKey: ['t'], queryFn: () => unwrap(g()) })\n`,
|
|
3221
|
+
)
|
|
3222
|
+
expect(code).toBe(0)
|
|
3223
|
+
expect(rules).not.toContain('query-dual-import')
|
|
3224
|
+
})
|
|
3225
|
+
})
|
|
3226
|
+
|
|
3227
|
+
// ── query-fn-unwrap (F5) ─────────────────────────────────────────────────────
|
|
3228
|
+
|
|
3229
|
+
describe('basalt/query-fn-unwrap', () => {
|
|
3230
|
+
const IMPORT = `import { unwrap } from 'basalt-ui/query'\n`
|
|
3231
|
+
|
|
3232
|
+
it('flags a queryFn that fetches with no unwrap', () => {
|
|
3233
|
+
const { code, rules } = run(
|
|
3234
|
+
`${IMPORT}export const o = { queryKey: ['t'], queryFn: () => fetch('/api/t') }\n`,
|
|
3235
|
+
)
|
|
3236
|
+
expect(code).toBe(1)
|
|
3237
|
+
expect(rules).toContain('query-fn-unwrap')
|
|
3238
|
+
})
|
|
3239
|
+
|
|
3240
|
+
it('flags the async .json() form', () => {
|
|
3241
|
+
const { code, rules } = run(
|
|
3242
|
+
`${IMPORT}export const o = {\n queryKey: ['t'],\n` +
|
|
3243
|
+
` queryFn: async () => (await fetch('/api/t')).json(),\n}\n`,
|
|
3244
|
+
)
|
|
3245
|
+
expect(code).toBe(1)
|
|
3246
|
+
expect(rules).toContain('query-fn-unwrap')
|
|
3247
|
+
})
|
|
3248
|
+
|
|
3249
|
+
it('does NOT flag a queryFn that wraps the call', () => {
|
|
3250
|
+
const { code, rules } = run(
|
|
3251
|
+
`${IMPORT}export const o = { queryKey: ['t'], queryFn: () => unwrap(fetch('/api/t')) }\n`,
|
|
3252
|
+
)
|
|
3253
|
+
expect(code).toBe(0)
|
|
3254
|
+
expect(rules).not.toContain('query-fn-unwrap')
|
|
3255
|
+
})
|
|
3256
|
+
|
|
3257
|
+
it('does NOT flag a queryFn with no fetch in it', () => {
|
|
3258
|
+
const { code, rules } = run(
|
|
3259
|
+
`${IMPORT}export const o = { queryKey: ['t'], queryFn: () => client.t.get() }\n`,
|
|
3260
|
+
)
|
|
3261
|
+
expect(code).toBe(0)
|
|
3262
|
+
expect(rules).not.toContain('query-fn-unwrap')
|
|
3263
|
+
})
|
|
3264
|
+
|
|
3265
|
+
// The scope gate: with no basalt-ui/query import this is an opinion about `fetch`, which basalt
|
|
3266
|
+
// does not have.
|
|
3267
|
+
it('does NOT flag a file that never imports basalt-ui/query', () => {
|
|
3268
|
+
const { code, rules } = run(
|
|
3269
|
+
`export const o = { queryKey: ['t'], queryFn: () => fetch('/api/t') }\n`,
|
|
3270
|
+
)
|
|
3271
|
+
expect(code).toBe(0)
|
|
3272
|
+
expect(rules).not.toContain('query-fn-unwrap')
|
|
3273
|
+
})
|
|
3274
|
+
|
|
3275
|
+
// The documented blind spot — a reference, not a function literal. Pinned so a widening moves it.
|
|
3276
|
+
it('does NOT see a queryFn passed as a reference', () => {
|
|
3277
|
+
const { code, rules } = run(
|
|
3278
|
+
`${IMPORT}const load = () => fetch('/api/t')\n` +
|
|
3279
|
+
`export const o = { queryKey: ['t'], queryFn: load }\n`,
|
|
3280
|
+
)
|
|
3281
|
+
expect(code).toBe(0)
|
|
3282
|
+
expect(rules).not.toContain('query-fn-unwrap')
|
|
3283
|
+
})
|
|
3284
|
+
|
|
3285
|
+
it('honours a theme-allow naming the rule', () => {
|
|
3286
|
+
const { code, rules } = run(
|
|
3287
|
+
`${IMPORT}export const o = {\n queryKey: ['t'],\n` +
|
|
3288
|
+
` // theme-allow query-fn-unwrap — raw Response is the point, this reads a blob\n` +
|
|
3289
|
+
` queryFn: () => fetch('/api/t'),\n}\n`,
|
|
3290
|
+
)
|
|
3291
|
+
expect(code).toBe(0)
|
|
3292
|
+
expect(rules).not.toContain('query-fn-unwrap')
|
|
3293
|
+
})
|
|
3294
|
+
})
|
|
3295
|
+
|
|
3296
|
+
// ── deprecated-export (B4) ───────────────────────────────────────────────────
|
|
3297
|
+
|
|
3298
|
+
describe('basalt/deprecated-export', () => {
|
|
3299
|
+
it('flags a deprecated named import', () => {
|
|
3300
|
+
const { code, rules, output } = run(
|
|
3301
|
+
`import { field } from 'basalt-ui/forms'\nexport const f = field\n`,
|
|
3302
|
+
)
|
|
3303
|
+
expect(code).toBe(1)
|
|
3304
|
+
expect(rules).toContain('deprecated-export')
|
|
3305
|
+
expect(output).toContain('inputProps')
|
|
3306
|
+
})
|
|
3307
|
+
|
|
3308
|
+
it('does NOT flag the replacement', () => {
|
|
3309
|
+
const { code, rules } = run(
|
|
3310
|
+
`import { inputProps } from 'basalt-ui/forms'\nexport const f = inputProps\n`,
|
|
3311
|
+
)
|
|
3312
|
+
expect(code).toBe(0)
|
|
3313
|
+
expect(rules).not.toContain('deprecated-export')
|
|
3314
|
+
})
|
|
3315
|
+
|
|
3316
|
+
it('does NOT flag a same-named import from somewhere else', () => {
|
|
3317
|
+
const { code, rules } = run(`import { field } from 'basalt-ui/state'\nexport const f = field\n`)
|
|
3318
|
+
expect(code).toBe(0)
|
|
3319
|
+
expect(rules).not.toContain('deprecated-export')
|
|
3320
|
+
})
|
|
3321
|
+
|
|
3322
|
+
it('flags each deprecated BasaltProvider connectivity prop', () => {
|
|
3323
|
+
const { code, rules, output } = run(
|
|
3324
|
+
`import { BasaltProvider } from 'basalt-ui'\n` +
|
|
3325
|
+
`export const App = () => (\n` +
|
|
3326
|
+
` <BasaltProvider sseUrl="/e" healthUrl="/h" healthIntervalMs={5000}>{null}</BasaltProvider>\n)\n`,
|
|
3327
|
+
)
|
|
3328
|
+
expect(code).toBe(1)
|
|
3329
|
+
expect(rules).toContain('deprecated-export')
|
|
3330
|
+
expect(output).toContain('connectivity')
|
|
3331
|
+
})
|
|
3332
|
+
|
|
3333
|
+
it('does NOT flag the connectivity object prop', () => {
|
|
3334
|
+
const { code, rules } = run(
|
|
3335
|
+
`import { BasaltProvider } from 'basalt-ui'\n` +
|
|
3336
|
+
`export const App = () => <BasaltProvider connectivity={{ sseUrl: '/e' }}>{null}</BasaltProvider>\n`,
|
|
3337
|
+
)
|
|
3338
|
+
expect(code).toBe(0)
|
|
3339
|
+
expect(rules).not.toContain('deprecated-export')
|
|
3340
|
+
})
|
|
3341
|
+
|
|
3342
|
+
it("does NOT flag an sseUrl on a consumer's own component", () => {
|
|
3343
|
+
const { code, rules } = run(
|
|
3344
|
+
`import { BasaltProvider } from './provider'\n` +
|
|
3345
|
+
`export const App = () => <BasaltProvider sseUrl="/e">{null}</BasaltProvider>\n`,
|
|
3346
|
+
)
|
|
3347
|
+
expect(code).toBe(0)
|
|
3348
|
+
expect(rules).not.toContain('deprecated-export')
|
|
3349
|
+
})
|
|
3350
|
+
|
|
3351
|
+
it('honours a theme-allow naming the rule', () => {
|
|
3352
|
+
const { code, rules } = run(
|
|
3353
|
+
`// theme-allow deprecated-export — pinned to 1.27.0 until the forms port lands\n` +
|
|
3354
|
+
`import { field } from 'basalt-ui/forms'\nexport const f = field\n`,
|
|
3355
|
+
)
|
|
3356
|
+
expect(code).toBe(0)
|
|
3357
|
+
expect(rules).not.toContain('deprecated-export')
|
|
3358
|
+
})
|
|
3359
|
+
|
|
3360
|
+
// `field` → `inputProps` is NOT a drop-in rename any more (`inputProps` returns no `key`), so
|
|
3361
|
+
// the row carries `fix: false`: the diagnostic still fires, but `--fix` must leave the import
|
|
3362
|
+
// alone — the rewrite belongs to `basalt/forms-field-key`, which also inserts the key.
|
|
3363
|
+
it('leaves a row with `fix: false` untouched under --fix while still reporting it', () => {
|
|
3364
|
+
const source = `import { field } from 'basalt-ui/forms'\nexport const f = field\n`
|
|
3365
|
+
writeFileSync(resolve(dir, 'fixture.tsx'), source)
|
|
3366
|
+
Bun.spawnSync([OXLINT_BIN, '-c', '.oxlintrc.json', '--fix', 'fixture.tsx'], { cwd: dir })
|
|
3367
|
+
expect(readFileSync(resolve(dir, 'fixture.tsx'), 'utf8')).toBe(source)
|
|
3368
|
+
const { rules } = run(source)
|
|
3369
|
+
expect(rules).toContain('deprecated-export')
|
|
3370
|
+
})
|
|
3371
|
+
})
|
|
3372
|
+
|
|
3373
|
+
// ── forms-field-key ──────────────────────────────────────────────────────────
|
|
3374
|
+
|
|
3375
|
+
describe('basalt/forms-field-key', () => {
|
|
3376
|
+
const FORMS = `import { inputProps } from 'basalt-ui/forms'\n`
|
|
3377
|
+
|
|
3378
|
+
it('flags a spread inputProps() with no sibling key', () => {
|
|
3379
|
+
const { code, rules, output } = run(
|
|
3380
|
+
`${FORMS}export const C = () => <TextInput {...inputProps(form, 'email')} />\n`,
|
|
3381
|
+
)
|
|
3382
|
+
expect(code).toBe(1)
|
|
3383
|
+
expect(rules).toContain('forms-field-key')
|
|
3384
|
+
expect(output).toContain('no sibling `key`')
|
|
3385
|
+
})
|
|
3386
|
+
|
|
3387
|
+
it('does NOT flag the documented two-call idiom', () => {
|
|
3388
|
+
const { code, rules } = run(
|
|
3389
|
+
`import { fieldKey, inputProps } from 'basalt-ui/forms'\n` +
|
|
3390
|
+
`export const C = () => <TextInput key={fieldKey(form, 'e')} {...inputProps(form, 'e')} />\n`,
|
|
3391
|
+
)
|
|
3392
|
+
expect(code).toBe(0)
|
|
3393
|
+
expect(rules).not.toContain('forms-field-key')
|
|
3394
|
+
})
|
|
3395
|
+
|
|
3396
|
+
// An `inputProps` a consumer wrote themselves is not basalt's — the same provenance gate the
|
|
3397
|
+
// control and chart rules take, one lane over.
|
|
3398
|
+
it('does NOT flag a same-named helper imported from somewhere else', () => {
|
|
3399
|
+
const { code, rules } = run(
|
|
3400
|
+
`import { inputProps } from './my-helpers'\n` +
|
|
3401
|
+
`export const C = () => <TextInput {...inputProps(form, 'email')} />\n`,
|
|
3402
|
+
)
|
|
3403
|
+
expect(code).toBe(0)
|
|
3404
|
+
expect(rules).not.toContain('forms-field-key')
|
|
3405
|
+
})
|
|
3406
|
+
|
|
3407
|
+
it('resolves an aliased import — `inputProps as f` is still inputProps', () => {
|
|
3408
|
+
const { code, rules } = run(
|
|
3409
|
+
`import { inputProps as f } from 'basalt-ui/forms'\n` +
|
|
3410
|
+
`export const C = () => <TextInput {...f(form, 'email')} />\n`,
|
|
3411
|
+
)
|
|
3412
|
+
expect(code).toBe(1)
|
|
3413
|
+
expect(rules).toContain('forms-field-key')
|
|
3414
|
+
})
|
|
3415
|
+
|
|
3416
|
+
// The second message: `field` still RETURNS `key`, so it is reported whether or not the element
|
|
3417
|
+
// carries one — the remedy is the rename, never an added attribute.
|
|
3418
|
+
it('flags a spread field() even when the element already has a key', () => {
|
|
3419
|
+
const { code, rules, output } = run(
|
|
3420
|
+
`import { field } from 'basalt-ui/forms'\n` +
|
|
3421
|
+
`export const C = () => <TextInput key="e" {...field(form, 'email')} />\n`,
|
|
3422
|
+
)
|
|
3423
|
+
expect(code).toBe(1)
|
|
3424
|
+
expect(rules).toContain('forms-field-key')
|
|
3425
|
+
expect(output).toContain('@deprecated 1.27 alias')
|
|
3426
|
+
})
|
|
3427
|
+
|
|
3428
|
+
it('honours a theme-allow naming the rule', () => {
|
|
3429
|
+
const { code, rules } = run(
|
|
3430
|
+
`${FORMS}export const C = () => (\n` +
|
|
3431
|
+
` // theme-allow forms-field-key — this input is remounted by its parent\n` +
|
|
3432
|
+
` <TextInput {...inputProps(form, 'email')} />\n)\n`,
|
|
3433
|
+
)
|
|
3434
|
+
expect(code).toBe(0)
|
|
3435
|
+
expect(rules).not.toContain('forms-field-key')
|
|
3436
|
+
})
|
|
3437
|
+
|
|
3438
|
+
// The autofix has to produce COMPILING code: an inserted `fieldKey(…)` with no import is a worse
|
|
3439
|
+
// outcome than the missing key it replaced.
|
|
3440
|
+
it('autofixes the key in, with the same arguments, and adds fieldKey to the import once', () => {
|
|
3441
|
+
const source =
|
|
3442
|
+
`${FORMS}export const C = () => (\n` +
|
|
3443
|
+
` <form>\n` +
|
|
3444
|
+
` <TextInput {...inputProps(form, 'email')} label="Email" />\n` +
|
|
3445
|
+
` <TextInput {...inputProps(form, 'name')} />\n` +
|
|
3446
|
+
` </form>\n)\n`
|
|
3447
|
+
writeFileSync(resolve(dir, 'fixture.tsx'), source)
|
|
3448
|
+
Bun.spawnSync([OXLINT_BIN, '-c', '.oxlintrc.json', '--fix', 'fixture.tsx'], { cwd: dir })
|
|
3449
|
+
const fixed = readFileSync(resolve(dir, 'fixture.tsx'), 'utf8')
|
|
3450
|
+
expect(fixed).toContain(`import { inputProps, fieldKey } from 'basalt-ui/forms'`)
|
|
3451
|
+
expect(fixed).toContain(`key={fieldKey(form, 'email')} {...inputProps(form, 'email')}`)
|
|
3452
|
+
expect(fixed).toContain(`key={fieldKey(form, 'name')} {...inputProps(form, 'name')}`)
|
|
3453
|
+
// Once, not twice: two overlapping edits to one import declaration would drop one of them.
|
|
3454
|
+
expect(fixed.match(/fieldKey,|, fieldKey/g)).toHaveLength(1)
|
|
3455
|
+
})
|
|
3456
|
+
|
|
3457
|
+
it('does not re-add fieldKey when it is already imported', () => {
|
|
3458
|
+
const source =
|
|
3459
|
+
`import { fieldKey, inputProps } from 'basalt-ui/forms'\n` +
|
|
3460
|
+
`export const C = () => <TextInput {...inputProps(form, 'email')} />\n`
|
|
3461
|
+
writeFileSync(resolve(dir, 'fixture.tsx'), source)
|
|
3462
|
+
Bun.spawnSync([OXLINT_BIN, '-c', '.oxlintrc.json', '--fix', 'fixture.tsx'], { cwd: dir })
|
|
3463
|
+
const fixed = readFileSync(resolve(dir, 'fixture.tsx'), 'utf8')
|
|
3464
|
+
expect(fixed).toContain(`import { fieldKey, inputProps } from 'basalt-ui/forms'`)
|
|
3465
|
+
expect(fixed).toContain(`key={fieldKey(form, 'email')}`)
|
|
3466
|
+
})
|
|
3467
|
+
})
|
|
3468
|
+
|
|
3469
|
+
// ── the deprecation ledger (B4) ──────────────────────────────────────────────
|
|
3470
|
+
|
|
3471
|
+
describe('DEPRECATED_EXPORTS', () => {
|
|
3472
|
+
const pkgVersion = (
|
|
3473
|
+
JSON.parse(readFileSync(resolve(import.meta.dirname, '..', 'package.json'), 'utf8')) as {
|
|
3474
|
+
version: string
|
|
3475
|
+
}
|
|
3476
|
+
).version
|
|
3477
|
+
|
|
3478
|
+
it('carries a replacement and a removeIn at least one minor out on every row', () => {
|
|
3479
|
+
const [major, minor] = pkgVersion.split('.').map(Number)
|
|
3480
|
+
const floor = `${major}.${(minor as number) + 1}.0`
|
|
3481
|
+
for (const row of DEPRECATED_EXPORTS) {
|
|
3482
|
+
expect([row.name, row.replacement.length > 0]).toEqual([row.name, true])
|
|
3483
|
+
expect([row.name, /^\d+\.\d+\.\d+$/.test(row.removeIn)]).toEqual([row.name, true])
|
|
3484
|
+
expect([row.name, compareSemver(row.removeIn, floor) >= 0]).toEqual([row.name, true])
|
|
3485
|
+
}
|
|
3486
|
+
})
|
|
3487
|
+
|
|
3488
|
+
it('names a real published subpath on every row', () => {
|
|
3489
|
+
const exports = Object.keys(
|
|
3490
|
+
(
|
|
3491
|
+
JSON.parse(readFileSync(resolve(import.meta.dirname, '..', 'package.json'), 'utf8')) as {
|
|
3492
|
+
exports: Record<string, unknown>
|
|
3493
|
+
}
|
|
3494
|
+
).exports,
|
|
3495
|
+
)
|
|
3496
|
+
for (const row of DEPRECATED_EXPORTS) {
|
|
3497
|
+
const subpath = row.subpath === 'basalt-ui' ? '.' : row.subpath.replace('basalt-ui', '.')
|
|
3498
|
+
expect([row.subpath, exports.includes(subpath)]).toEqual([row.subpath, true])
|
|
3499
|
+
}
|
|
3500
|
+
})
|
|
3501
|
+
|
|
3502
|
+
/**
|
|
3503
|
+
* The barrel half of the lifecycle: a JSDoc `@deprecated` on a published barrel with no ledger
|
|
3504
|
+
* row is an export nothing nudges anyone off. Deliberately scoped to `src/**' + '/index.ts(x)` —
|
|
3505
|
+
* the files a consumer's import resolves through. To add one: write the `@deprecated` JSDoc, add
|
|
3506
|
+
* the `DEPRECATED_EXPORTS` row, add the `MIGRATING.md` row.
|
|
3507
|
+
*/
|
|
3508
|
+
it('has a row for every @deprecated JSDoc tag on a published barrel', () => {
|
|
3509
|
+
const barrels = [
|
|
3510
|
+
...new Bun.Glob('src/**/index.{ts,tsx}').scanSync({
|
|
3511
|
+
cwd: resolve(import.meta.dirname, '..'),
|
|
3512
|
+
absolute: true,
|
|
3513
|
+
}),
|
|
3514
|
+
]
|
|
3515
|
+
const names = new Set(DEPRECATED_EXPORTS.map((row) => row.prop ?? row.name))
|
|
3516
|
+
for (const file of barrels) {
|
|
3517
|
+
const lines = readFileSync(file, 'utf8').split('\n')
|
|
3518
|
+
for (const [index, line] of lines.entries()) {
|
|
3519
|
+
if (!/^\s*\*\s*@deprecated\b/.test(line)) continue
|
|
3520
|
+
const declared = lines
|
|
3521
|
+
.slice(index, index + 12)
|
|
3522
|
+
.map((l) => /^\s*(?:readonly\s+)?([A-Za-z_$][\w$]*)\??\s*[:(]/.exec(l)?.[1])
|
|
3523
|
+
.find((name) => name !== undefined)
|
|
3524
|
+
expect([file, declared, declared !== undefined && names.has(declared)]).toEqual([
|
|
3525
|
+
file,
|
|
3526
|
+
declared,
|
|
3527
|
+
true,
|
|
3528
|
+
])
|
|
3529
|
+
}
|
|
3530
|
+
}
|
|
3531
|
+
})
|
|
3532
|
+
})
|
|
3533
|
+
|
|
3534
|
+
// ── the `Ships:` claim above each rule (F26) ─────────────────────────────────
|
|
3535
|
+
|
|
3536
|
+
/**
|
|
3537
|
+
* F26 was a rule's own doc comment claiming a level it did not ship at, with nothing able to catch
|
|
3538
|
+
* it: the C16 ledger is asserted against the preset, and a COMMENT is outside the ledger. So the
|
|
3539
|
+
* claim is now one normalised line — `// Ships: <level>` directly above each rule's `const` — and
|
|
3540
|
+
* this is the assertion that makes it as checkable as the ledger it restates.
|
|
3541
|
+
*/
|
|
3542
|
+
describe('the `Ships:` line above each rule', () => {
|
|
3543
|
+
const source = readFileSync(resolve(import.meta.dirname, 'oxlint-plugin.js'), 'utf8')
|
|
3544
|
+
const lines = source.split('\n')
|
|
3545
|
+
const shipped = JSON.parse(readFileSync(resolve(import.meta.dirname, 'oxlint.json'), 'utf8')) as {
|
|
3546
|
+
rules: Record<string, unknown>
|
|
3547
|
+
}
|
|
3548
|
+
// The registry block maps rule id → the `const` implementing it.
|
|
3549
|
+
const registry = [
|
|
3550
|
+
...source.slice(source.indexOf('export default {')).matchAll(/^ {4}'([\w-]+)': (\w+),$/gm),
|
|
3551
|
+
].map(([, id, binding]) => ({ id: id as string, binding: binding as string }))
|
|
3552
|
+
|
|
3553
|
+
it('covers every registered rule', () => {
|
|
3554
|
+
expect(registry.map((r) => r.id).toSorted()).toEqual(Object.keys(basaltPlugin.rules).toSorted())
|
|
3555
|
+
})
|
|
3556
|
+
|
|
3557
|
+
for (const { id, binding } of registry) {
|
|
3558
|
+
it(`${id}: the claim matches oxlint.json`, () => {
|
|
3559
|
+
const at = lines.indexOf(`const ${binding} = {`)
|
|
3560
|
+
expect([id, at]).not.toEqual([id, -1])
|
|
3561
|
+
const claim = /^\/\/ Ships: (.+)$/.exec(lines[at - 1] as string)?.[1]
|
|
3562
|
+
const level = shipped.rules[`basalt/${id}`]
|
|
3563
|
+
if (level === undefined) {
|
|
3564
|
+
expect([id, claim]).toEqual([id, 'repo-local only'])
|
|
3565
|
+
return
|
|
3566
|
+
}
|
|
3567
|
+
if (Object.hasOwn(PLUGIN_RULE_GRACE, id)) {
|
|
3568
|
+
const { promote } = PLUGIN_RULE_GRACE[id] as GraceEntry
|
|
3569
|
+
expect([id, claim]).toEqual([id, `warn (grace → ${promote})`])
|
|
3570
|
+
expect([id, level]).toEqual([id, 'warn'])
|
|
3571
|
+
return
|
|
3572
|
+
}
|
|
3573
|
+
if (Object.hasOwn(PLUGIN_RULE_ADVISORY, id)) {
|
|
3574
|
+
expect([id, claim]).toEqual([id, 'warn (advisory)'])
|
|
3575
|
+
expect([id, level]).toEqual([id, 'warn'])
|
|
3576
|
+
return
|
|
3577
|
+
}
|
|
3578
|
+
expect([id, claim]).toEqual([id, 'error'])
|
|
3579
|
+
expect([id, level]).toEqual([id, 'error'])
|
|
3580
|
+
})
|
|
3581
|
+
}
|
|
3582
|
+
})
|