basalt-ui 1.32.0 → 1.33.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/CHANGELOG.md +50 -0
- package/MIGRATING.md +228 -20
- package/agent/rules/basalt-mantine.md +2 -2
- package/agent/rules/basalt-state.md +2 -2
- package/agent/rules/basalt-tokens.md +1 -1
- package/configs/oxlint-plugin.js +160 -89
- package/configs/oxlint-plugin.test.ts +273 -10
- package/configs/oxlint.json +2 -2
- package/dist/agent/coalesce.d.ts.map +1 -1
- package/dist/agent/coalesce.js +1 -2
- package/dist/agent/merge.d.ts.map +1 -1
- package/dist/agent/merge.js +1 -2
- package/dist/agent-chat/relative-time.d.ts +9 -0
- package/dist/agent-chat/relative-time.d.ts.map +1 -1
- package/dist/agent-chat/relative-time.js +1 -2
- package/dist/agent-chat/thread-message.d.ts.map +1 -1
- package/dist/agent-chat/thread-message.js +1 -2
- package/dist/charts/kinds/BandStrip.d.ts.map +1 -1
- package/dist/charts/kinds/BandStrip.js +3 -3
- package/dist/charts/kinds/Bars.d.ts.map +1 -1
- package/dist/charts/kinds/Bars.js +1 -2
- package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
- package/dist/charts/kinds/StackedArea.js +1 -2
- package/dist/charts/primitives/legend-disclosure.d.ts.map +1 -1
- package/dist/cli/audit-allows.d.ts +1 -1
- package/dist/cli/audit-allows.d.ts.map +1 -1
- package/dist/cli/audit-allows.js +4 -4
- package/dist/cli/check-theme.d.ts.map +1 -1
- package/dist/cli/check-theme.js +1 -2
- package/dist/cli/config.d.ts +197 -0
- package/dist/cli/config.d.ts.map +1 -0
- package/dist/cli/config.js +20 -0
- package/dist/cli/doctor.js +1 -1
- package/dist/cli/guard-hook.d.ts.map +1 -1
- package/dist/cli/guard-hook.js +2 -8
- package/dist/cli/index.d.ts +2 -196
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +1 -18
- 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.map +1 -1
- package/dist/commands/define-overlays.js +1 -2
- package/dist/common/index.d.ts +0 -1
- package/dist/common/index.d.ts.map +1 -1
- package/dist/common/index.js +0 -2
- package/dist/common/validate.d.ts +2 -2
- package/dist/common/validate.d.ts.map +1 -1
- package/dist/common/validate.js +2 -3
- package/dist/content/article-card.d.ts.map +1 -1
- package/dist/content/article-card.js +4 -1
- package/dist/controls/enum-filter.d.ts.map +1 -1
- package/dist/controls/enum-filter.js +3 -13
- package/dist/controls/filter-pill.d.ts +8 -0
- package/dist/controls/filter-pill.d.ts.map +1 -1
- package/dist/controls/filter-pill.js +15 -0
- package/dist/controls/multi-select-filter.d.ts.map +1 -1
- package/dist/controls/multi-select-filter.js +3 -13
- package/dist/controls/number-filter.d.ts +3 -0
- package/dist/controls/number-filter.d.ts.map +1 -1
- package/dist/controls/number-filter.js +12 -4
- package/dist/controls/range-filter.d.ts.map +1 -1
- package/dist/controls/range-filter.js +1 -2
- package/dist/data/data-table.d.ts +9 -2
- package/dist/data/data-table.d.ts.map +1 -1
- package/dist/data/data-table.js +2 -3
- package/dist/forms/form-layout.d.ts +6 -1
- package/dist/forms/form-layout.d.ts.map +1 -1
- package/dist/forms/form-layout.module.css +3 -0
- package/dist/guard/index.d.ts +18 -4
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +67 -31
- package/dist/guard/types.d.ts +3 -3
- package/dist/guard/types.d.ts.map +1 -1
- package/dist/guard/vx-names.d.ts +3 -0
- package/dist/guard/vx-names.d.ts.map +1 -0
- package/dist/guard/vx-names.js +257 -0
- package/dist/notifications/mount-guard.d.ts.map +1 -1
- package/dist/notifications/mount-guard.js +1 -2
- package/dist/provider/index.d.ts.map +1 -1
- package/dist/provider/index.js +2 -3
- package/dist/provider/lab-theme.d.ts +5 -4
- package/dist/provider/lab-theme.d.ts.map +1 -1
- package/dist/provider/lab-theme.js +1 -3
- package/dist/router-tanstack/search-store.d.ts +18 -0
- package/dist/router-tanstack/search-store.d.ts.map +1 -1
- package/dist/router-tanstack/search-store.js +33 -1
- package/dist/shell/mobile-nav-model.d.ts.map +1 -1
- package/dist/shell/mobile-nav-model.js +3 -4
- package/dist/shell/page-bar.d.ts +24 -0
- package/dist/shell/page-bar.d.ts.map +1 -1
- package/dist/shell/page-bar.js +1 -2
- package/dist/surfaces.d.ts +1 -1
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +2 -0
- package/dist/tokens/index.d.ts +2 -0
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +10 -0
- package/dist/tokens/size-classes.d.ts +13 -0
- package/dist/tokens/size-classes.d.ts.map +1 -1
- package/dist/tokens/size-classes.js +8 -0
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/src/agent/coalesce.ts +1 -2
- package/src/agent/merge.ts +1 -2
- package/src/agent-chat/relative-time.ts +1 -2
- package/src/agent-chat/thread-message.tsx +2 -3
- package/src/charts/kinds/BandStrip.tsx +7 -3
- package/src/charts/kinds/Bars.tsx +1 -2
- package/src/charts/kinds/StackedArea.tsx +4 -2
- package/src/charts/primitives/legend-disclosure.tsx +1 -0
- package/src/cli/audit-allows.ts +10 -9
- package/src/cli/check-theme.ts +1 -2
- package/src/cli/config.ts +209 -0
- package/src/cli/doctor.ts +2 -2
- package/src/cli/guard-hook.ts +2 -8
- package/src/cli/index.ts +2 -206
- package/src/commands/define-commands.ts +2 -2
- package/src/commands/define-overlays.ts +1 -2
- package/src/common/index.ts +0 -1
- package/src/common/validate.ts +4 -5
- package/src/content/article-card.tsx +1 -0
- package/src/controls/enum-filter.tsx +3 -13
- package/src/controls/filter-pill.tsx +20 -0
- package/src/controls/multi-select-filter.tsx +3 -13
- package/src/controls/number-filter.tsx +24 -3
- package/src/controls/range-filter.tsx +1 -2
- package/src/data/data-table.tsx +11 -5
- package/src/forms/form-layout.module.css +3 -0
- package/src/forms/form-layout.tsx +6 -1
- package/src/guard/index.ts +151 -62
- package/src/guard/types.ts +3 -2
- package/src/guard/vx-names.ts +257 -0
- package/src/notifications/mount-guard.ts +1 -2
- package/src/provider/index.tsx +2 -3
- package/src/provider/lab-theme.ts +28 -21
- package/src/router-tanstack/search-store.ts +73 -1
- package/src/shell/mobile-nav-model.ts +3 -5
- package/src/shell/page-bar.tsx +25 -4
- package/src/surfaces.ts +2 -0
- package/src/tokens/index.ts +10 -0
- package/src/tokens/size-classes.ts +14 -0
- package/dist/common/is-dev.d.ts +0 -3
- package/dist/common/is-dev.d.ts.map +0 -1
- package/dist/common/is-dev.js +0 -6
- package/src/common/is-dev.ts +0 -4
package/configs/oxlint-plugin.js
CHANGED
|
@@ -177,6 +177,7 @@ export const KNOWN_RULE_IDS = new Set([
|
|
|
177
177
|
'in-body-page-title',
|
|
178
178
|
'raw-selection-control',
|
|
179
179
|
'raw-media-query',
|
|
180
|
+
'unknown-vx-token',
|
|
180
181
|
// Retired guard kinds — no longer in GUARD_RULES, but a `theme-allow` naming one must still
|
|
181
182
|
// parse as a real (dead) waiver rather than an unknown-id typo. See RETIRED_RULE_IDS above.
|
|
182
183
|
...RETIRED_RULE_IDS,
|
|
@@ -390,8 +391,12 @@ function hasFileDeclaration(context, ruleId) {
|
|
|
390
391
|
)
|
|
391
392
|
}
|
|
392
393
|
|
|
393
|
-
/**
|
|
394
|
-
|
|
394
|
+
/**
|
|
395
|
+
* Test/spec files — design guidance does not apply to a fixture. Mirrors `src/cli`'s own SKIP, plus
|
|
396
|
+
* a `test/`/`tests/` directory (1.32.1): a test-setup polyfill (`tests/setup/dom.ts` stubbing
|
|
397
|
+
* `window.matchMedia`) is neither a `.test` file nor under `__tests__`, and needed a waiver.
|
|
398
|
+
*/
|
|
399
|
+
const TEST_FILE = /\.(?:test|spec)\.[cm]?[jt]sx?$|(?:^|[\\/])(?:__tests__|tests?)[\\/]/
|
|
395
400
|
|
|
396
401
|
function isTestFile(context) {
|
|
397
402
|
return TEST_FILE.test(getFilename(context))
|
|
@@ -2698,9 +2703,8 @@ const CONTROL_OUTSIDE_HOME_MESSAGE =
|
|
|
2698
2703
|
|
|
2699
2704
|
/**
|
|
2700
2705
|
* The cross-file half of C1, and the one rule here that is openly a HEURISTIC: "this control has
|
|
2701
|
-
* no home" is a claim about layout intent, which no AST can see.
|
|
2702
|
-
*
|
|
2703
|
-
* (docs/CONTROLS-SPEC.md §9 wave 7).
|
|
2706
|
+
* no home" is a claim about layout intent, which no AST can see. That is why it sat in grace from
|
|
2707
|
+
* 1.26.0 to 1.33.0, promoting only once basalt's tree and nine consumers measured at zero.
|
|
2704
2708
|
*
|
|
2705
2709
|
* Four exemptions carry the false-positive load: an overlay/settings-row/form-row ancestor
|
|
2706
2710
|
* ({@link CONTROL_HOST_TAGS}, `FormRow`/`FormGroup` included), an {@link isOverlayConventionFile}
|
|
@@ -2708,7 +2712,7 @@ const CONTROL_OUTSIDE_HOME_MESSAGE =
|
|
|
2708
2712
|
* `@mantine/form` (a form is the third home and its inputs are not filters), and the owner
|
|
2709
2713
|
* exemption — a file DEFINING a basalt control cannot be told to use one.
|
|
2710
2714
|
*/
|
|
2711
|
-
// Ships:
|
|
2715
|
+
// Ships: error
|
|
2712
2716
|
const controlOutsideHome = {
|
|
2713
2717
|
meta: {
|
|
2714
2718
|
type: 'suggestion',
|
|
@@ -3972,26 +3976,57 @@ function isResponsiveObjectValue(node, namedOnly = false) {
|
|
|
3972
3976
|
return !namedOnly || names.some((name) => name !== 'base')
|
|
3973
3977
|
}
|
|
3974
3978
|
|
|
3979
|
+
/** The `const` initializers {@link reachableObjects} can resolve an Identifier to. */
|
|
3980
|
+
const RESOLVABLE_INIT_TYPES = new Set([
|
|
3981
|
+
'ObjectExpression',
|
|
3982
|
+
'ConditionalExpression',
|
|
3983
|
+
'LogicalExpression',
|
|
3984
|
+
'Identifier',
|
|
3985
|
+
])
|
|
3986
|
+
|
|
3975
3987
|
/**
|
|
3976
|
-
*
|
|
3977
|
-
*
|
|
3978
|
-
*
|
|
3979
|
-
*
|
|
3980
|
-
*
|
|
3981
|
-
* `ObjectExpression` left the Identifier-in-a-property shape trivially bypassable.
|
|
3988
|
+
* Every object literal a value can evaluate to: an Identifier resolves through `consts` (the
|
|
3989
|
+
* same-file name→init map) FIRST, then a conditional contributes both branches and a logical both
|
|
3990
|
+
* sides — in any nesting, so `const span = wide ? { base: 12, md: 4 } : 12` then `span={span}` is
|
|
3991
|
+
* the same shape as writing the conditional inline (rb's hero Grid was a false negative until
|
|
3992
|
+
* 1.32.1). `seen` guards an Identifier cycle (`const a = b`, `const b = a`) and a shared object.
|
|
3982
3993
|
*/
|
|
3983
|
-
function
|
|
3984
|
-
const resolved =
|
|
3985
|
-
|
|
3986
|
-
if (resolved === null || resolved === undefined || resolved.type !== 'ObjectExpression')
|
|
3987
|
-
return false
|
|
3988
|
-
if (seen.has(resolved)) return false
|
|
3994
|
+
function reachableObjects(node, consts, seen = new Set()) {
|
|
3995
|
+
const resolved = node?.type === 'Identifier' ? consts.get(node.name) : node
|
|
3996
|
+
if (resolved === null || resolved === undefined || seen.has(resolved)) return []
|
|
3989
3997
|
seen.add(resolved)
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3998
|
+
switch (resolved.type) {
|
|
3999
|
+
case 'ObjectExpression':
|
|
4000
|
+
return [resolved]
|
|
4001
|
+
case 'Identifier':
|
|
4002
|
+
return reachableObjects(resolved, consts, seen)
|
|
4003
|
+
case 'ConditionalExpression':
|
|
4004
|
+
return [resolved.consequent, resolved.alternate].flatMap((branch) =>
|
|
4005
|
+
reachableObjects(branch, consts, seen),
|
|
4006
|
+
)
|
|
4007
|
+
case 'LogicalExpression':
|
|
4008
|
+
return [resolved.left, resolved.right].flatMap((side) => reachableObjects(side, consts, seen))
|
|
4009
|
+
default:
|
|
4010
|
+
return []
|
|
4011
|
+
}
|
|
4012
|
+
}
|
|
4013
|
+
|
|
4014
|
+
/**
|
|
4015
|
+
* Does this prop value CONTAIN a responsive object — any object it can evaluate to
|
|
4016
|
+
* ({@link reachableObjects}), or one nested one level down in a property of one
|
|
4017
|
+
* (`footer={{ height: { base: 60, sm: 0 } }}`, Mantine's own AppShell idiom), the property value
|
|
4018
|
+
* resolving the same way. Matching only a nested `ObjectExpression` left the Identifier-in-a-
|
|
4019
|
+
* property shape trivially bypassable.
|
|
4020
|
+
*/
|
|
4021
|
+
function containsResponsiveObjectValue(node, consts, namedOnly = false, seen = new Set()) {
|
|
4022
|
+
return reachableObjects(node, consts, seen).some(
|
|
4023
|
+
(object) =>
|
|
4024
|
+
isResponsiveObjectValue(object, namedOnly) ||
|
|
4025
|
+
object.properties.some(
|
|
4026
|
+
(prop) =>
|
|
4027
|
+
prop.type === 'Property' &&
|
|
4028
|
+
containsResponsiveObjectValue(prop.value, consts, namedOnly, seen),
|
|
4029
|
+
),
|
|
3995
4030
|
)
|
|
3996
4031
|
}
|
|
3997
4032
|
|
|
@@ -4031,8 +4066,9 @@ const CONTAINER_ROUTED_PROPS = {
|
|
|
4031
4066
|
* `type` has to be the string literal `"container"`; a dynamic value is not a decision this rule
|
|
4032
4067
|
* can read. `Grid.Col` takes its grid's mode by walking to the nearest enclosing `Grid` element.
|
|
4033
4068
|
* `consts` is the rule's same-file name→object map, so `breakpoints={map}` resolves like any value.
|
|
4069
|
+
* The one IMPORTED map trusted is basalt's own `CONTAINER_GRID_BREAKPOINTS` ({@link isShippedGridMap}).
|
|
4034
4070
|
*/
|
|
4035
|
-
function containerGridMode(opening, mantineImports, consts) {
|
|
4071
|
+
function containerGridMode(opening, mantineImports, consts, sourceCode) {
|
|
4036
4072
|
let tag = resolveMantineTag(opening.name, mantineImports)
|
|
4037
4073
|
let grid = opening
|
|
4038
4074
|
if (tag === 'Grid.Col') {
|
|
@@ -4061,12 +4097,69 @@ function containerGridMode(opening, mantineImports, consts) {
|
|
|
4061
4097
|
const breakpoints = attr('breakpoints')
|
|
4062
4098
|
if (breakpoints === undefined) return 'fallback'
|
|
4063
4099
|
let map = attrValue(breakpoints)
|
|
4100
|
+
if (isShippedGridMap(sourceCode, map)) return 'container'
|
|
4064
4101
|
if (map?.type === 'Identifier') map = consts.get(map.name)
|
|
4065
4102
|
const nonEmpty =
|
|
4066
4103
|
map?.type === 'ObjectExpression' && map.properties.some((prop) => prop.type === 'Property')
|
|
4067
4104
|
return nonEmpty ? 'container' : 'fallback'
|
|
4068
4105
|
}
|
|
4069
4106
|
|
|
4107
|
+
/**
|
|
4108
|
+
* Whether `node` reads basalt's own `CONTAINER_GRID_BREAKPOINTS` — its value is known, so it is
|
|
4109
|
+
* the one imported `breakpoints` map trusted. Resolved through the scope manager to the BINDING the
|
|
4110
|
+
* identifier reads, never by local name: a parameter or inner `const` shadowing the import is not
|
|
4111
|
+
* the import. The binding has to be an `ImportSpecifier` naming it from `basalt-ui/tokens` (any
|
|
4112
|
+
* alias, not type-only), a `.CONTAINER_GRID_BREAKPOINTS` member of that module's namespace import,
|
|
4113
|
+
* or a `const` whose initializer resolves the same way. Without a scope manager nothing is trusted
|
|
4114
|
+
* — reported, the conservative answer.
|
|
4115
|
+
*/
|
|
4116
|
+
function isShippedGridMap(sourceCode, node, seen = new Set()) {
|
|
4117
|
+
if (node?.type === 'MemberExpression') return isShippedGridMapMember(sourceCode, node)
|
|
4118
|
+
if (node?.type !== 'Identifier' || seen.has(node)) return false
|
|
4119
|
+
seen.add(node)
|
|
4120
|
+
const def = soleDefinition(sourceCode, node)
|
|
4121
|
+
if (def?.type === 'ImportBinding') {
|
|
4122
|
+
const spec = def.node
|
|
4123
|
+
const decl = def.parent
|
|
4124
|
+
if (decl?.type !== 'ImportDeclaration' || decl.importKind === 'type') return false
|
|
4125
|
+
if (decl.source?.value !== 'basalt-ui/tokens') return false
|
|
4126
|
+
if (spec.type !== 'ImportSpecifier' || spec.importKind === 'type') return false
|
|
4127
|
+
const imported = spec.imported
|
|
4128
|
+
const importedName = imported.type === 'Identifier' ? imported.name : imported.value
|
|
4129
|
+
return importedName === 'CONTAINER_GRID_BREAKPOINTS'
|
|
4130
|
+
}
|
|
4131
|
+
if (def?.type === 'Variable' && def.parent?.kind === 'const')
|
|
4132
|
+
return isShippedGridMap(sourceCode, def.node.init, seen)
|
|
4133
|
+
return false
|
|
4134
|
+
}
|
|
4135
|
+
|
|
4136
|
+
/** `tokens.CONTAINER_GRID_BREAKPOINTS` (or `tokens['CONTAINER_GRID_BREAKPOINTS']`) on `import * as tokens from 'basalt-ui/tokens'`. */
|
|
4137
|
+
function isShippedGridMapMember(sourceCode, node) {
|
|
4138
|
+
const property = node.property
|
|
4139
|
+
const name = node.computed
|
|
4140
|
+
? isStringLiteral(property) && property.value
|
|
4141
|
+
: property?.type === 'Identifier' && property.name
|
|
4142
|
+
if (name !== 'CONTAINER_GRID_BREAKPOINTS' || node.object?.type !== 'Identifier') return false
|
|
4143
|
+
const def = soleDefinition(sourceCode, node.object)
|
|
4144
|
+
return (
|
|
4145
|
+
def?.type === 'ImportBinding' &&
|
|
4146
|
+
def.node.type === 'ImportNamespaceSpecifier' &&
|
|
4147
|
+
def.parent?.importKind !== 'type' &&
|
|
4148
|
+
def.parent?.source?.value === 'basalt-ui/tokens'
|
|
4149
|
+
)
|
|
4150
|
+
}
|
|
4151
|
+
|
|
4152
|
+
/** The one definition of the variable `identifier` reads, or `undefined` (unresolved, or more than one). */
|
|
4153
|
+
function soleDefinition(sourceCode, identifier) {
|
|
4154
|
+
if (typeof sourceCode?.getScope !== 'function') return undefined
|
|
4155
|
+
for (let scope = sourceCode.getScope(identifier); scope; scope = scope.upper) {
|
|
4156
|
+
const variable = scope.set.get(identifier.name)
|
|
4157
|
+
if (variable === undefined) continue
|
|
4158
|
+
return variable.defs.length === 1 ? variable.defs[0] : undefined
|
|
4159
|
+
}
|
|
4160
|
+
return undefined
|
|
4161
|
+
}
|
|
4162
|
+
|
|
4070
4163
|
/**
|
|
4071
4164
|
* The shell components that are `visibleFrom`/`hiddenFrom`'s legitimate home — the size-class
|
|
4072
4165
|
* axis's one JSX escape hatch, alongside overlays. Matched by DECLARATION name, the same
|
|
@@ -4099,7 +4192,7 @@ const RAW_BREAKPOINT_EXEMPT_FILE = /(?:^|[\\/])(?:use-media-query\.ts|ChartToolt
|
|
|
4099
4192
|
export const RAW_BREAKPOINT_RECIPE =
|
|
4100
4193
|
'A page’s own layout keys on its container: <SimpleGrid type="container" cols={{ base: 1, ' +
|
|
4101
4194
|
"'480px': 2, '800px': 3 }}> (px keys = CONTAINER_CLASSES, basalt-ui/tokens), or <Grid " +
|
|
4102
|
-
'type="container" breakpoints={
|
|
4195
|
+
'type="container" breakpoints={CONTAINER_GRID_BREAKPOINTS}> (basalt-ui/tokens), with Grid.Col ' +
|
|
4103
4196
|
'span={{ base: 12, sm: 6 }} keyed through that map; a Flex direction swap becomes that SimpleGrid (equal ' +
|
|
4104
4197
|
'columns), a wrapping Group, or a CSS-module @container basalt-grid rule. useSizeClass() is for ' +
|
|
4105
4198
|
'shell chrome only. Recipes: basalt-ui MIGRATING.md § basalt/raw-breakpoint.'
|
|
@@ -4108,6 +4201,33 @@ const RAW_BREAKPOINT_RESPONSIVE_PROP_MESSAGE =
|
|
|
4108
4201
|
'Responsive-object prop keyed on theme breakpoints — those are viewport widths. ' +
|
|
4109
4202
|
`${RAW_BREAKPOINT_RECIPE} (basalt/raw-breakpoint)`
|
|
4110
4203
|
|
|
4204
|
+
/** rb r2 #5: a `(hover: none)` read was flagged with advice that only covered widths. */
|
|
4205
|
+
const RAW_BREAKPOINT_POINTER_HINT =
|
|
4206
|
+
'A hover/pointer capability read belongs in CSS: @media (pointer: coarse) in a module (the ' +
|
|
4207
|
+
'query --vx-hit keys on) — (hover: none) is not a sanctioned query.'
|
|
4208
|
+
|
|
4209
|
+
const RAW_BREAKPOINT_BASE_ONLY_MESSAGE =
|
|
4210
|
+
'Responsive-object prop with only a base key — there is no breakpoint in it at all, so drop the ' +
|
|
4211
|
+
'object and write the value: cols={{ base: 3 }} → cols={3}. (basalt/raw-breakpoint)'
|
|
4212
|
+
|
|
4213
|
+
/**
|
|
4214
|
+
* Is every responsive object this value can evaluate to ({@link reachableObjects} — the same
|
|
4215
|
+
* unwrap detection uses) one carrying ONLY `base`? That is no breakpoint decision, and the viewport
|
|
4216
|
+
* recipe is the wrong advice for it — the fix is the bare value. Message-only: the shape still
|
|
4217
|
+
* reports, as it always has.
|
|
4218
|
+
*/
|
|
4219
|
+
function isBaseOnlyResponsiveObject(node, consts) {
|
|
4220
|
+
const responsive = reachableObjects(node, consts).filter((object) =>
|
|
4221
|
+
isResponsiveObjectValue(object),
|
|
4222
|
+
)
|
|
4223
|
+
return (
|
|
4224
|
+
responsive.length > 0 &&
|
|
4225
|
+
responsive.every((object) =>
|
|
4226
|
+
object.properties.every((prop) => propertyKeyName(prop) === 'base'),
|
|
4227
|
+
)
|
|
4228
|
+
)
|
|
4229
|
+
}
|
|
4230
|
+
|
|
4111
4231
|
const RAW_BREAKPOINT_CONTAINER_KEY_MESSAGE =
|
|
4112
4232
|
'Theme breakpoint name under type="container" — Mantine does not resolve it there: ' +
|
|
4113
4233
|
"SimpleGrid uses each key verbatim as a (min-width: …) length, so 'sm' is a dead query, " +
|
|
@@ -4121,11 +4241,12 @@ const RAW_BREAKPOINT_VISIBLE_HIDDEN_MESSAGE =
|
|
|
4121
4241
|
const RAW_BREAKPOINT_HOOK_MESSAGE =
|
|
4122
4242
|
"useMediaQuery/useMatches/useViewportSize — a raw viewport read outside the framework's " +
|
|
4123
4243
|
'one seam. Shell chrome reads useSizeClass(); a page swapping its own layout (a table for a card ' +
|
|
4124
|
-
`list, a column count) is a container decision. ${
|
|
4244
|
+
`list, a column count) is a container decision. ${RAW_BREAKPOINT_POINTER_HINT} ` +
|
|
4245
|
+
`${RAW_BREAKPOINT_RECIPE} (basalt/raw-breakpoint)`
|
|
4125
4246
|
|
|
4126
4247
|
const RAW_BREAKPOINT_GLOBAL_MESSAGE =
|
|
4127
4248
|
'window.matchMedia/window.innerWidth read directly — the viewport read useSizeClass() owns ' +
|
|
4128
|
-
`for shell chrome. ${RAW_BREAKPOINT_RECIPE} (basalt/raw-breakpoint)`
|
|
4249
|
+
`for shell chrome. ${RAW_BREAKPOINT_POINTER_HINT} ${RAW_BREAKPOINT_RECIPE} (basalt/raw-breakpoint)`
|
|
4129
4250
|
|
|
4130
4251
|
/** The three viewport hooks the law names, whatever module they are imported from. */
|
|
4131
4252
|
const RAW_BREAKPOINT_HOOK_NAMES = new Set(['useMediaQuery', 'useMatches', 'useViewportSize'])
|
|
@@ -4141,7 +4262,7 @@ const RAW_BREAKPOINT_HOOK_NAMES = new Set(['useMediaQuery', 'useMatches', 'useVi
|
|
|
4141
4262
|
* declares a shell home (or a control's own C9 swap) is exempt for all four shapes uniformly — the
|
|
4142
4263
|
* shell's own responsive chrome is the sanctioned use, whatever shape it takes.
|
|
4143
4264
|
*/
|
|
4144
|
-
// Ships:
|
|
4265
|
+
// Ships: error
|
|
4145
4266
|
const rawBreakpoint = {
|
|
4146
4267
|
meta: {
|
|
4147
4268
|
type: 'suggestion',
|
|
@@ -4168,12 +4289,14 @@ const rawBreakpoint = {
|
|
|
4168
4289
|
let definesShellHome = false
|
|
4169
4290
|
const visibleHiddenCandidates = []
|
|
4170
4291
|
const responsiveObjectCandidates = []
|
|
4171
|
-
// Same-file `const NAME = { base, sm }` bindings
|
|
4172
|
-
//
|
|
4292
|
+
// Same-file `const NAME = { base, sm }` bindings (or a conditional/logical/alias init, see
|
|
4293
|
+
// RESOLVABLE_INIT_TYPES), resolved when a JSX attribute or a nested property passes the
|
|
4294
|
+
// Identifier — no scope manager here, so this is a flat name→init map.
|
|
4173
4295
|
// A name declared more than once in the file is ambiguous (its two bindings cannot be told
|
|
4174
4296
|
// apart), so it is dropped from the map entirely rather than guessed at last-wins.
|
|
4175
4297
|
const responsiveObjectConsts = new Map()
|
|
4176
4298
|
const seenDeclaratorNames = new Set()
|
|
4299
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode?.()
|
|
4177
4300
|
|
|
4178
4301
|
const noteShellHomeOwner = (name) => {
|
|
4179
4302
|
if (typeof name === 'string' && SHELL_HOME_NAMES.has(name)) definesShellHome = true
|
|
@@ -4217,7 +4340,7 @@ const rawBreakpoint = {
|
|
|
4217
4340
|
return
|
|
4218
4341
|
}
|
|
4219
4342
|
seenDeclaratorNames.add(name)
|
|
4220
|
-
if (node.init?.type
|
|
4343
|
+
if (RESOLVABLE_INIT_TYPES.has(node.init?.type)) responsiveObjectConsts.set(name, node.init)
|
|
4221
4344
|
},
|
|
4222
4345
|
JSXAttribute(node) {
|
|
4223
4346
|
const name = node.name?.name
|
|
@@ -4258,16 +4381,16 @@ const rawBreakpoint = {
|
|
|
4258
4381
|
? CONTAINER_ROUTED_PROPS[tag]
|
|
4259
4382
|
: undefined
|
|
4260
4383
|
const mode = routed?.has(node.name.name)
|
|
4261
|
-
? containerGridMode(owner, mantineImports, responsiveObjectConsts)
|
|
4384
|
+
? containerGridMode(owner, mantineImports, responsiveObjectConsts, sourceCode)
|
|
4262
4385
|
: undefined
|
|
4263
4386
|
if (mode === 'container') continue
|
|
4264
4387
|
if (!containsResponsiveObjectValue(value, responsiveObjectConsts, mode !== undefined))
|
|
4265
4388
|
continue
|
|
4266
4389
|
if (hasThemeAllow(context, node, 'raw-breakpoint')) continue
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4390
|
+
let message = RAW_BREAKPOINT_RESPONSIVE_PROP_MESSAGE
|
|
4391
|
+
if (mode !== undefined) message = RAW_BREAKPOINT_CONTAINER_KEY_MESSAGE
|
|
4392
|
+
else if (isBaseOnlyResponsiveObject(value, responsiveObjectConsts))
|
|
4393
|
+
message = RAW_BREAKPOINT_BASE_ONLY_MESSAGE
|
|
4271
4394
|
context.report({ node, message })
|
|
4272
4395
|
}
|
|
4273
4396
|
for (const node of visibleHiddenCandidates) {
|
|
@@ -4309,59 +4432,7 @@ const rawBreakpoint = {
|
|
|
4309
4432
|
*
|
|
4310
4433
|
* @type {Record<string, GraceEntry>}
|
|
4311
4434
|
*/
|
|
4312
|
-
export const PLUGIN_RULE_GRACE = {
|
|
4313
|
-
'control-outside-home': {
|
|
4314
|
-
since: '1.26.0',
|
|
4315
|
-
promote: '1.33.0',
|
|
4316
|
-
why:
|
|
4317
|
-
"Extended 1.32.0 -> 1.33.0 at the 1.32.0 release, the last extension: basalt's own tree is at 0 (consumer-loop r1 dogfood) and argo is at 2, but RE-MEASURED 2026-09-30 the fleet carries ~33 (image-gen 11, rb 7, image-share 6, weatherorb 5, email-gateway 4, argo 2). The 1.32.0 fleet upgrade clears them while they warn, together with raw-breakpoint, so 1.33.0 promotes all four ids against a measured-clean fleet. " +
|
|
4318
|
-
'Extended 1.31.0 -> 1.32.0 at the 1.31.0 release: the basalt tree still carries incumbents (data-table, notifications center, theme-lab) and the argo sweep has not run. ' +
|
|
4319
|
-
'the wave-6 control guards (docs/CONTROLS-SPEC.md §6). The one openly HEURISTIC rule of the ' +
|
|
4320
|
-
'set — "this control has no home" is a claim about layout intent, so its false-positive load ' +
|
|
4321
|
-
'is carried by four exemptions (overlay/settings-row ancestor, the overlay FILENAME ' +
|
|
4322
|
-
'convention below, @mantine/form, owner definition) rather than by certainty. The other five ' +
|
|
4323
|
-
'wave-6 rules promoted at 1.27.0; this one has not, across three re-datings. ' +
|
|
4324
|
-
'RE-MEASURED 2026-09-09, every consumer freshly migrated to 1.29.2 and basalt built from ' +
|
|
4325
|
-
'master: 29 incumbents across five repos — image-gen 10, rb 8, image-share 6, the playground ' +
|
|
4326
|
-
'3, argo 2 — and zero in linewatch, meteo and obsidian. The 1.28.0 prediction held: ' +
|
|
4327
|
-
"{@link isOverlayConventionFile} took argo's 9 cross-file overlay warns down to 2, which is " +
|
|
4328
|
-
'the part of the remainder that was ever going to fall to an exemption. ' +
|
|
4329
|
-
'Extended to 1.31.0, and the argument is NOT the count — it is that 1.30.0 is the first ' +
|
|
4330
|
-
'release in which this rule names an answer the 29 can reach. Every home it listed was a ' +
|
|
4331
|
-
'shell slot (PageBar / Section / WidgetHeader), so a consumer that mounts `BasaltProvider` ' +
|
|
4332
|
-
'and no `BasaltShell` had no reachable home at all: image-gen is a Tauri window with a ' +
|
|
4333
|
-
'hand-rolled header and carries 10 of the 29 by itself, and `bound-control-outside-home` ' +
|
|
4334
|
-
'closed the `ViewTabs` escape it would otherwise have taken. 1.30.0 adds the missing ' +
|
|
4335
|
-
'sentence to both messages ({@link SHELL_LESS_HOME_HINT} — a `PageBar` outside a shell ' +
|
|
4336
|
-
'renders in flow, sticky, with its own title, which is what `src/shell/page-bar.tsx` ' +
|
|
4337
|
-
'actually does). Promoting a rule to `error` in the same minor that first makes it ' +
|
|
4338
|
-
'satisfiable hands a consumer zero minors to act on the answer, which is exactly the failure ' +
|
|
4339
|
-
'the grace mechanism exists to prevent. 1.31.0 is when the 29 have been through the new home ' +
|
|
4340
|
-
'and the remainder is measured against that number. A file outside the convention that is ' +
|
|
4341
|
-
"still an overlay's own body declares it with " +
|
|
4342
|
-
'`theme-allow-file control-outside-home — overlay`.',
|
|
4343
|
-
},
|
|
4344
|
-
'raw-breakpoint': {
|
|
4345
|
-
since: '1.31.0',
|
|
4346
|
-
promote: '1.33.0',
|
|
4347
|
-
why:
|
|
4348
|
-
'New in the wave-11 responsive/touch guards. Catches four independent shapes at once — a ' +
|
|
4349
|
-
'responsive-object Mantine prop, visibleFrom/hiddenFrom outside the three shell homes, the ' +
|
|
4350
|
-
'three raw viewport hooks, and window.matchMedia/innerWidth — none of which basalt policed ' +
|
|
4351
|
-
'before this wave, so every consumer on an earlier minor has a green build with all four ' +
|
|
4352
|
-
"shapes already in it. basalt-ui/content's own article-card.tsx (a `SimpleGrid cols={{ base: " +
|
|
4353
|
-
"1, sm: 2, lg }}`) was one incumbent measured in basalt's own tree at ship time — a real " +
|
|
4354
|
-
'number, not zero, so it shipped warn rather than error. ' +
|
|
4355
|
-
'Extended 1.32.0 -> 1.33.0: 1.31.0 shipped the rule naming "a container query" but no ' +
|
|
4356
|
-
'primitive, and MIGRATING never mentioned it (argo: 42 warns, no documented target). 1.32.0 ' +
|
|
4357
|
-
'is the first minor with a reachable, documented answer (Mantine type="container" grids keyed ' +
|
|
4358
|
-
'on CONTAINER_CLASSES, exempted here), and promoting in the minor that first makes a rule ' +
|
|
4359
|
-
'satisfiable hands a consumer zero minors to act on it — the control-outside-home ' +
|
|
4360
|
-
"extension's reasoning. 1.33.0 is when the incumbents are expected to have moved to a " +
|
|
4361
|
-
'container grid or a theme-allow with a stated reason. Promotes together with its CSS twin, ' +
|
|
4362
|
-
'the `raw-media-query` guard kind.',
|
|
4363
|
-
},
|
|
4364
|
-
}
|
|
4435
|
+
export const PLUGIN_RULE_GRACE = {}
|
|
4365
4436
|
|
|
4366
4437
|
/**
|
|
4367
4438
|
* Plugin rules that stay `warn` in the shipped preset PERMANENTLY — outside the C16 version gate,
|