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
|
@@ -51,6 +51,12 @@
|
|
|
51
51
|
* // An open threshold — a pill holding a stepper, applied on blur or Enter.
|
|
52
52
|
* // minDuration: field.number({ fallback: 0, min: 0, max: 600 })
|
|
53
53
|
* <NumberFilter field={lines.field.minDuration} label="Min duration" step={30} />
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* // "No filter" is the fallback, not a row: `clearable` writes it back, and the pill reads the
|
|
57
|
+
* // filter's name while no preset is picked.
|
|
58
|
+
* // difficulty: field.number({ fallback: 0, min: 0, max: 5, int: true })
|
|
59
|
+
* <NumberFilter field={library.field.difficulty} label="Difficulty" options={LEVELS} clearable />
|
|
54
60
|
*/
|
|
55
61
|
import { NumberInput } from '@mantine/core'
|
|
56
62
|
import { useEffect, useRef, useState } from 'react'
|
|
@@ -62,7 +68,7 @@ import classes from './controls.module.css'
|
|
|
62
68
|
import { EnumFilter } from './enum-filter'
|
|
63
69
|
import type { ChoiceHandle } from './enum-filter'
|
|
64
70
|
import { useFilterRegistration, useFilterSurface } from './filter-context'
|
|
65
|
-
import { FilterPill } from './filter-pill'
|
|
71
|
+
import { FilterClearButton, FilterPill } from './filter-pill'
|
|
66
72
|
import { useControlName } from './filter-sheet'
|
|
67
73
|
import { PanelRow } from './panel-row'
|
|
68
74
|
|
|
@@ -89,6 +95,9 @@ export type NumberFilterProps = BasaltProps & {
|
|
|
89
95
|
* declared none. Absent on both, the input keeps Mantine's own default of 1.
|
|
90
96
|
*/
|
|
91
97
|
readonly step?: number
|
|
98
|
+
/** Adds a `Clear` action that clears the field — the same `field.clear()` `Reset all` calls.
|
|
99
|
+
* Parity with `SelectFilter`/`EnumFilter`, in both forms. */
|
|
100
|
+
readonly clearable?: boolean
|
|
92
101
|
}
|
|
93
102
|
|
|
94
103
|
export function NumberFilter(props: NumberFilterProps): ReactNode {
|
|
@@ -97,7 +106,7 @@ export function NumberFilter(props: NumberFilterProps): ReactNode {
|
|
|
97
106
|
assertRequiredProps('NumberFilter', props, ['field'], {
|
|
98
107
|
field: 'bind it to a store field (`store.field.<name>`), never a value/onChange pair.',
|
|
99
108
|
})
|
|
100
|
-
const { field, label, icon, options, step, className, style } = props
|
|
109
|
+
const { field, label, icon, options, step, clearable, className, style } = props
|
|
101
110
|
// Two BRANCHES, two components, and never a conditional hook: the radio form's state lives in
|
|
102
111
|
// `EnumFilter` and the stepper's in `NumberStepper`, so which one mounts is decided once by a prop
|
|
103
112
|
// that does not change across a mount's life.
|
|
@@ -107,6 +116,7 @@ export function NumberFilter(props: NumberFilterProps): ReactNode {
|
|
|
107
116
|
field={field}
|
|
108
117
|
label={label}
|
|
109
118
|
options={options}
|
|
119
|
+
{...(clearable === true && { clearable: true })}
|
|
110
120
|
{...(icon !== undefined && { icon })}
|
|
111
121
|
{...(className !== undefined && { className })}
|
|
112
122
|
{...(style !== undefined && { style })}
|
|
@@ -120,6 +130,7 @@ export function NumberFilter(props: NumberFilterProps): ReactNode {
|
|
|
120
130
|
<NumberStepper
|
|
121
131
|
field={field}
|
|
122
132
|
label={label}
|
|
133
|
+
{...(clearable === true && { clearable: true })}
|
|
123
134
|
{...(icon !== undefined && { icon })}
|
|
124
135
|
{...(grain !== undefined && { step: grain })}
|
|
125
136
|
{...(className !== undefined && { className })}
|
|
@@ -147,6 +158,7 @@ function NumberChoice({
|
|
|
147
158
|
label,
|
|
148
159
|
icon,
|
|
149
160
|
options,
|
|
161
|
+
clearable,
|
|
150
162
|
className,
|
|
151
163
|
style,
|
|
152
164
|
}: BasaltProps & {
|
|
@@ -154,6 +166,7 @@ function NumberChoice({
|
|
|
154
166
|
readonly label: string
|
|
155
167
|
readonly icon?: ReactNode
|
|
156
168
|
readonly options: readonly NumberFilterOption[]
|
|
169
|
+
readonly clearable?: boolean
|
|
157
170
|
}): ReactNode {
|
|
158
171
|
const [value, setValue] = field.use()
|
|
159
172
|
const rows = options.map((option) => ({ value: String(option.value), label: option.label }))
|
|
@@ -182,6 +195,7 @@ function NumberChoice({
|
|
|
182
195
|
label={label}
|
|
183
196
|
numeric
|
|
184
197
|
options={rows}
|
|
198
|
+
{...(clearable === true && { clearable: true })}
|
|
185
199
|
{...(icon !== undefined && { icon })}
|
|
186
200
|
{...(className !== undefined && { className })}
|
|
187
201
|
{...(style !== undefined && { style })}
|
|
@@ -195,6 +209,7 @@ function NumberStepper({
|
|
|
195
209
|
label,
|
|
196
210
|
icon,
|
|
197
211
|
step,
|
|
212
|
+
clearable,
|
|
198
213
|
className,
|
|
199
214
|
style,
|
|
200
215
|
}: BasaltProps & {
|
|
@@ -202,6 +217,7 @@ function NumberStepper({
|
|
|
202
217
|
readonly label: string
|
|
203
218
|
readonly icon?: ReactNode
|
|
204
219
|
readonly step?: number
|
|
220
|
+
readonly clearable?: boolean
|
|
205
221
|
}): ReactNode {
|
|
206
222
|
const [value, setValue] = field.use()
|
|
207
223
|
const surface = useFilterSurface()
|
|
@@ -265,7 +281,12 @@ function NumberStepper({
|
|
|
265
281
|
{...(className !== undefined && { className })}
|
|
266
282
|
{...(style !== undefined && { style })}
|
|
267
283
|
>
|
|
268
|
-
<div className={classes.optionList}>
|
|
284
|
+
<div className={classes.optionList}>
|
|
285
|
+
{input}
|
|
286
|
+
{/* Same placement as `EnumFilter`'s: popover only — the sheet and panel rows reset through
|
|
287
|
+
the `FilterSet` registration above. The draft follows the clear via its effect. */}
|
|
288
|
+
{clearable === true && !isDefault && <FilterClearButton onClear={() => field.clear()} />}
|
|
289
|
+
</div>
|
|
269
290
|
</FilterPill>
|
|
270
291
|
)
|
|
271
292
|
}
|
|
@@ -32,7 +32,6 @@ import { useEffect, useRef, useState } from 'react'
|
|
|
32
32
|
import type { ComponentType, ReactNode } from 'react'
|
|
33
33
|
import type { BasaltProps } from '../common/props'
|
|
34
34
|
import { assertRequiredProps } from '../common/validate'
|
|
35
|
-
import { isDev } from '../common/is-dev'
|
|
36
35
|
import type { FieldHandle, RangeField, RangeValue } from '../state'
|
|
37
36
|
import classes from './controls.module.css'
|
|
38
37
|
import { useFilterRegistration, useFilterSurface } from './filter-context'
|
|
@@ -60,7 +59,7 @@ const noPickerWarned = new Set<string>()
|
|
|
60
59
|
* the type cannot tell "deliberately preset-only" from "forgot the import".
|
|
61
60
|
*/
|
|
62
61
|
function warnCustomWithoutPicker(label: string, unreachable: boolean): void {
|
|
63
|
-
if (!unreachable ||
|
|
62
|
+
if (!unreachable || process.env.NODE_ENV === 'production' || noPickerWarned.has(label)) return
|
|
64
63
|
noPickerWarned.add(label)
|
|
65
64
|
// oxlint-disable-next-line no-console -- a dev-time wiring warning has no other channel
|
|
66
65
|
console.warn(
|
package/src/data/data-table.tsx
CHANGED
|
@@ -98,7 +98,6 @@ import {
|
|
|
98
98
|
useRowDisclosure,
|
|
99
99
|
} from './data-table-state'
|
|
100
100
|
import { dataQueryBranch } from './query-branch'
|
|
101
|
-
import { isDev } from '../common/is-dev'
|
|
102
101
|
import classes from './data-table.module.css'
|
|
103
102
|
|
|
104
103
|
// ── Column alignment ──────────────────────────────────────────────────────────
|
|
@@ -303,7 +302,7 @@ function resolveFacetColumn<T>(
|
|
|
303
302
|
): Column<T, unknown> | undefined {
|
|
304
303
|
const column = table.getColumn(facet.columnId)
|
|
305
304
|
if (column) return column
|
|
306
|
-
if (
|
|
305
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
307
306
|
const knownIds = table
|
|
308
307
|
.getAllColumns()
|
|
309
308
|
.map((known) => known.id)
|
|
@@ -608,7 +607,8 @@ export type BasaltDataTableProps<T> = BasaltProps &
|
|
|
608
607
|
* list can never disagree with its table; a fixed card order is `initialSorting`, not a second
|
|
609
608
|
* sort. Everything around the body stays: the header (title · count · toolbar), the bulk bar,
|
|
610
609
|
* the pager, and `query` / `isLoading` / `emptyState`. `onRowActivate` makes each card
|
|
611
|
-
* activatable exactly as it does a row (click, or Enter on the focused card
|
|
610
|
+
* activatable exactly as it does a row (click, or Enter on the focused card — never Space, the
|
|
611
|
+
* browser's own page scroll on a focused non-button). The selection
|
|
612
612
|
* checkbox is not projected — a card list with a selection keeps its bulk bar but no ticks.
|
|
613
613
|
*
|
|
614
614
|
* An activatable card takes `tabIndex=0` + `data-activatable`, never `role="button"`: a card is
|
|
@@ -616,7 +616,9 @@ export type BasaltDataTableProps<T> = BasaltProps &
|
|
|
616
616
|
* descendant presentational, so those nested controls would vanish from the accessibility tree.
|
|
617
617
|
* The affordance is the cursor and the focus ring, as on the row. `maxHeight` caps the card list
|
|
618
618
|
* in a `ScrollArea`; `minWidth` does not apply — it is a horizontal floor for columns, and a
|
|
619
|
-
* card list has none.
|
|
619
|
+
* card list has none. `stickyHeader` has nothing to stick while the cards render. A card order
|
|
620
|
+
* with no user sort is `data`'s own order — sort it there (or `initialSorting`) for a domain
|
|
621
|
+
* order.
|
|
620
622
|
*
|
|
621
623
|
* Reach for it when the row carries more than the columns a phone can hold (a status + label +
|
|
622
624
|
* age header, a clamped title, a prose line). A table that only needs to shed a column or two
|
|
@@ -761,6 +763,10 @@ export type BasaltDataTableProps<T> = BasaltProps &
|
|
|
761
763
|
* its own card instead of widening the page. Reach for this only to hold a MINIMUM table width
|
|
762
764
|
* — e.g. so eight columns keep their readable widths and scroll rather than compress.
|
|
763
765
|
*
|
|
766
|
+
* Setting it also switches the column fold OFF (a declared floor is the caller saying "this
|
|
767
|
+
* table is at least this wide", which folding columns out would contradict), so a `minWidth`
|
|
768
|
+
* table scrolls where the same table without it folds. `maxHeight` alone keeps the fold.
|
|
769
|
+
*
|
|
764
770
|
* The one table whose wrapper is MEASURED rather than declared is `stickyHeader` with neither
|
|
765
771
|
* this nor `maxHeight`; see that prop and `useMeasuredContainment` in the implementation for
|
|
766
772
|
* why, and pair it with `maxHeight` if the table must both scroll and stick.
|
|
@@ -1224,7 +1230,7 @@ const reportedBreaches = new Set<string>()
|
|
|
1224
1230
|
function enforceManualPaginationContract(breaches: ManualPaginationBreach[]): void {
|
|
1225
1231
|
if (breaches.length === 0) return
|
|
1226
1232
|
const message = manualPaginationMessage(breaches)
|
|
1227
|
-
if (
|
|
1233
|
+
if (process.env.NODE_ENV !== 'production') throw new Error(message)
|
|
1228
1234
|
if (reportedBreaches.has(message)) return
|
|
1229
1235
|
reportedBreaches.add(message)
|
|
1230
1236
|
console.error(message)
|
|
@@ -138,9 +138,12 @@
|
|
|
138
138
|
gap: var(--vx-space-stack-xs);
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
/* Bottom-aligned, not stretched: a trailing button beside a labelled FormRow keeps its own height
|
|
142
|
+
and lines up with the input, not with the label above it. */
|
|
141
143
|
.groupControls[data-direction='row'] {
|
|
142
144
|
flex-direction: row;
|
|
143
145
|
flex-wrap: wrap;
|
|
146
|
+
align-items: flex-end;
|
|
144
147
|
gap: var(--vx-space-control-gap);
|
|
145
148
|
}
|
|
146
149
|
|
|
@@ -213,7 +213,12 @@ export type FormGroupProps = BasaltProps & {
|
|
|
213
213
|
error?: ReactNode
|
|
214
214
|
/** Marks the group required — same two-channel treatment as `FormRow`. */
|
|
215
215
|
required?: boolean
|
|
216
|
-
/**
|
|
216
|
+
/**
|
|
217
|
+
* `column` stacks the options; `row` lays them out inline, wraps, and bottom-aligns them, so a
|
|
218
|
+
* trailing button sits level with the inputs instead of stretching to the tallest child. Give a
|
|
219
|
+
* `FormRow` inside a `row` group (or any flex row) a definite width — it is an inline-size
|
|
220
|
+
* container, so its intrinsic width is zero and it collapses otherwise. @default 'column'
|
|
221
|
+
*/
|
|
217
222
|
direction?: 'row' | 'column'
|
|
218
223
|
/** The cluster — checkboxes, radios, chips. */
|
|
219
224
|
children: ReactNode
|
package/src/guard/index.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ./guard — headless policy core. Mantine-free, dependency-free.
|
|
3
3
|
*
|
|
4
|
-
* GUARD_RULES: the closed registry of all
|
|
4
|
+
* GUARD_RULES: the closed registry of all 28 violation kinds.
|
|
5
5
|
* checkSource: pure (text, relPath, cfg) → Finding[]. No FS, no walk, no console.
|
|
6
6
|
*/
|
|
7
7
|
import type { Finding, GuardConfig, GuardKind, GuardSeverity } from './types'
|
|
8
|
+
import { BASALT_VX_NAMES } from './vx-names'
|
|
8
9
|
|
|
9
10
|
export type { Finding, GuardConfig, GuardKind, GuardSeverity }
|
|
10
11
|
|
|
@@ -177,27 +178,23 @@ const CSS_NON_COARSE_POINTER_QUERY =
|
|
|
177
178
|
* The `@container` names the container-class axis declares — a consumer container carrying any
|
|
178
179
|
* other name has invented its own container-query breakpoint instead of using the framework's.
|
|
179
180
|
* `basalt-card` is the one shipped today (`src/dashboard/section.module.css`);
|
|
180
|
-
* `basalt-form`/`basalt-
|
|
181
|
-
*
|
|
181
|
+
* `basalt-form`/`basalt-grid` are the declared remaining two, reserved here so a consumer adopting
|
|
182
|
+
* one ahead of the framework does not immediately trip this guard. `basalt-main` was a third until
|
|
183
|
+
* 1.32.1 dropped it: nothing ever declared it, so the guard accepted a query that never matched.
|
|
182
184
|
*
|
|
183
185
|
* `basalt-grid` is also what `StatGroup`/`WidgetGrid` declare (`src/dashboard/*.module.css`) —
|
|
184
186
|
* they shipped as `basalt-stat-group`/`basalt-widget-grid` until 1.32.0 moved them onto this table.
|
|
185
187
|
*/
|
|
186
|
-
const DECLARED_CONTAINER_NAMES = new Set([
|
|
187
|
-
'basalt-card',
|
|
188
|
-
'basalt-form',
|
|
189
|
-
'basalt-main',
|
|
190
|
-
'basalt-grid',
|
|
191
|
-
])
|
|
188
|
+
const DECLARED_CONTAINER_NAMES = new Set(['basalt-card', 'basalt-form', 'basalt-grid'])
|
|
192
189
|
|
|
193
190
|
/**
|
|
194
191
|
* `CONTAINER_CLASSES` (`src/tokens/size-classes.ts`) boundaries, duplicated per the `CTL_THEME_TAGS`
|
|
195
192
|
* convention in the oxlint plugin — this module is dependency-free by contract (file header) and
|
|
196
193
|
* must not import `../tokens`. Pinned against the real table by `check-source.test.ts`. Judged only
|
|
197
|
-
* against `basalt-card
|
|
198
|
-
* the container's own floor, not a class boundary, but `(width >= 0px)` is a legitimate
|
|
199
|
-
* "scope this rule to the container, whatever its width" (`widget-header.module.css`'s
|
|
200
|
-
* — allowed for that reason, not because it names a real `CONTAINER_CLASSES` step.
|
|
194
|
+
* against `basalt-card` and unnamed queries: the other declared names have no shipped numeric table
|
|
195
|
+
* yet. `0` is the container's own floor, not a class boundary, but `(width >= 0px)` is a legitimate
|
|
196
|
+
* idiom for "scope this rule to the container, whatever its width" (`widget-header.module.css`'s
|
|
197
|
+
* `cqi` clamp) — allowed for that reason, not because it names a real `CONTAINER_CLASSES` step.
|
|
201
198
|
*/
|
|
202
199
|
const CONTAINER_CLASS_LITERALS_PX = new Set([0, 240, 239.9, 480, 479.9, 800, 799.9])
|
|
203
200
|
|
|
@@ -290,6 +287,62 @@ const INLINE_FONT_SIZE =
|
|
|
290
287
|
const CARD_SURFACE_TAG = /<(?:Card|Paper)(?![\w.])(?:=>|[^>])*?>/g
|
|
291
288
|
const WITH_BORDER_PROP = /\bwithBorder\b(?!\s*=\s*\{\s*false\s*\})/
|
|
292
289
|
|
|
290
|
+
/**
|
|
291
|
+
* A `var(--vx-*)` REFERENCE with a literal name. The name must end on an alphanumeric and must not
|
|
292
|
+
* run into a `${…}` interpolation, so `var(--vx-fill-${hue})` — a name computed at runtime — never
|
|
293
|
+
* matches a truncated `--vx-fill`.
|
|
294
|
+
*/
|
|
295
|
+
const VX_VAR_REF = /var\(\s*(--vx-[A-Za-z0-9-]*[A-Za-z0-9])(?![A-Za-z0-9-]|\$\{)/g
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* A `--vx-*` name the file DECLARES — a consumer's own custom property, never unknown. Only a real
|
|
299
|
+
* declaration position counts, because the set is file-wide and one false hit silences every
|
|
300
|
+
* finding for that name in the file:
|
|
301
|
+
*
|
|
302
|
+
* - a CSS declaration (`--vx-x: …`, in a stylesheet or in CSS text inside a template literal),
|
|
303
|
+
* directly after a line start, `{` or `;`;
|
|
304
|
+
* - a quoted style-object key (`{ '--vx-x': … }`), directly after `{` or `,` — but not a TS
|
|
305
|
+
* member key (`'--vx-x'?: …`, or one typed `string`/`number`);
|
|
306
|
+
* - `setProperty('--vx-x', …)`.
|
|
307
|
+
*
|
|
308
|
+
* A quoted name used as an EXPRESSION — a ternary arm, a `case` label, an argument — is a reference,
|
|
309
|
+
* not a declaration, and never matches.
|
|
310
|
+
*/
|
|
311
|
+
const VX_LOCAL_DECLARATION =
|
|
312
|
+
/(?:^|[{;])\s*(--vx-[A-Za-z0-9-]*[A-Za-z0-9])\s*:|[{,]\s*(['"])(--vx-[A-Za-z0-9-]*[A-Za-z0-9])\2\s*:(?!\s*(?:string|number)\b)|\bsetProperty\(\s*['"](--vx-[A-Za-z0-9-]*[A-Za-z0-9])['"]/gm
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* `--vx-*` names basalt once emitted and no longer does, with the replacement MIGRATING names
|
|
316
|
+
* (`null`: deleted with no replacement). Hand-kept on purpose: it is history, it only grows, and
|
|
317
|
+
* `vx-names.test.ts` pins that none of these is live and every replacement is.
|
|
318
|
+
*/
|
|
319
|
+
const REMOVED_VX_NAMES: Readonly<Record<string, string | null>> = {
|
|
320
|
+
'--vx-space-touch-target': '--vx-hit',
|
|
321
|
+
'--vx-space-touch-control-height': '--vx-hit',
|
|
322
|
+
'--vx-space-app-header-mobile-actions-height': '--vx-space-sticky-header-clearance',
|
|
323
|
+
'--vx-space-sticky-header-clearance-mobile': '--vx-space-sticky-header-clearance',
|
|
324
|
+
'--vx-space-sidebar-brand-inset-top': null,
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const KNOWN_VX_NAMES: ReadonlySet<string> = new Set(BASALT_VX_NAMES)
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* The bare family roots basalt never emits — `--vx-space`, `--vx-surface`, `--vx-status`, … — derived
|
|
331
|
+
* from the `--vx-<family>-` prefixes of the emitted names, minus any root that is itself a token
|
|
332
|
+
* (`--vx-hit`, `--vx-accent`). Nobody emits one: basalt only ever writes `<root>-<key>`, and so does
|
|
333
|
+
* `groupTokens`. It is the one shape of unknown name basalt can judge WITHOUT seeing the consumer's
|
|
334
|
+
* palette.
|
|
335
|
+
*
|
|
336
|
+
* An unknown name INSIDE a family (`--vx-surface-2`) is deliberately not judged. `groupTokens(name,
|
|
337
|
+
* map)` takes any `name`, so `--vx-surface-custom` may be a consumer group declared in a file this
|
|
338
|
+
* scan cannot see, and a per-file guard has no honest way to tell that from a typo.
|
|
339
|
+
*/
|
|
340
|
+
const BASALT_VX_FAMILY_ROOTS: ReadonlySet<string> = new Set(
|
|
341
|
+
BASALT_VX_NAMES.map((n) => /^(--vx-[A-Za-z0-9]+)-/.exec(n)?.[1]).filter(
|
|
342
|
+
(root): root is string => root !== undefined && !KNOWN_VX_NAMES.has(root),
|
|
343
|
+
),
|
|
344
|
+
)
|
|
345
|
+
|
|
293
346
|
// Raw Mantine ramp step used for surface color — gray/dark + a step digit.
|
|
294
347
|
const OFF_SYSTEM_SURFACE_VAR = /var\(--mantine-color-(gray|dark)-\d/g
|
|
295
348
|
|
|
@@ -691,9 +744,12 @@ const PROSE_CONTEXT_TAG = /<(?:Prose|ArticleLayout|Modal|Drawer)(?![\w])/
|
|
|
691
744
|
* read differently in the two lanes. The plugin gates them on the tag's basalt IMPORT; a 12-line
|
|
692
745
|
* regex window has no import graph, so this lane matches by name and accepts the missed warn on a
|
|
693
746
|
* consumer's own `PanelRow` — the same direction every approximation in this scan already leans.
|
|
747
|
+
*
|
|
748
|
+
* The name set is the plugin's `CONTROL_HOST_TAGS` ∪ `BASALT_HOST_TAGS`; `oxlint-plugin.test.ts` ›
|
|
749
|
+
* lane parity asserts the two agree (1.32.1 — `FormRow`/`FormGroup` had drifted out of this lane).
|
|
694
750
|
*/
|
|
695
751
|
const CONTROL_HOST_TAG =
|
|
696
|
-
/<(?:SettingsRow|Modal|Drawer|Popover\.Dropdown|Menu\.Dropdown|Composer|FilterSet|PageAside|PanelRow)(?![\w])/
|
|
752
|
+
/<(?:SettingsRow|FormRow|FormGroup|Modal|Drawer|Popover\.Dropdown|Menu\.Dropdown|Composer|FilterSet|PageAside|PanelRow)(?![\w])/
|
|
697
753
|
|
|
698
754
|
/**
|
|
699
755
|
* How far ABOVE a control the host-tag window reaches, in lines.
|
|
@@ -862,38 +918,14 @@ export type GraceEntry = { since: string; promote: string; why: string }
|
|
|
862
918
|
* }
|
|
863
919
|
*/
|
|
864
920
|
export const GRACE_PERIOD_KINDS: Partial<Record<GuardKind, GraceEntry>> = {
|
|
865
|
-
'
|
|
866
|
-
since: '1.
|
|
867
|
-
promote: '1.
|
|
868
|
-
why:
|
|
869
|
-
"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. " +
|
|
870
|
-
'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. ' +
|
|
871
|
-
'new in the wave-6 control guards (docs/CONTROLS-SPEC.md §6, law C1). The text lane cannot ' +
|
|
872
|
-
'see ancestry, so "no home" is approximated by a 12-line host-tag window — the loosest ' +
|
|
873
|
-
'reading in the guard, and the reason this one lands warn rather than error even though its ' +
|
|
874
|
-
'law is settled. ONE LAW, TWO LANES, ONE PROMOTION: it moves with its AST twin, ' +
|
|
875
|
-
'`basalt/control-outside-home`, whose PLUGIN_RULE_GRACE entry carries the full argument and ' +
|
|
876
|
-
'the measurement, and it must never be promoted or extended alone — a consumer waiving one ' +
|
|
877
|
-
'spelling and failing the other is the exact confusion `WAIVER_ID_ALIASES` exists to close. ' +
|
|
878
|
-
'RE-MEASURED 2026-09-09 across the whole fleet on 1.29.2: 29 AST-lane incumbents in five ' +
|
|
879
|
-
"repos, plus three of basalt's own (data-table, notifications/center, theme-lab). Extended " +
|
|
880
|
-
'to 1.31.0 because 1.30.0 is the FIRST release in which either lane names a home a ' +
|
|
881
|
-
'provider-only consumer can reach — every home both messages listed was a shell slot, and ' +
|
|
882
|
-
'the shell-less `PageBar` sentence ships in this minor. Promoting in the same minor that ' +
|
|
883
|
-
'makes a rule satisfiable gives nobody a window to act. 1.31.0 is when the 29 are ' +
|
|
884
|
-
're-measured against that number.',
|
|
885
|
-
},
|
|
886
|
-
'raw-media-query': {
|
|
887
|
-
since: '1.31.0',
|
|
888
|
-
promote: '1.33.0',
|
|
921
|
+
'unknown-vx-token': {
|
|
922
|
+
since: '1.33.0',
|
|
923
|
+
promote: '1.34.0',
|
|
889
924
|
why:
|
|
890
|
-
|
|
891
|
-
'
|
|
892
|
-
'
|
|
893
|
-
|
|
894
|
-
'shipped warn. Extended 1.32.0 -> 1.33.0 with its twin: 1.31.0 named ' +
|
|
895
|
-
'no reachable container primitive, and 1.32.0 is the first minor that documents one ' +
|
|
896
|
-
'(`CONTAINER_CLASSES` + the declared `@container` names, MIGRATING § `basalt/raw-breakpoint`).',
|
|
925
|
+
'New kind (consumer-loop r2 B3): obsidian lost its 44px touch floor to a removed ' +
|
|
926
|
+
'`var(--vx-space-touch-target)` that nothing reported. A new kind rejects code every previous ' +
|
|
927
|
+
'release accepted, so it lands warn for one minor. Measured at ship time: argo, linewatch, rb, ' +
|
|
928
|
+
'rollhook, obsidian, weatherorb, image-share, email-gateway, image-gen 0.',
|
|
897
929
|
},
|
|
898
930
|
}
|
|
899
931
|
|
|
@@ -1133,6 +1165,13 @@ export const WAIVER_ID_ALIASES: Readonly<Record<string, readonly string[]>> = {
|
|
|
1133
1165
|
'raw-selection-control': ['control-outside-home'],
|
|
1134
1166
|
}
|
|
1135
1167
|
|
|
1168
|
+
/**
|
|
1169
|
+
* The ids that are a guard kind AND an oxlint plugin rule — one law, two lanes, one annotation.
|
|
1170
|
+
* {@link PLUGIN_RULE_IDS} leaves them out by design, so `--audit-allows` would otherwise never ask
|
|
1171
|
+
* oxlint about them. `waiver-audit.test.ts` pins this against the plugin's own rule table.
|
|
1172
|
+
*/
|
|
1173
|
+
const DUAL_LANE_IDS: ReadonlySet<string> = new Set(['in-body-page-title'])
|
|
1174
|
+
|
|
1136
1175
|
/** The shortest string accepted as a written reason — enough to exclude a stray separator. */
|
|
1137
1176
|
const MIN_ALLOW_REASON_LENGTH = 4
|
|
1138
1177
|
|
|
@@ -1974,7 +2013,7 @@ type GuardRule = {
|
|
|
1974
2013
|
}
|
|
1975
2014
|
|
|
1976
2015
|
/**
|
|
1977
|
-
* The closed registry of all
|
|
2016
|
+
* The closed registry of all 28 guard kinds. The triad test asserts
|
|
1978
2017
|
* `surface.guardKinds ⊆ keyof GUARD_RULES` at runtime.
|
|
1979
2018
|
*
|
|
1980
2019
|
* raw-surface, raw-html-layout, and sub-16-input-font are handled inline in checkSource
|
|
@@ -2200,7 +2239,7 @@ export const GUARD_RULES = {
|
|
|
2200
2239
|
// JSX-tag-shaped (`<Select …>`) — never appears in CSS text.
|
|
2201
2240
|
appliesTo: (relPath) => !relPath.endsWith('.css'),
|
|
2202
2241
|
message:
|
|
2203
|
-
'Raw selection control with no home — a filter or tab belongs in a PageBar / Section / WidgetHeader slot and takes a `field` (a FieldHandle), so it owns the URL write and the localStorage mirror instead of carrying value/onChange (docs/CONTROLS-SPEC.md laws C1–C3). Use the bound control from basalt-ui/controls. A settings row, an overlay and an @mantine/form file are the declared non-homes and never report. No shell? PageBar needs no BasaltShell: outside one it renders in flow, sticky, with its own title — so its slots are a reachable home for a provider-only app. This is the TEXT lane of basalt/control-outside-home, which answers the same question against the real AST — where the two disagree, the plugin is right.',
|
|
2242
|
+
'Raw selection control with no home — a filter or tab belongs in a PageBar / Section / WidgetHeader slot and takes a `field` (a FieldHandle), so it owns the URL write and the localStorage mirror instead of carrying value/onChange (docs/CONTROLS-SPEC.md laws C1–C3). Use the bound control from basalt-ui/controls. A settings row, a FormRow / FormGroup, an overlay and an @mantine/form file are the declared non-homes and never report. No shell? PageBar needs no BasaltShell: outside one it renders in flow, sticky, with its own title — so its slots are a reachable home for a provider-only app. This is the TEXT lane of basalt/control-outside-home, which answers the same question against the real AST — where the two disagree, the plugin is right.',
|
|
2204
2243
|
},
|
|
2205
2244
|
'inline-font-size': {
|
|
2206
2245
|
kind: 'inline-font-size',
|
|
@@ -2214,7 +2253,18 @@ export const GUARD_RULES = {
|
|
|
2214
2253
|
// CSS-only: a width @media, an @container name/literal or a non-coarse pointer query has no JSX
|
|
2215
2254
|
// spelling — the AST-side twin of this law is the oxlint plugin's basalt/raw-breakpoint.
|
|
2216
2255
|
appliesTo: (relPath) => relPath.endsWith('.css'),
|
|
2217
|
-
message: `Raw viewport/container breakpoint in CSS — a width @media belongs to shell/** against SIZE_CLASSES (useSizeClass() is the JS seam); an @container name must be one of ${[...DECLARED_CONTAINER_NAMES].join(' / ')} at a boundary in ${[...CONTAINER_CLASS_LITERALS_PX].map((px) => `${px}px`).join(' / ')}; and a pointer query other than (pointer: coarse) or (pointer: fine) has no framework meaning. Same law the oxlint plugin enforces as basalt/raw-breakpoint over JSX; this is the CSS half.`,
|
|
2256
|
+
message: `Raw viewport/container breakpoint in CSS — a width @media belongs to shell/** against SIZE_CLASSES (useSizeClass() is the JS seam); an @container name must be one of ${[...DECLARED_CONTAINER_NAMES].join(' / ')}, and a basalt-card or unnamed @container query must sit at a boundary in ${[...CONTAINER_CLASS_LITERALS_PX].map((px) => `${px}px`).join(' / ')} (the other declared names have no numeric table yet, so their literals are not judged); and a pointer query other than (pointer: coarse) or (pointer: fine) has no framework meaning. Same law the oxlint plugin enforces as basalt/raw-breakpoint over JSX; this is the CSS half.`,
|
|
2257
|
+
},
|
|
2258
|
+
'unknown-vx-token': {
|
|
2259
|
+
kind: 'unknown-vx-token',
|
|
2260
|
+
pattern: VX_VAR_REF, // handled inline (removed map + family roots); entry keeps the registry complete
|
|
2261
|
+
message: `var() names a --vx-* token basalt does not emit, so it resolves to its fallback (or to nothing) with no error anywhere. Two shapes are judged, the only two basalt can judge without seeing your palette: a REMOVED name — ${Object.entries(
|
|
2262
|
+
REMOVED_VX_NAMES,
|
|
2263
|
+
)
|
|
2264
|
+
.map(([from, to]) => `${from} → ${to ?? '(deleted, no replacement)'}`)
|
|
2265
|
+
.join(
|
|
2266
|
+
'; ',
|
|
2267
|
+
)} — and a bare family root nothing emits (${[...BASALT_VX_FAMILY_ROOTS].join(', ')}): name the token under it from basalt-ui/tokens.css. Any other unknown name is never judged, because defineSeries/groupTokens may declare it in a file this scan cannot see.`,
|
|
2218
2268
|
},
|
|
2219
2269
|
} as const satisfies Record<GuardKind, GuardRule>
|
|
2220
2270
|
|
|
@@ -2390,6 +2440,22 @@ export function checkSource(text: string, relPath: string, cfg: GuardConfig): Fi
|
|
|
2390
2440
|
[...codeText.matchAll(STYLE_BOUND_IDENTIFIER)].map((m) => m[1] ?? ''),
|
|
2391
2441
|
)
|
|
2392
2442
|
|
|
2443
|
+
/** `--vx-*` names this file declares — its own custom properties, which `unknown-vx-token` skips. */
|
|
2444
|
+
const locallyDeclaredVx = new Set(
|
|
2445
|
+
[...codeText.matchAll(VX_LOCAL_DECLARATION)].map((m) => m[1] ?? m[3] ?? m[4] ?? ''),
|
|
2446
|
+
)
|
|
2447
|
+
|
|
2448
|
+
// unknown-vx-token: a removed name, or a bare family root nothing emits — see the roots' doc.
|
|
2449
|
+
// Scanned over the whole comment-stripped text rather than per line, so a formatter-wrapped
|
|
2450
|
+
// `var(\n --vx-x\n)` is seen; the finding lands on the line the NAME is on.
|
|
2451
|
+
for (const m of codeText.matchAll(VX_VAR_REF)) {
|
|
2452
|
+
const name = m[1] ?? ''
|
|
2453
|
+
if (locallyDeclaredVx.has(name)) continue
|
|
2454
|
+
if (!Object.hasOwn(REMOVED_VX_NAMES, name) && !BASALT_VX_FAMILY_ROOTS.has(name)) continue
|
|
2455
|
+
const at = (m.index ?? 0) + m[0].length - name.length
|
|
2456
|
+
push('unknown-vx-token', lineStarts.findLastIndex((start) => start <= at) + 1, name)
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2393
2459
|
for (let i = 0; i < lines.length; i++) {
|
|
2394
2460
|
const line = codeLines[i] ?? ''
|
|
2395
2461
|
|
|
@@ -2481,7 +2547,7 @@ export function checkSource(text: string, relPath: string, cfg: GuardConfig): Fi
|
|
|
2481
2547
|
}
|
|
2482
2548
|
// Literal boundaries are judged against `basalt-card`'s own table (see
|
|
2483
2549
|
// `CONTAINER_CLASS_LITERALS_PX`'s doc) — an unnamed query has no OTHER table to judge
|
|
2484
|
-
// against, so it is judged against this one too; the other
|
|
2550
|
+
// against, so it is judged against this one too; the other declared names stay
|
|
2485
2551
|
// unjudged until they ship their own.
|
|
2486
2552
|
if (name !== '' && name !== 'basalt-card') continue
|
|
2487
2553
|
for (const lit of body.matchAll(CSS_CONTAINER_LITERAL)) {
|
|
@@ -2794,6 +2860,15 @@ export type AllowAnnotationSite = {
|
|
|
2794
2860
|
readonly guardKinds: readonly GuardKind[]
|
|
2795
2861
|
/** The subset of `rules` the oxlint plugin owns — outside `checkSource`'s reach. */
|
|
2796
2862
|
readonly pluginRules: readonly string[]
|
|
2863
|
+
/**
|
|
2864
|
+
* Every id an `oxlint` run can prove live: `pluginRules`, plus a guard kind that is ALSO a plugin
|
|
2865
|
+
* rule (`in-body-page-title`) or aliases one ({@link WAIVER_ID_ALIASES}). A waiver is live if
|
|
2866
|
+
* EITHER lane still needs it — judging only `pluginRules` called rb's live `in-body-page-title`
|
|
2867
|
+
* waivers dead, and deleting them made oxlint exit 1. A bare LINE-scope annotation covers every
|
|
2868
|
+
* plugin rule (the plugin's `allowCovers`), so it lists all of them; a bare file-scope one waives
|
|
2869
|
+
* nothing in either lane and lists none.
|
|
2870
|
+
*/
|
|
2871
|
+
readonly oxlintRules: readonly string[]
|
|
2797
2872
|
/** Words that occupied the id slot but name no rule. Their presence makes the parse fail closed. */
|
|
2798
2873
|
readonly unknownRules: readonly string[]
|
|
2799
2874
|
/** A written reason follows the ids. */
|
|
@@ -2832,19 +2907,33 @@ export function findAllowAnnotations(
|
|
|
2832
2907
|
cfg.allowComment,
|
|
2833
2908
|
syntax,
|
|
2834
2909
|
)
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2910
|
+
const everyOxlintRule = [...PLUGIN_RULE_IDS, ...DUAL_LANE_IDS]
|
|
2911
|
+
return declared.map(({ line, annotation }) => {
|
|
2912
|
+
const bare = annotation.rules.length === 0 && annotation.unknownRules.length === 0
|
|
2913
|
+
return {
|
|
2914
|
+
line,
|
|
2915
|
+
scope: annotation.scope,
|
|
2916
|
+
rules: [...annotation.rules],
|
|
2917
|
+
guardKinds: annotation.rules.filter((id): id is GuardKind => Object.hasOwn(GUARD_RULES, id)),
|
|
2918
|
+
pluginRules: annotation.rules.filter(
|
|
2919
|
+
(id) => !Object.hasOwn(GUARD_RULES, id) && PLUGIN_RULE_IDS.has(id),
|
|
2920
|
+
),
|
|
2921
|
+
oxlintRules:
|
|
2922
|
+
bare && annotation.scope === 'line'
|
|
2923
|
+
? everyOxlintRule
|
|
2924
|
+
: [
|
|
2925
|
+
...new Set(
|
|
2926
|
+
annotation.rules
|
|
2927
|
+
.flatMap((id) => [id, ...(WAIVER_ID_ALIASES[id] ?? [])])
|
|
2928
|
+
.filter((id) => PLUGIN_RULE_IDS.has(id) || DUAL_LANE_IDS.has(id)),
|
|
2929
|
+
),
|
|
2930
|
+
],
|
|
2931
|
+
unknownRules: [...annotation.unknownRules],
|
|
2932
|
+
hasReason: annotation.hasReason,
|
|
2933
|
+
bare,
|
|
2934
|
+
text: lines[line - 1] ?? '',
|
|
2935
|
+
}
|
|
2936
|
+
})
|
|
2848
2937
|
}
|
|
2849
2938
|
|
|
2850
2939
|
/** The token a neutralized annotation is rewritten to. Shares no substring with `theme-allow`. */
|
package/src/guard/types.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Guard types — Mantine-free, dependency-free (zero imports beyond TS types).
|
|
3
3
|
*
|
|
4
|
-
* GuardKind is the closed set of
|
|
4
|
+
* GuardKind is the closed set of 28 violation kinds the theme guard can emit — `unframed-chart`
|
|
5
5
|
* retired at the C2 consolidation wave: its only case (a hand-authored `<ChartLegend items={[...]}>`
|
|
6
6
|
* array literal) is fully subsumed by the oxlint plugin's `basalt/chart-legend-literal`, which also
|
|
7
7
|
* catches the `.map()`-over-a-non-series form the regex never could. `'unframed-chart'` stays a
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* GuardConfig is the per-run configuration that drives checkSource.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
/** The
|
|
14
|
+
/** The 28 theme-guard violation kinds. */
|
|
15
15
|
export type GuardKind =
|
|
16
16
|
| 'raw-hex'
|
|
17
17
|
| 'raw-color-fn'
|
|
@@ -40,6 +40,7 @@ export type GuardKind =
|
|
|
40
40
|
| 'in-body-page-title'
|
|
41
41
|
| 'raw-selection-control'
|
|
42
42
|
| 'raw-media-query'
|
|
43
|
+
| 'unknown-vx-token'
|
|
43
44
|
|
|
44
45
|
/**
|
|
45
46
|
* How hard a finding lands. `error` fails the build; `warn` reports and passes.
|