basalt-ui 1.32.1 → 1.34.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 +1 -1
- package/CHANGELOG.md +65 -0
- package/MIGRATING.md +218 -25
- package/README.md +3 -0
- package/agent/rules/basalt-state.md +2 -2
- package/agent/rules/basalt-tokens.md +1 -1
- package/agent/skills/basalt-app/SKILL.md +5 -3
- package/configs/oxlint-basalt.json +33 -0
- package/configs/oxlint-plugin.js +89 -67
- package/configs/oxlint-plugin.test.ts +224 -19
- package/configs/oxlint.json +2 -30
- 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 +38 -38
- 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.d.ts.map +1 -1
- package/dist/cli/doctor.js +4 -6
- package/dist/cli/guard-hook.d.ts.map +1 -1
- package/dist/cli/guard-hook.js +7 -41
- package/dist/cli/index.d.ts +31 -257
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +64 -37
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +7 -16
- 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/common/vx-families.d.ts +7 -0
- package/dist/common/vx-families.d.ts.map +1 -0
- package/dist/common/vx-families.js +32 -0
- package/dist/common/vx-family.d.ts +11 -0
- package/dist/common/vx-family.d.ts.map +1 -0
- package/dist/common/vx-family.js +12 -0
- 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.map +1 -1
- package/dist/data/data-table.js +2 -3
- package/dist/guard/index.d.ts +17 -3
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +62 -23
- 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/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -2
- 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/app-mobile-nav.d.ts.map +1 -1
- package/dist/shell/app-mobile-nav.js +2 -1
- package/dist/shell/app-mobile-nav.module.css +12 -0
- package/dist/shell/app-sidebar.d.ts.map +1 -1
- package/dist/shell/app-sidebar.js +32 -36
- package/dist/shell/app-sidebar.module.css +6 -0
- package/dist/shell/index.d.ts +0 -1
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +0 -2
- package/dist/shell/mobile-nav-model.d.ts.map +1 -1
- package/dist/shell/mobile-nav-model.js +3 -4
- package/dist/shell/nav-glyph.d.ts +14 -0
- package/dist/shell/nav-glyph.d.ts.map +1 -0
- package/dist/shell/nav-glyph.js +21 -0
- package/dist/shell/nav-types.d.ts +1 -0
- package/dist/shell/nav-types.d.ts.map +1 -1
- package/dist/shell/page-bar.d.ts.map +1 -1
- package/dist/shell/page-bar.js +1 -2
- package/dist/shell/sidebar-search.d.ts.map +1 -1
- package/dist/shell/sidebar-search.js +1 -3
- package/dist/surfaces.d.ts +3 -3
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +4 -2
- package/dist/tokens/index.d.ts +1 -1
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +24 -2
- package/dist/tokens/size-classes.d.ts +25 -0
- package/dist/tokens/size-classes.d.ts.map +1 -1
- package/dist/tokens/size-classes.js +14 -0
- package/llms.txt +4 -4
- 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 +61 -39
- package/src/cli/config.ts +209 -0
- package/src/cli/doctor.ts +5 -7
- package/src/cli/guard-hook.ts +9 -42
- package/src/cli/index.ts +101 -294
- package/src/cli/sync.ts +7 -16
- 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/common/vx-families.ts +36 -0
- package/src/common/vx-family.ts +22 -0
- 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 +2 -3
- package/src/guard/index.ts +134 -47
- package/src/guard/types.ts +3 -2
- package/src/guard/vx-names.ts +257 -0
- package/src/index.ts +0 -2
- 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/app-mobile-nav.module.css +12 -0
- package/src/shell/app-mobile-nav.tsx +8 -1
- package/src/shell/app-sidebar.module.css +6 -0
- package/src/shell/app-sidebar.tsx +52 -50
- package/src/shell/index.tsx +0 -1
- package/src/shell/mobile-nav-model.ts +3 -5
- package/src/shell/nav-glyph.tsx +38 -0
- package/src/shell/nav-types.ts +1 -0
- package/src/shell/page-bar.tsx +1 -4
- package/src/shell/sidebar-search.tsx +1 -5
- package/src/surfaces.ts +4 -2
- package/src/tokens/index.ts +34 -2
- package/src/tokens/size-classes.ts +27 -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/dist/shell/use-breakpoint.d.ts +0 -10
- package/dist/shell/use-breakpoint.d.ts.map +0 -1
- package/dist/shell/use-breakpoint.js +0 -11
- package/src/common/is-dev.ts +0 -4
- package/src/shell/use-breakpoint.ts +0 -37
|
@@ -29,7 +29,6 @@ import type { ReactNode } from 'react'
|
|
|
29
29
|
import { useEffect } from 'react'
|
|
30
30
|
import { missingLayer } from '../common/errors'
|
|
31
31
|
import type { Slot } from '../register'
|
|
32
|
-
import { isDev } from '../common/is-dev'
|
|
33
32
|
|
|
34
33
|
// ── Lazy @mantine/modals resolution ───────────────────────────────────────────
|
|
35
34
|
// @mantine/modals is an OPTIONAL peer — never imported at module evaluation time, so importing
|
|
@@ -372,7 +371,7 @@ export const overlays = {
|
|
|
372
371
|
open<K extends OverlayKey>(key: K, props: OverlayProps<K>): void {
|
|
373
372
|
const spec = activeOverlays[key as string]
|
|
374
373
|
if (spec === undefined) {
|
|
375
|
-
if (
|
|
374
|
+
if (process.env.NODE_ENV !== 'production')
|
|
376
375
|
console.warn(`[basalt] overlays.open: no overlay registered for "${String(key)}"`)
|
|
377
376
|
return
|
|
378
377
|
}
|
package/src/common/index.ts
CHANGED
|
@@ -19,5 +19,4 @@ export {
|
|
|
19
19
|
} from './errors'
|
|
20
20
|
export { useValidateProps, assertRequiredProps, resetValidatedProps } from './validate'
|
|
21
21
|
export { scrollParentOf, SCROLLPORT_ATTRIBUTE } from './scroll-parent'
|
|
22
|
-
export { isDev } from './is-dev'
|
|
23
22
|
export { MOTION_DURATION, MOTION_EASE_STANDARD, MOTION_SPRING } from './motion'
|
package/src/common/validate.ts
CHANGED
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
* mean warns once in dev and renders on (`useValidateProps`).
|
|
8
8
|
*/
|
|
9
9
|
import { useEffect } from 'react'
|
|
10
|
-
import { isDev } from './is-dev'
|
|
11
10
|
import { requiredProp } from './errors'
|
|
12
11
|
|
|
13
12
|
/** A check returns the message(s) it has, or nothing. `null`/`undefined` entries are ignored. */
|
|
@@ -23,8 +22,8 @@ const reported = new Set<string>()
|
|
|
23
22
|
|
|
24
23
|
/**
|
|
25
24
|
* Dev-only prop validation. `console.error`s each message ONCE per `(component, message)` pair, and
|
|
26
|
-
* is a no-op in production:
|
|
27
|
-
* constant-folds, so the whole body disappears from a production build.
|
|
25
|
+
* is a no-op in production: the gate is a literal `process.env.NODE_ENV` read, which every bundler
|
|
26
|
+
* constant-folds, so the whole body disappears from a production build (see `dev-gate.test.ts`).
|
|
28
27
|
*
|
|
29
28
|
* **The dedup key is `${component} ${message}`, and nothing else** — not the instance, not the
|
|
30
29
|
* props. Two instances of one component producing the SAME message say it once between them, which
|
|
@@ -46,7 +45,7 @@ export function useValidateProps(
|
|
|
46
45
|
deps: unknown[],
|
|
47
46
|
): void {
|
|
48
47
|
useEffect(() => {
|
|
49
|
-
if (
|
|
48
|
+
if (process.env.NODE_ENV === 'production') return
|
|
50
49
|
const result = check()
|
|
51
50
|
const messages = Array.isArray(result) ? result : [result]
|
|
52
51
|
for (const raw of messages) {
|
|
@@ -67,7 +66,7 @@ export function useValidateProps(
|
|
|
67
66
|
* are about to win in the very next render.
|
|
68
67
|
*/
|
|
69
68
|
export function reportOnce(component: string, message: string): void {
|
|
70
|
-
if (
|
|
69
|
+
if (process.env.NODE_ENV === 'production') return
|
|
71
70
|
const key = `${component} ${message}`
|
|
72
71
|
if (reported.has(key)) return
|
|
73
72
|
reported.add(key)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// GENERATED by scripts/gen-vx-names.ts — do not edit. `vx-names.test.ts` fails on drift.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The families every emitted `--vx-*` name lives in (`basaltVxFamilies`) — basalt-owned:
|
|
5
|
+
* `unknown-vx-token` judges every name inside one, and `groupTokens`/`seriesTokens` warn on a
|
|
6
|
+
* consumer ref there.
|
|
7
|
+
*/
|
|
8
|
+
export const BASALT_VX_FAMILIES: readonly string[] = [
|
|
9
|
+
'accent',
|
|
10
|
+
'area',
|
|
11
|
+
'axis',
|
|
12
|
+
'bad',
|
|
13
|
+
'crosshair',
|
|
14
|
+
'divider',
|
|
15
|
+
'dot',
|
|
16
|
+
'faint',
|
|
17
|
+
'fill',
|
|
18
|
+
'good',
|
|
19
|
+
'grid',
|
|
20
|
+
'hit',
|
|
21
|
+
'ink',
|
|
22
|
+
'keyboard',
|
|
23
|
+
'legend',
|
|
24
|
+
'line',
|
|
25
|
+
'muted',
|
|
26
|
+
'neutral',
|
|
27
|
+
'prose',
|
|
28
|
+
'radius',
|
|
29
|
+
'shadow',
|
|
30
|
+
'space',
|
|
31
|
+
'status',
|
|
32
|
+
'surface',
|
|
33
|
+
'text',
|
|
34
|
+
'tooltip',
|
|
35
|
+
'warn',
|
|
36
|
+
]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A `--vx-*` name's FAMILY — its leading lowercase run: `--vx-surface-bg` → `surface`, `--vx-ink2`
|
|
3
|
+
* → `ink`, `--vx-tooltipBg` → `tooltip`, `--vx-fillHover-cyan` → `fill`. Dependency-free, because
|
|
4
|
+
* three sides must agree on it byte for byte: the generator that writes `BASALT_VX_FAMILIES`, the
|
|
5
|
+
* guard that judges every name inside one of those families, and the token factories that refuse a
|
|
6
|
+
* consumer name inside one — the refusal is what makes the guard's judgement honest.
|
|
7
|
+
*/
|
|
8
|
+
export function vxFamilyOf(name: string): string {
|
|
9
|
+
return /^--vx-([a-z]*)/.exec(name)?.[1] ?? ''
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Families a consumer extends without any factory: the theme emits `--vx-on-<color>` for EVERY
|
|
14
|
+
* `theme.colors` entry, a consumer's own colors included, so `on` is never basalt's alone.
|
|
15
|
+
*/
|
|
16
|
+
const CONSUMER_EXTENSIBLE_FAMILIES: ReadonlySet<string> = new Set(['on'])
|
|
17
|
+
|
|
18
|
+
/** The basalt-owned families among `names` — run over every name basalt emits (`collectVxNames`). */
|
|
19
|
+
export function basaltVxFamilies(names: readonly string[]): string[] {
|
|
20
|
+
const families = new Set(names.map(vxFamilyOf))
|
|
21
|
+
return [...families].filter((f) => f !== '' && !CONSUMER_EXTENSIBLE_FAMILIES.has(f)).toSorted()
|
|
22
|
+
}
|
|
@@ -166,6 +166,7 @@ export type ArticleGridProps = {
|
|
|
166
166
|
|
|
167
167
|
export function ArticleGrid({ children, cols = 3 }: ArticleGridProps) {
|
|
168
168
|
return (
|
|
169
|
+
// theme-allow raw-breakpoint — `cols` is documented per viewport `lg`; container keys would re-flow every consumer's grid, so that move ships opt-in
|
|
169
170
|
<SimpleGrid cols={{ base: 1, sm: 2, lg: cols }} spacing="sm">
|
|
170
171
|
{children}
|
|
171
172
|
</SimpleGrid>
|
|
@@ -26,13 +26,13 @@
|
|
|
26
26
|
* its doc). Both public wrappers still type `field` exactly, so a wrong field kind is still a type
|
|
27
27
|
* error at the call site; the widening lives here, where nothing consumer-facing points at it.
|
|
28
28
|
*/
|
|
29
|
-
import {
|
|
29
|
+
import { Radio, Stack } from '@mantine/core'
|
|
30
30
|
import type { ReactNode } from 'react'
|
|
31
31
|
import type { BasaltProps } from '../common/props'
|
|
32
32
|
import type { FieldOption } from '../state'
|
|
33
33
|
import classes from './controls.module.css'
|
|
34
34
|
import { POPOVER_ROW_HIT_GAP, useFilterRegistration, useFilterSurface } from './filter-context'
|
|
35
|
-
import { FilterPill } from './filter-pill'
|
|
35
|
+
import { FilterClearButton, FilterPill } from './filter-pill'
|
|
36
36
|
import { useControlName } from './filter-sheet'
|
|
37
37
|
import { PanelChoice, PanelRow } from './panel-row'
|
|
38
38
|
import type { FilterOption } from './select-filter'
|
|
@@ -157,17 +157,7 @@ export function EnumFilter<T extends string>({
|
|
|
157
157
|
/>
|
|
158
158
|
))}
|
|
159
159
|
</Stack>
|
|
160
|
-
{clearable === true && !isDefault && (
|
|
161
|
-
<Button
|
|
162
|
-
variant="subtle"
|
|
163
|
-
size="ctl"
|
|
164
|
-
onClick={() => {
|
|
165
|
-
field.clear()
|
|
166
|
-
}}
|
|
167
|
-
>
|
|
168
|
-
Clear
|
|
169
|
-
</Button>
|
|
170
|
-
)}
|
|
160
|
+
{clearable === true && !isDefault && <FilterClearButton onClear={() => field.clear()} />}
|
|
171
161
|
</Radio.Group>
|
|
172
162
|
)
|
|
173
163
|
|
|
@@ -122,3 +122,23 @@ export function FilterPill({
|
|
|
122
122
|
</Popover>
|
|
123
123
|
)
|
|
124
124
|
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The popover's `Clear` — one button for `EnumFilter`, `NumberFilter` and `MultiSelectFilter`. Its
|
|
128
|
+
* mousedown keeps focus where it is: a stepper commits its draft on blur, so a press that blurred
|
|
129
|
+
* first would land the typed value in the URL a tick before the clear replaced it.
|
|
130
|
+
*/
|
|
131
|
+
export function FilterClearButton({ onClear }: { readonly onClear: () => void }): ReactNode {
|
|
132
|
+
return (
|
|
133
|
+
<Button
|
|
134
|
+
variant="subtle"
|
|
135
|
+
size="ctl"
|
|
136
|
+
onMouseDown={(event) => {
|
|
137
|
+
event.preventDefault()
|
|
138
|
+
}}
|
|
139
|
+
onClick={onClear}
|
|
140
|
+
>
|
|
141
|
+
Clear
|
|
142
|
+
</Button>
|
|
143
|
+
)
|
|
144
|
+
}
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* // arrive as a map rather than baked into the labels the way the runtime catalogue does it.
|
|
23
23
|
* <MultiSelectFilter field={shipments.field.origin} label="Origin" counts={countsByOrigin} />
|
|
24
24
|
*/
|
|
25
|
-
import {
|
|
25
|
+
import { Checkbox, Stack } from '@mantine/core'
|
|
26
26
|
import { useState } from 'react'
|
|
27
27
|
import type { CSSProperties, ReactNode } from 'react'
|
|
28
28
|
import type { BasaltProps } from '../common/props'
|
|
@@ -31,7 +31,7 @@ import type { FieldHandle, MultiField } from '../state'
|
|
|
31
31
|
import type { FilterOption } from './select-filter'
|
|
32
32
|
import classes from './controls.module.css'
|
|
33
33
|
import { POPOVER_ROW_HIT_GAP, useFilterRegistration, useFilterSurface } from './filter-context'
|
|
34
|
-
import { FilterPill } from './filter-pill'
|
|
34
|
+
import { FilterClearButton, FilterPill } from './filter-pill'
|
|
35
35
|
import { CheckGlyph, useControlName } from './filter-sheet'
|
|
36
36
|
import { PanelRow } from './panel-row'
|
|
37
37
|
|
|
@@ -153,17 +153,7 @@ export function MultiSelectFilter<T extends string>(props: MultiSelectFilterProp
|
|
|
153
153
|
/>
|
|
154
154
|
))}
|
|
155
155
|
</Stack>
|
|
156
|
-
{!isDefault && (
|
|
157
|
-
<Button
|
|
158
|
-
variant="subtle"
|
|
159
|
-
size="ctl"
|
|
160
|
-
onClick={() => {
|
|
161
|
-
field.clear()
|
|
162
|
-
}}
|
|
163
|
-
>
|
|
164
|
-
Clear
|
|
165
|
-
</Button>
|
|
166
|
-
)}
|
|
156
|
+
{!isDefault && <FilterClearButton onClear={() => field.clear()} />}
|
|
167
157
|
</Checkbox.Group>
|
|
168
158
|
)
|
|
169
159
|
|
|
@@ -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)
|
|
@@ -1231,7 +1230,7 @@ const reportedBreaches = new Set<string>()
|
|
|
1231
1230
|
function enforceManualPaginationContract(breaches: ManualPaginationBreach[]): void {
|
|
1232
1231
|
if (breaches.length === 0) return
|
|
1233
1232
|
const message = manualPaginationMessage(breaches)
|
|
1234
|
-
if (
|
|
1233
|
+
if (process.env.NODE_ENV !== 'production') throw new Error(message)
|
|
1235
1234
|
if (reportedBreaches.has(message)) return
|
|
1236
1235
|
reportedBreaches.add(message)
|
|
1237
1236
|
console.error(message)
|
package/src/guard/index.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
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 { vxFamilyOf } from '../common/vx-family'
|
|
9
|
+
import { BASALT_VX_FAMILIES } from '../common/vx-families'
|
|
10
|
+
import { BASALT_VX_NAMES } from './vx-names'
|
|
8
11
|
|
|
9
12
|
export type { Finding, GuardConfig, GuardKind, GuardSeverity }
|
|
10
13
|
|
|
@@ -286,6 +289,57 @@ const INLINE_FONT_SIZE =
|
|
|
286
289
|
const CARD_SURFACE_TAG = /<(?:Card|Paper)(?![\w.])(?:=>|[^>])*?>/g
|
|
287
290
|
const WITH_BORDER_PROP = /\bwithBorder\b(?!\s*=\s*\{\s*false\s*\})/
|
|
288
291
|
|
|
292
|
+
/**
|
|
293
|
+
* A `var(--vx-*)` REFERENCE with a literal name. The name must end on an alphanumeric and must not
|
|
294
|
+
* run into a `${…}` interpolation, so `var(--vx-fill-${hue})` — a name computed at runtime — never
|
|
295
|
+
* matches a truncated `--vx-fill`.
|
|
296
|
+
*/
|
|
297
|
+
const VX_VAR_REF = /var\(\s*(--vx-[A-Za-z0-9-]*[A-Za-z0-9])(?![A-Za-z0-9-]|\$\{)/g
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* A `--vx-*` name the file DECLARES — a consumer's own custom property, never unknown. Only a real
|
|
301
|
+
* declaration position counts, because the set is file-wide and one false hit silences every
|
|
302
|
+
* finding for that name in the file:
|
|
303
|
+
*
|
|
304
|
+
* - a CSS declaration (`--vx-x: …`, in a stylesheet or in CSS text inside a template literal),
|
|
305
|
+
* directly after a line start, `{` or `;`;
|
|
306
|
+
* - a quoted style-object key (`{ '--vx-x': … }`), directly after `{` or `,` — but not a TS
|
|
307
|
+
* member key (`'--vx-x'?: …`, or one typed `string`/`number`);
|
|
308
|
+
* - `setProperty('--vx-x', …)`.
|
|
309
|
+
*
|
|
310
|
+
* A quoted name used as an EXPRESSION — a ternary arm, a `case` label, an argument — is a reference,
|
|
311
|
+
* not a declaration, and never matches.
|
|
312
|
+
*/
|
|
313
|
+
const VX_LOCAL_DECLARATION =
|
|
314
|
+
/(?:^|[{;])\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
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* `--vx-*` names basalt once emitted and no longer does, with the replacement MIGRATING names
|
|
318
|
+
* (`null`: deleted with no replacement). Hand-kept on purpose: it is history, it only grows, and
|
|
319
|
+
* `vx-names.test.ts` pins that none of these is live and every replacement is.
|
|
320
|
+
*/
|
|
321
|
+
const REMOVED_VX_NAMES: Readonly<Record<string, string | null>> = {
|
|
322
|
+
'--vx-space-touch-target': '--vx-hit',
|
|
323
|
+
'--vx-space-touch-control-height': '--vx-hit',
|
|
324
|
+
'--vx-space-app-header-mobile-actions-height': '--vx-space-sticky-header-clearance',
|
|
325
|
+
'--vx-space-sticky-header-clearance-mobile': '--vx-space-sticky-header-clearance',
|
|
326
|
+
'--vx-space-sidebar-brand-inset-top': null,
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const KNOWN_VX_NAMES: ReadonlySet<string> = new Set(BASALT_VX_NAMES)
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* The families basalt owns (`vxFamilyOf`: `surface`, `space`, `ink`, `keyboard`, …), which the
|
|
333
|
+
* generator reads off every name basalt emits. Every name inside one is judged against the exact
|
|
334
|
+
* emitted set, so `--vx-surface-2` and a bare root like `--vx-space` are both findings.
|
|
335
|
+
*
|
|
336
|
+
* That judgement is honest only because no consumer ref may point into these families:
|
|
337
|
+
* `groupTokens`/`seriesTokens` warn on a name inside one (and throw once this kind promotes). A name in any OTHER family (argo's
|
|
338
|
+
* `--vx-activity-*`, rb's `--vx-confidence-*`) may be a consumer group declared in a file this scan
|
|
339
|
+
* cannot see, and is never judged.
|
|
340
|
+
*/
|
|
341
|
+
const BASALT_VX_FAMILY_SET: ReadonlySet<string> = new Set(BASALT_VX_FAMILIES)
|
|
342
|
+
|
|
289
343
|
// Raw Mantine ramp step used for surface color — gray/dark + a step digit.
|
|
290
344
|
const OFF_SYSTEM_SURFACE_VAR = /var\(--mantine-color-(gray|dark)-\d/g
|
|
291
345
|
|
|
@@ -861,38 +915,14 @@ export type GraceEntry = { since: string; promote: string; why: string }
|
|
|
861
915
|
* }
|
|
862
916
|
*/
|
|
863
917
|
export const GRACE_PERIOD_KINDS: Partial<Record<GuardKind, GraceEntry>> = {
|
|
864
|
-
'
|
|
865
|
-
since: '1.
|
|
866
|
-
promote: '1.
|
|
918
|
+
'unknown-vx-token': {
|
|
919
|
+
since: '1.34.0',
|
|
920
|
+
promote: '1.35.0',
|
|
867
921
|
why:
|
|
868
|
-
|
|
869
|
-
'
|
|
870
|
-
'
|
|
871
|
-
'
|
|
872
|
-
'reading in the guard, and the reason this one lands warn rather than error even though its ' +
|
|
873
|
-
'law is settled. ONE LAW, TWO LANES, ONE PROMOTION: it moves with its AST twin, ' +
|
|
874
|
-
'`basalt/control-outside-home`, whose PLUGIN_RULE_GRACE entry carries the full argument and ' +
|
|
875
|
-
'the measurement, and it must never be promoted or extended alone — a consumer waiving one ' +
|
|
876
|
-
'spelling and failing the other is the exact confusion `WAIVER_ID_ALIASES` exists to close. ' +
|
|
877
|
-
'RE-MEASURED 2026-09-09 across the whole fleet on 1.29.2: 29 AST-lane incumbents in five ' +
|
|
878
|
-
"repos, plus three of basalt's own (data-table, notifications/center, theme-lab). Extended " +
|
|
879
|
-
'to 1.31.0 because 1.30.0 is the FIRST release in which either lane names a home a ' +
|
|
880
|
-
'provider-only consumer can reach — every home both messages listed was a shell slot, and ' +
|
|
881
|
-
'the shell-less `PageBar` sentence ships in this minor. Promoting in the same minor that ' +
|
|
882
|
-
'makes a rule satisfiable gives nobody a window to act. 1.31.0 is when the 29 are ' +
|
|
883
|
-
're-measured against that number.',
|
|
884
|
-
},
|
|
885
|
-
'raw-media-query': {
|
|
886
|
-
since: '1.31.0',
|
|
887
|
-
promote: '1.33.0',
|
|
888
|
-
why:
|
|
889
|
-
"The CSS-text twin of the oxlint plugin's `basalt/raw-breakpoint` — ONE LAW, TWO LANES, ONE " +
|
|
890
|
-
'PROMOTION, so it moves with that rule and never alone (`configs/oxlint-plugin.test.ts` › twin lanes share one promotion pins the pair). New in ' +
|
|
891
|
-
'the wave-11 responsive/touch guards; reaching CSS is a NEW file type for this check, same ' +
|
|
892
|
-
"reasoning as `css-raw-surface`, with incumbents in basalt's own tree at ship time, so it " +
|
|
893
|
-
'shipped warn. Extended 1.32.0 -> 1.33.0 with its twin: 1.31.0 named ' +
|
|
894
|
-
'no reachable container primitive, and 1.32.0 is the first minor that documents one ' +
|
|
895
|
-
'(`CONTAINER_CLASSES` + the declared `@container` names, MIGRATING § `basalt/raw-breakpoint`).',
|
|
922
|
+
'New kind at 1.33.0 (consumer-loop r2 B3): obsidian lost its 44px touch floor to a removed ' +
|
|
923
|
+
'`var(--vx-space-touch-target)` that nothing reported. WIDENED at 1.34.0 (image-gen r4: ' +
|
|
924
|
+
'`var(--vx-surface-2)` rendered transparent, unreported) to every unemitted name inside a ' +
|
|
925
|
+
'basalt-owned family, so the grace restarts: warn through 1.34.x, error at 1.35.0.',
|
|
896
926
|
},
|
|
897
927
|
}
|
|
898
928
|
|
|
@@ -1132,6 +1162,13 @@ export const WAIVER_ID_ALIASES: Readonly<Record<string, readonly string[]>> = {
|
|
|
1132
1162
|
'raw-selection-control': ['control-outside-home'],
|
|
1133
1163
|
}
|
|
1134
1164
|
|
|
1165
|
+
/**
|
|
1166
|
+
* The ids that are a guard kind AND an oxlint plugin rule — one law, two lanes, one annotation.
|
|
1167
|
+
* {@link PLUGIN_RULE_IDS} leaves them out by design, so `--audit-allows` would otherwise never ask
|
|
1168
|
+
* oxlint about them. `waiver-audit.test.ts` pins this against the plugin's own rule table.
|
|
1169
|
+
*/
|
|
1170
|
+
const DUAL_LANE_IDS: ReadonlySet<string> = new Set(['in-body-page-title'])
|
|
1171
|
+
|
|
1135
1172
|
/** The shortest string accepted as a written reason — enough to exclude a stray separator. */
|
|
1136
1173
|
const MIN_ALLOW_REASON_LENGTH = 4
|
|
1137
1174
|
|
|
@@ -1973,7 +2010,7 @@ type GuardRule = {
|
|
|
1973
2010
|
}
|
|
1974
2011
|
|
|
1975
2012
|
/**
|
|
1976
|
-
* The closed registry of all
|
|
2013
|
+
* The closed registry of all 28 guard kinds. The triad test asserts
|
|
1977
2014
|
* `surface.guardKinds ⊆ keyof GUARD_RULES` at runtime.
|
|
1978
2015
|
*
|
|
1979
2016
|
* raw-surface, raw-html-layout, and sub-16-input-font are handled inline in checkSource
|
|
@@ -2065,7 +2102,7 @@ export const GUARD_RULES = {
|
|
|
2065
2102
|
// JSX-tag-shaped (`<Card withBorder>`) — never appears in CSS text.
|
|
2066
2103
|
appliesTo: (relPath) => !relPath.endsWith('.css'),
|
|
2067
2104
|
message:
|
|
2068
|
-
'withBorder on a Card/Paper double-draws the edge — card depth is --vx-shadow-card, which already bakes a 1px ring into the shadow. Drop the prop (docs/DESIGN-SPEC.md doctrine inversion #1).',
|
|
2105
|
+
'withBorder on a Card/Paper double-draws the edge — card depth is --vx-shadow-card, which already bakes a 1px ring into the shadow. Drop the prop (docs/DESIGN-SPEC.md doctrine inversion #1) — and fix the inset in the same edit: an explicit p/padding or radius on that Card/Paper is the other half of the idiom, which only the oxlint lane sees (basalt/card-inset); leave both to the theme or use py="xs" px="sm".',
|
|
2069
2106
|
},
|
|
2070
2107
|
'off-system-surface-var': {
|
|
2071
2108
|
kind: 'off-system-surface-var',
|
|
@@ -2215,6 +2252,17 @@ export const GUARD_RULES = {
|
|
|
2215
2252
|
appliesTo: (relPath) => relPath.endsWith('.css'),
|
|
2216
2253
|
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.`,
|
|
2217
2254
|
},
|
|
2255
|
+
'unknown-vx-token': {
|
|
2256
|
+
kind: 'unknown-vx-token',
|
|
2257
|
+
pattern: VX_VAR_REF, // handled inline (basalt-owned families); entry keeps the registry complete
|
|
2258
|
+
message: `var() names a --vx-* token basalt does not emit, so it resolves to its fallback (or to nothing) with no error anywhere. Judged: every name inside a basalt-owned family (${[...BASALT_VX_FAMILY_SET].join(', ')}) — pick the token from basalt-ui/tokens.css; your own series belong in a family of their own (groupTokens warns on one inside basalt's). Removed names and their replacements: ${Object.entries(
|
|
2259
|
+
REMOVED_VX_NAMES,
|
|
2260
|
+
)
|
|
2261
|
+
.map(([from, to]) => `${from} → ${to ?? '(deleted, no replacement)'}`)
|
|
2262
|
+
.join(
|
|
2263
|
+
'; ',
|
|
2264
|
+
)}. A name in any other family is never judged, because groupTokens may declare it in a file this scan cannot see.`,
|
|
2265
|
+
},
|
|
2218
2266
|
} as const satisfies Record<GuardKind, GuardRule>
|
|
2219
2267
|
|
|
2220
2268
|
/**
|
|
@@ -2389,6 +2437,22 @@ export function checkSource(text: string, relPath: string, cfg: GuardConfig): Fi
|
|
|
2389
2437
|
[...codeText.matchAll(STYLE_BOUND_IDENTIFIER)].map((m) => m[1] ?? ''),
|
|
2390
2438
|
)
|
|
2391
2439
|
|
|
2440
|
+
/** `--vx-*` names this file declares — its own custom properties, which `unknown-vx-token` skips. */
|
|
2441
|
+
const locallyDeclaredVx = new Set(
|
|
2442
|
+
[...codeText.matchAll(VX_LOCAL_DECLARATION)].map((m) => m[1] ?? m[3] ?? m[4] ?? ''),
|
|
2443
|
+
)
|
|
2444
|
+
|
|
2445
|
+
// unknown-vx-token: any unemitted name inside a basalt-owned family — see BASALT_VX_FAMILY_SET.
|
|
2446
|
+
// Scanned over the whole comment-stripped text rather than per line, so a formatter-wrapped
|
|
2447
|
+
// `var(\n --vx-x\n)` is seen; the finding lands on the line the NAME is on.
|
|
2448
|
+
for (const m of codeText.matchAll(VX_VAR_REF)) {
|
|
2449
|
+
const name = m[1] ?? ''
|
|
2450
|
+
if (locallyDeclaredVx.has(name)) continue
|
|
2451
|
+
if (KNOWN_VX_NAMES.has(name) || !BASALT_VX_FAMILY_SET.has(vxFamilyOf(name))) continue
|
|
2452
|
+
const at = (m.index ?? 0) + m[0].length - name.length
|
|
2453
|
+
push('unknown-vx-token', lineStarts.findLastIndex((start) => start <= at) + 1, name)
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2392
2456
|
for (let i = 0; i < lines.length; i++) {
|
|
2393
2457
|
const line = codeLines[i] ?? ''
|
|
2394
2458
|
|
|
@@ -2793,6 +2857,15 @@ export type AllowAnnotationSite = {
|
|
|
2793
2857
|
readonly guardKinds: readonly GuardKind[]
|
|
2794
2858
|
/** The subset of `rules` the oxlint plugin owns — outside `checkSource`'s reach. */
|
|
2795
2859
|
readonly pluginRules: readonly string[]
|
|
2860
|
+
/**
|
|
2861
|
+
* Every id an `oxlint` run can prove live: `pluginRules`, plus a guard kind that is ALSO a plugin
|
|
2862
|
+
* rule (`in-body-page-title`) or aliases one ({@link WAIVER_ID_ALIASES}). A waiver is live if
|
|
2863
|
+
* EITHER lane still needs it — judging only `pluginRules` called rb's live `in-body-page-title`
|
|
2864
|
+
* waivers dead, and deleting them made oxlint exit 1. A bare LINE-scope annotation covers every
|
|
2865
|
+
* plugin rule (the plugin's `allowCovers`), so it lists all of them; a bare file-scope one waives
|
|
2866
|
+
* nothing in either lane and lists none.
|
|
2867
|
+
*/
|
|
2868
|
+
readonly oxlintRules: readonly string[]
|
|
2796
2869
|
/** Words that occupied the id slot but name no rule. Their presence makes the parse fail closed. */
|
|
2797
2870
|
readonly unknownRules: readonly string[]
|
|
2798
2871
|
/** A written reason follows the ids. */
|
|
@@ -2831,19 +2904,33 @@ export function findAllowAnnotations(
|
|
|
2831
2904
|
cfg.allowComment,
|
|
2832
2905
|
syntax,
|
|
2833
2906
|
)
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2907
|
+
const everyOxlintRule = [...PLUGIN_RULE_IDS, ...DUAL_LANE_IDS]
|
|
2908
|
+
return declared.map(({ line, annotation }) => {
|
|
2909
|
+
const bare = annotation.rules.length === 0 && annotation.unknownRules.length === 0
|
|
2910
|
+
return {
|
|
2911
|
+
line,
|
|
2912
|
+
scope: annotation.scope,
|
|
2913
|
+
rules: [...annotation.rules],
|
|
2914
|
+
guardKinds: annotation.rules.filter((id): id is GuardKind => Object.hasOwn(GUARD_RULES, id)),
|
|
2915
|
+
pluginRules: annotation.rules.filter(
|
|
2916
|
+
(id) => !Object.hasOwn(GUARD_RULES, id) && PLUGIN_RULE_IDS.has(id),
|
|
2917
|
+
),
|
|
2918
|
+
oxlintRules:
|
|
2919
|
+
bare && annotation.scope === 'line'
|
|
2920
|
+
? everyOxlintRule
|
|
2921
|
+
: [
|
|
2922
|
+
...new Set(
|
|
2923
|
+
annotation.rules
|
|
2924
|
+
.flatMap((id) => [id, ...(WAIVER_ID_ALIASES[id] ?? [])])
|
|
2925
|
+
.filter((id) => PLUGIN_RULE_IDS.has(id) || DUAL_LANE_IDS.has(id)),
|
|
2926
|
+
),
|
|
2927
|
+
],
|
|
2928
|
+
unknownRules: [...annotation.unknownRules],
|
|
2929
|
+
hasReason: annotation.hasReason,
|
|
2930
|
+
bare,
|
|
2931
|
+
text: lines[line - 1] ?? '',
|
|
2932
|
+
}
|
|
2933
|
+
})
|
|
2847
2934
|
}
|
|
2848
2935
|
|
|
2849
2936
|
/** The token a neutralized annotation is rewritten to. Shares no substring with `theme-allow`. */
|