basalt-ui 1.27.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 +59 -0
- package/MIGRATING.md +613 -20
- package/README.md +33 -3
- package/agent/rules/basalt-batteries.md +25 -10
- package/agent/rules/basalt-controls.md +56 -31
- package/agent/rules/basalt-mantine.md +17 -6
- package/agent/rules/basalt-tokens.md +1 -1
- package/configs/oxlint-plugin.js +1056 -28
- package/configs/oxlint-plugin.test.ts +1066 -0
- package/configs/oxlint.json +7 -0
- 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 +3 -2
- package/dist/controls/enum-filter.d.ts.map +1 -1
- package/dist/controls/enum-filter.js +28 -13
- package/dist/controls/filter-context.d.ts +8 -4
- 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 +2 -0
- package/dist/controls/index.d.ts.map +1 -1
- package/dist/controls/index.js +4 -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 +122 -20
- package/dist/controls/number-filter.d.ts +7 -5
- package/dist/controls/number-filter.d.ts.map +1 -1
- package/dist/controls/number-filter.js +40 -11
- 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 +2 -1
- package/dist/controls/range-filter.d.ts.map +1 -1
- package/dist/controls/range-filter.js +57 -35
- 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 +25 -10
- 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 +28 -11
- 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 +7 -1
- 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 +44 -38
- package/dist/dashboard/stat-card.d.ts.map +1 -1
- package/dist/dashboard/stat-card.js +55 -42
- package/dist/dashboard/stat-card.module.css +46 -31
- package/dist/dashboard/stat-card.module.css.d.ts +1 -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 +384 -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.map +1 -1
- package/dist/guard/index.js +26 -6
- 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/search-store.d.ts.map +1 -1
- package/dist/router-tanstack/search-store.js +1 -1
- 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 +23 -3
- package/dist/state/fields.d.ts.map +1 -1
- package/dist/state/fields.js +13 -2
- 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.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 +42 -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 +69 -4
- package/dist/widget-header/widget-header.d.ts.map +1 -1
- package/dist/widget-header/widget-header.js +42 -27
- package/dist/widget-header/widget-header.module.css +72 -7
- 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 +32 -16
- package/src/controls/filter-context.tsx +14 -6
- 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 +7 -0
- package/src/controls/multi-select-filter.tsx +177 -27
- package/src/controls/number-filter.tsx +61 -23
- 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 +76 -39
- package/src/controls/search-filter.tsx +30 -12
- 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 +36 -16
- 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 +13 -1
- package/src/dashboard/query-state.tsx +154 -84
- package/src/dashboard/settings-section.tsx +87 -31
- package/src/dashboard/stat-card.module.css +46 -31
- package/src/dashboard/stat-card.module.css.d.ts +1 -0
- package/src/dashboard/stat-card.tsx +185 -127
- 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 +919 -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 +73 -18
- package/src/index.ts +74 -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 +1 -0
- package/src/router-tanstack/search-store.ts +3 -10
- 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 +43 -10
- package/src/state/persisted-or-local.ts +50 -0
- package/src/state.ts +0 -9
- package/src/surfaces.ts +42 -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 +72 -7
- package/src/widget-header/widget-header.tsx +150 -71
- 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/range-filter.type-guard.test.d.ts +0 -2
- package/dist/controls/range-filter.type-guard.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/state/persisted.test.d.ts +0 -2
- package/dist/state/persisted.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 -1053
- 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 -137
- package/src/controls/range-filter.type-guard.test.tsx +0 -122
- 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 -385
- package/src/data/data-table.test.tsx +0 -434
- package/src/guard/check-source.test.ts +0 -3388
- package/src/guard/grace.test.ts +0 -158
- 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 -882
- package/src/router-tanstack/search-store.test.ts +0 -578
- 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 -512
- package/src/state/persisted.test.ts +0 -91
- 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
package/configs/oxlint-plugin.js
CHANGED
|
@@ -104,11 +104,18 @@ export const KNOWN_RULE_IDS = new Set([
|
|
|
104
104
|
'raw-scroll-container',
|
|
105
105
|
'hand-rolled-filter',
|
|
106
106
|
'control-outside-home',
|
|
107
|
+
'bound-control-outside-home',
|
|
107
108
|
'control-size-literal',
|
|
108
109
|
'page-bar-budget',
|
|
109
110
|
'responsive-twin',
|
|
110
111
|
'search-literal-link',
|
|
111
112
|
'use-search-from-literal',
|
|
113
|
+
'provider-above-router',
|
|
114
|
+
'duplicate-notifications-mount',
|
|
115
|
+
'query-dual-import',
|
|
116
|
+
'query-fn-unwrap',
|
|
117
|
+
'deprecated-export',
|
|
118
|
+
'forms-field-key',
|
|
112
119
|
// `in-body-page-title` is BOTH a plugin rule and a guard kind — one id, two lanes, so one
|
|
113
120
|
// annotation waives both. It is listed once, below, with the guard kinds.
|
|
114
121
|
'visx-boundary',
|
|
@@ -443,6 +450,16 @@ function isInStyleContext(context, node) {
|
|
|
443
450
|
return false
|
|
444
451
|
}
|
|
445
452
|
|
|
453
|
+
/**
|
|
454
|
+
* No `/src/tokens/` exemption, and none is needed: both rules below only ever visit `JSXAttribute`/
|
|
455
|
+
* `Property` nodes, and `src/tokens/**` is pure data — `.ts`, never `.tsx`, zero React — so the
|
|
456
|
+
* visitor structurally cannot fire there. A path-based exemption used to sit here gated on
|
|
457
|
+
* `isBasaltScopedFile`, which tests "does the nearest package.json depend on basalt-ui", not
|
|
458
|
+
* "is this file basalt's own" — a consumer package that merely depends on basalt-ui could put raw
|
|
459
|
+
* literals under its own `src/tokens/` and silence both rules. Deleting the exemption is simpler
|
|
460
|
+
* than re-gating it on package identity: the simplest gate is no gate.
|
|
461
|
+
*/
|
|
462
|
+
// Ships: error
|
|
446
463
|
const noRawFontSize = {
|
|
447
464
|
meta: {
|
|
448
465
|
type: 'suggestion',
|
|
@@ -452,7 +469,7 @@ const noRawFontSize = {
|
|
|
452
469
|
schema: [],
|
|
453
470
|
},
|
|
454
471
|
create(context) {
|
|
455
|
-
if (
|
|
472
|
+
if (isTestFile(context)) return {}
|
|
456
473
|
|
|
457
474
|
return {
|
|
458
475
|
JSXAttribute(node) {
|
|
@@ -496,16 +513,19 @@ const SIZE_ATTRS = new Set(['size', 'fz', 'fontSize'])
|
|
|
496
513
|
* which is not always a font size — on `Text` it is, on `ThemeIcon`/`ActionIcon` it is a box
|
|
497
514
|
* dimension — so one shared id would mean a consumer silencing the icon case also silences every
|
|
498
515
|
* off-scale font size, the bundled-rule mistake the three boundary rules were split apart to avoid.
|
|
499
|
-
* And a separate id can carry its own severity: this rule ships `
|
|
500
|
-
*
|
|
501
|
-
*
|
|
502
|
-
* `no-raw-font-size` in place would have
|
|
516
|
+
* And a separate id can carry its own severity: this rule ships `error` in the consumer preset —
|
|
517
|
+
* it is a NEW form the sibling rule never caught (a numeric-only check let every string form walk
|
|
518
|
+
* straight past it), not a narrowing of code that previously passed, so the grace-minor doctrine in
|
|
519
|
+
* the package CLAUDE.md does not apply to it. Widening `no-raw-font-size` in place would have meant
|
|
520
|
+
* the widened check inherited whatever level the sibling id already carried, with no way to give
|
|
521
|
+
* this specific form its own severity.
|
|
503
522
|
*
|
|
504
523
|
* Numeric `size={32}` is deliberately NOT flagged. It is the documented Mantine idiom for icon
|
|
505
524
|
* dimensions and flagging it would fire on nearly every icon in an app for no design-system gain —
|
|
506
525
|
* the token system has an opinion about type scale and spacing, not about how many pixels wide one
|
|
507
526
|
* glyph box is.
|
|
508
527
|
*/
|
|
528
|
+
// Ships: error
|
|
509
529
|
const rawSizeLiteral = {
|
|
510
530
|
meta: {
|
|
511
531
|
type: 'suggestion',
|
|
@@ -515,7 +535,7 @@ const rawSizeLiteral = {
|
|
|
515
535
|
schema: [],
|
|
516
536
|
},
|
|
517
537
|
create(context) {
|
|
518
|
-
if (
|
|
538
|
+
if (isTestFile(context)) return {}
|
|
519
539
|
|
|
520
540
|
return {
|
|
521
541
|
JSXAttribute(node) {
|
|
@@ -546,6 +566,7 @@ function attrValue(attr) {
|
|
|
546
566
|
return unwrapExpressionContainer(attr.value)
|
|
547
567
|
}
|
|
548
568
|
|
|
569
|
+
// Ships: error
|
|
549
570
|
const cardInset = {
|
|
550
571
|
meta: {
|
|
551
572
|
type: 'suggestion',
|
|
@@ -635,6 +656,7 @@ function subtreeHasChart(node) {
|
|
|
635
656
|
return false
|
|
636
657
|
}
|
|
637
658
|
|
|
659
|
+
// Ships: error
|
|
638
660
|
const chartInRawSurface = {
|
|
639
661
|
meta: {
|
|
640
662
|
type: 'suggestion',
|
|
@@ -720,6 +742,7 @@ function enclosingHomeOwner(node) {
|
|
|
720
742
|
* code is small enough to fix rather than schedule. A consumer who disagrees turns the whole rule
|
|
721
743
|
* down in their own config.
|
|
722
744
|
*/
|
|
745
|
+
// Ships: error
|
|
723
746
|
const rawScrollContainer = {
|
|
724
747
|
meta: {
|
|
725
748
|
type: 'suggestion',
|
|
@@ -854,6 +877,7 @@ function boundaryVisitor(reportFor) {
|
|
|
854
877
|
// segment named `charts` is deliberately permissive, not restrictive — it only ever widens where
|
|
855
878
|
// visx is allowed, never narrows where it's flagged elsewhere. "Your visx lives in a charts/ dir"
|
|
856
879
|
// is the intended convention.
|
|
880
|
+
// Ships: error
|
|
857
881
|
const visxBoundary = {
|
|
858
882
|
meta: {
|
|
859
883
|
type: 'suggestion',
|
|
@@ -885,6 +909,7 @@ const visxBoundary = {
|
|
|
885
909
|
// Rule 6 — visx-tooltip: @visx/tooltip banned everywhere, including inside charts. Fires ahead of
|
|
886
910
|
// (and instead of) basalt/visx-boundary for that one specifier — basalt's own src/ never imports
|
|
887
911
|
// @visx/tooltip, so no theme-allow-style exemption is needed here either.
|
|
912
|
+
// Ships: error
|
|
888
913
|
const visxTooltip = {
|
|
889
914
|
meta: {
|
|
890
915
|
type: 'suggestion',
|
|
@@ -908,6 +933,7 @@ const visxTooltip = {
|
|
|
908
933
|
// @mantine/* installed (see the file-header comment for the cycle/fork + packaging rationale).
|
|
909
934
|
// Repo-local only — never registered in the shipped consumer preset, since this governs basalt's
|
|
910
935
|
// own internal layering/packaging, not a consumer contract.
|
|
936
|
+
// Ships: repo-local only
|
|
911
937
|
const tokenLayerBoundary = {
|
|
912
938
|
meta: {
|
|
913
939
|
type: 'suggestion',
|
|
@@ -969,6 +995,7 @@ function propertyKeyName(node) {
|
|
|
969
995
|
|
|
970
996
|
// Deliberately NO `hasThemeAllow` escape — see basalt/visx-boundary's comment for why these
|
|
971
997
|
// guards use their own token (`basalt-agent-allow`, see the file-header comment) instead.
|
|
998
|
+
// Ships: error
|
|
972
999
|
const agentResumeGuard = {
|
|
973
1000
|
meta: {
|
|
974
1001
|
type: 'problem',
|
|
@@ -1015,6 +1042,7 @@ const RAW_USE_CHAT_SOURCES = new Set(['@ai-sdk/react', 'ai/react'])
|
|
|
1015
1042
|
const RAW_USE_CHAT_NAMES = new Set(['useChat', 'useCompletion'])
|
|
1016
1043
|
|
|
1017
1044
|
// Deliberately NO `hasThemeAllow` escape — see the file-header comment.
|
|
1045
|
+
// Ships: error
|
|
1018
1046
|
const agentNoRawUseChat = {
|
|
1019
1047
|
meta: {
|
|
1020
1048
|
type: 'problem',
|
|
@@ -1210,6 +1238,7 @@ function aiSdkMajorMessage(consumerMajor) {
|
|
|
1210
1238
|
// version skew is still a fact about two package.json files, but a written, intentional
|
|
1211
1239
|
// producer/consumer pair (see `doctor`'s `aiMajorSkewReason`) needs a way to say so at the import
|
|
1212
1240
|
// site too, not just be permanently blocked.
|
|
1241
|
+
// Ships: error
|
|
1213
1242
|
const aiSdkMajor = {
|
|
1214
1243
|
meta: {
|
|
1215
1244
|
type: 'problem',
|
|
@@ -1302,6 +1331,7 @@ const HAND_ROLLED_PLOT_MESSAGE =
|
|
|
1302
1331
|
'the file. To waive just THIS node and stay policed on the rest, write `theme-allow ' +
|
|
1303
1332
|
'hand-rolled-plot — <why>` on its own line above it. (basalt/hand-rolled-plot)'
|
|
1304
1333
|
|
|
1334
|
+
// Ships: error
|
|
1305
1335
|
const handRolledPlot = {
|
|
1306
1336
|
meta: {
|
|
1307
1337
|
type: 'suggestion',
|
|
@@ -1412,6 +1442,7 @@ function isMapCall(node) {
|
|
|
1412
1442
|
)
|
|
1413
1443
|
}
|
|
1414
1444
|
|
|
1445
|
+
// Ships: error
|
|
1415
1446
|
const chartLegendLiteral = {
|
|
1416
1447
|
meta: {
|
|
1417
1448
|
type: 'suggestion',
|
|
@@ -1678,6 +1709,7 @@ function shadowBasaltExportMessage(name) {
|
|
|
1678
1709
|
* tokens does not. `warn`, not `error`: a name collision is evidence, not proof, and a consumer
|
|
1679
1710
|
* with a genuinely different `Composer` should be told once, not blocked.
|
|
1680
1711
|
*/
|
|
1712
|
+
// Ships: warn (advisory)
|
|
1681
1713
|
const shadowBasaltExport = {
|
|
1682
1714
|
meta: {
|
|
1683
1715
|
type: 'suggestion',
|
|
@@ -1806,6 +1838,7 @@ const HAND_ROLLED_SHELL_MESSAGE =
|
|
|
1806
1838
|
* it. The module that DEFINES `BasaltShell` is exempt by declaration, not by path — a rule saying
|
|
1807
1839
|
* "compose X" cannot fire inside X.
|
|
1808
1840
|
*/
|
|
1841
|
+
// Ships: error
|
|
1809
1842
|
const handRolledShell = {
|
|
1810
1843
|
meta: {
|
|
1811
1844
|
type: 'suggestion',
|
|
@@ -1928,11 +1961,26 @@ const BOUND_TAGS = new Set([
|
|
|
1928
1961
|
* The names whose DECLARATION means this file defines a basalt control rather than consuming one —
|
|
1929
1962
|
* the owner exemption, the same shape as `hand-rolled-plot`'s `notesOwnerDefinition`: a rule that
|
|
1930
1963
|
* says "use a bound control" cannot fire inside the module that IS the bound control.
|
|
1964
|
+
*
|
|
1965
|
+
* **The declaration alone is not enough, and the second half is what makes the set safe to widen.**
|
|
1966
|
+
* `PanelRow`, `EnumFilter` and `SliderControl` are ordinary names a consumer writes for their own
|
|
1967
|
+
* helpers, and a bare-name match let one local `function PanelRow(…)` switch `control-outside-home`,
|
|
1968
|
+
* `bound-control-outside-home` and `responsive-twin` off for that WHOLE file. So the exemption also
|
|
1969
|
+
* requires the file to import NOTHING from `basalt-ui*`: basalt's own control sources import each
|
|
1970
|
+
* other RELATIVELY, while a consumer of basalt always names the package. A file that consumes
|
|
1971
|
+
* basalt is not the module that defines its controls, whatever it calls its helpers.
|
|
1931
1972
|
*/
|
|
1932
1973
|
const CONTROL_OWNER_NAMES = new Set([
|
|
1933
1974
|
...BOUND_TAGS,
|
|
1934
1975
|
'FilterSet',
|
|
1935
1976
|
'FilterPill',
|
|
1977
|
+
// The INTERNAL control modules, listed for the same reason the public names are: `EnumFilter` is
|
|
1978
|
+
// the shared body behind SelectFilter/CompareFilter/NumberFilter, and `PanelRow`/`SliderControl`
|
|
1979
|
+
// are the aside's row primitives (`docs/ASIDE-SPEC.md` §3). A module that IS a bound control
|
|
1980
|
+
// cannot be told to use one. This is a RELAXATION, so it needs no grace entry (C16).
|
|
1981
|
+
'EnumFilter',
|
|
1982
|
+
'PanelRow',
|
|
1983
|
+
'SliderControl',
|
|
1936
1984
|
'SyncButton',
|
|
1937
1985
|
'ActionGroup',
|
|
1938
1986
|
'OverflowMenu',
|
|
@@ -1940,19 +1988,25 @@ const CONTROL_OWNER_NAMES = new Set([
|
|
|
1940
1988
|
])
|
|
1941
1989
|
|
|
1942
1990
|
/**
|
|
1943
|
-
* Where a raw selection control legitimately lives outside a TIERED home: a settings row,
|
|
1944
|
-
* overlay, a composer. Every one of these is a place the tier does not apply, so the advice
|
|
1945
|
-
* it into a home slot") would be wrong rather than merely unwelcome.
|
|
1991
|
+
* Where a raw selection control legitimately lives outside a TIERED home: a settings row, a form
|
|
1992
|
+
* row, an overlay, a composer. Every one of these is a place the tier does not apply, so the advice
|
|
1993
|
+
* ("move it into a home slot") would be wrong rather than merely unwelcome.
|
|
1946
1994
|
*
|
|
1947
1995
|
* `SettingsRow` is the form row — law C1's third home, not an exception to C1 — so
|
|
1948
1996
|
* `control-outside-home` treating it as a home is the whole of the enforcement a settings page
|
|
1949
1997
|
* gets, and the two tier rules deliberately do not reach inside it (see {@link SLOT_ATTRS}).
|
|
1998
|
+
* `FormRow`/`FormGroup` (`basalt-ui/forms`, `src/forms/form-layout.tsx`) are the SAME home written
|
|
1999
|
+
* for a `<form>` rather than a settings page — a `TextInput` bound to a field is read and typed
|
|
2000
|
+
* into at Mantine's own tier, not a 30px chrome affordance — so they are added here rather than
|
|
2001
|
+
* given a second walk.
|
|
1950
2002
|
*
|
|
1951
2003
|
* Declared with the shared slot walk rather than beside `control-outside-home` because three rules
|
|
1952
2004
|
* read it: that rule's ancestry exemption, and {@link hostedInsideSlot} for the two tier rules.
|
|
1953
2005
|
*/
|
|
1954
2006
|
const CONTROL_HOST_TAGS = new Set([
|
|
1955
2007
|
'SettingsRow',
|
|
2008
|
+
'FormRow',
|
|
2009
|
+
'FormGroup',
|
|
1956
2010
|
'Modal',
|
|
1957
2011
|
'Drawer',
|
|
1958
2012
|
'Popover.Dropdown',
|
|
@@ -1960,6 +2014,24 @@ const CONTROL_HOST_TAGS = new Set([
|
|
|
1960
2014
|
'Composer',
|
|
1961
2015
|
])
|
|
1962
2016
|
|
|
2017
|
+
/**
|
|
2018
|
+
* The basalt elements whose CHILDREN are a home — the SUBTREE homes, where the body IS the surface
|
|
2019
|
+
* (`docs/ASIDE-SPEC.md` §3). `FilterSet` mounts the pill surface, `PageAside` the panel surface,
|
|
2020
|
+
* `PanelRow` the row itself (its `end` prop resolves here too: the attribute hangs off `PanelRow`'s
|
|
2021
|
+
* own opening element, so the ancestry walk reaches the element).
|
|
2022
|
+
*
|
|
2023
|
+
* Read by BOTH C1 rules, because the two must not disagree about what a home IS — before this set
|
|
2024
|
+
* existed `bound-control-outside-home` treated all three as homes and `control-outside-home` knew
|
|
2025
|
+
* none of them, so a raw `<Select>` inside a `<PageAside><PanelRow>` reported while the bound
|
|
2026
|
+
* control beside it did not.
|
|
2027
|
+
*
|
|
2028
|
+
* Unlike {@link CONTROL_HOST_TAGS} these are PROVENANCE-gated, the same test
|
|
2029
|
+
* {@link SLOT_OWNER_TAGS} applies and for the same reason: `PanelRow` is an ordinary name for a
|
|
2030
|
+
* consumer's own layout helper, and reading one as a basalt home would exempt a genuinely homeless
|
|
2031
|
+
* control. The overlay hosts stay name-matched because `Modal`/`Drawer` are Mantine's, not basalt's.
|
|
2032
|
+
*/
|
|
2033
|
+
const BASALT_HOST_TAGS = new Set(['FilterSet', 'PageAside', 'PanelRow'])
|
|
2034
|
+
|
|
1963
2035
|
/**
|
|
1964
2036
|
* A file whose NAME declares it is an overlay's or a form's own body — the cross-file half of
|
|
1965
2037
|
* {@link CONTROL_HOST_TAGS}, and the one exemption here that is a CONVENTION rather than a fact
|
|
@@ -1978,10 +2050,32 @@ const CONTROL_HOST_TAGS = new Set([
|
|
|
1978
2050
|
* with 9 known false positives. `-panel.tsx` is the loosest member and the one to reconsider first
|
|
1979
2051
|
* if the set ever needs narrowing.
|
|
1980
2052
|
*
|
|
2053
|
+
* TWO dialects, because a repo picks one file-naming law and the convention has to survive either:
|
|
2054
|
+
* `edit-session-modal.tsx` (kebab) and `CbbiPanel.tsx` (PascalCase — what a repo mandating
|
|
2055
|
+
* `PascalCase.tsx` for component files writes, basalt's own root `CLAUDE.md` included). The kebab
|
|
2056
|
+
* form alone could never match a component file in such a repo, which is exactly how the CBBI panel
|
|
2057
|
+
* — a `PageAside` child whose `<PageAside>` is in the PARENT page — collected four warns that no
|
|
2058
|
+
* ancestry walk in that file could ever have avoided.
|
|
2059
|
+
*
|
|
2060
|
+
* Both dialects require the leading SUBJECT: `Panel.tsx` and `modal.tsx` are page modules in every
|
|
2061
|
+
* consumer that has one, and a bare-noun exemption would switch the rules off wholesale.
|
|
2062
|
+
*
|
|
1981
2063
|
* Basename only — a `modal/` DIRECTORY is not the convention, because a directory of modals holds
|
|
1982
2064
|
* the page pieces around them too.
|
|
1983
2065
|
*/
|
|
1984
|
-
const
|
|
2066
|
+
const OVERLAY_CONVENTION_KEBAB = /^[^/]*-(?:modal|drawer|popover|panel|form)\.[jt]sx$/
|
|
2067
|
+
const OVERLAY_CONVENTION_PASCAL = /^[A-Z]\w*(?:Modal|Drawer|Popover|Panel|Form)\.[jt]sx$/
|
|
2068
|
+
|
|
2069
|
+
/**
|
|
2070
|
+
* The single predicate both `control-outside-home` and `bound-control-outside-home` ask — see
|
|
2071
|
+
* {@link OVERLAY_CONVENTION_KEBAB} for the whole trade. One function, so the two rules can never
|
|
2072
|
+
* disagree about what the convention IS; `src/guard/index.ts` carries the text-lane copy of the
|
|
2073
|
+
* same two dialects (`raw-selection-control`), pinned by both test files.
|
|
2074
|
+
*/
|
|
2075
|
+
function isOverlayConventionFile(filename) {
|
|
2076
|
+
const basename = filename.slice(filename.lastIndexOf('/') + 1)
|
|
2077
|
+
return OVERLAY_CONVENTION_KEBAB.test(basename) || OVERLAY_CONVENTION_PASCAL.test(basename)
|
|
2078
|
+
}
|
|
1985
2079
|
|
|
1986
2080
|
/** How far an ancestry walk climbs before giving up — a JSX tree this deep is not a slot value. */
|
|
1987
2081
|
const ANCESTRY_MAX_DEPTH = 60
|
|
@@ -2190,6 +2284,34 @@ function hasAncestorTag(node, tags) {
|
|
|
2190
2284
|
return false
|
|
2191
2285
|
}
|
|
2192
2286
|
|
|
2287
|
+
/**
|
|
2288
|
+
* The BINDINGS of every JSX ancestor of `node`, nearest first — {@link hasAncestorTag} for a
|
|
2289
|
+
* PROVENANCE-gated set. It returns names rather than a boolean for the same reason
|
|
2290
|
+
* {@link slotOwnerBinding} does: whether that `PanelRow` is basalt's is answered by the import list,
|
|
2291
|
+
* which is only complete at `Program:exit`, while the ancestry is only walkable during the visit.
|
|
2292
|
+
*
|
|
2293
|
+
* `tags` is OPTIONAL, and omitting it is the second real use rather than a convenience: a rule
|
|
2294
|
+
* whose ancestor set is not known until the import list is complete cannot filter by tag NAME at
|
|
2295
|
+
* all (`RouterProvider as Router` is an ordinary thing to write), so it takes every binding and
|
|
2296
|
+
* resolves them at exit. `provider-above-router` is that caller.
|
|
2297
|
+
*/
|
|
2298
|
+
function ancestorTagBindings(node, tags) {
|
|
2299
|
+
const found = []
|
|
2300
|
+
let current = node.parent
|
|
2301
|
+
for (let depth = 0; current !== null && current !== undefined; depth++) {
|
|
2302
|
+
if (depth > ANCESTRY_MAX_DEPTH || current.type === 'Program') break
|
|
2303
|
+
if (
|
|
2304
|
+
current.type === 'JSXElement' &&
|
|
2305
|
+
(tags === undefined || tags.has(jsxTagName(current.openingElement?.name) ?? ''))
|
|
2306
|
+
) {
|
|
2307
|
+
const root = jsxRootName(current.openingElement?.name)
|
|
2308
|
+
if (root !== undefined) found.push(root)
|
|
2309
|
+
}
|
|
2310
|
+
current = current.parent
|
|
2311
|
+
}
|
|
2312
|
+
return found
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2193
2315
|
/** Identifiers a slot attribute's value resolves through — `filters={pills}`, `actions={[a, b]}`. */
|
|
2194
2316
|
function collectSlotValueIdentifiers(node, into, depth = 0) {
|
|
2195
2317
|
if (node === null || node === undefined || depth > 8) return
|
|
@@ -2239,6 +2361,14 @@ function createSlotContext(context) {
|
|
|
2239
2361
|
* depending on JSX order.
|
|
2240
2362
|
*/
|
|
2241
2363
|
const slotBoundIdentifiers = new Map()
|
|
2364
|
+
/**
|
|
2365
|
+
* Hoisted identifier → every SUBTREE-home BINDING whose children it was rendered as.
|
|
2366
|
+
*
|
|
2367
|
+
* The same hop `slotBoundIdentifiers` makes, one home shape over: `const rows = <SelectFilter/>`
|
|
2368
|
+
* handed to `<PageAside>{rows}</PageAside>` is inside that aside, and the ancestry walk cannot
|
|
2369
|
+
* see it because at the `const` there is no `PageAside` above the node at all.
|
|
2370
|
+
*/
|
|
2371
|
+
const subtreeHostedIdentifiers = new Map()
|
|
2242
2372
|
return {
|
|
2243
2373
|
/** Feed every ImportDeclaration here — this is where the owner provenance comes from. */
|
|
2244
2374
|
noteImport(node) {
|
|
@@ -2256,12 +2386,32 @@ function createSlotContext(context) {
|
|
|
2256
2386
|
else owners.add(owner)
|
|
2257
2387
|
}
|
|
2258
2388
|
},
|
|
2389
|
+
/**
|
|
2390
|
+
* Feed every JSXExpressionContainer here — a `{expr}` CHILD of a subtree home
|
|
2391
|
+
* ({@link BASALT_HOST_TAGS}) hosts the identifiers that expression resolves through.
|
|
2392
|
+
*/
|
|
2393
|
+
noteChild(node) {
|
|
2394
|
+
const parent = node.parent
|
|
2395
|
+
if (parent === null || parent === undefined || parent.type !== 'JSXElement') return
|
|
2396
|
+
const name = parent.openingElement?.name
|
|
2397
|
+
if (!BASALT_HOST_TAGS.has(jsxTagName(name) ?? '')) return
|
|
2398
|
+
const owner = jsxRootName(name)
|
|
2399
|
+
if (owner === undefined) return
|
|
2400
|
+
const identifiers = new Set()
|
|
2401
|
+
collectSlotValueIdentifiers(node.expression, identifiers)
|
|
2402
|
+
for (const id of identifiers) {
|
|
2403
|
+
const owners = subtreeHostedIdentifiers.get(id)
|
|
2404
|
+
if (owners === undefined) subtreeHostedIdentifiers.set(id, new Set([owner]))
|
|
2405
|
+
else owners.add(owner)
|
|
2406
|
+
}
|
|
2407
|
+
},
|
|
2259
2408
|
/** Snapshot `node`'s ancestry while it is walkable — the owner NAME, resolved later. */
|
|
2260
2409
|
capture(node) {
|
|
2261
2410
|
const attr = enclosingSlotAttribute(node)
|
|
2262
2411
|
return {
|
|
2263
2412
|
directOwner: attr === undefined ? undefined : slotOwnerBinding(attr),
|
|
2264
2413
|
declaratorName: enclosingDeclaratorName(node),
|
|
2414
|
+
subtreeHosts: ancestorTagBindings(node, BASALT_HOST_TAGS),
|
|
2265
2415
|
}
|
|
2266
2416
|
},
|
|
2267
2417
|
/** Is a captured node inside a BASALT home slot — directly, or via a hoisted binding? */
|
|
@@ -2281,6 +2431,21 @@ function createSlotContext(context) {
|
|
|
2281
2431
|
isHome(name) {
|
|
2282
2432
|
return name !== undefined && homes.has(name)
|
|
2283
2433
|
},
|
|
2434
|
+
/**
|
|
2435
|
+
* Is a captured node inside a basalt SUBTREE home — directly by ancestry, or via a hoisted
|
|
2436
|
+
* binding rendered as one's `{expr}` child? Both halves resolve the binding through `homes`,
|
|
2437
|
+
* so a consumer's own `PanelRow` is not one.
|
|
2438
|
+
*/
|
|
2439
|
+
inSubtreeHome(captured) {
|
|
2440
|
+
for (const binding of captured.subtreeHosts) {
|
|
2441
|
+
if (homes.has(binding)) return true
|
|
2442
|
+
}
|
|
2443
|
+
if (captured.declaratorName === undefined) return false
|
|
2444
|
+
for (const owner of subtreeHostedIdentifiers.get(captured.declaratorName) ?? []) {
|
|
2445
|
+
if (homes.has(owner)) return true
|
|
2446
|
+
}
|
|
2447
|
+
return false
|
|
2448
|
+
},
|
|
2284
2449
|
/**
|
|
2285
2450
|
* EVERY basalt EXPORT whose slot a captured node sits in — `['ChartCard']` for
|
|
2286
2451
|
* `<ChartCard actions={…}>` and for `<Card actions={…}>` under an alias, `[]` when the node is in
|
|
@@ -2303,14 +2468,24 @@ function createSlotContext(context) {
|
|
|
2303
2468
|
}
|
|
2304
2469
|
}
|
|
2305
2470
|
|
|
2306
|
-
/** Collects
|
|
2471
|
+
/** Collects the owner-definition verdict for the control layer — see {@link CONTROL_OWNER_NAMES}. */
|
|
2307
2472
|
function createControlOwnerProbe() {
|
|
2308
|
-
const state = { definesControl: false }
|
|
2473
|
+
const state = { definesControl: false, importsBasalt: false }
|
|
2309
2474
|
const note = (name) => {
|
|
2310
2475
|
if (typeof name === 'string' && CONTROL_OWNER_NAMES.has(name)) state.definesControl = true
|
|
2311
2476
|
}
|
|
2312
2477
|
return {
|
|
2313
|
-
|
|
2478
|
+
/** Feed every ImportDeclaration here — a `basalt-ui*` import disqualifies the exemption. */
|
|
2479
|
+
noteImport(node) {
|
|
2480
|
+
const source = node.source?.value
|
|
2481
|
+
if (typeof source === 'string' && BASALT_IMPORT_SOURCE.test(source)) {
|
|
2482
|
+
state.importsBasalt = true
|
|
2483
|
+
}
|
|
2484
|
+
},
|
|
2485
|
+
/** Does this file DEFINE a basalt control rather than consume one? Both halves are required. */
|
|
2486
|
+
isOwner() {
|
|
2487
|
+
return state.definesControl && !state.importsBasalt
|
|
2488
|
+
},
|
|
2314
2489
|
visitors: {
|
|
2315
2490
|
FunctionDeclaration(node) {
|
|
2316
2491
|
note(node.id?.name)
|
|
@@ -2343,6 +2518,7 @@ const HAND_ROLLED_FILTER_MESSAGE =
|
|
|
2343
2518
|
* `SettingsRow.control`, the form-row home — see {@link SLOT_ATTRS} for why a raw input is right
|
|
2344
2519
|
* there.
|
|
2345
2520
|
*/
|
|
2521
|
+
// Ships: error
|
|
2346
2522
|
const handRolledFilter = {
|
|
2347
2523
|
meta: {
|
|
2348
2524
|
type: 'problem',
|
|
@@ -2392,9 +2568,9 @@ const handRolledFilter = {
|
|
|
2392
2568
|
const CONTROL_OUTSIDE_HOME_MESSAGE =
|
|
2393
2569
|
'Raw Mantine selection control with no home — a filter, tab or action belongs in exactly one of ' +
|
|
2394
2570
|
'the three homes (a PageBar / Section / WidgetHeader slot, or a form row), and a home is entered ' +
|
|
2395
|
-
'through a slot prop (law C1). A settings row,
|
|
2396
|
-
'form (@mantine/form) are the declared non-homes and never
|
|
2397
|
-
'(basalt/control-outside-home)'
|
|
2571
|
+
'through a slot prop (law C1). A settings row, a form row (FormRow/FormGroup), an overlay ' +
|
|
2572
|
+
'(Modal/Drawer/Popover/Menu) and a form (@mantine/form) are the declared non-homes and never ' +
|
|
2573
|
+
'report. (basalt/control-outside-home)'
|
|
2398
2574
|
|
|
2399
2575
|
/**
|
|
2400
2576
|
* The cross-file half of C1, and the one rule here that is openly a HEURISTIC: "this control has
|
|
@@ -2402,11 +2578,13 @@ const CONTROL_OUTSIDE_HOME_MESSAGE =
|
|
|
2402
2578
|
* and stays warn until the playground and the five consumer repos run it with ≤3 waivers
|
|
2403
2579
|
* (docs/CONTROLS-SPEC.md §9 wave 7).
|
|
2404
2580
|
*
|
|
2405
|
-
* Four exemptions carry the false-positive load: an overlay/settings-row ancestor
|
|
2406
|
-
* {@link
|
|
2407
|
-
*
|
|
2408
|
-
*
|
|
2581
|
+
* Four exemptions carry the false-positive load: an overlay/settings-row/form-row ancestor
|
|
2582
|
+
* ({@link CONTROL_HOST_TAGS}, `FormRow`/`FormGroup` included), an {@link isOverlayConventionFile}
|
|
2583
|
+
* basename (the CROSS-FILE case — the `<Modal>` lives in the parent), a file that imports
|
|
2584
|
+
* `@mantine/form` (a form is the third home and its inputs are not filters), and the owner
|
|
2585
|
+
* exemption — a file DEFINING a basalt control cannot be told to use one.
|
|
2409
2586
|
*/
|
|
2587
|
+
// Ships: warn (grace → 1.30.0)
|
|
2410
2588
|
const controlOutsideHome = {
|
|
2411
2589
|
meta: {
|
|
2412
2590
|
type: 'suggestion',
|
|
@@ -2415,7 +2593,7 @@ const controlOutsideHome = {
|
|
|
2415
2593
|
},
|
|
2416
2594
|
create(context) {
|
|
2417
2595
|
if (isTestFile(context)) return {}
|
|
2418
|
-
if (
|
|
2596
|
+
if (isOverlayConventionFile(getFilename(context))) return {}
|
|
2419
2597
|
const slots = createSlotContext(context)
|
|
2420
2598
|
const owner = createControlOwnerProbe()
|
|
2421
2599
|
const mantineImports = new Map()
|
|
@@ -2427,11 +2605,15 @@ const controlOutsideHome = {
|
|
|
2427
2605
|
ImportDeclaration(node) {
|
|
2428
2606
|
collectMantineImports(node, mantineImports)
|
|
2429
2607
|
slots.noteImport(node)
|
|
2608
|
+
owner.noteImport(node)
|
|
2430
2609
|
if ((node.source?.value ?? '') === '@mantine/form') importsMantineForm = true
|
|
2431
2610
|
},
|
|
2432
2611
|
JSXAttribute(node) {
|
|
2433
2612
|
slots.note(node)
|
|
2434
2613
|
},
|
|
2614
|
+
JSXExpressionContainer(node) {
|
|
2615
|
+
slots.noteChild(node)
|
|
2616
|
+
},
|
|
2435
2617
|
JSXOpeningElement(node) {
|
|
2436
2618
|
// Cheap pre-filter: the tag as WRITTEN, or any Mantine binding (an alias resolves at exit).
|
|
2437
2619
|
const root = jsxRootName(node.name)
|
|
@@ -2444,11 +2626,11 @@ const controlOutsideHome = {
|
|
|
2444
2626
|
})
|
|
2445
2627
|
},
|
|
2446
2628
|
'Program:exit'() {
|
|
2447
|
-
if (importsMantineForm || owner.
|
|
2629
|
+
if (importsMantineForm || owner.isOwner()) return
|
|
2448
2630
|
for (const { node, name, captured, hosted } of candidates) {
|
|
2449
2631
|
const tag = resolveMantineTag(name, mantineImports)
|
|
2450
2632
|
if (tag === undefined || !RAW_FILTER_TAGS.has(tag)) continue
|
|
2451
|
-
if (hosted || slots.resolve(captured)) continue
|
|
2633
|
+
if (hosted || slots.resolve(captured) || slots.inSubtreeHome(captured)) continue
|
|
2452
2634
|
if (hasThemeAllow(context, node, 'control-outside-home')) continue
|
|
2453
2635
|
context.report({ node, message: CONTROL_OUTSIDE_HOME_MESSAGE })
|
|
2454
2636
|
}
|
|
@@ -2536,6 +2718,7 @@ export const CTL_THEME_TAGS = new Set([
|
|
|
2536
2718
|
*/
|
|
2537
2719
|
const TIERLESS_SLOT_OWNERS = new Set(['ChartCard'])
|
|
2538
2720
|
|
|
2721
|
+
// Ships: error
|
|
2539
2722
|
const controlSizeLiteral = {
|
|
2540
2723
|
meta: {
|
|
2541
2724
|
type: 'suggestion',
|
|
@@ -2655,6 +2838,7 @@ function actionsAttributeValue(node) {
|
|
|
2655
2838
|
* The four countable halves of C6. `error` from the start: every one of them is arithmetic over a
|
|
2656
2839
|
* literal the file wrote down, with no heuristic and no layout intent to guess at.
|
|
2657
2840
|
*/
|
|
2841
|
+
// Ships: error
|
|
2658
2842
|
const pageBarBudget = {
|
|
2659
2843
|
meta: {
|
|
2660
2844
|
type: 'problem',
|
|
@@ -2770,6 +2954,7 @@ function titleOrderOf(node) {
|
|
|
2770
2954
|
* lanes on purpose, so one `theme-allow in-body-page-title — <why>` waives both rather than
|
|
2771
2955
|
* needing a different word per enforcement surface.
|
|
2772
2956
|
*/
|
|
2957
|
+
// Ships: error
|
|
2773
2958
|
const inBodyPageTitle = {
|
|
2774
2959
|
meta: {
|
|
2775
2960
|
type: 'suggestion',
|
|
@@ -2842,6 +3027,7 @@ function subtreeControlTags(node, into = new Set(), depth = 0) {
|
|
|
2842
3027
|
* subtrees both render the same control tag. A file that DEFINES a basalt control is exempt — the
|
|
2843
3028
|
* one legitimate double mount is the CSS swap inside the control itself.
|
|
2844
3029
|
*/
|
|
3030
|
+
// Ships: error
|
|
2845
3031
|
const responsiveTwin = {
|
|
2846
3032
|
meta: {
|
|
2847
3033
|
type: 'suggestion',
|
|
@@ -2878,10 +3064,13 @@ const responsiveTwin = {
|
|
|
2878
3064
|
|
|
2879
3065
|
return {
|
|
2880
3066
|
...owner.visitors,
|
|
3067
|
+
ImportDeclaration(node) {
|
|
3068
|
+
owner.noteImport(node)
|
|
3069
|
+
},
|
|
2881
3070
|
JSXElement: inspectChildren,
|
|
2882
3071
|
JSXFragment: inspectChildren,
|
|
2883
3072
|
'Program:exit'() {
|
|
2884
|
-
if (owner.
|
|
3073
|
+
if (owner.isOwner()) return
|
|
2885
3074
|
for (const node of twins) {
|
|
2886
3075
|
if (hasThemeAllow(context, node, 'responsive-twin')) continue
|
|
2887
3076
|
context.report({ node, message: RESPONSIVE_TWIN_MESSAGE })
|
|
@@ -2920,6 +3109,7 @@ function insideNavDefinition(node) {
|
|
|
2920
3109
|
return false
|
|
2921
3110
|
}
|
|
2922
3111
|
|
|
3112
|
+
// Ships: error
|
|
2923
3113
|
const searchLiteralLink = {
|
|
2924
3114
|
meta: {
|
|
2925
3115
|
type: 'suggestion',
|
|
@@ -2953,6 +3143,7 @@ const USE_SEARCH_FROM_LITERAL_MESSAGE =
|
|
|
2953
3143
|
'(law C10) — take the value as a prop, or read it through the store field. ' +
|
|
2954
3144
|
'(basalt/use-search-from-literal)'
|
|
2955
3145
|
|
|
3146
|
+
// Ships: error
|
|
2956
3147
|
const useSearchFromLiteral = {
|
|
2957
3148
|
meta: {
|
|
2958
3149
|
type: 'suggestion',
|
|
@@ -2979,6 +3170,750 @@ const useSearchFromLiteral = {
|
|
|
2979
3170
|
},
|
|
2980
3171
|
}
|
|
2981
3172
|
|
|
3173
|
+
// ── Rule 24 — bound-control-outside-home ────────────────────────────────────────────────────────
|
|
3174
|
+
|
|
3175
|
+
/**
|
|
3176
|
+
* A bound basalt control with no home — the pill-shaped controls, and `SliderControl` is
|
|
3177
|
+
* deliberately NOT among them: it has no pill form at all. It always renders a `PanelRow`
|
|
3178
|
+
* (`docs/ASIDE-SPEC.md` §3), so a `<Section>` body or a plain page stack is a legitimate place to
|
|
3179
|
+
* write one and "this renders as a stray pill" would be false there. {@link BOUND_TAGS} is the
|
|
3180
|
+
* whole set; every member is a `FieldHandle`-bound `basalt-ui/controls` export, so unlike
|
|
3181
|
+
* {@link RAW_FILTER_TAGS} there is no question of what it is FOR — only of where it was written.
|
|
3182
|
+
*/
|
|
3183
|
+
const BOUND_CONTROL_OUTSIDE_HOME_MESSAGE =
|
|
3184
|
+
'Bound basalt control with no home — a SelectFilter / ToggleFilter / RangeFilter / ' +
|
|
3185
|
+
'MultiSelectFilter / NumberFilter / SearchFilter / ViewTabs renders as a stray pill when it is ' +
|
|
3186
|
+
'written into a Section body or a page stack. It belongs in a home slot (`filters` / `tabs` / ' +
|
|
3187
|
+
'`filtersEnd` / `actions` / `sync` / `control`), inside a <FilterSet>, or inside a <PageAside> ' +
|
|
3188
|
+
'— where the panel surface renders it as a row instead (law C1, docs/ASIDE-SPEC.md §3). ' +
|
|
3189
|
+
'SliderControl is not policed: it renders its own PanelRow and has no pill form. An overlay ' +
|
|
3190
|
+
'(Modal/Drawer/Popover/Menu), a settings row and a form row (FormRow/FormGroup) are the ' +
|
|
3191
|
+
'declared non-homes and never report. (basalt/bound-control-outside-home)'
|
|
3192
|
+
|
|
3193
|
+
/**
|
|
3194
|
+
* A BOUND basalt control that is in no home at all — ledger G5 (`docs/ASIDE-SPEC.md` §2), the half
|
|
3195
|
+
* of law C1 that had no guard: `control-outside-home` matches raw Mantine tags only, so a
|
|
3196
|
+
* `<SelectFilter field={…}/>` dropped into a `Section` body passed silently and shipped as a stray
|
|
3197
|
+
* pill.
|
|
3198
|
+
*
|
|
3199
|
+
* A NEW id rather than a widening of `control-outside-home`, per C16: a level is per-id, so
|
|
3200
|
+
* widening that rule would have landed this form at whatever level it holds, with no grace of its
|
|
3201
|
+
* own — and widening it in a minor would restart its grace besides.
|
|
3202
|
+
*
|
|
3203
|
+
* Provenance is the tag's basalt IMPORT ({@link collectBasaltImportMap}), never its bare name: a
|
|
3204
|
+
* consumer's own `SelectFilter` is not this one. The exemptions are the sibling rule's minus the
|
|
3205
|
+
* `@mantine/form` one — a form input is a raw input, and a `FieldHandle`-bound filter in a form
|
|
3206
|
+
* module is homeless there exactly as it is anywhere else.
|
|
3207
|
+
*
|
|
3208
|
+
* **There is deliberately NO text-lane twin in `src/guard`.** `raw-selection-control` can key on a
|
|
3209
|
+
* bare Mantine tag name because that name is Mantine's whatever the file does with it; a bound
|
|
3210
|
+
* control's name says nothing without the import graph and the ancestry, and a regex over a
|
|
3211
|
+
* 12-line window has neither. A text heuristic here would report a consumer's own `SelectFilter` in
|
|
3212
|
+
* its own `filters` slot — signal the plugin already has, noise the guard would add.
|
|
3213
|
+
*/
|
|
3214
|
+
// Ships: warn (grace → 1.30.0)
|
|
3215
|
+
const boundControlOutsideHome = {
|
|
3216
|
+
meta: {
|
|
3217
|
+
type: 'suggestion',
|
|
3218
|
+
docs: { description: 'Warn on a bound basalt control that is in no basalt home.' },
|
|
3219
|
+
schema: [],
|
|
3220
|
+
},
|
|
3221
|
+
create(context) {
|
|
3222
|
+
if (isTestFile(context)) return {}
|
|
3223
|
+
if (isOverlayConventionFile(getFilename(context))) return {}
|
|
3224
|
+
const slots = createSlotContext(context)
|
|
3225
|
+
const owner = createControlOwnerProbe()
|
|
3226
|
+
const controls = new Map()
|
|
3227
|
+
const ownTree = isBasaltOwnSource(getFilename(context))
|
|
3228
|
+
const candidates = []
|
|
3229
|
+
|
|
3230
|
+
return {
|
|
3231
|
+
...owner.visitors,
|
|
3232
|
+
ImportDeclaration(node) {
|
|
3233
|
+
collectBasaltImportMap(node, controls, ownTree)
|
|
3234
|
+
slots.noteImport(node)
|
|
3235
|
+
owner.noteImport(node)
|
|
3236
|
+
},
|
|
3237
|
+
JSXAttribute(node) {
|
|
3238
|
+
slots.note(node)
|
|
3239
|
+
},
|
|
3240
|
+
JSXExpressionContainer(node) {
|
|
3241
|
+
slots.noteChild(node)
|
|
3242
|
+
},
|
|
3243
|
+
JSXOpeningElement(node) {
|
|
3244
|
+
// Cheap pre-filter on the tag as WRITTEN; an alias resolves through `controls` at exit.
|
|
3245
|
+
const root = jsxRootName(node.name)
|
|
3246
|
+
if (root === undefined) return
|
|
3247
|
+
if (!BOUND_TAGS.has(jsxTagName(node.name) ?? '') && !controls.has(root)) return
|
|
3248
|
+
candidates.push({
|
|
3249
|
+
node,
|
|
3250
|
+
root,
|
|
3251
|
+
captured: slots.capture(node),
|
|
3252
|
+
hosted: hasAncestorTag(node, CONTROL_HOST_TAGS),
|
|
3253
|
+
})
|
|
3254
|
+
},
|
|
3255
|
+
'Program:exit'() {
|
|
3256
|
+
if (owner.isOwner()) return
|
|
3257
|
+
for (const { node, root, captured, hosted } of candidates) {
|
|
3258
|
+
const exported = controls.get(root)
|
|
3259
|
+
if (exported === undefined || !BOUND_TAGS.has(exported)) continue
|
|
3260
|
+
if (hosted || slots.resolve(captured) || slots.inSubtreeHome(captured)) continue
|
|
3261
|
+
if (hasThemeAllow(context, node, 'bound-control-outside-home')) continue
|
|
3262
|
+
context.report({ node, message: BOUND_CONTROL_OUTSIDE_HOME_MESSAGE })
|
|
3263
|
+
}
|
|
3264
|
+
},
|
|
3265
|
+
}
|
|
3266
|
+
},
|
|
3267
|
+
}
|
|
3268
|
+
|
|
3269
|
+
// ── Rule 25 — provider-above-router ─────────────────────────────────────────────────────────────
|
|
3270
|
+
|
|
3271
|
+
/** `@tanstack/react-router` and its subpaths — never `@tanstack/react-router-devtools`. */
|
|
3272
|
+
const TANSTACK_ROUTER_SOURCE = /^@tanstack\/react-router(?:\/|$)/
|
|
3273
|
+
|
|
3274
|
+
const PROVIDER_ABOVE_ROUTER_MESSAGE =
|
|
3275
|
+
'<BasaltProvider> is rendered INSIDE <RouterProvider> — invert them. Mantine context has to ' +
|
|
3276
|
+
'exist before any route renders, so the mount order is BasaltProvider → the data layer → ' +
|
|
3277
|
+
'RouterProvider (agent/rules/basalt-mantine.md). A route that renders a themed component ' +
|
|
3278
|
+
'before the provider mounts reads Mantine defaults, not the basalt theme. ' +
|
|
3279
|
+
'(basalt/provider-above-router)'
|
|
3280
|
+
|
|
3281
|
+
/**
|
|
3282
|
+
* `BasaltProvider` mounted BELOW a TanStack `RouterProvider` — F5, the first of the three
|
|
3283
|
+
* `basalt-mantine.md` laws that shipped under a `not guarded: —` banner.
|
|
3284
|
+
*
|
|
3285
|
+
* Both tags are provenance-gated: `BasaltProvider` through {@link collectBasaltImportMap} (so a
|
|
3286
|
+
* consumer's own component of that name is not this one) and `RouterProvider` through an
|
|
3287
|
+
* `@tanstack/react-router` import, by its LOCAL binding rather than its written name. The inverse
|
|
3288
|
+
* nesting — the correct one — is not a finding, and neither is either tag alone.
|
|
3289
|
+
*
|
|
3290
|
+
* **STATIC JSX ancestry only, and that is the whole heuristic.** The two providers have to be
|
|
3291
|
+
* written in one JSX tree in one file for this to see anything. The common real shape (`main.tsx`
|
|
3292
|
+
* renders `<BasaltProvider><App/></BasaltProvider>` and `App` renders `<RouterProvider/>`) is
|
|
3293
|
+
* INVERTED cross-file and correct; the inverted-and-wrong cross-file shape is equally invisible.
|
|
3294
|
+
* A rule that followed the composition would need the module graph, which an oxlint rule does not
|
|
3295
|
+
* have — so this catches the one-file case and the law stays advisory for the rest. That is why it
|
|
3296
|
+
* lands at `warn`.
|
|
3297
|
+
*/
|
|
3298
|
+
// Ships: warn (grace → 1.30.0)
|
|
3299
|
+
const providerAboveRouter = {
|
|
3300
|
+
meta: {
|
|
3301
|
+
type: 'suggestion',
|
|
3302
|
+
docs: { description: 'Warn on a BasaltProvider rendered inside a TanStack RouterProvider.' },
|
|
3303
|
+
schema: [],
|
|
3304
|
+
},
|
|
3305
|
+
create(context) {
|
|
3306
|
+
if (isTestFile(context)) return {}
|
|
3307
|
+
const ownTree = isBasaltOwnSource(getFilename(context))
|
|
3308
|
+
const basalt = new Map()
|
|
3309
|
+
const routers = new Set()
|
|
3310
|
+
const candidates = []
|
|
3311
|
+
|
|
3312
|
+
return {
|
|
3313
|
+
ImportDeclaration(node) {
|
|
3314
|
+
collectBasaltImportMap(node, basalt, ownTree)
|
|
3315
|
+
const source = node.source?.value
|
|
3316
|
+
if (typeof source !== 'string' || !TANSTACK_ROUTER_SOURCE.test(source)) return
|
|
3317
|
+
for (const spec of node.specifiers ?? []) {
|
|
3318
|
+
const imported = spec.imported?.name ?? spec.imported?.value
|
|
3319
|
+
if (imported === 'RouterProvider' && typeof spec.local?.name === 'string') {
|
|
3320
|
+
routers.add(spec.local.name)
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
},
|
|
3324
|
+
JSXOpeningElement(node) {
|
|
3325
|
+
const root = jsxRootName(node.name)
|
|
3326
|
+
if (root === undefined || !basalt.has(root)) return
|
|
3327
|
+
candidates.push({ node, root, ancestors: ancestorTagBindings(node) })
|
|
3328
|
+
},
|
|
3329
|
+
'Program:exit'() {
|
|
3330
|
+
for (const { node, root, ancestors } of candidates) {
|
|
3331
|
+
if (basalt.get(root) !== 'BasaltProvider') continue
|
|
3332
|
+
if (!ancestors.some((name) => routers.has(name))) continue
|
|
3333
|
+
if (hasThemeAllow(context, node, 'provider-above-router')) continue
|
|
3334
|
+
context.report({ node, message: PROVIDER_ABOVE_ROUTER_MESSAGE })
|
|
3335
|
+
}
|
|
3336
|
+
},
|
|
3337
|
+
}
|
|
3338
|
+
},
|
|
3339
|
+
}
|
|
3340
|
+
|
|
3341
|
+
// ── Rule 26 — duplicate-notifications-mount ─────────────────────────────────────────────────────
|
|
3342
|
+
|
|
3343
|
+
const DUPLICATE_NOTIFICATIONS_MOUNT_MESSAGE =
|
|
3344
|
+
'<BasaltNotifications /> mounted alongside <BasaltOverlays notifications> — the two double-mount ' +
|
|
3345
|
+
"Mantine's <Notifications>, so a single notify() renders twice. BasaltOverlays already composes " +
|
|
3346
|
+
'the notifications layer; drop the standalone mount, or pass `notifications={false}` to ' +
|
|
3347
|
+
'BasaltOverlays if the standalone one is the mount you want. ' +
|
|
3348
|
+
'(basalt/duplicate-notifications-mount)'
|
|
3349
|
+
|
|
3350
|
+
/**
|
|
3351
|
+
* The ancestor chain from `node` up to its `Program`, OUTERMOST first — captured during the visit,
|
|
3352
|
+
* because `node.parent` is only walkable then, and compared at `Program:exit`. Unlike
|
|
3353
|
+
* {@link ancestorTagBindings} it keeps the NODES, which is what makes a nearest-common-ancestor
|
|
3354
|
+
* question answerable at all.
|
|
3355
|
+
*/
|
|
3356
|
+
function ancestorChain(node) {
|
|
3357
|
+
const chain = []
|
|
3358
|
+
let current = node
|
|
3359
|
+
for (let depth = 0; current !== null && current !== undefined; depth++) {
|
|
3360
|
+
if (depth > ANCESTRY_MAX_DEPTH) break
|
|
3361
|
+
chain.push(current)
|
|
3362
|
+
if (current.type === 'Program') break
|
|
3363
|
+
current = current.parent
|
|
3364
|
+
}
|
|
3365
|
+
return chain.toReversed()
|
|
3366
|
+
}
|
|
3367
|
+
|
|
3368
|
+
/**
|
|
3369
|
+
* True when two nodes sit in DIFFERENT branches of ONE conditional — `a ? <X/> : <Y/>`, or the two
|
|
3370
|
+
* operands of one `&&`/`||`. Exactly one of the pair ever renders, so a double-mount rule firing on
|
|
3371
|
+
* them would be reporting a mount that cannot happen.
|
|
3372
|
+
*
|
|
3373
|
+
* Deliberately the NEAREST common ancestor only, and deliberately expression-level: two separate
|
|
3374
|
+
* `cond && <X/>` expressions under one parent are NOT exclusive (both conditions can hold), and an
|
|
3375
|
+
* `if`/`return` split across statements is a control-flow question this does not pretend to answer.
|
|
3376
|
+
* The runtime guard in `src/commands/overlays-mount.tsx` is the lane that counts real mounts.
|
|
3377
|
+
*/
|
|
3378
|
+
function mutuallyExclusive(chainA, chainB) {
|
|
3379
|
+
let i = 0
|
|
3380
|
+
while (i < chainA.length && i < chainB.length && chainA[i] === chainB[i]) i++
|
|
3381
|
+
const ancestor = chainA[i - 1]
|
|
3382
|
+
const branchA = chainA[i]
|
|
3383
|
+
const branchB = chainB[i]
|
|
3384
|
+
if (ancestor === undefined || branchA === undefined || branchB === undefined) return false
|
|
3385
|
+
const [first, second] =
|
|
3386
|
+
ancestor.type === 'ConditionalExpression'
|
|
3387
|
+
? [ancestor.consequent, ancestor.alternate]
|
|
3388
|
+
: ancestor.type === 'LogicalExpression'
|
|
3389
|
+
? [ancestor.left, ancestor.right]
|
|
3390
|
+
: [undefined, undefined]
|
|
3391
|
+
if (first === undefined || second === undefined) return false
|
|
3392
|
+
return (branchA === first && branchB === second) || (branchA === second && branchB === first)
|
|
3393
|
+
}
|
|
3394
|
+
|
|
3395
|
+
/**
|
|
3396
|
+
* `<BasaltOverlays notifications>` and `<BasaltNotifications />` in ONE file — F5, the second
|
|
3397
|
+
* `basalt-mantine.md` law with no guard, and the static half of a warning that already exists at
|
|
3398
|
+
* runtime (`src/commands/overlays-mount.tsx` warns in dev when both mount).
|
|
3399
|
+
*
|
|
3400
|
+
* A pair split across the two branches of one conditional is EXEMPT: `a ? <BasaltNotifications/> :
|
|
3401
|
+
* <BasaltOverlays/>` is a file with two mounts written and one mount rendered, and reporting it
|
|
3402
|
+
* taught a consumer to reach for a waiver over code that was already right.
|
|
3403
|
+
*
|
|
3404
|
+
* `notifications` DEFAULTS to true on `BasaltOverlays`, so the absence of the attribute counts as
|
|
3405
|
+
* enabled and only an explicit `notifications={false}` clears it — the same polarity the component
|
|
3406
|
+
* itself reads. Both tags are provenance-gated through {@link collectBasaltImportMap}.
|
|
3407
|
+
*
|
|
3408
|
+
* Same-FILE only, and deliberately so: the runtime warning is the lane that sees a cross-file
|
|
3409
|
+
* double mount, because it counts real mounts rather than JSX. This is the cheap half that catches
|
|
3410
|
+
* it before the app runs, which is the whole reason a static twin is worth having.
|
|
3411
|
+
*/
|
|
3412
|
+
// Ships: warn (grace → 1.30.0)
|
|
3413
|
+
const duplicateNotificationsMount = {
|
|
3414
|
+
meta: {
|
|
3415
|
+
type: 'suggestion',
|
|
3416
|
+
docs: {
|
|
3417
|
+
description: 'Warn when BasaltOverlays and a standalone BasaltNotifications both mount.',
|
|
3418
|
+
},
|
|
3419
|
+
schema: [],
|
|
3420
|
+
},
|
|
3421
|
+
create(context) {
|
|
3422
|
+
if (isTestFile(context)) return {}
|
|
3423
|
+
const ownTree = isBasaltOwnSource(getFilename(context))
|
|
3424
|
+
const basalt = new Map()
|
|
3425
|
+
const mounts = []
|
|
3426
|
+
|
|
3427
|
+
return {
|
|
3428
|
+
ImportDeclaration(node) {
|
|
3429
|
+
collectBasaltImportMap(node, basalt, ownTree)
|
|
3430
|
+
},
|
|
3431
|
+
JSXOpeningElement(node) {
|
|
3432
|
+
const root = jsxRootName(node.name)
|
|
3433
|
+
if (root === undefined || !basalt.has(root)) return
|
|
3434
|
+
// `notifications` DEFAULTS to true, so only an explicit `={false}` clears the layer.
|
|
3435
|
+
const disabled = (node.attributes ?? []).some(
|
|
3436
|
+
(attr) =>
|
|
3437
|
+
attr.type === 'JSXAttribute' &&
|
|
3438
|
+
attr.name?.name === 'notifications' &&
|
|
3439
|
+
unwrapExpressionContainer(attr.value)?.value === false,
|
|
3440
|
+
)
|
|
3441
|
+
mounts.push({ node, root, disabled, chain: ancestorChain(node) })
|
|
3442
|
+
},
|
|
3443
|
+
'Program:exit'() {
|
|
3444
|
+
const overlays = mounts.filter(
|
|
3445
|
+
({ root, disabled }) => basalt.get(root) === 'BasaltOverlays' && !disabled,
|
|
3446
|
+
)
|
|
3447
|
+
if (overlays.length === 0) return
|
|
3448
|
+
for (const { node, root, chain } of mounts) {
|
|
3449
|
+
if (basalt.get(root) !== 'BasaltNotifications') continue
|
|
3450
|
+
// Every enabled overlays mount on the other branch of one conditional → nothing to report.
|
|
3451
|
+
if (overlays.every((overlay) => mutuallyExclusive(overlay.chain, chain))) continue
|
|
3452
|
+
if (hasThemeAllow(context, node, 'duplicate-notifications-mount')) continue
|
|
3453
|
+
context.report({ node, message: DUPLICATE_NOTIFICATIONS_MOUNT_MESSAGE })
|
|
3454
|
+
}
|
|
3455
|
+
},
|
|
3456
|
+
}
|
|
3457
|
+
},
|
|
3458
|
+
}
|
|
3459
|
+
|
|
3460
|
+
// ── Rule 27 — query-dual-import ─────────────────────────────────────────────────────────────────
|
|
3461
|
+
|
|
3462
|
+
/** `@tanstack/react-query` and its subpaths — never `@tanstack/react-query-devtools`. */
|
|
3463
|
+
const TANSTACK_QUERY_SOURCE = /^@tanstack\/react-query(?:\/|$)/
|
|
3464
|
+
|
|
3465
|
+
/** The `basalt-ui/query` subpath, exactly — the seam the hooks are supposed to arrive through. */
|
|
3466
|
+
const BASALT_QUERY_SOURCE = 'basalt-ui/query'
|
|
3467
|
+
|
|
3468
|
+
/** The root barrel — where `QueryState` and the dashboard composites live. */
|
|
3469
|
+
const BASALT_ROOT_SOURCE = 'basalt-ui'
|
|
3470
|
+
|
|
3471
|
+
const QUERY_DUAL_IMPORT_MESSAGE =
|
|
3472
|
+
'Dual query import — this file imports from @tanstack/react-query AND from basalt-ui/query. ' +
|
|
3473
|
+
'Take the hooks, the client factory and `unwrap` from basalt-ui/query only: two entry points to ' +
|
|
3474
|
+
'one library is how a second QueryClient, a second devtools bundle and a duplicated ' +
|
|
3475
|
+
'`toErrorMessage` reach an app (agent/rules/basalt-batteries.md). ' +
|
|
3476
|
+
'(basalt/query-dual-import)'
|
|
3477
|
+
|
|
3478
|
+
const QUERY_ROOT_IMPORT_MESSAGE =
|
|
3479
|
+
'Raw @tanstack/react-query import in a basalt file — the hooks come through basalt-ui/query, ' +
|
|
3480
|
+
'which re-exports them beside `createBasaltQueryClient`, `unwrap`, `toErrorMessage` and the lazy ' +
|
|
3481
|
+
'devtools (agent/rules/basalt-batteries.md). Import from basalt-ui/query instead. ' +
|
|
3482
|
+
'(basalt/query-dual-import)'
|
|
3483
|
+
|
|
3484
|
+
/**
|
|
3485
|
+
* The `basalt-batteries.md` law "import query hooks from `basalt-ui/query`, never dual-import
|
|
3486
|
+
* `@tanstack/react-query`" — F5, and one of the two `./query` surfaces that shipped advisory.
|
|
3487
|
+
*
|
|
3488
|
+
* TWO forms, one id, because they are one law read at two strengths. The DUAL form (both
|
|
3489
|
+
* specifiers present) is the law verbatim. The softer form — a raw `@tanstack/react-query` import
|
|
3490
|
+
* in a file that also imports the basalt root barrel — is a heuristic: it says "this file is
|
|
3491
|
+
* already a basalt file, so the query seam is right there", and it is the form that actually
|
|
3492
|
+
* catches the drift, since a file that has never heard of `basalt-ui/query` cannot dual-import.
|
|
3493
|
+
* The softer form is why the whole rule lands at `warn` rather than `error`.
|
|
3494
|
+
*
|
|
3495
|
+
* Gated on the LITERAL specifier, never on `ownTree` relative resolution: basalt's own
|
|
3496
|
+
* `src/query/**` and `src/dashboard/query-state.tsx` import `@tanstack/react-query` because they
|
|
3497
|
+
* ARE the seam, and they reach their siblings relatively. A file that writes `basalt-ui/query` is
|
|
3498
|
+
* by definition a consumer of the published surface.
|
|
3499
|
+
*/
|
|
3500
|
+
// Ships: warn (grace → 1.30.0)
|
|
3501
|
+
const queryDualImport = {
|
|
3502
|
+
meta: {
|
|
3503
|
+
type: 'suggestion',
|
|
3504
|
+
docs: { description: 'Warn on a raw @tanstack/react-query import beside a basalt import.' },
|
|
3505
|
+
schema: [],
|
|
3506
|
+
},
|
|
3507
|
+
create(context) {
|
|
3508
|
+
if (isTestFile(context)) return {}
|
|
3509
|
+
const raw = []
|
|
3510
|
+
let basaltQuery = false
|
|
3511
|
+
let basaltRoot = false
|
|
3512
|
+
|
|
3513
|
+
return {
|
|
3514
|
+
ImportDeclaration(node) {
|
|
3515
|
+
const source = node.source?.value
|
|
3516
|
+
if (typeof source !== 'string') return
|
|
3517
|
+
if (source === BASALT_QUERY_SOURCE) basaltQuery = true
|
|
3518
|
+
else if (source === BASALT_ROOT_SOURCE) basaltRoot = true
|
|
3519
|
+
// `import type { UseQueryResult } from '@tanstack/react-query'` is erased at compile time
|
|
3520
|
+
// — there is no runtime import to route through the seam, and typing against the library's
|
|
3521
|
+
// own result type is exactly what `QueryStateLike` documents as legitimate. Same skip
|
|
3522
|
+
// `agent-no-raw-usechat` and `ai-sdk-major` take.
|
|
3523
|
+
else if (TANSTACK_QUERY_SOURCE.test(source) && node.importKind !== 'type') raw.push(node)
|
|
3524
|
+
},
|
|
3525
|
+
'Program:exit'() {
|
|
3526
|
+
if (!basaltQuery && !basaltRoot) return
|
|
3527
|
+
const message = basaltQuery ? QUERY_DUAL_IMPORT_MESSAGE : QUERY_ROOT_IMPORT_MESSAGE
|
|
3528
|
+
for (const node of raw) {
|
|
3529
|
+
if (hasThemeAllow(context, node, 'query-dual-import')) continue
|
|
3530
|
+
context.report({ node, message })
|
|
3531
|
+
}
|
|
3532
|
+
},
|
|
3533
|
+
}
|
|
3534
|
+
},
|
|
3535
|
+
}
|
|
3536
|
+
|
|
3537
|
+
// ── Rule 28 — query-fn-unwrap ───────────────────────────────────────────────────────────────────
|
|
3538
|
+
|
|
3539
|
+
const QUERY_FN_UNWRAP_MESSAGE =
|
|
3540
|
+
'queryFn fetches without unwrap() — `unwrap` throws on the error branch AND on a null data with ' +
|
|
3541
|
+
'no error (a 204, a silent transport failure), and both are caller bugs that have to surface as ' +
|
|
3542
|
+
'a rejected query rather than as `data: null` rendered through the success branch ' +
|
|
3543
|
+
'(agent/rules/basalt-batteries.md). Wrap the call: `queryFn: () => unwrap(client.thing.get())`. ' +
|
|
3544
|
+
'(basalt/query-fn-unwrap)'
|
|
3545
|
+
|
|
3546
|
+
/** A bare `fetch(` / `unwrap(` call, as written — the two halves of this rule's text heuristic. */
|
|
3547
|
+
const FETCH_CALL = /\bfetch\s*\(/
|
|
3548
|
+
const UNWRAP_CALL = /\bunwrap\s*\(/
|
|
3549
|
+
|
|
3550
|
+
/**
|
|
3551
|
+
* A `queryFn` that fetches and never `unwrap`s it — F5, the second unguarded `./query` law.
|
|
3552
|
+
*
|
|
3553
|
+
* **The heuristic is deliberately one sentence long, and it is a TEXT test inside a SYNTACTIC
|
|
3554
|
+
* range**: the rule takes the source span of the `queryFn` property's own function value and fires
|
|
3555
|
+
* only when `fetch(` appears in it and `unwrap(` does not. Nothing resolves, nothing follows a
|
|
3556
|
+
* helper out of the span.
|
|
3557
|
+
*
|
|
3558
|
+
* What that buys and what it costs, both stated so neither is a surprise. It sees the two shapes
|
|
3559
|
+
* the doctrine is actually written against — `queryFn: () => fetch(url)` and
|
|
3560
|
+
* `queryFn: async () => (await fetch(url)).json()`. It does NOT see a `queryFn: fetchThing`
|
|
3561
|
+
* reference, an Eden/Treaty call (`client.thing.get()` contains no `fetch(`), or an `unwrap`
|
|
3562
|
+
* applied one frame out; and it counts an `unwrap(` anywhere in the span, including one applied to
|
|
3563
|
+
* something else. A stricter version would need the call graph. The narrow version fires on the
|
|
3564
|
+
* form a consumer writes by reflex on day one, which is the one the law exists for.
|
|
3565
|
+
*
|
|
3566
|
+
* Scoped to files importing `basalt-ui/query` literally, for the reason {@link queryDualImport}
|
|
3567
|
+
* gives: without that gate it is an opinion about `fetch`, which basalt does not have.
|
|
3568
|
+
*/
|
|
3569
|
+
// Ships: warn (grace → 1.30.0)
|
|
3570
|
+
const queryFnUnwrap = {
|
|
3571
|
+
meta: {
|
|
3572
|
+
type: 'suggestion',
|
|
3573
|
+
docs: { description: 'Warn on a queryFn that fetches without wrapping the call in unwrap().' },
|
|
3574
|
+
schema: [],
|
|
3575
|
+
},
|
|
3576
|
+
create(context) {
|
|
3577
|
+
if (isTestFile(context)) return {}
|
|
3578
|
+
const candidates = []
|
|
3579
|
+
let basaltQuery = false
|
|
3580
|
+
|
|
3581
|
+
return {
|
|
3582
|
+
ImportDeclaration(node) {
|
|
3583
|
+
if (node.source?.value === BASALT_QUERY_SOURCE) basaltQuery = true
|
|
3584
|
+
},
|
|
3585
|
+
Property(node) {
|
|
3586
|
+
if (propertyKeyName(node) !== 'queryFn') return
|
|
3587
|
+
const value = node.value
|
|
3588
|
+
if (value?.type !== 'ArrowFunctionExpression' && value?.type !== 'FunctionExpression') {
|
|
3589
|
+
return
|
|
3590
|
+
}
|
|
3591
|
+
candidates.push({ node, value })
|
|
3592
|
+
},
|
|
3593
|
+
'Program:exit'() {
|
|
3594
|
+
if (!basaltQuery) return
|
|
3595
|
+
const text = context.sourceCode?.text ?? ''
|
|
3596
|
+
for (const { node, value } of candidates) {
|
|
3597
|
+
const [start, end] = value.range ?? []
|
|
3598
|
+
if (typeof start !== 'number' || typeof end !== 'number') continue
|
|
3599
|
+
const body = text.slice(start, end)
|
|
3600
|
+
if (!FETCH_CALL.test(body) || UNWRAP_CALL.test(body)) continue
|
|
3601
|
+
if (hasThemeAllow(context, node, 'query-fn-unwrap')) continue
|
|
3602
|
+
context.report({ node, message: QUERY_FN_UNWRAP_MESSAGE })
|
|
3603
|
+
}
|
|
3604
|
+
},
|
|
3605
|
+
}
|
|
3606
|
+
},
|
|
3607
|
+
}
|
|
3608
|
+
|
|
3609
|
+
// ── Rule 29 — deprecated-export ─────────────────────────────────────────────────────────────────
|
|
3610
|
+
|
|
3611
|
+
/**
|
|
3612
|
+
* @typedef {{
|
|
3613
|
+
* subpath: string
|
|
3614
|
+
* name: string
|
|
3615
|
+
* prop?: string
|
|
3616
|
+
* replacement: string
|
|
3617
|
+
* removeIn: string
|
|
3618
|
+
* fix?: false
|
|
3619
|
+
* }} DeprecatedExport
|
|
3620
|
+
*/
|
|
3621
|
+
|
|
3622
|
+
/**
|
|
3623
|
+
* The deprecation ledger — every export that is `@deprecated` but still SHIPPED, with what to
|
|
3624
|
+
* write instead and the minor it goes away in. B4: majors are banned here, so a rename cannot be
|
|
3625
|
+
* signalled by a version number; the lifecycle is instead "ship the alias, lint the old name, write
|
|
3626
|
+
* the MIGRATING row, remove it a minor later" (`../CLAUDE.md`, "Deprecation lifecycle").
|
|
3627
|
+
*
|
|
3628
|
+
* A row without `prop` is a NAMED IMPORT from `subpath` — reported on the specifier, with an
|
|
3629
|
+
* autofix that rewrites it to `{ <replacement> as <local> }` so no call site has to move in the
|
|
3630
|
+
* same edit. **That autofix is the row's contract**: only a DROP-IN rename belongs here. A
|
|
3631
|
+
* deprecation whose replacement takes a different shape (`createSearchParamStore` →
|
|
3632
|
+
* `createSearchStore` + a `fields` object) would need the fix suppressed before it could be a row,
|
|
3633
|
+
* and until then it lives in `MIGRATING.md` alone. A row WITH `prop` is a JSX ATTRIBUTE on the component `name`, imported from `subpath`
|
|
3634
|
+
* — no autofix, because the three connectivity props collapse into ONE object prop and a
|
|
3635
|
+
* per-attribute rewrite would produce three `connectivity=` attributes on one tag.
|
|
3636
|
+
*
|
|
3637
|
+
* **Adding a row is the whole job.** Deprecate the export in its JSDoc, add the row here with a
|
|
3638
|
+
* `removeIn` at least one minor out, add the `MIGRATING.md` row, and this rule starts nudging.
|
|
3639
|
+
* `oxlint-plugin.test.ts` scans every `src/**\/index.ts(x)` barrel for a JSDoc `@deprecated` and
|
|
3640
|
+
* fails when one has no row here, so the barrel half cannot be forgotten. Removing the export is a
|
|
3641
|
+
* separate commit that deletes the row in the same change.
|
|
3642
|
+
*
|
|
3643
|
+
* @type {readonly DeprecatedExport[]}
|
|
3644
|
+
*/
|
|
3645
|
+
export const DEPRECATED_EXPORTS = [
|
|
3646
|
+
{
|
|
3647
|
+
subpath: 'basalt-ui/forms',
|
|
3648
|
+
name: 'field',
|
|
3649
|
+
replacement: 'inputProps',
|
|
3650
|
+
removeIn: '1.29.0',
|
|
3651
|
+
// Not a drop-in: `inputProps` no longer returns `key`, so a rename alone would silently drop
|
|
3652
|
+
// the reconciler key. The nudge stays; the rewrite is `basalt/forms-field-key`'s job.
|
|
3653
|
+
fix: false,
|
|
3654
|
+
},
|
|
3655
|
+
{
|
|
3656
|
+
subpath: 'basalt-ui',
|
|
3657
|
+
name: 'BasaltProvider',
|
|
3658
|
+
prop: 'sseUrl',
|
|
3659
|
+
replacement: 'connectivity={{ sseUrl }}',
|
|
3660
|
+
removeIn: '1.29.0',
|
|
3661
|
+
},
|
|
3662
|
+
{
|
|
3663
|
+
subpath: 'basalt-ui',
|
|
3664
|
+
name: 'BasaltProvider',
|
|
3665
|
+
prop: 'healthUrl',
|
|
3666
|
+
replacement: 'connectivity={{ healthUrl }}',
|
|
3667
|
+
removeIn: '1.29.0',
|
|
3668
|
+
},
|
|
3669
|
+
{
|
|
3670
|
+
subpath: 'basalt-ui',
|
|
3671
|
+
name: 'BasaltProvider',
|
|
3672
|
+
prop: 'healthIntervalMs',
|
|
3673
|
+
replacement: 'connectivity={{ healthIntervalMs }}',
|
|
3674
|
+
removeIn: '1.29.0',
|
|
3675
|
+
},
|
|
3676
|
+
]
|
|
3677
|
+
|
|
3678
|
+
function deprecatedExportMessage(row) {
|
|
3679
|
+
const what =
|
|
3680
|
+
row.prop === undefined
|
|
3681
|
+
? `\`${row.name}\` from ${row.subpath}`
|
|
3682
|
+
: `\`${row.prop}\` on <${row.name}>`
|
|
3683
|
+
return (
|
|
3684
|
+
`Deprecated basalt export — ${what} is superseded by \`${row.replacement}\` and is removed in ` +
|
|
3685
|
+
`${row.removeIn}. It still resolves today, so nothing is broken yet; migrate before that minor ` +
|
|
3686
|
+
'(packages/basalt-ui/MIGRATING.md carries the row). ' +
|
|
3687
|
+
'(basalt/deprecated-export)'
|
|
3688
|
+
)
|
|
3689
|
+
}
|
|
3690
|
+
|
|
3691
|
+
/**
|
|
3692
|
+
* A shipped-but-`@deprecated` export still being imported or written — B4, the sunset half of the
|
|
3693
|
+
* lifecycle whose absence the blueprint audit found (§8): the grace mechanism covers a rule getting
|
|
3694
|
+
* STRICTER, and nothing covered an export going away.
|
|
3695
|
+
*
|
|
3696
|
+
* Permanently `warn` ({@link PLUGIN_RULE_ADVISORY}), for the same reason `shadow-basalt-export` is:
|
|
3697
|
+
* a deprecation is a schedule, not a defect. The code compiles and runs; promoting this to `error`
|
|
3698
|
+
* would break a consumer's build for something that still works, which is the one thing the
|
|
3699
|
+
* no-majors doctrine exists to avoid. The `removeIn` minor is where it becomes an error — by the
|
|
3700
|
+
* export ceasing to exist.
|
|
3701
|
+
*/
|
|
3702
|
+
// Ships: warn (advisory)
|
|
3703
|
+
const deprecatedExport = {
|
|
3704
|
+
meta: {
|
|
3705
|
+
type: 'suggestion',
|
|
3706
|
+
docs: { description: 'Warn on an import or prop that basalt has deprecated but still ships.' },
|
|
3707
|
+
schema: [],
|
|
3708
|
+
fixable: 'code',
|
|
3709
|
+
},
|
|
3710
|
+
create(context) {
|
|
3711
|
+
if (isTestFile(context)) return {}
|
|
3712
|
+
if (isBasaltOwnSource(getFilename(context))) return {}
|
|
3713
|
+
const importRows = DEPRECATED_EXPORTS.filter((row) => row.prop === undefined)
|
|
3714
|
+
const propRows = DEPRECATED_EXPORTS.filter((row) => row.prop !== undefined)
|
|
3715
|
+
const components = new Map()
|
|
3716
|
+
const candidates = []
|
|
3717
|
+
|
|
3718
|
+
return {
|
|
3719
|
+
ImportDeclaration(node) {
|
|
3720
|
+
const source = node.source?.value
|
|
3721
|
+
if (typeof source !== 'string') return
|
|
3722
|
+
for (const spec of node.specifiers ?? []) {
|
|
3723
|
+
const imported = spec.imported?.name ?? spec.imported?.value
|
|
3724
|
+
const local = spec.local?.name
|
|
3725
|
+
if (typeof imported !== 'string' || typeof local !== 'string') continue
|
|
3726
|
+
for (const row of propRows) {
|
|
3727
|
+
if (source === row.subpath && imported === row.name) components.set(local, row.name)
|
|
3728
|
+
}
|
|
3729
|
+
const row = importRows.find((r) => r.subpath === source && r.name === imported)
|
|
3730
|
+
if (row === undefined) continue
|
|
3731
|
+
if (hasThemeAllow(context, spec, 'deprecated-export')) continue
|
|
3732
|
+
context.report({
|
|
3733
|
+
node: spec,
|
|
3734
|
+
message: deprecatedExportMessage(row),
|
|
3735
|
+
...(row.fix === false
|
|
3736
|
+
? {}
|
|
3737
|
+
: { fix: (fixer) => fixer.replaceText(spec, `${row.replacement} as ${local}`) }),
|
|
3738
|
+
})
|
|
3739
|
+
}
|
|
3740
|
+
},
|
|
3741
|
+
JSXAttribute(node) {
|
|
3742
|
+
const name = node.name?.name
|
|
3743
|
+
if (typeof name !== 'string') return
|
|
3744
|
+
const owner = node.parent
|
|
3745
|
+
if (owner === null || owner === undefined || owner.type !== 'JSXOpeningElement') return
|
|
3746
|
+
const root = jsxRootName(owner.name)
|
|
3747
|
+
if (root === undefined) return
|
|
3748
|
+
candidates.push({ node, root, name })
|
|
3749
|
+
},
|
|
3750
|
+
'Program:exit'() {
|
|
3751
|
+
for (const { node, root, name } of candidates) {
|
|
3752
|
+
const component = components.get(root)
|
|
3753
|
+
if (component === undefined) continue
|
|
3754
|
+
const row = propRows.find((r) => r.name === component && r.prop === name)
|
|
3755
|
+
if (row === undefined) continue
|
|
3756
|
+
if (hasThemeAllow(context, node, 'deprecated-export')) continue
|
|
3757
|
+
context.report({ node, message: deprecatedExportMessage(row) })
|
|
3758
|
+
}
|
|
3759
|
+
},
|
|
3760
|
+
}
|
|
3761
|
+
},
|
|
3762
|
+
}
|
|
3763
|
+
|
|
3764
|
+
// ── Rule 30 — forms-field-key ───────────────────────────────────────────────────────────────────
|
|
3765
|
+
|
|
3766
|
+
/** The subpath the forms idiom is imported from — the only provenance this rule accepts. */
|
|
3767
|
+
const FORMS_IMPORT_SOURCE = 'basalt-ui/forms'
|
|
3768
|
+
|
|
3769
|
+
/** The three names this rule resolves through the local→imported map. */
|
|
3770
|
+
const FORMS_SPREAD_NAMES = new Set(['inputProps', 'field'])
|
|
3771
|
+
|
|
3772
|
+
const FORMS_FIELD_KEY_MESSAGES = {
|
|
3773
|
+
missing:
|
|
3774
|
+
'A spread `inputProps(…)` with no sibling `key`. The field loses its reconciler key, so an ' +
|
|
3775
|
+
'uncontrolled input keeps its old text through `form.reset()`, `form.setValues()` and a ' +
|
|
3776
|
+
'removed list row — nothing throws and nothing type-checks differently. Write ' +
|
|
3777
|
+
'`key={fieldKey(form, path)} {...inputProps(form, path)}`: `inputProps` stopped returning ' +
|
|
3778
|
+
'`key` because a spread `key` is a React 19 warning, and a call site that merely kept the ' +
|
|
3779
|
+
'spread compiles unchanged and silently stops resetting. ' +
|
|
3780
|
+
'(basalt/forms-field-key)',
|
|
3781
|
+
deprecatedAlias:
|
|
3782
|
+
'A spread `field(…)`. That is the @deprecated 1.27 alias and it still bundles `key` INTO the ' +
|
|
3783
|
+
'returned object, so it still logs React 19\'s `A props object containing a "key" prop is ' +
|
|
3784
|
+
'being spread into JSX` on every render — kept byte-identical on purpose, because a ' +
|
|
3785
|
+
'deprecation is a schedule and not a silent behaviour change. Migrate to ' +
|
|
3786
|
+
'`key={fieldKey(form, path)} {...inputProps(form, path)}` (packages/basalt-ui/MIGRATING.md ' +
|
|
3787
|
+
'carries the row); the alias itself is removed in 1.29.0. ' +
|
|
3788
|
+
'(basalt/forms-field-key)',
|
|
3789
|
+
}
|
|
3790
|
+
|
|
3791
|
+
/** The `key` attribute among a JSXOpeningElement's attributes, if it has one written literally. */
|
|
3792
|
+
function hasKeyAttribute(opening) {
|
|
3793
|
+
for (const attr of opening?.attributes ?? []) {
|
|
3794
|
+
if (attr?.type === 'JSXAttribute' && attr.name?.name === 'key') return true
|
|
3795
|
+
}
|
|
3796
|
+
return false
|
|
3797
|
+
}
|
|
3798
|
+
|
|
3799
|
+
/**
|
|
3800
|
+
* A field spread that lost its reconciler key — the guard for the one change in the 1.28.0 forms
|
|
3801
|
+
* minor that a consumer's compiler cannot see.
|
|
3802
|
+
*
|
|
3803
|
+
* `inputProps` used to return `getInputProps(path)` AND `key` in one object, so
|
|
3804
|
+
* `<TextInput {...inputProps(form, 'x')} />` covered both. It no longer returns `key` (a spread
|
|
3805
|
+
* `key` is a React 19 warning), and that exact call site still type-checks, still renders and still
|
|
3806
|
+
* validates — it just stops resetting. There is no compile error, no throw and no console line to
|
|
3807
|
+
* find it by, which is precisely the profile a lint rule exists for.
|
|
3808
|
+
*
|
|
3809
|
+
* TWO forms under this one id, because they are one law read from either end:
|
|
3810
|
+
*
|
|
3811
|
+
* - `{...inputProps(…)}` with no sibling `key` — the post-migration mistake. Autofixed: the `key`
|
|
3812
|
+
* attribute is inserted with the SAME arguments, and `fieldKey` is added to the existing
|
|
3813
|
+
* `basalt-ui/forms` import when it is not already there, so `--fix` produces working code rather
|
|
3814
|
+
* than an unresolved identifier.
|
|
3815
|
+
* - `{...field(…)}` — the pre-migration call site. Reported ALWAYS, key or no key, and deliberately
|
|
3816
|
+
* NOT autofixed: `field` still returns the bundle, so inserting a second `key` would be wrong,
|
|
3817
|
+
* and the honest remedy is the rename `basalt/deprecated-export` already nudges. This arm is a
|
|
3818
|
+
* pointer at the migration, one lane over from that rule's import-level nudge.
|
|
3819
|
+
*
|
|
3820
|
+
* Provenance is the `basalt-ui/forms` specifier (plus a relative import inside basalt's own `src/`,
|
|
3821
|
+
* or the dogfood surface goes silent — the same gate `collectBasaltImports` uses), read through the
|
|
3822
|
+
* local→imported map so `import { inputProps as f }` is covered. `inputProps` is an ordinary enough
|
|
3823
|
+
* name that firing on an unimported one would be an opinion about a consumer's own helper.
|
|
3824
|
+
*/
|
|
3825
|
+
// Ships: warn (grace → 1.30.0)
|
|
3826
|
+
const formsFieldKey = {
|
|
3827
|
+
meta: {
|
|
3828
|
+
type: 'suggestion',
|
|
3829
|
+
docs: { description: 'Warn on a spread field object that lost (or still bundles) its key.' },
|
|
3830
|
+
schema: [],
|
|
3831
|
+
fixable: 'code',
|
|
3832
|
+
},
|
|
3833
|
+
create(context) {
|
|
3834
|
+
if (isTestFile(context)) return {}
|
|
3835
|
+
const ownTree = isBasaltOwnSource(getFilename(context))
|
|
3836
|
+
/** local name → the name it was imported as, for the three forms names. */
|
|
3837
|
+
const imported = new Map()
|
|
3838
|
+
/** The forms ImportDeclaration the `fieldKey` specifier is added to, if one exists. */
|
|
3839
|
+
let formsImport
|
|
3840
|
+
const candidates = []
|
|
3841
|
+
|
|
3842
|
+
return {
|
|
3843
|
+
ImportDeclaration(node) {
|
|
3844
|
+
const source = node.source?.value
|
|
3845
|
+
if (typeof source !== 'string') return
|
|
3846
|
+
if (source !== FORMS_IMPORT_SOURCE && !(ownTree && RELATIVE_IMPORT_SOURCE.test(source))) {
|
|
3847
|
+
return
|
|
3848
|
+
}
|
|
3849
|
+
for (const spec of node.specifiers ?? []) {
|
|
3850
|
+
const local = spec.local?.name
|
|
3851
|
+
const name = spec.imported?.name ?? spec.imported?.value
|
|
3852
|
+
if (typeof local !== 'string' || typeof name !== 'string') continue
|
|
3853
|
+
if (name === 'fieldKey' || FORMS_SPREAD_NAMES.has(name)) {
|
|
3854
|
+
imported.set(local, name)
|
|
3855
|
+
if (source === FORMS_IMPORT_SOURCE) formsImport = node
|
|
3856
|
+
}
|
|
3857
|
+
}
|
|
3858
|
+
},
|
|
3859
|
+
JSXSpreadAttribute(node) {
|
|
3860
|
+
const call = node.argument
|
|
3861
|
+
if (call?.type !== 'CallExpression' || call.callee?.type !== 'Identifier') return
|
|
3862
|
+
const opening = node.parent
|
|
3863
|
+
if (opening === null || opening === undefined || opening.type !== 'JSXOpeningElement')
|
|
3864
|
+
return
|
|
3865
|
+
candidates.push({ node, call, local: call.callee.name, opening })
|
|
3866
|
+
},
|
|
3867
|
+
'Program:exit'() {
|
|
3868
|
+
const text = context.sourceCode?.text ?? ''
|
|
3869
|
+
const hasFieldKey = [...imported.values()].includes('fieldKey')
|
|
3870
|
+
let importFixed = false
|
|
3871
|
+
|
|
3872
|
+
for (const { node, call, local, opening } of candidates) {
|
|
3873
|
+
const name = imported.get(local)
|
|
3874
|
+
if (name === undefined) continue
|
|
3875
|
+
if (hasThemeAllow(context, node, 'forms-field-key')) continue
|
|
3876
|
+
|
|
3877
|
+
if (name === 'field') {
|
|
3878
|
+
context.report({ node, message: FORMS_FIELD_KEY_MESSAGES.deprecatedAlias })
|
|
3879
|
+
continue
|
|
3880
|
+
}
|
|
3881
|
+
if (hasKeyAttribute(opening)) continue
|
|
3882
|
+
|
|
3883
|
+
const args = call.arguments ?? []
|
|
3884
|
+
const first = args[0]?.range
|
|
3885
|
+
const last = args[args.length - 1]?.range
|
|
3886
|
+
// No arguments to copy means no honest `fieldKey(…)` to write — report without a fix
|
|
3887
|
+
// rather than emit one that does not compile.
|
|
3888
|
+
if (first === undefined || last === undefined) {
|
|
3889
|
+
context.report({ node, message: FORMS_FIELD_KEY_MESSAGES.missing })
|
|
3890
|
+
continue
|
|
3891
|
+
}
|
|
3892
|
+
const argsText = text.slice(first[0], last[1])
|
|
3893
|
+
// The import specifier is added by the FIRST fixed occurrence only: three fields in one
|
|
3894
|
+
// file would otherwise emit three overlapping edits to one import, and an overlapping
|
|
3895
|
+
// fix is dropped rather than merged.
|
|
3896
|
+
const needsImport = !hasFieldKey && !importFixed && formsImport !== undefined
|
|
3897
|
+
const lastSpec = needsImport
|
|
3898
|
+
? formsImport.specifiers?.[(formsImport.specifiers?.length ?? 0) - 1]
|
|
3899
|
+
: undefined
|
|
3900
|
+
if (needsImport && lastSpec !== undefined) importFixed = true
|
|
3901
|
+
|
|
3902
|
+
context.report({
|
|
3903
|
+
node,
|
|
3904
|
+
message: FORMS_FIELD_KEY_MESSAGES.missing,
|
|
3905
|
+
fix: (fixer) => {
|
|
3906
|
+
const fixes = [fixer.insertTextBefore(node, `key={fieldKey(${argsText})} `)]
|
|
3907
|
+
if (lastSpec !== undefined) fixes.push(fixer.insertTextAfter(lastSpec, ', fieldKey'))
|
|
3908
|
+
return fixes
|
|
3909
|
+
},
|
|
3910
|
+
})
|
|
3911
|
+
}
|
|
3912
|
+
},
|
|
3913
|
+
}
|
|
3914
|
+
},
|
|
3915
|
+
}
|
|
3916
|
+
|
|
2982
3917
|
// ── Grace ledger ────────────────────────────────────────────────────────────────────────────────
|
|
2983
3918
|
|
|
2984
3919
|
/**
|
|
@@ -3012,7 +3947,7 @@ const useSearchFromLiteral = {
|
|
|
3012
3947
|
export const PLUGIN_RULE_GRACE = {
|
|
3013
3948
|
'control-outside-home': {
|
|
3014
3949
|
since: '1.26.0',
|
|
3015
|
-
promote: '1.
|
|
3950
|
+
promote: '1.30.0',
|
|
3016
3951
|
why:
|
|
3017
3952
|
'the wave-6 control guards (docs/CONTROLS-SPEC.md §6). The one openly HEURISTIC rule of the ' +
|
|
3018
3953
|
'set — "this control has no home" is a claim about layout intent, so its false-positive load ' +
|
|
@@ -3022,11 +3957,87 @@ export const PLUGIN_RULE_GRACE = {
|
|
|
3022
3957
|
'cautious: the wave-7 run found 9 remaining warns in argo, every one of them a control in a ' +
|
|
3023
3958
|
'modal/form module whose `<Modal>` is rendered by the PARENT — the cross-file case law C1 is ' +
|
|
3024
3959
|
'explicitly advisory about, which no ancestry walk inside one file can ever see. ' +
|
|
3025
|
-
'{@link
|
|
3026
|
-
'
|
|
3027
|
-
|
|
3960
|
+
'{@link isOverlayConventionFile} exempts that declared naming convention, in BOTH the kebab ' +
|
|
3961
|
+
'(`edit-session-modal.tsx`) and the PascalCase (`CbbiPanel.tsx`) dialect. Re-dated ' +
|
|
3962
|
+
'2026-08-28 — the argo wave-7 migration has not run; the PascalCase overlay convention (this ' +
|
|
3963
|
+
'change) is expected to clear most of the 9. 1.30.0 is when the remainder is re-measured. A ' +
|
|
3964
|
+
"file outside the convention that is still an overlay's own body declares it with " +
|
|
3028
3965
|
'`theme-allow-file control-outside-home — overlay`.',
|
|
3029
3966
|
},
|
|
3967
|
+
'bound-control-outside-home': {
|
|
3968
|
+
since: '1.28.0',
|
|
3969
|
+
promote: '1.30.0',
|
|
3970
|
+
why:
|
|
3971
|
+
'ASIDE-SPEC G5 — a bound control in a section body rendered as a stray pill and nothing saw ' +
|
|
3972
|
+
'it. `control-outside-home` matches raw Mantine tags only, so the whole basalt half of law ' +
|
|
3973
|
+
'C1 was unguarded: `<SelectFilter field={…}/>` written into a `Section` body or a bare page ' +
|
|
3974
|
+
'stack passed every lane. A NEW id rather than a widening (C16 — a level is per-id), landing ' +
|
|
3975
|
+
'warn because it inherits the sibling rule\'s heuristic: "this control has no home" is a ' +
|
|
3976
|
+
'claim about layout intent, and the ancestry it reads now spans four subtree homes ' +
|
|
3977
|
+
'(FilterSet / PageAside / PanelRow / the overlay hosts) that only shipped in 1.27.0. 1.30.0 ' +
|
|
3978
|
+
'is when the incumbent count across argo, linewatch, image-share, rb, image-gen and the ' +
|
|
3979
|
+
'playground is re-measured.',
|
|
3980
|
+
},
|
|
3981
|
+
'provider-above-router': {
|
|
3982
|
+
since: '1.28.0',
|
|
3983
|
+
promote: '1.30.0',
|
|
3984
|
+
why:
|
|
3985
|
+
'F5 — one of the three basalt-mantine.md laws that shipped under a `not guarded: —` banner. ' +
|
|
3986
|
+
'It reads STATIC JSX ancestry inside one file, which is a genuine fraction of the law: the ' +
|
|
3987
|
+
'usual real shape composes the two providers across modules, where no oxlint rule can see ' +
|
|
3988
|
+
'the nesting at all. Landing it at `error` would state a certainty the implementation does ' +
|
|
3989
|
+
'not have. 1.30.0 is when the incumbent count across argo, linewatch, image-share, rb, ' +
|
|
3990
|
+
'image-gen and the playground is measured — every one of them mounts a router, so the ' +
|
|
3991
|
+
'false-positive question has a real sample.',
|
|
3992
|
+
},
|
|
3993
|
+
'duplicate-notifications-mount': {
|
|
3994
|
+
since: '1.28.0',
|
|
3995
|
+
promote: '1.30.0',
|
|
3996
|
+
why:
|
|
3997
|
+
'F5 — the static half of a double-mount the runtime already warns about in dev ' +
|
|
3998
|
+
'(`src/commands/overlays-mount.tsx`). Same-file only, so like its sibling it guards a ' +
|
|
3999
|
+
'fraction of the law and cannot claim `error`. It also reads a prop DEFAULT (`notifications` ' +
|
|
4000
|
+
'is true when unwritten), which is the kind of polarity a consumer disagrees with exactly ' +
|
|
4001
|
+
'once before it is worth knowing about — a minor at `warn` is how that disagreement arrives ' +
|
|
4002
|
+
'as feedback rather than as a broken build.',
|
|
4003
|
+
},
|
|
4004
|
+
'query-dual-import': {
|
|
4005
|
+
since: '1.28.0',
|
|
4006
|
+
promote: '1.30.0',
|
|
4007
|
+
why:
|
|
4008
|
+
'F5 — the basalt-batteries.md law with no guard. TWO forms under one id: the dual import ' +
|
|
4009
|
+
'(both `@tanstack/react-query` and `basalt-ui/query` in one file) is the law verbatim, and ' +
|
|
4010
|
+
'the softer form — a raw `@tanstack/react-query` import in a file that also imports the ' +
|
|
4011
|
+
'basalt root barrel — is the heuristic that actually catches the drift, since a file that ' +
|
|
4012
|
+
'never names `basalt-ui/query` cannot dual-import. The soft form is what holds the whole ' +
|
|
4013
|
+
'rule at `warn`: "this file is already a basalt file" is a claim about intent. 1.30.0 is ' +
|
|
4014
|
+
'when the consumer count is measured and the soft form is either kept or split off.',
|
|
4015
|
+
},
|
|
4016
|
+
'forms-field-key': {
|
|
4017
|
+
since: '1.28.0',
|
|
4018
|
+
promote: '1.30.0',
|
|
4019
|
+
why:
|
|
4020
|
+
"the 1.28.0 forms minor took `key` out of `inputProps`' return, and the OLD call site " +
|
|
4021
|
+
"(`<TextInput {...inputProps(form, 'x')} />`) survives that change with no compile error, " +
|
|
4022
|
+
'no throw and no console line — it just stops resetting. This rule is the only thing that ' +
|
|
4023
|
+
'reports it, which is an argument for `error`, not `warn`; it lands at `warn` anyway ' +
|
|
4024
|
+
'because it fires on code every consumer is CURRENTLY shipping, and the grace minor is what ' +
|
|
4025
|
+
'turns "your build is broken by the upgrade" into "your editor lists the eleven lines". ' +
|
|
4026
|
+
'The `field(` arm is the same law read from the pre-migration end, and that alias is removed ' +
|
|
4027
|
+
'in 1.29.0 anyway. 1.30.0 is when argo, linewatch, image-share, rb, image-gen and the ' +
|
|
4028
|
+
'playground have all been through the autofix and the remainder is measured.',
|
|
4029
|
+
},
|
|
4030
|
+
'query-fn-unwrap': {
|
|
4031
|
+
since: '1.28.0',
|
|
4032
|
+
promote: '1.30.0',
|
|
4033
|
+
why:
|
|
4034
|
+
'F5 — the second unguarded ./query law. Openly a TEXT heuristic inside a syntactic range: ' +
|
|
4035
|
+
'`fetch(` present in the queryFn span and `unwrap(` absent. It cannot see a `queryFn: ' +
|
|
4036
|
+
'fetchThing` reference, an Eden/Treaty call, or an unwrap one frame out, and it counts an ' +
|
|
4037
|
+
'`unwrap(` applied to anything. That is a deliberate floor rather than a first draft, and a ' +
|
|
4038
|
+
'rule whose stated reach is this narrow has no business failing a build. 1.30.0 is when the ' +
|
|
4039
|
+
'measured false-positive load decides whether it promotes or stays advisory.',
|
|
4040
|
+
},
|
|
3030
4041
|
}
|
|
3031
4042
|
|
|
3032
4043
|
/**
|
|
@@ -3050,6 +4061,16 @@ export const PLUGIN_RULE_ADVISORY = {
|
|
|
3050
4061
|
'strong evidence and not proof, and renaming defeats it outright, so this stays a permanent ' +
|
|
3051
4062
|
'warn rather than a grace entry with no honest promote date.',
|
|
3052
4063
|
},
|
|
4064
|
+
'deprecated-export': {
|
|
4065
|
+
since: '1.28.0',
|
|
4066
|
+
why:
|
|
4067
|
+
'B4, the sunset half of the deprecation lifecycle (`../CLAUDE.md`). A deprecation is a ' +
|
|
4068
|
+
'SCHEDULE, not a defect: the code compiles, runs and is correct until the `removeIn` minor, ' +
|
|
4069
|
+
'so promoting this to `error` would fail a consumer build over something that still works — ' +
|
|
4070
|
+
'the one outcome the no-majors doctrine exists to avoid. There is no version at which that ' +
|
|
4071
|
+
'stops being true, which is what makes it advisory rather than a grace entry: enforcement ' +
|
|
4072
|
+
'arrives by the export ceasing to exist, not by this rule changing level.',
|
|
4073
|
+
},
|
|
3053
4074
|
}
|
|
3054
4075
|
|
|
3055
4076
|
// ── Plugin export ───────────────────────────────────────────────────────────────────────────────
|
|
@@ -3068,12 +4089,19 @@ export default {
|
|
|
3068
4089
|
'raw-scroll-container': rawScrollContainer,
|
|
3069
4090
|
'hand-rolled-filter': handRolledFilter,
|
|
3070
4091
|
'control-outside-home': controlOutsideHome,
|
|
4092
|
+
'bound-control-outside-home': boundControlOutsideHome,
|
|
3071
4093
|
'control-size-literal': controlSizeLiteral,
|
|
3072
4094
|
'page-bar-budget': pageBarBudget,
|
|
3073
4095
|
'in-body-page-title': inBodyPageTitle,
|
|
3074
4096
|
'responsive-twin': responsiveTwin,
|
|
3075
4097
|
'search-literal-link': searchLiteralLink,
|
|
3076
4098
|
'use-search-from-literal': useSearchFromLiteral,
|
|
4099
|
+
'provider-above-router': providerAboveRouter,
|
|
4100
|
+
'duplicate-notifications-mount': duplicateNotificationsMount,
|
|
4101
|
+
'query-dual-import': queryDualImport,
|
|
4102
|
+
'query-fn-unwrap': queryFnUnwrap,
|
|
4103
|
+
'deprecated-export': deprecatedExport,
|
|
4104
|
+
'forms-field-key': formsFieldKey,
|
|
3077
4105
|
'visx-boundary': visxBoundary,
|
|
3078
4106
|
'visx-tooltip': visxTooltip,
|
|
3079
4107
|
'token-layer-boundary': tokenLayerBoundary,
|