basalt-ui 1.24.0 → 1.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +8 -6
- package/CHANGELOG.md +25 -0
- package/MIGRATING.md +547 -21
- package/README.md +46 -33
- package/agent/rules/basalt-batteries.md +205 -0
- package/agent/rules/basalt-charts.md +125 -555
- package/agent/rules/basalt-controls.md +155 -0
- package/agent/rules/basalt-mantine.md +146 -439
- package/agent/rules/basalt-state.md +113 -96
- package/agent/rules/basalt-tokens.md +126 -288
- package/agent/skills/basalt-app/SKILL.md +70 -195
- package/agent/skills/basalt-charts/SKILL.md +54 -234
- package/agent/skills/basalt-design/SKILL.md +56 -248
- package/agent/templates/CLAUDE-block.md.tpl +31 -40
- package/agent/templates/DESIGN.md.tpl +22 -51
- package/configs/oxlint-plugin.js +1275 -31
- package/configs/oxlint-plugin.test.ts +947 -11
- package/configs/oxlint.json +11 -3
- package/dist/charts/cursor/resolve.d.ts +18 -0
- package/dist/charts/cursor/resolve.d.ts.map +1 -1
- package/dist/charts/cursor/resolve.js +15 -6
- package/dist/charts/cursor/scope.d.ts +6 -3
- package/dist/charts/cursor/scope.d.ts.map +1 -1
- package/dist/charts/cursor/store.d.ts +5 -1
- package/dist/charts/cursor/store.d.ts.map +1 -1
- package/dist/charts/cursor/store.js +4 -4
- package/dist/charts/hooks/useChartCursor.d.ts.map +1 -1
- package/dist/charts/hooks/useChartCursor.js +9 -8
- package/dist/charts/index.d.ts +2 -2
- package/dist/charts/index.d.ts.map +1 -1
- package/dist/charts/primitives/ChartCard.d.ts +34 -9
- package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
- package/dist/charts/primitives/ChartCard.js +29 -114
- package/dist/charts/primitives/ChartCard.test.d.ts +2 -0
- package/dist/charts/primitives/ChartCard.test.d.ts.map +1 -0
- package/dist/charts/sparklines/BarSparkline.d.ts +26 -2
- package/dist/charts/sparklines/BarSparkline.d.ts.map +1 -1
- package/dist/charts/sparklines/BarSparkline.js +18 -4
- package/dist/cli/check-coverage.test.d.ts +2 -0
- package/dist/cli/check-coverage.test.d.ts.map +1 -0
- package/dist/cli/index.d.ts +67 -4
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +225 -7
- package/dist/connectivity/connectivity-indicator.d.ts +4 -1
- package/dist/connectivity/connectivity-indicator.d.ts.map +1 -1
- package/dist/connectivity/connectivity-indicator.js +1 -1
- package/dist/content/index.d.ts +5 -3
- package/dist/content/index.d.ts.map +1 -1
- package/dist/content/index.js +4 -2
- package/dist/content/prose.module.css +7 -28
- package/dist/controls/actions.d.ts +169 -0
- package/dist/controls/actions.d.ts.map +1 -0
- package/dist/controls/actions.js +287 -0
- package/dist/controls/actions.module.css +62 -0
- package/dist/controls/actions.module.css.d.ts +13 -0
- package/dist/controls/actions.test.d.ts +2 -0
- package/dist/controls/actions.test.d.ts.map +1 -0
- package/dist/controls/actions.type-guard.test.d.ts +2 -0
- package/dist/controls/actions.type-guard.test.d.ts.map +1 -0
- package/dist/controls/compare-filter.d.ts +41 -0
- package/dist/controls/compare-filter.d.ts.map +1 -0
- package/dist/controls/compare-filter.js +23 -0
- package/dist/controls/control-group.d.ts +19 -0
- package/dist/controls/control-group.d.ts.map +1 -0
- package/dist/controls/control-group.js +20 -0
- package/dist/controls/control-group.test.d.ts +2 -0
- package/dist/controls/control-group.test.d.ts.map +1 -0
- package/dist/controls/controls.module.css +542 -0
- package/dist/controls/controls.module.css.d.ts +35 -0
- package/dist/controls/controls.router.test.d.ts +2 -0
- package/dist/controls/controls.router.test.d.ts.map +1 -0
- package/dist/controls/enum-filter.d.ts +30 -0
- package/dist/controls/enum-filter.d.ts.map +1 -0
- package/dist/controls/enum-filter.js +91 -0
- package/dist/controls/filter-context.d.ts +55 -0
- package/dist/controls/filter-context.d.ts.map +1 -0
- package/dist/controls/filter-context.js +69 -0
- package/dist/controls/filter-pill.d.ts +28 -0
- package/dist/controls/filter-pill.d.ts.map +1 -0
- package/dist/controls/filter-pill.js +70 -0
- package/dist/controls/filter-set.d.ts +13 -0
- package/dist/controls/filter-set.d.ts.map +1 -0
- package/dist/controls/filter-set.js +125 -0
- package/dist/controls/filter-set.test.d.ts +2 -0
- package/dist/controls/filter-set.test.d.ts.map +1 -0
- package/dist/controls/filter-sheet.d.ts +117 -0
- package/dist/controls/filter-sheet.d.ts.map +1 -0
- package/dist/controls/filter-sheet.js +146 -0
- package/dist/controls/glyphs.d.ts +27 -0
- package/dist/controls/glyphs.d.ts.map +1 -0
- package/dist/controls/glyphs.js +49 -0
- package/dist/controls/index.d.ts +19 -0
- package/dist/controls/index.d.ts.map +1 -0
- package/dist/controls/index.js +44 -0
- package/dist/controls/multi-select-filter.d.ts +23 -0
- package/dist/controls/multi-select-filter.d.ts.map +1 -0
- package/dist/controls/multi-select-filter.js +89 -0
- package/dist/controls/peer-boundary.test.d.ts +2 -0
- package/dist/controls/peer-boundary.test.d.ts.map +1 -0
- package/dist/controls/props-contract.test.d.ts +2 -0
- package/dist/controls/props-contract.test.d.ts.map +1 -0
- package/dist/controls/range-filter.d.ts +29 -0
- package/dist/controls/range-filter.d.ts.map +1 -0
- package/dist/controls/range-filter.js +120 -0
- package/dist/controls/search-filter.d.ts +10 -0
- package/dist/controls/search-filter.d.ts.map +1 -0
- package/dist/controls/search-filter.js +81 -0
- package/dist/controls/select-filter.d.ts +74 -0
- package/dist/controls/select-filter.d.ts.map +1 -0
- package/dist/controls/select-filter.js +10 -0
- package/dist/controls/sync-button.d.ts +38 -0
- package/dist/controls/sync-button.d.ts.map +1 -0
- package/dist/controls/sync-button.js +119 -0
- package/dist/controls/sync-button.module.css +63 -0
- package/dist/controls/sync-button.module.css.d.ts +11 -0
- package/dist/controls/sync-button.test.d.ts +2 -0
- package/dist/controls/sync-button.test.d.ts.map +1 -0
- package/dist/controls/toggle-filter.d.ts +9 -0
- package/dist/controls/toggle-filter.d.ts.map +1 -0
- package/dist/controls/toggle-filter.js +42 -0
- package/dist/controls/view-tabs.d.ts +23 -0
- package/dist/controls/view-tabs.d.ts.map +1 -0
- package/dist/controls/view-tabs.js +59 -0
- package/dist/controls-dates/date-range-picker.d.ts +4 -0
- package/dist/controls-dates/date-range-picker.d.ts.map +1 -0
- package/dist/controls-dates/date-range-picker.js +18 -0
- package/dist/controls-dates/index.d.ts +9 -0
- package/dist/controls-dates/index.d.ts.map +1 -0
- package/dist/controls-dates/index.js +4 -0
- package/dist/dashboard/delta-badge.d.ts +6 -12
- package/dist/dashboard/delta-badge.d.ts.map +1 -1
- package/dist/dashboard/delta-badge.js +1 -50
- package/dist/dashboard/query-state.d.ts +1 -1
- package/dist/dashboard/settings-section.d.ts +6 -4
- package/dist/dashboard/settings-section.d.ts.map +1 -1
- package/dist/dashboard/settings-section.js +23 -16
- package/dist/dashboard/settings-section.module.css +10 -0
- package/dist/dashboard/settings-section.module.css.d.ts +1 -0
- package/dist/dashboard/settings-section.test.d.ts +2 -0
- package/dist/dashboard/settings-section.test.d.ts.map +1 -0
- package/dist/dashboard/stat-card.d.ts +44 -9
- package/dist/dashboard/stat-card.d.ts.map +1 -1
- package/dist/dashboard/stat-card.js +80 -48
- package/dist/dashboard/stat-card.module.css +82 -0
- package/dist/dashboard/stat-card.module.css.d.ts +11 -0
- package/dist/data/data-table.d.ts +102 -20
- package/dist/data/data-table.d.ts.map +1 -1
- package/dist/data/data-table.js +166 -65
- package/dist/data/table.d.ts +1 -1
- package/dist/data/table.d.ts.map +1 -1
- package/dist/guard/grace.test.d.ts +20 -0
- package/dist/guard/grace.test.d.ts.map +1 -0
- package/dist/guard/index.d.ts +66 -3
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +82 -7
- package/dist/guard/types.d.ts +3 -3
- package/dist/guard/types.d.ts.map +1 -1
- package/dist/index.d.ts +12 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -10
- package/dist/nav/types.d.ts +78 -2
- package/dist/nav/types.d.ts.map +1 -1
- package/dist/notifications/bell.d.ts +4 -1
- package/dist/notifications/bell.d.ts.map +1 -1
- package/dist/notifications/index.d.ts +1 -1
- package/dist/router-tanstack/field.d.ts +14 -0
- package/dist/router-tanstack/field.d.ts.map +1 -0
- package/dist/router-tanstack/field.js +4 -0
- package/dist/router-tanstack/index.d.ts +6 -2
- package/dist/router-tanstack/index.d.ts.map +1 -1
- package/dist/router-tanstack/index.js +4 -0
- package/dist/router-tanstack/multi-search-param-store.d.ts +4 -52
- package/dist/router-tanstack/multi-search-param-store.d.ts.map +1 -1
- package/dist/router-tanstack/multi-search-param-store.js +19 -27
- package/dist/router-tanstack/search-param-store.d.ts +3 -87
- package/dist/router-tanstack/search-param-store.d.ts.map +1 -1
- package/dist/router-tanstack/search-param-store.js +16 -45
- package/dist/router-tanstack/search-store.d.ts +81 -0
- package/dist/router-tanstack/search-store.d.ts.map +1 -0
- package/dist/router-tanstack/search-store.js +187 -0
- package/dist/router-tanstack/search-store.router.test.d.ts +2 -0
- package/dist/router-tanstack/search-store.router.test.d.ts.map +1 -0
- package/dist/router-tanstack/search-store.test.d.ts +2 -0
- package/dist/router-tanstack/search-store.test.d.ts.map +1 -0
- package/dist/router-tanstack/use-nav.d.ts +0 -2
- package/dist/router-tanstack/use-nav.d.ts.map +1 -1
- package/dist/router-tanstack/use-nav.js +34 -12
- package/dist/router-tanstack/use-nav.test.d.ts +2 -0
- package/dist/router-tanstack/use-nav.test.d.ts.map +1 -0
- package/dist/section/index.d.ts +2 -0
- package/dist/section/index.d.ts.map +1 -0
- package/dist/section/index.js +4 -0
- package/dist/section/section.d.ts +24 -0
- package/dist/section/section.d.ts.map +1 -0
- package/dist/section/section.js +126 -0
- package/dist/section/section.module.css +104 -0
- package/dist/section/section.module.css.d.ts +11 -0
- package/dist/section/section.test.d.ts +2 -0
- package/dist/section/section.test.d.ts.map +1 -0
- package/dist/shell/app-breadcrumbs.d.ts.map +1 -1
- package/dist/shell/app-breadcrumbs.js +18 -15
- package/dist/shell/app-header.module.css +77 -37
- package/dist/shell/app-header.module.css.d.ts +2 -1
- package/dist/shell/app-mobile-nav.d.ts +9 -2
- package/dist/shell/app-mobile-nav.d.ts.map +1 -1
- package/dist/shell/app-mobile-nav.js +98 -11
- package/dist/shell/app-mobile-nav.module.css +8 -0
- package/dist/shell/app-mobile-nav.module.css.d.ts +1 -0
- package/dist/shell/app-sidebar.d.ts +55 -28
- package/dist/shell/app-sidebar.d.ts.map +1 -1
- package/dist/shell/app-sidebar.js +116 -67
- package/dist/shell/app-sidebar.module.css +303 -20
- package/dist/shell/app-sidebar.module.css.d.ts +19 -1
- package/dist/shell/index.d.ts +55 -21
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +40 -18
- package/dist/shell/mobile-nav-model.d.ts +13 -2
- package/dist/shell/mobile-nav-model.d.ts.map +1 -1
- package/dist/shell/mobile-nav-model.js +15 -1
- package/dist/shell/nav-count-badge.d.ts +1 -1
- package/dist/shell/page-bar.d.ts +51 -0
- package/dist/shell/page-bar.d.ts.map +1 -0
- package/dist/shell/page-bar.js +154 -0
- package/dist/shell/page-bar.module.css +178 -0
- package/dist/shell/page-bar.module.css.d.ts +17 -0
- package/dist/shell/page-bar.test.d.ts +2 -0
- package/dist/shell/page-bar.test.d.ts.map +1 -0
- package/dist/shell/sidebar-block-model.d.ts +48 -0
- package/dist/shell/sidebar-block-model.d.ts.map +1 -0
- package/dist/shell/sidebar-block-model.js +39 -0
- package/dist/shell/sidebar-blocks.d.ts +32 -0
- package/dist/shell/sidebar-blocks.d.ts.map +1 -0
- package/dist/shell/sidebar-blocks.js +169 -0
- package/dist/shell/sidebar-search.d.ts +13 -1
- package/dist/shell/sidebar-search.d.ts.map +1 -1
- package/dist/shell/sidebar-search.js +34 -1
- package/dist/shell/sidebar-search.module.css +26 -0
- package/dist/shell/sidebar-search.module.css.d.ts +2 -0
- package/dist/state/fields.d.ts +392 -0
- package/dist/state/fields.d.ts.map +1 -0
- package/dist/state/fields.js +369 -0
- package/dist/state/fields.test.d.ts +2 -0
- package/dist/state/fields.test.d.ts.map +1 -0
- package/dist/state/persisted.d.ts +48 -0
- package/dist/state/persisted.d.ts.map +1 -0
- package/dist/state/persisted.js +105 -0
- package/dist/state.d.ts +11 -56
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +14 -120
- package/dist/styles.css +13 -0
- package/dist/surfaces.d.ts +106 -24
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +155 -19
- package/dist/theme/controls.module.css +16 -1
- package/dist/theme/ctl-theme.d.ts +32 -0
- package/dist/theme/ctl-theme.d.ts.map +1 -0
- package/dist/theme/ctl-theme.js +54 -0
- package/dist/theme/ctl-theme.test.d.ts +2 -0
- package/dist/theme/ctl-theme.test.d.ts.map +1 -0
- package/dist/theme/ctl-tier-coverage.test.d.ts +2 -0
- package/dist/theme/ctl-tier-coverage.test.d.ts.map +1 -0
- package/dist/theme/divider-law.test.d.ts +2 -0
- package/dist/theme/divider-law.test.d.ts.map +1 -0
- package/dist/theme/icon-slot.d.ts +44 -0
- package/dist/theme/icon-slot.d.ts.map +1 -0
- package/dist/theme/icon-slot.js +21 -0
- package/dist/theme/icon-slot.module.css +47 -0
- package/dist/theme/icon-slot.module.css.d.ts +8 -0
- package/dist/theme/icon-slot.test.d.ts +2 -0
- package/dist/theme/icon-slot.test.d.ts.map +1 -0
- package/dist/theme/index.d.ts +1 -0
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/index.js +82 -5
- package/dist/theme/layout-rhythm-css.test.d.ts +2 -0
- package/dist/theme/layout-rhythm-css.test.d.ts.map +1 -0
- package/dist/theme/segmented-control.module.css +20 -0
- package/dist/theme/shadow-surfaces.test.d.ts.map +1 -1
- package/dist/tokens/index.d.ts +12 -0
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +38 -8
- package/dist/tokens/palette.d.ts +97 -61
- package/dist/tokens/palette.d.ts.map +1 -1
- package/dist/tokens/palette.js +76 -38
- package/dist/tokens.css +10 -2
- package/dist/widget-header/delta-badge.d.ts +13 -0
- package/dist/widget-header/delta-badge.d.ts.map +1 -0
- package/dist/widget-header/delta-badge.js +29 -0
- package/dist/widget-header/delta-badge.test.d.ts +2 -0
- package/dist/widget-header/delta-badge.test.d.ts.map +1 -0
- package/dist/widget-header/index.d.ts +3 -0
- package/dist/widget-header/index.d.ts.map +1 -0
- package/dist/widget-header/index.js +6 -0
- package/dist/widget-header/no-mantine.test.d.ts +2 -0
- package/dist/widget-header/no-mantine.test.d.ts.map +1 -0
- package/dist/widget-header/widget-header.d.ts +37 -0
- package/dist/widget-header/widget-header.d.ts.map +1 -0
- package/dist/widget-header/widget-header.js +77 -0
- package/dist/widget-header/widget-header.module.css +335 -0
- package/dist/widget-header/widget-header.module.css.d.ts +24 -0
- package/dist/widget-header/widget-header.test.d.ts +2 -0
- package/dist/widget-header/widget-header.test.d.ts.map +1 -0
- package/llms.txt +30 -17
- package/package.json +14 -1
- package/src/charts/cursor/resolve.test.ts +29 -1
- package/src/charts/cursor/resolve.ts +38 -6
- package/src/charts/cursor/scope.tsx +6 -3
- package/src/charts/cursor/store.test.ts +19 -1
- package/src/charts/cursor/store.ts +10 -5
- package/src/charts/hooks/useChartCursor.ts +16 -8
- package/src/charts/index.ts +2 -2
- package/src/charts/primitives/CartesianChart.test.tsx +124 -0
- package/src/charts/primitives/ChartCard.test.tsx +66 -0
- package/src/charts/primitives/ChartCard.tsx +86 -146
- package/src/charts/sparklines/BarSparkline.tsx +43 -5
- package/src/cli/check-coverage.test.ts +297 -0
- package/src/cli/check-theme.test.ts +38 -14
- package/src/cli/index.ts +392 -16
- package/src/cli/init-sync.test.ts +97 -0
- package/src/connectivity/connectivity-indicator.tsx +16 -9
- package/src/content/index.ts +16 -3
- package/src/content/prose.module.css +7 -28
- package/src/content/prose.module.css.test.ts +8 -24
- package/src/controls/actions.module.css +62 -0
- package/src/controls/actions.module.css.d.ts +13 -0
- package/src/controls/actions.test.tsx +252 -0
- package/src/controls/actions.tsx +655 -0
- package/src/controls/actions.type-guard.test.ts +63 -0
- package/src/controls/compare-filter.tsx +64 -0
- package/src/controls/control-group.test.tsx +229 -0
- package/src/controls/control-group.tsx +63 -0
- package/src/controls/controls.module.css +542 -0
- package/src/controls/controls.module.css.d.ts +35 -0
- package/src/controls/controls.router.test.tsx +637 -0
- package/src/controls/enum-filter.tsx +159 -0
- package/src/controls/filter-context.tsx +153 -0
- package/src/controls/filter-pill.tsx +120 -0
- package/src/controls/filter-set.test.tsx +472 -0
- package/src/controls/filter-set.tsx +225 -0
- package/src/controls/filter-sheet.tsx +285 -0
- package/src/controls/glyphs.tsx +81 -0
- package/src/controls/index.ts +48 -0
- package/src/controls/multi-select-filter.tsx +140 -0
- package/src/controls/peer-boundary.test.ts +85 -0
- package/src/controls/props-contract.test.tsx +106 -0
- package/src/controls/range-filter.tsx +184 -0
- package/src/controls/search-filter.tsx +123 -0
- package/src/controls/select-filter.tsx +89 -0
- package/src/controls/sync-button.module.css +63 -0
- package/src/controls/sync-button.module.css.d.ts +11 -0
- package/src/controls/sync-button.test.tsx +183 -0
- package/src/controls/sync-button.tsx +197 -0
- package/src/controls/toggle-filter.tsx +60 -0
- package/src/controls/view-tabs.tsx +110 -0
- package/src/controls-dates/date-range-picker.tsx +42 -0
- package/src/controls-dates/index.ts +8 -0
- package/src/dashboard/delta-badge.tsx +4 -74
- package/src/dashboard/query-state.tsx +1 -1
- package/src/dashboard/settings-section.module.css +10 -0
- package/src/dashboard/settings-section.module.css.d.ts +1 -0
- package/src/dashboard/settings-section.test.tsx +56 -0
- package/src/dashboard/settings-section.tsx +33 -30
- package/src/dashboard/stat-card.module.css +82 -0
- package/src/dashboard/stat-card.module.css.d.ts +11 -0
- package/src/dashboard/stat-card.test.tsx +67 -1
- package/src/dashboard/stat-card.tsx +209 -68
- package/src/data/data-table.test.tsx +282 -6
- package/src/data/data-table.tsx +429 -94
- package/src/data/table.ts +2 -0
- package/src/guard/check-source.test.ts +126 -13
- package/src/guard/grace.test.ts +149 -0
- package/src/guard/index.ts +172 -29
- package/src/guard/types.ts +4 -2
- package/src/index.ts +46 -5
- package/src/nav/types.ts +84 -2
- package/src/notifications/bell.tsx +6 -3
- package/src/notifications/index.ts +1 -1
- package/src/router-tanstack/field.ts +31 -0
- package/src/router-tanstack/index.ts +24 -2
- package/src/router-tanstack/multi-search-param-store.test.ts +71 -3
- package/src/router-tanstack/multi-search-param-store.ts +46 -77
- package/src/router-tanstack/search-param-store.ts +42 -159
- package/src/router-tanstack/search-store.router.test.tsx +661 -0
- package/src/router-tanstack/search-store.test.ts +418 -0
- package/src/router-tanstack/search-store.ts +380 -0
- package/src/router-tanstack/use-nav.test.tsx +187 -0
- package/src/router-tanstack/use-nav.ts +72 -14
- package/src/section/index.ts +1 -0
- package/src/section/section.module.css +104 -0
- package/src/section/section.module.css.d.ts +11 -0
- package/src/section/section.test.tsx +204 -0
- package/src/section/section.tsx +220 -0
- package/src/shell/app-breadcrumbs.tsx +44 -31
- package/src/shell/app-header.module.css +77 -37
- package/src/shell/app-header.module.css.d.ts +2 -1
- package/src/shell/app-mobile-nav.module.css +8 -0
- package/src/shell/app-mobile-nav.module.css.d.ts +1 -0
- package/src/shell/app-mobile-nav.tsx +147 -8
- package/src/shell/app-sidebar-account.tsx +2 -2
- package/src/shell/app-sidebar.module.css +303 -20
- package/src/shell/app-sidebar.module.css.d.ts +19 -1
- package/src/shell/app-sidebar.test.tsx +456 -41
- package/src/shell/app-sidebar.tsx +276 -140
- package/src/shell/index.test.tsx +291 -5
- package/src/shell/index.tsx +137 -38
- package/src/shell/mobile-nav-model.test.ts +228 -7
- package/src/shell/mobile-nav-model.ts +53 -3
- package/src/shell/nav-count-badge.tsx +1 -1
- package/src/shell/page-bar.module.css +178 -0
- package/src/shell/page-bar.module.css.d.ts +17 -0
- package/src/shell/page-bar.test.tsx +316 -0
- package/src/shell/page-bar.tsx +315 -0
- package/src/shell/sidebar-block-model.ts +83 -0
- package/src/shell/sidebar-blocks.tsx +301 -0
- package/src/shell/sidebar-search.module.css +26 -0
- package/src/shell/sidebar-search.module.css.d.ts +2 -0
- package/src/shell/sidebar-search.tsx +71 -2
- package/src/state/fields.test.tsx +258 -0
- package/src/state/fields.ts +892 -0
- package/src/state/persisted.ts +211 -0
- package/src/state.ts +46 -240
- package/src/styles.css +13 -0
- package/src/surfaces.ts +203 -36
- package/src/theme/controls.module.css +16 -1
- package/src/theme/ctl-theme.test.tsx +275 -0
- package/src/theme/ctl-theme.tsx +138 -0
- package/src/theme/ctl-tier-coverage.test.ts +170 -0
- package/src/theme/density-relations.test.ts +106 -51
- package/src/theme/divider-law.test.ts +259 -0
- package/src/theme/icon-slot.module.css +47 -0
- package/src/theme/icon-slot.module.css.d.ts +8 -0
- package/src/theme/icon-slot.test.tsx +275 -0
- package/src/theme/icon-slot.tsx +59 -0
- package/src/theme/index.ts +165 -3
- package/src/theme/layout-rhythm-css.test.ts +72 -0
- package/src/theme/radius.test.ts +11 -1
- package/src/theme/segmented-control.module.css +20 -0
- package/src/theme/shadow-surfaces.test.ts +20 -7
- package/src/theme/spacing.test.ts +75 -27
- package/src/theme/use-basalt-spacing.ts +2 -2
- package/src/tokens/density.test.ts +7 -25
- package/src/tokens/index.ts +48 -8
- package/src/tokens/palette.ts +107 -73
- package/src/widget-header/delta-badge.test.tsx +60 -0
- package/src/widget-header/delta-badge.tsx +57 -0
- package/src/widget-header/index.ts +2 -0
- package/src/widget-header/no-mantine.test.ts +33 -0
- package/src/widget-header/widget-header.module.css +335 -0
- package/src/widget-header/widget-header.module.css.d.ts +24 -0
- package/src/widget-header/widget-header.test.tsx +187 -0
- package/src/widget-header/widget-header.tsx +168 -0
- package/agent/rules/basalt-agent.md +0 -452
- package/agent/rules/basalt-app.md +0 -246
- package/agent/rules/basalt-commands.md +0 -320
- package/agent/rules/basalt-content.md +0 -409
- package/agent/rules/basalt-data.md +0 -269
- package/agent/rules/basalt-forms.md +0 -186
- package/agent/rules/basalt-notifications.md +0 -264
- package/agent/rules/basalt-query.md +0 -187
- package/agent/rules/basalt-router.md +0 -336
- package/dist/content/article-filter-bar.d.ts +0 -40
- package/dist/content/article-filter-bar.d.ts.map +0 -1
- package/dist/content/article-filter-bar.js +0 -56
- package/dist/shell/page-header.d.ts +0 -13
- package/dist/shell/page-header.d.ts.map +0 -1
- package/dist/shell/page-header.js +0 -23
- package/src/content/article-filter-bar.tsx +0 -88
- package/src/shell/page-header.tsx +0 -35
|
@@ -12,8 +12,15 @@ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'nod
|
|
|
12
12
|
import { tmpdir } from 'node:os'
|
|
13
13
|
import { dirname, resolve } from 'node:path'
|
|
14
14
|
// eslint-disable-next-line -- the plugin is plain JS; the ledger + id set are named exports beside it
|
|
15
|
-
import basaltPlugin, {
|
|
16
|
-
|
|
15
|
+
import basaltPlugin, {
|
|
16
|
+
CTL_THEME_TAGS,
|
|
17
|
+
KNOWN_RULE_IDS,
|
|
18
|
+
PLUGIN_RULE_ADVISORY,
|
|
19
|
+
PLUGIN_RULE_GRACE,
|
|
20
|
+
} from './oxlint-plugin.js'
|
|
21
|
+
import { CTL_THEME } from '../src/theme/ctl-theme'
|
|
22
|
+
import { GUARD_RULES, PLUGIN_RULE_IDS } from '../src/guard/index.ts'
|
|
23
|
+
import { PLUGIN_RULE_ID_LIST, SURFACES } from '../src/surfaces.ts'
|
|
17
24
|
|
|
18
25
|
const PLUGIN_PATH = resolve(import.meta.dirname, 'oxlint-plugin.js')
|
|
19
26
|
const OXLINT_BIN = resolve(import.meta.dirname, '..', '..', '..', 'node_modules', '.bin', 'oxlint')
|
|
@@ -35,6 +42,15 @@ beforeEach(() => {
|
|
|
35
42
|
'basalt/chart-legend-literal': 'error',
|
|
36
43
|
'basalt/shadow-basalt-export': 'error',
|
|
37
44
|
'basalt/hand-rolled-shell': 'error',
|
|
45
|
+
'basalt/raw-scroll-container': 'error',
|
|
46
|
+
'basalt/hand-rolled-filter': 'error',
|
|
47
|
+
'basalt/control-outside-home': 'error',
|
|
48
|
+
'basalt/control-size-literal': 'error',
|
|
49
|
+
'basalt/page-bar-budget': 'error',
|
|
50
|
+
'basalt/in-body-page-title': 'error',
|
|
51
|
+
'basalt/responsive-twin': 'error',
|
|
52
|
+
'basalt/search-literal-link': 'error',
|
|
53
|
+
'basalt/use-search-from-literal': 'error',
|
|
38
54
|
'basalt/visx-boundary': 'error',
|
|
39
55
|
'basalt/visx-tooltip': 'error',
|
|
40
56
|
'basalt/token-layer-boundary': 'error',
|
|
@@ -1152,32 +1168,134 @@ export const C = () => <NavLink label="Settings" />\n`,
|
|
|
1152
1168
|
|
|
1153
1169
|
// ── grace ledger ↔ shipped preset ────────────────────────────────────────────
|
|
1154
1170
|
|
|
1171
|
+
type GraceEntry = { since: string; promote: string; why: string }
|
|
1172
|
+
|
|
1173
|
+
/**
|
|
1174
|
+
* Tiny local semver compare — no dependency. Assumes plain `x.y.z`, no pre-release identifiers.
|
|
1175
|
+
* Compares each component numerically, not lexically, so `'1.9.0' < '1.10.0'` and
|
|
1176
|
+
* `'1.3.0' < '1.26.0'` — a string compare would get both backwards.
|
|
1177
|
+
*/
|
|
1178
|
+
function compareSemver(a: string, b: string): number {
|
|
1179
|
+
const partsA = a.split('.').map(Number)
|
|
1180
|
+
const partsB = b.split('.').map(Number)
|
|
1181
|
+
for (let i = 0; i < 3; i++) {
|
|
1182
|
+
const diff = (partsA[i] ?? 0) - (partsB[i] ?? 0)
|
|
1183
|
+
if (diff !== 0) return diff > 0 ? 1 : -1
|
|
1184
|
+
}
|
|
1185
|
+
return 0
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
/**
|
|
1189
|
+
* The C16 gate itself (docs/CONTROLS-SPEC.md §1), extracted so it can be exercised against a
|
|
1190
|
+
* synthetic ledger as well as the real `PLUGIN_RULE_GRACE` — an `it.each` over the real ledger ran
|
|
1191
|
+
* zero assertions while it was empty, which proved nothing about the gate actually firing. Throws
|
|
1192
|
+
* naming the first offending entry when either invariant breaks: `since` must precede `promote`
|
|
1193
|
+
* for every entry, and no entry's `promote` may be `<=` the given version.
|
|
1194
|
+
*
|
|
1195
|
+
* `version` is `package.json`'s, i.e. the version already published, so this half can only fail
|
|
1196
|
+
* AFTER the release that shipped a due entry — `release.yml` runs no tests and the
|
|
1197
|
+
* `chore: release … [skip ci]` commit skips CI. The pre-release half is
|
|
1198
|
+
* `scripts/check-grace.ts`, run by `scripts/release.sh` against the version the dry run computed.
|
|
1199
|
+
*/
|
|
1200
|
+
function assertGraceLedger(ledger: Record<string, GraceEntry>, version: string): void {
|
|
1201
|
+
for (const [id, entry] of Object.entries(ledger)) {
|
|
1202
|
+
if (compareSemver(entry.since, entry.promote) >= 0) {
|
|
1203
|
+
throw new Error(
|
|
1204
|
+
`${id}: \`since\` (${entry.since}) must be before \`promote\` (${entry.promote}).`,
|
|
1205
|
+
)
|
|
1206
|
+
}
|
|
1207
|
+
if (compareSemver(version, entry.promote) >= 0) {
|
|
1208
|
+
throw new Error(
|
|
1209
|
+
`basalt/${id}: shipped preset is at ${version}, which has reached its promote version ` +
|
|
1210
|
+
`${entry.promote} — promote to error or extend \`promote\` with a reason.`,
|
|
1211
|
+
)
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
describe('compareSemver', () => {
|
|
1217
|
+
it('orders multi-digit components numerically, not lexically', () => {
|
|
1218
|
+
expect(compareSemver('1.9.0', '1.10.0')).toBeLessThan(0)
|
|
1219
|
+
expect(compareSemver('1.10.0', '1.9.0')).toBeGreaterThan(0)
|
|
1220
|
+
expect(compareSemver('1.3.0', '1.26.0')).toBeLessThan(0)
|
|
1221
|
+
expect(compareSemver('1.26.0', '1.3.0')).toBeGreaterThan(0)
|
|
1222
|
+
})
|
|
1223
|
+
|
|
1224
|
+
it('treats equal versions as equal', () => {
|
|
1225
|
+
expect(compareSemver('1.25.0', '1.25.0')).toBe(0)
|
|
1226
|
+
})
|
|
1227
|
+
})
|
|
1228
|
+
|
|
1229
|
+
describe('assertGraceLedger', () => {
|
|
1230
|
+
it('does not throw on a well-formed entry not yet due', () => {
|
|
1231
|
+
const ledger = { 'fake-rule': { since: '1.0.0', promote: '2.0.0', why: 'synthetic' } }
|
|
1232
|
+
expect(() => assertGraceLedger(ledger, '1.25.0')).not.toThrow()
|
|
1233
|
+
})
|
|
1234
|
+
|
|
1235
|
+
it('throws once the given version reaches promote', () => {
|
|
1236
|
+
const ledger = { 'fake-rule': { since: '1.0.0', promote: '1.25.0', why: 'synthetic' } }
|
|
1237
|
+
expect(() => assertGraceLedger(ledger, '1.25.0')).toThrow(/fake-rule/)
|
|
1238
|
+
})
|
|
1239
|
+
|
|
1240
|
+
it('throws once the given version is past promote', () => {
|
|
1241
|
+
const ledger = { 'fake-rule': { since: '1.0.0', promote: '1.9.0', why: 'synthetic' } }
|
|
1242
|
+
expect(() => assertGraceLedger(ledger, '1.10.0')).toThrow(/fake-rule/)
|
|
1243
|
+
})
|
|
1244
|
+
|
|
1245
|
+
it('throws when since does not precede promote', () => {
|
|
1246
|
+
const ledger = { 'fake-rule': { since: '1.5.0', promote: '1.5.0', why: 'synthetic' } }
|
|
1247
|
+
expect(() => assertGraceLedger(ledger, '1.0.0')).toThrow(/since/)
|
|
1248
|
+
})
|
|
1249
|
+
})
|
|
1250
|
+
|
|
1155
1251
|
describe('PLUGIN_RULE_GRACE', () => {
|
|
1156
1252
|
const shipped = JSON.parse(readFileSync(resolve(import.meta.dirname, 'oxlint.json'), 'utf8')) as {
|
|
1157
1253
|
rules: Record<string, unknown>
|
|
1158
1254
|
}
|
|
1159
|
-
const
|
|
1255
|
+
const pkgVersion = (
|
|
1256
|
+
JSON.parse(readFileSync(resolve(import.meta.dirname, '..', 'package.json'), 'utf8')) as {
|
|
1257
|
+
version: string
|
|
1258
|
+
}
|
|
1259
|
+
).version
|
|
1260
|
+
const graceEntries = Object.entries(PLUGIN_RULE_GRACE) as [string, GraceEntry][]
|
|
1261
|
+
const advisoryEntries = Object.entries(PLUGIN_RULE_ADVISORY) as [
|
|
1262
|
+
string,
|
|
1263
|
+
{ since: string; why: string },
|
|
1264
|
+
][]
|
|
1265
|
+
const graceIds = graceEntries.map(([id]) => id)
|
|
1266
|
+
const advisoryIds = advisoryEntries.map(([id]) => id)
|
|
1160
1267
|
const shippedBasaltIds = Object.keys(shipped.rules).filter((id) => id.startsWith('basalt/'))
|
|
1161
1268
|
|
|
1162
1269
|
// The mechanism whose ABSENCE let three rules sit at `warn` for up to twelve minors with nothing
|
|
1163
1270
|
// tracking them. Deleting a ledger entry IS the promotion, and this test makes flipping the
|
|
1164
1271
|
// shipped level part of the same commit.
|
|
1165
|
-
it('every
|
|
1166
|
-
for (const id of graceIds
|
|
1272
|
+
it('every grace and advisory entry is warn in the shipped preset', () => {
|
|
1273
|
+
for (const id of [...graceIds, ...advisoryIds])
|
|
1274
|
+
expect(shipped.rules[`basalt/${id}`]).toBe('warn')
|
|
1167
1275
|
})
|
|
1168
1276
|
|
|
1169
|
-
it('every shipped rule NOT in
|
|
1277
|
+
it('every shipped rule NOT in either ledger is error', () => {
|
|
1170
1278
|
for (const id of shippedBasaltIds) {
|
|
1171
|
-
|
|
1279
|
+
const bareId = id.slice('basalt/'.length)
|
|
1280
|
+
if (graceIds.includes(bareId) || advisoryIds.includes(bareId)) continue
|
|
1172
1281
|
expect([id, shipped.rules[id]]).toEqual([id, 'error'])
|
|
1173
1282
|
}
|
|
1174
1283
|
})
|
|
1175
1284
|
|
|
1176
|
-
it('every
|
|
1177
|
-
for (const id of
|
|
1178
|
-
|
|
1285
|
+
it('every entry carries a written why', () => {
|
|
1286
|
+
for (const [id, entry] of [...graceEntries, ...advisoryEntries]) {
|
|
1287
|
+
if (entry.why.length <= 40)
|
|
1288
|
+
throw new Error(`${id}: \`why\` must be longer than 40 characters.`)
|
|
1289
|
+
expect(entry.why.length).toBeGreaterThan(40)
|
|
1179
1290
|
}
|
|
1180
1291
|
})
|
|
1292
|
+
|
|
1293
|
+
// The C16 version gate, run against the REAL ledger and REAL package version — covers `since` <
|
|
1294
|
+
// `promote` and "no entry has reached its promote version" in one call. ADVISORY entries are
|
|
1295
|
+
// exempt by design (see `assertGraceLedger`'s synthetic coverage above for the gate itself).
|
|
1296
|
+
it('passes the C16 gate against the real ledger and package version', () => {
|
|
1297
|
+
expect(() => assertGraceLedger(Object.fromEntries(graceEntries), pkgVersion)).not.toThrow()
|
|
1298
|
+
})
|
|
1181
1299
|
})
|
|
1182
1300
|
|
|
1183
1301
|
// ── chart-legend-literal: the .map() half ────────────────────────────────────
|
|
@@ -1287,10 +1405,29 @@ describe('KNOWN_RULE_IDS', () => {
|
|
|
1287
1405
|
}
|
|
1288
1406
|
})
|
|
1289
1407
|
|
|
1408
|
+
// Deduped, because ONE id is deliberately in both registries: `in-body-page-title` is a plugin
|
|
1409
|
+
// rule AND a guard kind (law C8 has an AST half and a text half), so one `theme-allow
|
|
1410
|
+
// in-body-page-title — <why>` waives both lanes rather than needing a different word per lane.
|
|
1290
1411
|
it('contains nothing else — it is exactly the union of the two registries', () => {
|
|
1291
|
-
const expected = [
|
|
1412
|
+
const expected = [
|
|
1413
|
+
...new Set([...Object.keys(basaltPlugin.rules), ...Object.keys(GUARD_RULES)]),
|
|
1414
|
+
].toSorted()
|
|
1292
1415
|
expect([...KNOWN_RULE_IDS].toSorted()).toEqual(expected)
|
|
1293
1416
|
})
|
|
1417
|
+
|
|
1418
|
+
it('shares exactly one id between the two registries — the C8 pair', () => {
|
|
1419
|
+
const shared = Object.keys(basaltPlugin.rules).filter((id) => Object.hasOwn(GUARD_RULES, id))
|
|
1420
|
+
expect(shared).toEqual(['in-body-page-title'])
|
|
1421
|
+
})
|
|
1422
|
+
|
|
1423
|
+
// The guard's own copy (`PLUGIN_RULE_IDS`) is what `check-theme --audit-allows` classifies an
|
|
1424
|
+
// annotation against: an id in neither registry reads as a typo and is reported unscoped. A new
|
|
1425
|
+
// plugin rule missing from it would make every waiver naming it look like a mistake.
|
|
1426
|
+
it('every plugin rule is classifiable by the guard — its own id set or a guard kind', () => {
|
|
1427
|
+
for (const id of Object.keys(basaltPlugin.rules)) {
|
|
1428
|
+
expect([id, PLUGIN_RULE_IDS.has(id) || Object.hasOwn(GUARD_RULES, id)]).toEqual([id, true])
|
|
1429
|
+
}
|
|
1430
|
+
})
|
|
1294
1431
|
})
|
|
1295
1432
|
|
|
1296
1433
|
// ── theme-allow fails closed on a typo ─────────────────────────────────────────────────────────
|
|
@@ -1335,3 +1472,802 @@ describe('theme-allow with an unrecognized rule id', () => {
|
|
|
1335
1472
|
expect(code).toBe(0)
|
|
1336
1473
|
})
|
|
1337
1474
|
})
|
|
1475
|
+
|
|
1476
|
+
// ── the control-home rules (docs/CONTROLS-SPEC.md §6) ────────────────────────
|
|
1477
|
+
|
|
1478
|
+
const MANTINE_IMPORT = `import { Button, ScrollArea, SegmentedControl, Select } from '@mantine/core'\n`
|
|
1479
|
+
|
|
1480
|
+
/**
|
|
1481
|
+
* The home components, imported from basalt — the OWNER-tag provenance every slot rule now requires.
|
|
1482
|
+
*
|
|
1483
|
+
* Not decoration: `Section` is one of the most common local component names there is, and matching
|
|
1484
|
+
* the bare tag made a consumer's own `<Section actions={…}>` an `error`-level tiered home. A fixture
|
|
1485
|
+
* that renders `<PageBar>` out of thin air proves nothing a real consumer file would hit, so the
|
|
1486
|
+
* fixtures import what they render.
|
|
1487
|
+
*/
|
|
1488
|
+
const BASALT_IMPORT = `import { ChartCard, PageBar, Section, SettingsSection } from 'basalt-ui'\n`
|
|
1489
|
+
|
|
1490
|
+
describe('basalt/hand-rolled-filter', () => {
|
|
1491
|
+
it('flags a raw Mantine Select handed to a PageBar filters slot', () => {
|
|
1492
|
+
const { code, rules } = run(
|
|
1493
|
+
`${BASALT_IMPORT}${MANTINE_IMPORT}export const C = () => <PageBar filters={<Select data={[]} />} />\n`,
|
|
1494
|
+
)
|
|
1495
|
+
expect(code).toBe(1)
|
|
1496
|
+
expect(rules).toContain('hand-rolled-filter')
|
|
1497
|
+
})
|
|
1498
|
+
|
|
1499
|
+
it('flags it through a hoisted binding — the argo headerExtra shape', () => {
|
|
1500
|
+
const { code, rules } = run(
|
|
1501
|
+
`${BASALT_IMPORT}${MANTINE_IMPORT}const pills = <Select data={[]} />\n` +
|
|
1502
|
+
`export const C = () => <PageBar filters={pills} />\n`,
|
|
1503
|
+
)
|
|
1504
|
+
expect(code).toBe(1)
|
|
1505
|
+
expect(rules).toContain('hand-rolled-filter')
|
|
1506
|
+
})
|
|
1507
|
+
|
|
1508
|
+
it('flags one inside an ARRAY slot value (filtersEnd={[…]})', () => {
|
|
1509
|
+
const { rules } = run(
|
|
1510
|
+
`${BASALT_IMPORT}${MANTINE_IMPORT}export const C = () => <PageBar filtersEnd={[<Select key="a" data={[]} />]} />\n`,
|
|
1511
|
+
)
|
|
1512
|
+
expect(rules).toContain('hand-rolled-filter')
|
|
1513
|
+
})
|
|
1514
|
+
|
|
1515
|
+
// The body form is the whole reason the walk stops at the slot ATTRIBUTE: a Section's children
|
|
1516
|
+
// are a page region, not a control home, and a form there is C1's third home.
|
|
1517
|
+
// `control-outside-home` DOES report this one (a Section body is not a home either) — which is
|
|
1518
|
+
// the point of the split: the body form is a warn about placement, never an error about the slot.
|
|
1519
|
+
it("does NOT flag a Select in a Section's CHILDREN — the body form", () => {
|
|
1520
|
+
const { rules } = run(
|
|
1521
|
+
`${BASALT_IMPORT}${MANTINE_IMPORT}export const C = () => (\n <Section title="Filters">\n <Select data={[]} />\n </Section>\n)\n`,
|
|
1522
|
+
)
|
|
1523
|
+
expect(rules).not.toContain('hand-rolled-filter')
|
|
1524
|
+
})
|
|
1525
|
+
|
|
1526
|
+
it("does NOT flag a slot prop on a tag that is not a home (a consumer's own Toolbar)", () => {
|
|
1527
|
+
const { rules } = run(
|
|
1528
|
+
`${BASALT_IMPORT}${MANTINE_IMPORT}export const C = () => <Toolbar filters={<Select data={[]} />} />\n`,
|
|
1529
|
+
)
|
|
1530
|
+
expect(rules).not.toContain('hand-rolled-filter')
|
|
1531
|
+
})
|
|
1532
|
+
|
|
1533
|
+
it('does NOT flag a same-named component that is not the Mantine binding', () => {
|
|
1534
|
+
const { code, rules } = run(
|
|
1535
|
+
`${BASALT_IMPORT}import { Select } from './my-select'\nexport const C = () => <PageBar filters={<Select data={[]} />} />\n`,
|
|
1536
|
+
)
|
|
1537
|
+
expect(code).toBe(0)
|
|
1538
|
+
expect(rules).not.toContain('hand-rolled-filter')
|
|
1539
|
+
})
|
|
1540
|
+
|
|
1541
|
+
it('does NOT flag a bound basalt control in the same slot', () => {
|
|
1542
|
+
const { code, rules } = run(
|
|
1543
|
+
`${BASALT_IMPORT}export const C = () => <PageBar filters={<RangeFilter field={f} />} />\n`,
|
|
1544
|
+
)
|
|
1545
|
+
expect(code).toBe(0)
|
|
1546
|
+
expect(rules).not.toContain('hand-rolled-filter')
|
|
1547
|
+
})
|
|
1548
|
+
|
|
1549
|
+
// The settled reading of law C1's THIRD home (docs/CONTROLS-SPEC.md §5/§6): a form row keeps
|
|
1550
|
+
// Mantine's `md` tier, so a raw input bound to a setting is right there — and `control` /
|
|
1551
|
+
// `SettingsRow` are absent from the slot set for exactly that reason.
|
|
1552
|
+
it("does NOT flag a raw Select in a SettingsRow's control — the form-row home", () => {
|
|
1553
|
+
const { code, rules } = run(
|
|
1554
|
+
`${BASALT_IMPORT}${MANTINE_IMPORT}export const C = () => (\n <SettingsSection title="S">\n <SettingsRow label="Channel" control={<Select data={[]} />} />\n </SettingsSection>\n)\n`,
|
|
1555
|
+
)
|
|
1556
|
+
expect(code).toBe(0)
|
|
1557
|
+
expect(rules).not.toContain('hand-rolled-filter')
|
|
1558
|
+
expect(rules).not.toContain('control-outside-home')
|
|
1559
|
+
})
|
|
1560
|
+
|
|
1561
|
+
it("still flags one in a SettingsSection's actions header slot — that IS tiered", () => {
|
|
1562
|
+
const { rules } = run(
|
|
1563
|
+
`${BASALT_IMPORT}${MANTINE_IMPORT}export const C = () => <SettingsSection title="S" actions={<Select data={[]} />} />\n`,
|
|
1564
|
+
)
|
|
1565
|
+
expect(rules).toContain('hand-rolled-filter')
|
|
1566
|
+
})
|
|
1567
|
+
|
|
1568
|
+
it('flags a member tag whose ROOT binding is Mantine (Chip.Group)', () => {
|
|
1569
|
+
const { rules } = run(
|
|
1570
|
+
`${BASALT_IMPORT}import { Chip } from '@mantine/core'\nexport const C = () => <PageBar filters={<Chip.Group multiple />} />\n`,
|
|
1571
|
+
)
|
|
1572
|
+
expect(rules).toContain('hand-rolled-filter')
|
|
1573
|
+
})
|
|
1574
|
+
|
|
1575
|
+
it('honors a scoped theme-allow', () => {
|
|
1576
|
+
const { rules } = run(
|
|
1577
|
+
`${BASALT_IMPORT}${MANTINE_IMPORT}export const C = () => (\n <PageBar\n filters={\n // theme-allow hand-rolled-filter — not a store field\n <Select data={[]} />\n }\n />\n)\n`,
|
|
1578
|
+
)
|
|
1579
|
+
expect(rules).not.toContain('hand-rolled-filter')
|
|
1580
|
+
})
|
|
1581
|
+
|
|
1582
|
+
it('honors a theme-allow-file declaration', () => {
|
|
1583
|
+
const { rules } = run(
|
|
1584
|
+
`// theme-allow-file hand-rolled-filter — a legacy page, migrating next sprint\n` +
|
|
1585
|
+
`${BASALT_IMPORT}${MANTINE_IMPORT}export const C = () => <PageBar filters={<Select data={[]} />} />\n`,
|
|
1586
|
+
)
|
|
1587
|
+
expect(rules).not.toContain('hand-rolled-filter')
|
|
1588
|
+
})
|
|
1589
|
+
|
|
1590
|
+
// Colocating a trigger with its overlay in the slot is how a "create" modal and a "custom range"
|
|
1591
|
+
// popover are written. The overlay's contents portal out of the slot and are already declared
|
|
1592
|
+
// non-homes by `control-outside-home`, so the same code was exempt outside a slot and an `error`
|
|
1593
|
+
// inside one.
|
|
1594
|
+
it('does NOT flag a Select inside a Modal colocated in an actions slot', () => {
|
|
1595
|
+
const { code, rules } = run(
|
|
1596
|
+
`${BASALT_IMPORT}import { Button, Modal, Select } from '@mantine/core'\n` +
|
|
1597
|
+
`export const C = () => (\n <WidgetHeader\n actions={\n <>\n <Button onClick={open}>New</Button>\n <Modal opened>\n <Select data={[]} />\n </Modal>\n </>\n }\n />\n)\n`,
|
|
1598
|
+
)
|
|
1599
|
+
expect(code).toBe(0)
|
|
1600
|
+
expect(rules).not.toContain('hand-rolled-filter')
|
|
1601
|
+
})
|
|
1602
|
+
|
|
1603
|
+
it('does NOT flag one inside a Popover.Dropdown colocated in a filters slot', () => {
|
|
1604
|
+
const { rules } = run(
|
|
1605
|
+
`${BASALT_IMPORT}import { Button, Popover, Select } from '@mantine/core'\n` +
|
|
1606
|
+
`export const C = () => (\n <Section\n title="T"\n filters={\n <Popover>\n <Popover.Target>\n <Button>Custom range</Button>\n </Popover.Target>\n <Popover.Dropdown>\n <Select data={[]} />\n </Popover.Dropdown>\n </Popover>\n }\n />\n)\n`,
|
|
1607
|
+
)
|
|
1608
|
+
expect(rules).not.toContain('hand-rolled-filter')
|
|
1609
|
+
})
|
|
1610
|
+
|
|
1611
|
+
it('STILL flags a bare sibling Select in the same slot as an exempt overlay', () => {
|
|
1612
|
+
const { rules } = run(
|
|
1613
|
+
`${BASALT_IMPORT}import { Modal, Select } from '@mantine/core'\n` +
|
|
1614
|
+
`export const C = () => (\n <PageBar\n filters={\n <>\n <Select data={[]} />\n <Modal opened>\n <Select data={[]} />\n </Modal>\n </>\n }\n />\n)\n`,
|
|
1615
|
+
)
|
|
1616
|
+
expect(rules).toContain('hand-rolled-filter')
|
|
1617
|
+
})
|
|
1618
|
+
|
|
1619
|
+
// `Select as MantineSelect` is the canonical way a consumer wraps a Mantine component — the exact
|
|
1620
|
+
// case the provenance comment worried about — and keying membership on the LOCAL name made it
|
|
1621
|
+
// invisible.
|
|
1622
|
+
it('flags an ALIASED Mantine import in a slot', () => {
|
|
1623
|
+
const { code, rules } = run(
|
|
1624
|
+
`${BASALT_IMPORT}import { Select as MantineSelect } from '@mantine/core'\n` +
|
|
1625
|
+
`export const C = () => <PageBar filters={<MantineSelect data={[]} />} />\n`,
|
|
1626
|
+
)
|
|
1627
|
+
expect(code).toBe(1)
|
|
1628
|
+
expect(rules).toContain('hand-rolled-filter')
|
|
1629
|
+
})
|
|
1630
|
+
|
|
1631
|
+
it('flags a NAMESPACE-imported Mantine control in a slot', () => {
|
|
1632
|
+
const { rules } = run(
|
|
1633
|
+
`${BASALT_IMPORT}import * as M from '@mantine/core'\n` +
|
|
1634
|
+
`export const C = () => <PageBar filters={<M.SegmentedControl data={[]} />} />\n`,
|
|
1635
|
+
)
|
|
1636
|
+
expect(rules).toContain('hand-rolled-filter')
|
|
1637
|
+
})
|
|
1638
|
+
|
|
1639
|
+
it('flags an aliased MEMBER tag (Chip as C → C.Group)', () => {
|
|
1640
|
+
const { rules } = run(
|
|
1641
|
+
`${BASALT_IMPORT}import { Chip as Ch } from '@mantine/core'\n` +
|
|
1642
|
+
`export const C = () => <PageBar filters={<Ch.Group multiple />} />\n`,
|
|
1643
|
+
)
|
|
1644
|
+
expect(rules).toContain('hand-rolled-filter')
|
|
1645
|
+
})
|
|
1646
|
+
|
|
1647
|
+
// `control-outside-home` DOES report this one — a local `Section` is not a home, so the control
|
|
1648
|
+
// has none. Only this rule's silence is asserted; the exit code belongs to that warn.
|
|
1649
|
+
it("does NOT flag a slot on a consumer's own Section — provenance is on the OWNER too", () => {
|
|
1650
|
+
const { rules } = run(
|
|
1651
|
+
`import { Section } from './layout/section'\n${MANTINE_IMPORT}` +
|
|
1652
|
+
`export const C = () => <Section actions={<Select data={[]} />} />\n`,
|
|
1653
|
+
)
|
|
1654
|
+
expect(rules).not.toContain('hand-rolled-filter')
|
|
1655
|
+
})
|
|
1656
|
+
})
|
|
1657
|
+
|
|
1658
|
+
describe('basalt/control-outside-home', () => {
|
|
1659
|
+
it('flags a raw Mantine Select in no home at all', () => {
|
|
1660
|
+
const { code, rules } = run(
|
|
1661
|
+
`${MANTINE_IMPORT}export const C = () => <div><Select data={[]} /></div>\n`,
|
|
1662
|
+
)
|
|
1663
|
+
expect(code).toBe(1)
|
|
1664
|
+
expect(rules).toContain('control-outside-home')
|
|
1665
|
+
})
|
|
1666
|
+
|
|
1667
|
+
it('does NOT flag one inside a home slot — that is hand-rolled-filter s business', () => {
|
|
1668
|
+
const { rules } = run(
|
|
1669
|
+
`${BASALT_IMPORT}${MANTINE_IMPORT}export const C = () => <PageBar filters={<Select data={[]} />} />\n`,
|
|
1670
|
+
)
|
|
1671
|
+
expect(rules).not.toContain('control-outside-home')
|
|
1672
|
+
})
|
|
1673
|
+
|
|
1674
|
+
it.each(['SettingsRow', 'Modal', 'Drawer', 'Composer'])(
|
|
1675
|
+
'does NOT flag one under a declared non-home (%s)',
|
|
1676
|
+
(host) => {
|
|
1677
|
+
const { rules } = run(
|
|
1678
|
+
`${MANTINE_IMPORT}export const C = () => <${host}><Select data={[]} /></${host}>\n`,
|
|
1679
|
+
)
|
|
1680
|
+
expect(rules).not.toContain('control-outside-home')
|
|
1681
|
+
},
|
|
1682
|
+
)
|
|
1683
|
+
|
|
1684
|
+
it('does NOT flag one under a member-expression non-home (Menu.Dropdown)', () => {
|
|
1685
|
+
const { rules } = run(
|
|
1686
|
+
`${MANTINE_IMPORT}export const C = () => <Menu.Dropdown><Select data={[]} /></Menu.Dropdown>\n`,
|
|
1687
|
+
)
|
|
1688
|
+
expect(rules).not.toContain('control-outside-home')
|
|
1689
|
+
})
|
|
1690
|
+
|
|
1691
|
+
it('does NOT flag anything in a file that imports @mantine/form — a form is the third home', () => {
|
|
1692
|
+
const { code, rules } = run(
|
|
1693
|
+
`${MANTINE_IMPORT}import { useForm } from '@mantine/form'\n` +
|
|
1694
|
+
`export const C = () => <div><Select data={[]} /></div>\n`,
|
|
1695
|
+
)
|
|
1696
|
+
expect(code).toBe(0)
|
|
1697
|
+
expect(rules).not.toContain('control-outside-home')
|
|
1698
|
+
})
|
|
1699
|
+
|
|
1700
|
+
// The owner exemption, `hand-rolled-plot`'s shape: the module that IS the bound control cannot be
|
|
1701
|
+
// told to use one.
|
|
1702
|
+
// `shadow-basalt-export` reports the same fixture (a local `ViewTabs` collides with the shipped
|
|
1703
|
+
// one), so only this rule's own silence is asserted — the exit code belongs to that rule.
|
|
1704
|
+
it('does NOT flag the module that DEFINES a basalt control', () => {
|
|
1705
|
+
const { rules } = run(
|
|
1706
|
+
`${MANTINE_IMPORT}export function ViewTabs() {\n return <SegmentedControl data={[]} />\n}\n`,
|
|
1707
|
+
)
|
|
1708
|
+
expect(rules).not.toContain('control-outside-home')
|
|
1709
|
+
})
|
|
1710
|
+
|
|
1711
|
+
it('flags an ALIASED Mantine import with no home', () => {
|
|
1712
|
+
const { code, rules } = run(
|
|
1713
|
+
`import { Select as MantineSelect } from '@mantine/core'\n` +
|
|
1714
|
+
`export const C = () => <div><MantineSelect data={[]} /></div>\n`,
|
|
1715
|
+
)
|
|
1716
|
+
expect(code).toBe(1)
|
|
1717
|
+
expect(rules).toContain('control-outside-home')
|
|
1718
|
+
})
|
|
1719
|
+
|
|
1720
|
+
it('flags a NAMESPACE-imported Mantine control with no home', () => {
|
|
1721
|
+
const { rules } = run(
|
|
1722
|
+
`import * as M from '@mantine/core'\n` +
|
|
1723
|
+
`export const C = () => <div><M.SegmentedControl data={[]} /></div>\n`,
|
|
1724
|
+
)
|
|
1725
|
+
expect(rules).toContain('control-outside-home')
|
|
1726
|
+
})
|
|
1727
|
+
|
|
1728
|
+
it('honors a scoped theme-allow', () => {
|
|
1729
|
+
const { rules } = run(
|
|
1730
|
+
`${MANTINE_IMPORT}export const C = () => (\n <div>\n {/* theme-allow control-outside-home — a one-off admin picker */}\n <Select data={[]} />\n </div>\n)\n`,
|
|
1731
|
+
)
|
|
1732
|
+
expect(rules).not.toContain('control-outside-home')
|
|
1733
|
+
})
|
|
1734
|
+
})
|
|
1735
|
+
|
|
1736
|
+
describe('basalt/control-size-literal', () => {
|
|
1737
|
+
it.each(['size="xs"', 'w={200}', 'fullWidth', 'visibleFrom="sm"', 'hiddenFrom="sm"'])(
|
|
1738
|
+
'flags %s on an element inside a home slot',
|
|
1739
|
+
(prop) => {
|
|
1740
|
+
const { code, rules } = run(
|
|
1741
|
+
`${BASALT_IMPORT}${MANTINE_IMPORT}export const C = () => <PageBar actions={<Button ${prop}>Go</Button>} />\n`,
|
|
1742
|
+
)
|
|
1743
|
+
expect(code).toBe(1)
|
|
1744
|
+
expect(rules).toContain('control-size-literal')
|
|
1745
|
+
},
|
|
1746
|
+
)
|
|
1747
|
+
|
|
1748
|
+
it('flags it through a hoisted slot binding too', () => {
|
|
1749
|
+
const { rules } = run(
|
|
1750
|
+
`${BASALT_IMPORT}${MANTINE_IMPORT}const acts = <Button size="xs">Go</Button>\n` +
|
|
1751
|
+
`export const C = () => <PageBar actions={acts} />\n`,
|
|
1752
|
+
)
|
|
1753
|
+
expect(rules).toContain('control-size-literal')
|
|
1754
|
+
})
|
|
1755
|
+
|
|
1756
|
+
it("does NOT flag a size prop in a SettingsRow's control — the form tier is Mantine's md", () => {
|
|
1757
|
+
const { code, rules } = run(
|
|
1758
|
+
`${MANTINE_IMPORT}export const C = () => (\n <SettingsRow label="L" control={<Button size="compact-sm">Go</Button>} />\n)\n`,
|
|
1759
|
+
)
|
|
1760
|
+
expect(code).toBe(0)
|
|
1761
|
+
expect(rules).not.toContain('control-size-literal')
|
|
1762
|
+
})
|
|
1763
|
+
|
|
1764
|
+
it('does NOT flag a size prop on the HOME itself', () => {
|
|
1765
|
+
const { code, rules } = run(
|
|
1766
|
+
`${BASALT_IMPORT}${MANTINE_IMPORT}export const C = () => <PageBar w={200} actions={<Button>Go</Button>} />\n`,
|
|
1767
|
+
)
|
|
1768
|
+
expect(code).toBe(0)
|
|
1769
|
+
expect(rules).not.toContain('control-size-literal')
|
|
1770
|
+
})
|
|
1771
|
+
|
|
1772
|
+
it("does NOT flag a size prop in a Section's CHILDREN", () => {
|
|
1773
|
+
const { code, rules } = run(
|
|
1774
|
+
`${BASALT_IMPORT}${MANTINE_IMPORT}export const C = () => (\n <Section title="T">\n <Button size="xs">Go</Button>\n </Section>\n)\n`,
|
|
1775
|
+
)
|
|
1776
|
+
expect(code).toBe(0)
|
|
1777
|
+
expect(rules).not.toContain('control-size-literal')
|
|
1778
|
+
})
|
|
1779
|
+
|
|
1780
|
+
// The rule only reaches what the slot's own MantineThemeProvider re-tiers. An icon's `size` IS
|
|
1781
|
+
// its only sizing API and the slot theme cannot touch an SVG, so "drop the prop" produced a 24px
|
|
1782
|
+
// icon — on the shape the spec's own reference example writes.
|
|
1783
|
+
it("does NOT flag an icon's size inside a slot — the slot theme cannot size an SVG", () => {
|
|
1784
|
+
const { code, rules } = run(
|
|
1785
|
+
`${BASALT_IMPORT}${MANTINE_IMPORT}import { IconDownload } from '@tabler/icons-react'\n` +
|
|
1786
|
+
`export const C = () => (\n <PageBar actions={<Button leftSection={<IconDownload size={14} />}>Export</Button>} />\n)\n`,
|
|
1787
|
+
)
|
|
1788
|
+
expect(code).toBe(0)
|
|
1789
|
+
expect(rules).not.toContain('control-size-literal')
|
|
1790
|
+
})
|
|
1791
|
+
|
|
1792
|
+
it('does NOT flag a Badge or Loader size in a Section actions slot', () => {
|
|
1793
|
+
const { code, rules } = run(
|
|
1794
|
+
`${BASALT_IMPORT}import { Badge, Loader } from '@mantine/core'\n` +
|
|
1795
|
+
`export const C = ({ n, busy }) => (\n <Section title="T" actions={[<Badge key="c" size="sm">{n}</Badge>, busy ? <Loader key="l" size={14} /> : null]} />\n)\n`,
|
|
1796
|
+
)
|
|
1797
|
+
expect(code).toBe(0)
|
|
1798
|
+
expect(rules).not.toContain('control-size-literal')
|
|
1799
|
+
})
|
|
1800
|
+
|
|
1801
|
+
it("does NOT flag a Modal's own size, or an input's size inside that Modal", () => {
|
|
1802
|
+
const { code, rules } = run(
|
|
1803
|
+
`${BASALT_IMPORT}import { Button, Modal, TextInput } from '@mantine/core'\n` +
|
|
1804
|
+
`export const C = () => (\n <WidgetHeader\n actions={\n <>\n <Button>New</Button>\n <Modal opened size="lg">\n <TextInput size="md" />\n </Modal>\n </>\n }\n />\n)\n`,
|
|
1805
|
+
)
|
|
1806
|
+
expect(code).toBe(0)
|
|
1807
|
+
expect(rules).not.toContain('control-size-literal')
|
|
1808
|
+
})
|
|
1809
|
+
|
|
1810
|
+
it('STILL flags a raw filter size in the slot beside those exemptions', () => {
|
|
1811
|
+
const { rules } = run(
|
|
1812
|
+
`${BASALT_IMPORT}${MANTINE_IMPORT}export const C = () => (\n <PageBar filters={<Select size="xs" data={[]} />} />\n)\n`,
|
|
1813
|
+
)
|
|
1814
|
+
expect(rules).toContain('control-size-literal')
|
|
1815
|
+
})
|
|
1816
|
+
|
|
1817
|
+
it('flags an ALIASED Mantine Button size in a slot', () => {
|
|
1818
|
+
const { rules } = run(
|
|
1819
|
+
`${BASALT_IMPORT}import { Button as Btn } from '@mantine/core'\n` +
|
|
1820
|
+
`export const C = () => <PageBar actions={<Btn size="xs">Go</Btn>} />\n`,
|
|
1821
|
+
)
|
|
1822
|
+
expect(rules).toContain('control-size-literal')
|
|
1823
|
+
})
|
|
1824
|
+
|
|
1825
|
+
it("does NOT flag a size prop in a slot on a consumer's own Section", () => {
|
|
1826
|
+
const { code, rules } = run(
|
|
1827
|
+
`import { Section } from './layout/section'\n${MANTINE_IMPORT}` +
|
|
1828
|
+
`export const C = () => <Section actions={<Button size="sm">Go</Button>} />\n`,
|
|
1829
|
+
)
|
|
1830
|
+
expect(code).toBe(0)
|
|
1831
|
+
expect(rules).not.toContain('control-size-literal')
|
|
1832
|
+
})
|
|
1833
|
+
|
|
1834
|
+
it('honors a scoped theme-allow', () => {
|
|
1835
|
+
const { rules } = run(
|
|
1836
|
+
`${BASALT_IMPORT}${MANTINE_IMPORT}export const C = () => (\n <PageBar\n actions={\n <Button\n // theme-allow control-size-literal — a deliberate 24px tag\n size="xs"\n >\n Go\n </Button>\n }\n />\n)\n`,
|
|
1837
|
+
)
|
|
1838
|
+
expect(rules).not.toContain('control-size-literal')
|
|
1839
|
+
})
|
|
1840
|
+
})
|
|
1841
|
+
|
|
1842
|
+
// `control-size-literal`'s scope is a copy of the theme's own re-tiering list, because a plain-JS
|
|
1843
|
+
// plugin shipped to consumers cannot import Mantine-coupled TS. This is the pin that makes the copy
|
|
1844
|
+
// safe: a component added to (or dropped from) CTL_THEME without the rule following is a rule
|
|
1845
|
+
// telling someone to drop a prop nothing else sets — or silently ignoring one that matters.
|
|
1846
|
+
describe('CTL_THEME_TAGS ↔ CTL_THEME', () => {
|
|
1847
|
+
it('is exactly the component map the slot theme re-tiers', () => {
|
|
1848
|
+
expect([...(CTL_THEME_TAGS as Set<string>)].toSorted()).toEqual(
|
|
1849
|
+
Object.keys(CTL_THEME.components ?? {}).toSorted(),
|
|
1850
|
+
)
|
|
1851
|
+
})
|
|
1852
|
+
})
|
|
1853
|
+
|
|
1854
|
+
describe('basalt/page-bar-budget', () => {
|
|
1855
|
+
it('flags two PageBars in the SAME returned tree — the pair that mounts together', () => {
|
|
1856
|
+
const { code, rules } = run(
|
|
1857
|
+
`${BASALT_IMPORT}export const C = () => (\n <>\n <PageBar title="A" />\n <PageBar title="B" />\n </>\n)\n`,
|
|
1858
|
+
)
|
|
1859
|
+
expect(code).toBe(1)
|
|
1860
|
+
expect(rules).toContain('page-bar-budget')
|
|
1861
|
+
})
|
|
1862
|
+
|
|
1863
|
+
// The two false positives the per-FILE count produced. Neither renders two bars at once, so
|
|
1864
|
+
// nothing races for the portal node — and the only escape was a theme-allow on correct code.
|
|
1865
|
+
it('does NOT flag an early-return loading state and the loaded bar', () => {
|
|
1866
|
+
const { code, rules } = run(
|
|
1867
|
+
`${BASALT_IMPORT}export const C = ({ data }) => {\n if (!data) return <PageBar title="Jobs" />\n return <PageBar title="Jobs" actions={{ primary: { key: 'r', label: 'Run' } }} />\n}\n`,
|
|
1868
|
+
)
|
|
1869
|
+
expect(code).toBe(0)
|
|
1870
|
+
expect(rules).not.toContain('page-bar-budget')
|
|
1871
|
+
})
|
|
1872
|
+
|
|
1873
|
+
it('does NOT flag two page components exported from one file', () => {
|
|
1874
|
+
const { code, rules } = run(
|
|
1875
|
+
`${BASALT_IMPORT}export const ListPage = () => <PageBar title="Jobs" />\nexport const DetailPage = () => <PageBar title="Job" />\n`,
|
|
1876
|
+
)
|
|
1877
|
+
expect(code).toBe(0)
|
|
1878
|
+
expect(rules).not.toContain('page-bar-budget')
|
|
1879
|
+
})
|
|
1880
|
+
|
|
1881
|
+
it("does NOT flag a consumer's own PageBar-named component", () => {
|
|
1882
|
+
const { code, rules } = run(
|
|
1883
|
+
`import { PageBar } from './chrome/page-bar'\nexport const C = () => (\n <>\n <PageBar title="A" />\n <PageBar title="B" />\n </>\n)\n`,
|
|
1884
|
+
)
|
|
1885
|
+
expect(code).toBe(0)
|
|
1886
|
+
expect(rules).not.toContain('page-bar-budget')
|
|
1887
|
+
})
|
|
1888
|
+
|
|
1889
|
+
it('flags a 5-entry actions.secondary array', () => {
|
|
1890
|
+
const five = [1, 2, 3, 4, 5].map((n) => `{ key: '${n}', label: '${n}' }`).join(', ')
|
|
1891
|
+
const { code, rules } = run(
|
|
1892
|
+
`${BASALT_IMPORT}export const C = () => <PageBar actions={{ secondary: [${five}] }} />\n`,
|
|
1893
|
+
)
|
|
1894
|
+
expect(code).toBe(1)
|
|
1895
|
+
expect(rules).toContain('page-bar-budget')
|
|
1896
|
+
})
|
|
1897
|
+
|
|
1898
|
+
it('does NOT flag a 4-entry actions.secondary array', () => {
|
|
1899
|
+
const four = [1, 2, 3, 4].map((n) => `{ key: '${n}', label: '${n}' }`).join(', ')
|
|
1900
|
+
const { code, rules } = run(
|
|
1901
|
+
`${BASALT_IMPORT}export const C = () => <PageBar actions={{ secondary: [${four}] }} />\n`,
|
|
1902
|
+
)
|
|
1903
|
+
expect(code).toBe(0)
|
|
1904
|
+
expect(rules).not.toContain('page-bar-budget')
|
|
1905
|
+
})
|
|
1906
|
+
|
|
1907
|
+
it('flags a 4-entry Section actions array (budget 3)', () => {
|
|
1908
|
+
const four = [1, 2, 3, 4].map((n) => `{ key: '${n}', label: '${n}' }`).join(', ')
|
|
1909
|
+
const { rules } = run(
|
|
1910
|
+
`${BASALT_IMPORT}export const C = () => <Section actions={[${four}]} />\n`,
|
|
1911
|
+
)
|
|
1912
|
+
expect(rules).toContain('page-bar-budget')
|
|
1913
|
+
})
|
|
1914
|
+
|
|
1915
|
+
it('flags a second variant="filled" inside one slot', () => {
|
|
1916
|
+
const { rules } = run(
|
|
1917
|
+
`${BASALT_IMPORT}${MANTINE_IMPORT}export const C = () => (\n <PageBar actions={<><Button variant="filled">A</Button><Button variant="filled">B</Button></>} />\n)\n`,
|
|
1918
|
+
)
|
|
1919
|
+
expect(rules).toContain('page-bar-budget')
|
|
1920
|
+
})
|
|
1921
|
+
|
|
1922
|
+
it('does NOT flag one filled action beside a default one', () => {
|
|
1923
|
+
const { code, rules } = run(
|
|
1924
|
+
`${BASALT_IMPORT}${MANTINE_IMPORT}export const C = () => (\n <PageBar actions={<><Button variant="filled">A</Button><Button variant="default">B</Button></>} />\n)\n`,
|
|
1925
|
+
)
|
|
1926
|
+
expect(code).toBe(0)
|
|
1927
|
+
expect(rules).not.toContain('page-bar-budget')
|
|
1928
|
+
})
|
|
1929
|
+
|
|
1930
|
+
// The `filled` count is a PRIMARY-ACTION count: a filled Badge is Mantine's default Badge
|
|
1931
|
+
// variant and the ordinary way a header states a count (law C11 asks for one), and an overlay's
|
|
1932
|
+
// submit button is not in the bar at all.
|
|
1933
|
+
it('does NOT flag a filled Badge beside the one filled Button', () => {
|
|
1934
|
+
const { code, rules } = run(
|
|
1935
|
+
`${BASALT_IMPORT}import { Badge, Button } from '@mantine/core'\n` +
|
|
1936
|
+
`export const C = () => (\n <Section title="T" actions={<><Button variant="filled">Run</Button><Badge variant="filled">3</Badge></>} />\n)\n`,
|
|
1937
|
+
)
|
|
1938
|
+
expect(code).toBe(0)
|
|
1939
|
+
expect(rules).not.toContain('page-bar-budget')
|
|
1940
|
+
})
|
|
1941
|
+
|
|
1942
|
+
it('does NOT flag a filled ThemeIcon beside the one filled Button', () => {
|
|
1943
|
+
const { rules } = run(
|
|
1944
|
+
`${BASALT_IMPORT}import { Button, ThemeIcon } from '@mantine/core'\n` +
|
|
1945
|
+
`export const C = () => (\n <Section title="T" actions={<><Button variant="filled">Run</Button><ThemeIcon variant="filled" /></>} />\n)\n`,
|
|
1946
|
+
)
|
|
1947
|
+
expect(rules).not.toContain('page-bar-budget')
|
|
1948
|
+
})
|
|
1949
|
+
|
|
1950
|
+
it("does NOT flag a Modal's own filled submit button colocated in the slot", () => {
|
|
1951
|
+
const { rules } = run(
|
|
1952
|
+
`${BASALT_IMPORT}import { Button, Modal } from '@mantine/core'\n` +
|
|
1953
|
+
`export const C = () => (\n <PageBar actions={<><Button variant="filled">New</Button><Modal opened><Button variant="filled">Save</Button></Modal></>} />\n)\n`,
|
|
1954
|
+
)
|
|
1955
|
+
expect(rules).not.toContain('page-bar-budget')
|
|
1956
|
+
})
|
|
1957
|
+
|
|
1958
|
+
it('honors a scoped theme-allow', () => {
|
|
1959
|
+
const { rules } = run(
|
|
1960
|
+
`${BASALT_IMPORT}export const C = () => (\n <>\n <PageBar title="A" />\n {/* theme-allow page-bar-budget — the print-only variant */}\n <PageBar title="B" />\n </>\n)\n`,
|
|
1961
|
+
)
|
|
1962
|
+
expect(rules).not.toContain('page-bar-budget')
|
|
1963
|
+
})
|
|
1964
|
+
})
|
|
1965
|
+
|
|
1966
|
+
describe('basalt/in-body-page-title', () => {
|
|
1967
|
+
it.each([1, 2])('flags <Title order={%i}> in the body', (order) => {
|
|
1968
|
+
const { code, rules } = run(`export const C = () => <Title order={${order}}>Page</Title>\n`)
|
|
1969
|
+
expect(code).toBe(1)
|
|
1970
|
+
expect(rules).toContain('in-body-page-title')
|
|
1971
|
+
})
|
|
1972
|
+
|
|
1973
|
+
it('does NOT flag order={3} — a section heading', () => {
|
|
1974
|
+
const { code, rules } = run(`export const C = () => <Title order={3}>Section</Title>\n`)
|
|
1975
|
+
expect(code).toBe(0)
|
|
1976
|
+
expect(rules).not.toContain('in-body-page-title')
|
|
1977
|
+
})
|
|
1978
|
+
|
|
1979
|
+
it.each(['Prose', 'ArticleLayout', 'Modal', 'Drawer'])(
|
|
1980
|
+
'does NOT flag one under %s — that is a document heading',
|
|
1981
|
+
(host) => {
|
|
1982
|
+
const { rules } = run(
|
|
1983
|
+
`export const C = () => <${host}><Title order={1}>Doc</Title></${host}>\n`,
|
|
1984
|
+
)
|
|
1985
|
+
expect(rules).not.toContain('in-body-page-title')
|
|
1986
|
+
},
|
|
1987
|
+
)
|
|
1988
|
+
|
|
1989
|
+
it('does NOT flag one under a content/ path segment', () => {
|
|
1990
|
+
const { code, rules } = run(
|
|
1991
|
+
`export const C = () => <Title order={1}>Doc</Title>\n`,
|
|
1992
|
+
'content/page.tsx',
|
|
1993
|
+
)
|
|
1994
|
+
expect(code).toBe(0)
|
|
1995
|
+
expect(rules).not.toContain('in-body-page-title')
|
|
1996
|
+
})
|
|
1997
|
+
|
|
1998
|
+
it('honors a scoped theme-allow', () => {
|
|
1999
|
+
const { rules } = run(
|
|
2000
|
+
`// theme-allow in-body-page-title — a shell-less print view\nexport const C = () => <Title order={1}>Page</Title>\n`,
|
|
2001
|
+
)
|
|
2002
|
+
expect(rules).not.toContain('in-body-page-title')
|
|
2003
|
+
})
|
|
2004
|
+
})
|
|
2005
|
+
|
|
2006
|
+
describe('basalt/responsive-twin', () => {
|
|
2007
|
+
it('flags the same control mounted under visibleFrom and hiddenFrom', () => {
|
|
2008
|
+
const { code, rules } = run(
|
|
2009
|
+
`export const C = () => (\n <div>\n <SegmentedControl visibleFrom="sm" data={[]} />\n <SegmentedControl hiddenFrom="sm" data={[]} />\n </div>\n)\n`,
|
|
2010
|
+
)
|
|
2011
|
+
expect(code).toBe(1)
|
|
2012
|
+
expect(rules).toContain('responsive-twin')
|
|
2013
|
+
})
|
|
2014
|
+
|
|
2015
|
+
it('flags it when the control sits inside each half rather than being it', () => {
|
|
2016
|
+
const { rules } = run(
|
|
2017
|
+
`export const C = () => (\n <div>\n <Group visibleFrom="sm"><ViewTabs field={f} /></Group>\n <Stack hiddenFrom="sm"><ViewTabs field={f} /></Stack>\n </div>\n)\n`,
|
|
2018
|
+
)
|
|
2019
|
+
expect(rules).toContain('responsive-twin')
|
|
2020
|
+
})
|
|
2021
|
+
|
|
2022
|
+
it('does NOT flag two halves on DIFFERENT breakpoints', () => {
|
|
2023
|
+
const { code, rules } = run(
|
|
2024
|
+
`export const C = () => (\n <div>\n <SegmentedControl visibleFrom="sm" data={[]} />\n <SegmentedControl hiddenFrom="md" data={[]} />\n </div>\n)\n`,
|
|
2025
|
+
)
|
|
2026
|
+
expect(code).toBe(0)
|
|
2027
|
+
expect(rules).not.toContain('responsive-twin')
|
|
2028
|
+
})
|
|
2029
|
+
|
|
2030
|
+
it('does NOT flag a pair holding DIFFERENT controls', () => {
|
|
2031
|
+
const { rules } = run(
|
|
2032
|
+
`export const C = () => (\n <div>\n <SegmentedControl visibleFrom="sm" data={[]} />\n <Select hiddenFrom="sm" data={[]} />\n </div>\n)\n`,
|
|
2033
|
+
)
|
|
2034
|
+
expect(rules).not.toContain('responsive-twin')
|
|
2035
|
+
})
|
|
2036
|
+
|
|
2037
|
+
// Same fixture caveat as control-outside-home's owner test: the local `ViewTabs` name is itself a
|
|
2038
|
+
// `shadow-basalt-export` finding, so only this rule's silence is asserted.
|
|
2039
|
+
it('does NOT flag the module that DEFINES a basalt control — the CSS swap lives there', () => {
|
|
2040
|
+
const { rules } = run(
|
|
2041
|
+
`export function ViewTabs() {\n return (\n <div>\n <SegmentedControl visibleFrom="sm" data={[]} />\n <SegmentedControl hiddenFrom="sm" data={[]} />\n </div>\n )\n}\n`,
|
|
2042
|
+
)
|
|
2043
|
+
expect(rules).not.toContain('responsive-twin')
|
|
2044
|
+
})
|
|
2045
|
+
|
|
2046
|
+
it('honors a scoped theme-allow', () => {
|
|
2047
|
+
const { rules } = run(
|
|
2048
|
+
`export const C = () => (\n <div>\n <SegmentedControl visibleFrom="sm" data={[]} />\n {/* theme-allow responsive-twin — two genuinely different data sets */}\n <SegmentedControl hiddenFrom="sm" data={[]} />\n </div>\n)\n`,
|
|
2049
|
+
)
|
|
2050
|
+
expect(rules).not.toContain('responsive-twin')
|
|
2051
|
+
})
|
|
2052
|
+
})
|
|
2053
|
+
|
|
2054
|
+
describe('basalt/search-literal-link', () => {
|
|
2055
|
+
const NAV = (search: string) =>
|
|
2056
|
+
`export const nav = defineNav({\n sections: [\n { id: 'a', label: 'A', link: linkOptions({ to: '/dashboard'${search} }) },\n ],\n})\n`
|
|
2057
|
+
|
|
2058
|
+
it('flags a `search` object literal inside defineNav', () => {
|
|
2059
|
+
const { code, rules } = run(NAV(", search: { window: '30d' }"), 'nav.ts')
|
|
2060
|
+
expect(code).toBe(1)
|
|
2061
|
+
expect(rules).toContain('search-literal-link')
|
|
2062
|
+
})
|
|
2063
|
+
|
|
2064
|
+
it('flags it inside navGroup too', () => {
|
|
2065
|
+
const { rules } = run(
|
|
2066
|
+
`export const g = navGroup({ id: 'g', label: 'G' }, [\n { id: 'x', label: 'X', link: linkOptions({ to: '/d', search: { window: '30d' } }) },\n])\n`,
|
|
2067
|
+
'nav.ts',
|
|
2068
|
+
)
|
|
2069
|
+
expect(rules).toContain('search-literal-link')
|
|
2070
|
+
})
|
|
2071
|
+
|
|
2072
|
+
it('does NOT flag `search: store.linkSearch` passed by reference', () => {
|
|
2073
|
+
const { code, rules } = run(NAV(', search: dashboardFilters.linkSearch'), 'nav.ts')
|
|
2074
|
+
expect(code).toBe(0)
|
|
2075
|
+
expect(rules).not.toContain('search-literal-link')
|
|
2076
|
+
})
|
|
2077
|
+
|
|
2078
|
+
it('does NOT flag a linkOptions literal OUTSIDE a nav definition', () => {
|
|
2079
|
+
const { code, rules } = run(
|
|
2080
|
+
`export const target = linkOptions({ to: '/d', search: { window: '30d' } })\n`,
|
|
2081
|
+
'nav.ts',
|
|
2082
|
+
)
|
|
2083
|
+
expect(code).toBe(0)
|
|
2084
|
+
expect(rules).not.toContain('search-literal-link')
|
|
2085
|
+
})
|
|
2086
|
+
|
|
2087
|
+
it('honors a scoped theme-allow', () => {
|
|
2088
|
+
const { rules } = run(
|
|
2089
|
+
`export const nav = defineNav({\n sections: [\n {\n id: 'a',\n label: 'A',\n link: linkOptions({\n to: '/d',\n // theme-allow search-literal-link — a fixed report link, deliberately pinned\n search: { window: '30d' },\n }),\n },\n ],\n})\n`,
|
|
2090
|
+
'nav.ts',
|
|
2091
|
+
)
|
|
2092
|
+
expect(rules).not.toContain('search-literal-link')
|
|
2093
|
+
})
|
|
2094
|
+
})
|
|
2095
|
+
|
|
2096
|
+
describe('basalt/use-search-from-literal', () => {
|
|
2097
|
+
it('flags useSearch({ from: <literal> })', () => {
|
|
2098
|
+
const { code, rules } = run(
|
|
2099
|
+
`export const useRange = () => useSearch({ from: '/dashboard' })\n`,
|
|
2100
|
+
'hooks.ts',
|
|
2101
|
+
)
|
|
2102
|
+
expect(code).toBe(1)
|
|
2103
|
+
expect(rules).toContain('use-search-from-literal')
|
|
2104
|
+
})
|
|
2105
|
+
|
|
2106
|
+
it('does NOT flag useSearch({ strict: false })', () => {
|
|
2107
|
+
const { code, rules } = run(
|
|
2108
|
+
`export const useRange = () => useSearch({ strict: false })\n`,
|
|
2109
|
+
'hooks.ts',
|
|
2110
|
+
)
|
|
2111
|
+
expect(code).toBe(0)
|
|
2112
|
+
expect(rules).not.toContain('use-search-from-literal')
|
|
2113
|
+
})
|
|
2114
|
+
|
|
2115
|
+
it('honors a scoped theme-allow', () => {
|
|
2116
|
+
const { rules } = run(
|
|
2117
|
+
`export const useRange = () =>\n useSearch({\n // theme-allow use-search-from-literal — a route-owned loader, never rendered elsewhere\n from: '/dashboard',\n })\n`,
|
|
2118
|
+
'hooks.ts',
|
|
2119
|
+
)
|
|
2120
|
+
expect(rules).not.toContain('use-search-from-literal')
|
|
2121
|
+
})
|
|
2122
|
+
})
|
|
2123
|
+
|
|
2124
|
+
// ── raw-scroll-container — the C7 widening ───────────────────────────────────
|
|
2125
|
+
|
|
2126
|
+
describe('basalt/raw-scroll-container — the home widening (C7)', () => {
|
|
2127
|
+
it('flags overflowX inside a home slot', () => {
|
|
2128
|
+
const { rules } = run(
|
|
2129
|
+
`${BASALT_IMPORT}export const C = () => <PageBar filters={<div style={{ overflowX: 'auto' }} />} />\n`,
|
|
2130
|
+
)
|
|
2131
|
+
expect(rules).toContain('raw-scroll-container')
|
|
2132
|
+
})
|
|
2133
|
+
|
|
2134
|
+
// The C7 widening is about a sideways-scrolling row of CONTROLS, which only ever lives in a slot.
|
|
2135
|
+
// Counting the body made every wide table and every horizontally scrolling code block on a page
|
|
2136
|
+
// an `error` carrying that message — the exact pattern this rule's own comment calls legitimate.
|
|
2137
|
+
it('does NOT flag overflowX in a Section BODY — page content, not a control row', () => {
|
|
2138
|
+
const { code, rules } = run(
|
|
2139
|
+
`${BASALT_IMPORT}export const C = () => (\n <Section title="Raw rows">\n <div style={{ overflowX: 'scroll' }}>\n <table />\n </div>\n </Section>\n)\n`,
|
|
2140
|
+
)
|
|
2141
|
+
expect(code).toBe(0)
|
|
2142
|
+
expect(rules).not.toContain('raw-scroll-container')
|
|
2143
|
+
})
|
|
2144
|
+
|
|
2145
|
+
it('does NOT flag <ScrollArea scrollbars="x"> around a wide table in a ChartCard body', () => {
|
|
2146
|
+
const { rules } = run(
|
|
2147
|
+
`${BASALT_IMPORT}${MANTINE_IMPORT}export const C = () => (\n <ChartCard title="T">\n <ScrollArea scrollbars="x">\n <table />\n </ScrollArea>\n </ChartCard>\n)\n`,
|
|
2148
|
+
)
|
|
2149
|
+
expect(rules).not.toContain('raw-scroll-container')
|
|
2150
|
+
})
|
|
2151
|
+
|
|
2152
|
+
it("does NOT flag a slot on a consumer's own Section — not a basalt home", () => {
|
|
2153
|
+
const { code, rules } = run(
|
|
2154
|
+
`import { Section } from './layout/section'\n${MANTINE_IMPORT}` +
|
|
2155
|
+
`export const C = () => <Section actions={<ScrollArea scrollbars="x" />} />\n`,
|
|
2156
|
+
)
|
|
2157
|
+
expect(code).toBe(0)
|
|
2158
|
+
expect(rules).not.toContain('raw-scroll-container')
|
|
2159
|
+
})
|
|
2160
|
+
|
|
2161
|
+
it("does NOT flag a consumer's own ScrollArea in a basalt slot", () => {
|
|
2162
|
+
const { rules } = run(
|
|
2163
|
+
`${BASALT_IMPORT}import { ScrollArea } from './ui/scroll-area'\n` +
|
|
2164
|
+
`export const C = () => <PageBar filters={<ScrollArea scrollbars="x" />} />\n`,
|
|
2165
|
+
)
|
|
2166
|
+
expect(rules).not.toContain('raw-scroll-container')
|
|
2167
|
+
})
|
|
2168
|
+
|
|
2169
|
+
it('flags <ScrollArea scrollbars="x"> inside a home slot', () => {
|
|
2170
|
+
const { rules } = run(
|
|
2171
|
+
`${BASALT_IMPORT}${MANTINE_IMPORT}export const C = () => <PageBar filters={<ScrollArea scrollbars="x" />} />\n`,
|
|
2172
|
+
)
|
|
2173
|
+
expect(rules).toContain('raw-scroll-container')
|
|
2174
|
+
})
|
|
2175
|
+
|
|
2176
|
+
// The original doctrine, unchanged: a horizontal bar outside a home reserves no gutter width, so
|
|
2177
|
+
// a code block or a pinned-column table stays its own legitimate pattern.
|
|
2178
|
+
it('does NOT flag overflowX outside a home', () => {
|
|
2179
|
+
const { code, rules } = run(`const s = { overflowX: 'auto' }\n`, 'lib.ts')
|
|
2180
|
+
expect(code).toBe(0)
|
|
2181
|
+
expect(rules).not.toContain('raw-scroll-container')
|
|
2182
|
+
})
|
|
2183
|
+
|
|
2184
|
+
it('does NOT flag <ScrollArea scrollbars="x"> outside a home', () => {
|
|
2185
|
+
const { code, rules } = run(
|
|
2186
|
+
`${MANTINE_IMPORT}export const C = () => <ScrollArea scrollbars="x" />\n`,
|
|
2187
|
+
)
|
|
2188
|
+
expect(code).toBe(0)
|
|
2189
|
+
expect(rules).not.toContain('raw-scroll-container')
|
|
2190
|
+
})
|
|
2191
|
+
|
|
2192
|
+
it('honors a scoped theme-allow on the widened half', () => {
|
|
2193
|
+
const { rules } = run(
|
|
2194
|
+
`${BASALT_IMPORT}export const C = () => (\n <Section\n title="T"\n actions={\n // theme-allow raw-scroll-container — a pinned-column table owns its own scroll node\n <div style={{ overflowX: 'auto' }} />\n }\n />\n)\n`,
|
|
2195
|
+
)
|
|
2196
|
+
expect(rules).not.toContain('raw-scroll-container')
|
|
2197
|
+
})
|
|
2198
|
+
})
|
|
2199
|
+
|
|
2200
|
+
// ── shadow-basalt-export — the rename table ──────────────────────────────────
|
|
2201
|
+
|
|
2202
|
+
describe('basalt/shadow-basalt-export — SHADOW_ALIASES', () => {
|
|
2203
|
+
it.each([
|
|
2204
|
+
['PageHeader', 'PageBar'],
|
|
2205
|
+
['WindowSelector', 'RangeFilter'],
|
|
2206
|
+
['SectionTitle', 'Section'],
|
|
2207
|
+
['RefreshButton', 'SyncButton'],
|
|
2208
|
+
['HeroCard', 'StatCard'],
|
|
2209
|
+
])('flags a local %s and names %s as what it forks', (local, canonical) => {
|
|
2210
|
+
const { code, rules, output } = run(`export function ${local}() {\n return null\n}\n`)
|
|
2211
|
+
expect(code).toBe(1)
|
|
2212
|
+
expect(rules).toContain('shadow-basalt-export')
|
|
2213
|
+
expect(output).toContain(canonical)
|
|
2214
|
+
})
|
|
2215
|
+
|
|
2216
|
+
it('flags the const form too', () => {
|
|
2217
|
+
const { rules } = run(`export const FilterBar = () => null\n`)
|
|
2218
|
+
expect(rules).toContain('shadow-basalt-export')
|
|
2219
|
+
})
|
|
2220
|
+
|
|
2221
|
+
it('does NOT flag a name that is neither an export nor an alias', () => {
|
|
2222
|
+
const { code, rules } = run(`export const RangePickerish = () => null\n`)
|
|
2223
|
+
expect(code).toBe(0)
|
|
2224
|
+
expect(rules).not.toContain('shadow-basalt-export')
|
|
2225
|
+
})
|
|
2226
|
+
|
|
2227
|
+
it('is waivable with a scoped theme-allow', () => {
|
|
2228
|
+
const { rules } = run(
|
|
2229
|
+
`// theme-allow shadow-basalt-export — a marketing hero, unrelated to StatCard\nexport const HeroCard = () => null\n`,
|
|
2230
|
+
)
|
|
2231
|
+
expect(rules).not.toContain('shadow-basalt-export')
|
|
2232
|
+
})
|
|
2233
|
+
})
|
|
2234
|
+
|
|
2235
|
+
// ── SURFACES ↔ the plugin registry ───────────────────────────────────────────
|
|
2236
|
+
|
|
2237
|
+
describe('PLUGIN_RULE_ID_LIST ↔ the plugin', () => {
|
|
2238
|
+
// surfaces.ts cannot import the plugin (it is dependency-free by contract, and the plugin is a
|
|
2239
|
+
// standalone .js loaded out of a consumer's node_modules), so the list is hand-maintained and
|
|
2240
|
+
// THIS is the gate that keeps it honest.
|
|
2241
|
+
it('is exactly the plugin s registered rule ids', () => {
|
|
2242
|
+
expect([...PLUGIN_RULE_ID_LIST].toSorted()).toEqual(Object.keys(basaltPlugin.rules).toSorted())
|
|
2243
|
+
})
|
|
2244
|
+
|
|
2245
|
+
it('maps every plugin rule to exactly one surface', () => {
|
|
2246
|
+
const owners = new Map<string, string[]>()
|
|
2247
|
+
for (const [key, spec] of Object.entries(SURFACES)) {
|
|
2248
|
+
if (spec.kind !== 'doctrine') continue
|
|
2249
|
+
for (const id of spec.pluginRules) owners.set(id, [...(owners.get(id) ?? []), key])
|
|
2250
|
+
}
|
|
2251
|
+
for (const id of PLUGIN_RULE_ID_LIST) {
|
|
2252
|
+
expect([id, owners.get(id)?.length ?? 0]).toEqual([id, 1])
|
|
2253
|
+
}
|
|
2254
|
+
// …and no surface claims an id the plugin does not register (the type already forbids it; this
|
|
2255
|
+
// catches a list entry deleted without its surface).
|
|
2256
|
+
for (const id of owners.keys()) {
|
|
2257
|
+
expect([id, (PLUGIN_RULE_ID_LIST as readonly string[]).includes(id)]).toEqual([id, true])
|
|
2258
|
+
}
|
|
2259
|
+
})
|
|
2260
|
+
|
|
2261
|
+
it("puts the shell rule on the root barrel's surface (wave 6)", () => {
|
|
2262
|
+
expect(SURFACES['.'].pluginRules).toContain('hand-rolled-shell')
|
|
2263
|
+
})
|
|
2264
|
+
|
|
2265
|
+
it('gives ./controls the control-tier rules', () => {
|
|
2266
|
+
expect([...SURFACES['./controls'].pluginRules].toSorted()).toEqual([
|
|
2267
|
+
'control-outside-home',
|
|
2268
|
+
'control-size-literal',
|
|
2269
|
+
'hand-rolled-filter',
|
|
2270
|
+
'responsive-twin',
|
|
2271
|
+
])
|
|
2272
|
+
})
|
|
2273
|
+
})
|