basalt-ui 1.19.1 → 1.21.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 +19 -1
- package/CHANGELOG.md +39 -0
- package/MIGRATING.md +326 -0
- package/README.md +87 -9
- package/agent/rules/basalt-app.md +84 -0
- package/agent/rules/basalt-charts.md +32 -17
- package/agent/rules/basalt-content.md +4 -0
- package/agent/rules/basalt-mantine.md +25 -10
- package/agent/rules/basalt-router.md +79 -15
- package/agent/rules/basalt-state.md +21 -3
- package/agent/rules/basalt-tokens.md +132 -6
- package/agent/skills/basalt-app/SKILL.md +45 -6
- package/agent/skills/basalt-charts/SKILL.md +5 -3
- package/agent/templates/CLAUDE-block.md.tpl +11 -5
- package/configs/check.yml +11 -4
- package/configs/lefthook.yml +59 -7
- package/configs/oxlint-plugin.js +633 -30
- package/configs/oxlint-plugin.test.ts +594 -15
- package/configs/oxlint.json +10 -1
- package/dist/agent/stick-to-bottom.js +1 -1
- package/dist/agent-chat/thread-detail-panel.js +1 -1
- package/dist/agent-chat/thread-message.js +2 -2
- package/dist/agent-chat/tool-chip.js +1 -1
- package/dist/charts/kinds/Bars.d.ts +1 -1
- package/dist/charts/kinds/Bars.d.ts.map +1 -1
- package/dist/charts/kinds/Donut.d.ts +1 -1
- package/dist/charts/kinds/Donut.d.ts.map +1 -1
- package/dist/charts/kinds/Donut.js +1 -1
- package/dist/charts/kinds/DualPanel.d.ts +1 -1
- package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
- package/dist/charts/kinds/Heatmap.d.ts +1 -1
- package/dist/charts/kinds/Heatmap.d.ts.map +1 -1
- package/dist/charts/kinds/MultiLine.d.ts +1 -1
- package/dist/charts/kinds/MultiLine.d.ts.map +1 -1
- package/dist/charts/kinds/StackedArea.d.ts +1 -1
- package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
- package/dist/charts/kinds/ZonedLine.d.ts +1 -1
- package/dist/charts/kinds/ZonedLine.d.ts.map +1 -1
- package/dist/charts/primitives/ChartLegend.js +2 -2
- package/dist/charts/primitives/ChartTooltip.js +1 -1
- package/dist/cli/index.d.ts +146 -20
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +901 -101
- package/dist/cli/toolchain-wiring.test.d.ts +2 -0
- package/dist/cli/toolchain-wiring.test.d.ts.map +1 -0
- package/dist/cli/waiver-audit.test.d.ts +2 -0
- package/dist/cli/waiver-audit.test.d.ts.map +1 -0
- package/dist/dashboard/delta-badge.js +2 -2
- package/dist/dashboard/stat-card.js +1 -1
- package/dist/data/virtual-list.js +5 -5
- package/dist/guard/index.d.ts +101 -2
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +474 -62
- package/dist/guard/types.d.ts +25 -5
- package/dist/guard/types.d.ts.map +1 -1
- package/dist/register.d.ts +25 -0
- package/dist/register.d.ts.map +1 -1
- package/dist/router-tanstack/multi-search-param-store.d.ts +13 -0
- package/dist/router-tanstack/multi-search-param-store.d.ts.map +1 -1
- package/dist/router-tanstack/multi-search-param-store.js +4 -1
- package/dist/router-tanstack/search-param-store.d.ts +29 -5
- package/dist/router-tanstack/search-param-store.d.ts.map +1 -1
- package/dist/router-tanstack/search-param-store.js +31 -3
- package/dist/router-tanstack/search-param-store.test.d.ts +2 -0
- package/dist/router-tanstack/search-param-store.test.d.ts.map +1 -0
- package/dist/shell/app-mobile-nav.module.css +11 -3
- package/dist/shell/index.d.ts +12 -5
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +24 -6
- package/dist/surfaces.d.ts +1 -1
- package/dist/surfaces.js +1 -1
- package/dist/tokens/index.d.ts +5 -3
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +7 -1
- package/dist/tokens/palette.d.ts +35 -27
- package/dist/tokens/palette.d.ts.map +1 -1
- package/dist/tokens/palette.js +24 -18
- package/dist/tokens/text-scale.test.d.ts +2 -0
- package/dist/tokens/text-scale.test.d.ts.map +1 -0
- package/dist/tokens.css +21 -17
- package/dist/vite-color-mix.js +2 -2
- package/dist/vite.d.ts +24 -2
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +54 -13
- package/llms.txt +3 -3
- package/package.json +4 -3
- package/src/agent/stick-to-bottom.tsx +1 -1
- package/src/agent-chat/thread-detail-panel.tsx +1 -1
- package/src/agent-chat/thread-message.tsx +3 -3
- package/src/agent-chat/tool-chip.tsx +1 -1
- package/src/charts/kinds/Bars.tsx +1 -1
- package/src/charts/kinds/Donut.tsx +3 -2
- package/src/charts/kinds/DualPanel.tsx +2 -2
- package/src/charts/kinds/Heatmap.tsx +1 -1
- package/src/charts/kinds/MultiLine.tsx +1 -1
- package/src/charts/kinds/StackedArea.tsx +1 -1
- package/src/charts/kinds/ZonedLine.tsx +1 -1
- package/src/charts/primitives/ChartLegend.tsx +2 -2
- package/src/charts/primitives/ChartTooltip.tsx +1 -1
- package/src/cli/index.ts +1614 -164
- package/src/cli/init-sync.test.ts +21 -3
- package/src/cli/template-ledger.test.ts +1 -1
- package/src/cli/tokens-css.test.ts +27 -14
- package/src/cli/toolchain-wiring.test.ts +856 -0
- package/src/cli/waiver-audit.test.ts +131 -0
- package/src/dashboard/delta-badge.tsx +3 -3
- package/src/dashboard/stat-card.tsx +1 -1
- package/src/data/virtual-list.tsx +8 -8
- package/src/guard/check-source.test.ts +1104 -12
- package/src/guard/index.ts +1106 -103
- package/src/guard/types.ts +29 -4
- package/src/register.ts +32 -0
- package/src/router-tanstack/multi-search-param-store.ts +16 -1
- package/src/router-tanstack/search-param-store.test.ts +135 -0
- package/src/router-tanstack/search-param-store.ts +90 -8
- package/src/shell/app-mobile-nav.module.css +11 -3
- package/src/shell/app-mobile-nav.tsx +1 -1
- package/src/shell/index.test.tsx +52 -0
- package/src/shell/index.tsx +64 -13
- package/src/surfaces.ts +1 -1
- package/src/theme/density-relations.test.ts +8 -5
- package/src/theme/spacing.test.ts +2 -0
- package/src/theme-toggle/index.tsx +2 -2
- package/src/tokens/index.ts +28 -3
- package/src/tokens/palette.ts +24 -18
- package/src/tokens/text-scale.test.ts +51 -0
- package/src/vite-color-mix.ts +2 -2
- package/src/vite.test.ts +125 -6
- package/src/vite.ts +148 -20
package/dist/guard/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const HEX = /#[0-9a-fA-F]{3,8}\b/g;
|
|
2
|
-
const FUNC = /\b(?:rgba?|hsla?)\(/g;
|
|
2
|
+
const FUNC = /\b(?:rgba?|hsla?)\((?!\s*\$\{)/g;
|
|
3
3
|
const LOCALSTORAGE_THEME = /localStorage\s*\.\s*getItem\s*\(\s*['"]theme['"]\s*\)/g;
|
|
4
|
-
const RAW_FONT_FAMILY_VAR_ESCAPE = /var\(\s
|
|
4
|
+
const RAW_FONT_FAMILY_VAR_ESCAPE = /var\(\s*--/.source;
|
|
5
5
|
const RAW_FONT_FAMILY_KEYWORD = /(?:inherit|initial|unset|revert)\b/.source;
|
|
6
6
|
const RAW_FONT_FAMILY = new RegExp(
|
|
7
7
|
`\\bfontFamily\\s*[:=]\\s*\\{?(?!['"\`]?\\s*${RAW_FONT_FAMILY_VAR_ESCAPE})(?!['"\`]?\\s*${RAW_FONT_FAMILY_KEYWORD})['"\`][^'"\`]+['"\`]|\\bfont-family\\s*:\\s*(?!['"\`]?\\s*${RAW_FONT_FAMILY_VAR_ESCAPE})(?!['"\`]?\\s*${RAW_FONT_FAMILY_KEYWORD})(?:['"\`][^'"\`]+['"\`]|[A-Za-z][\\w-]*)`,
|
|
@@ -9,14 +9,42 @@ const RAW_FONT_FAMILY = new RegExp(
|
|
|
9
9
|
);
|
|
10
10
|
const SURFACE_BORDER = /\bborder(?:Top|Bottom|Left|Right)?\s*:\s*(?!['"`]?[^'"`]*(?:var\(|\$\{))(?!['"`]?(?:none|transparent|inherit|unset|revert)\b)['"`][^'"`]+['"`]/g;
|
|
11
11
|
const SURFACE_RADIUS = /\bborderRadius\s*:\s*(?:[0-9]+|['"`](?!\s*var\()[^'"`]*[0-9])/g;
|
|
12
|
-
const
|
|
12
|
+
const SHADOW_DECL = /\b(?:boxShadow|box-shadow)\s*:\s*(['"`])((?:[^'"`\\]|\\.)*)\1/g;
|
|
13
|
+
const SHADOW_KEEPS_CARD_DEPTH = /--vx-shadow-|\bVX\.shadow/;
|
|
14
|
+
const SHADOW_IS_COMPOSED = /var\(|\$\{/;
|
|
15
|
+
const CSS_SURFACE_RADIUS = /(?<![\w-])border-radius\s*:\s*([^;}]+)/g;
|
|
16
|
+
const CSS_SURFACE_SHADOW = /(?<![\w-])box-shadow\s*:\s*([^;}]+)/g;
|
|
17
|
+
const MICRO_RADIUS_CEILING_PX = 3;
|
|
18
|
+
const SHAPE_RADIUS_VALUES = /* @__PURE__ */ new Set(["50%", "9999px", "999px", "100%"]);
|
|
19
|
+
function isAllowedCssRadius(value) {
|
|
20
|
+
const rest = value.replace(/var\([^)]*\)/g, " ").trim();
|
|
21
|
+
if (rest === "") return true;
|
|
22
|
+
return rest.split(/\s+/).every((part) => {
|
|
23
|
+
if (SHAPE_RADIUS_VALUES.has(part)) return true;
|
|
24
|
+
const literal = /^[-+]?(\d+(?:\.\d+)?|\.\d+)(px|rem)?$/.exec(part);
|
|
25
|
+
if (literal === null) return false;
|
|
26
|
+
const px = literal[2] === "rem" ? Number(literal[1]) * ROOT_FONT_SIZE_PX : Number(literal[1]);
|
|
27
|
+
return px <= MICRO_RADIUS_CEILING_PX;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
const INLINE_FONT_SIZE = /(?<![\w-;])font-?[Ss]ize\s*:\s*['"]?(\d+(?:\.\d+)?)(?:px)?['"]?(?=[,;\s}]|$)/g;
|
|
13
31
|
const CARD_SURFACE_TAG = /<(?:Card|Paper)(?![\w.])(?:=>|[^>])*?>/g;
|
|
14
32
|
const WITH_BORDER_PROP = /\bwithBorder\b(?!\s*=\s*\{\s*false\s*\})/;
|
|
15
33
|
const OFF_SYSTEM_SURFACE_VAR = /var\(--mantine-color-(gray|dark)-\d/g;
|
|
16
34
|
const MANTINE_SHADE_INDEX = /\b(?:color|c|bg|backgroundColor|borderColor)\s*=\s*\{?\s*['"][a-z]+\.\d\b|var\(--mantine-color-(?!gray-|dark-)[a-z]+-\d/g;
|
|
17
|
-
const
|
|
35
|
+
const RAW_HTML_TAG_NAME = "<(?:div|span|section|header|nav|footer|aside|main|article|ul|ol)\\b";
|
|
36
|
+
const RAW_HTML_TAG = new RegExp(RAW_HTML_TAG_NAME);
|
|
37
|
+
const RAW_HTML_LAYOUT_TAG = new RegExp(`${RAW_HTML_TAG_NAME}(?:=>|[^>])*?>`, "g");
|
|
18
38
|
const INLINE_STYLE = /style=\{\{/;
|
|
39
|
+
const STYLE_IDENTIFIER = /style=\{\s*([A-Za-z_$][\w$]*)\s*\}/;
|
|
19
40
|
const LAYOUT_SURFACE_PROP = /\b(?:display|padding|margin|gap|flex|grid|border|background|width|height)\b/;
|
|
41
|
+
function hoistedObjectBody(codeText, name) {
|
|
42
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
43
|
+
const decl = new RegExp(`\\b(?:const|let|var)\\s+${escaped}\\b[^=;]*=\\s*\\{([^}]*)\\}`).exec(
|
|
44
|
+
codeText
|
|
45
|
+
);
|
|
46
|
+
return decl?.[1] ?? "";
|
|
47
|
+
}
|
|
20
48
|
const BOX_SIDE = "(?:Top|Bottom|Left|Right|Block|Inline)(?:Start|End)?";
|
|
21
49
|
const BOX_SIDE_KEBAB = "(?:top|bottom|left|right|block|inline)(?:-(?:start|end))?";
|
|
22
50
|
const SPACING_PROP = `(?:padding|margin)(?:${BOX_SIDE}|-${BOX_SIDE_KEBAB})?|gap|rowGap|columnGap|row-gap|column-gap`;
|
|
@@ -26,6 +54,38 @@ const INLINE_SPACING = new RegExp(
|
|
|
26
54
|
"g"
|
|
27
55
|
);
|
|
28
56
|
const MICRO_SPACING_CEILING_PX = 10;
|
|
57
|
+
const STYLE_ATTRIBUTE_OPENER = /\b(?:style|styles|sx|css)\s*=\s*\{?\s*$/;
|
|
58
|
+
const OBJECT_DECLARATION = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::([^=]*))?=\s*$/;
|
|
59
|
+
const STYLE_OBJECT_NAME = /(?:Style|Styles|Css)$|^(?:style|styles|css|sx)$/;
|
|
60
|
+
const STYLE_OBJECT_TYPE = /\bCSS(?:Properties|Object)\b|\bStyle(?:s|Prop)?\b/;
|
|
61
|
+
const STYLE_BOUND_IDENTIFIER = /\b(?:style|styles|sx|css)=\{\s*([A-Za-z_$][\w$]*)\s*\}/g;
|
|
62
|
+
const NESTED_OBJECT_KEY = /(?:[\w$]+|'[^']*'|"[^"]*"|\[[^\]]*\])\s*:\s*$/;
|
|
63
|
+
const MAX_STYLE_CONTEXT_SCAN = 4e3;
|
|
64
|
+
function isStyleObjectContext(codeText, at, styleIdentifiers) {
|
|
65
|
+
let depth = 0;
|
|
66
|
+
const stop = Math.max(0, at - MAX_STYLE_CONTEXT_SCAN);
|
|
67
|
+
for (let i = at - 1; i >= stop; i--) {
|
|
68
|
+
const ch = codeText[i];
|
|
69
|
+
if (ch === "}") depth++;
|
|
70
|
+
else if (ch === "{") {
|
|
71
|
+
if (depth > 0) {
|
|
72
|
+
depth--;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const before = codeText.slice(Math.max(0, i - 120), i);
|
|
76
|
+
if (STYLE_ATTRIBUTE_OPENER.test(before)) return true;
|
|
77
|
+
const declaration = OBJECT_DECLARATION.exec(before);
|
|
78
|
+
if (declaration !== null) {
|
|
79
|
+
const [, name = "", annotation = ""] = declaration;
|
|
80
|
+
return STYLE_OBJECT_NAME.test(name) || STYLE_OBJECT_TYPE.test(annotation) || styleIdentifiers.has(name);
|
|
81
|
+
}
|
|
82
|
+
if (before.endsWith("{")) continue;
|
|
83
|
+
if (NESTED_OBJECT_KEY.test(before)) continue;
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
29
89
|
const ROOT_FONT_SIZE_PX = 16;
|
|
30
90
|
function isSubScaleCssSpacing(line, matchIndex) {
|
|
31
91
|
const colon = line.indexOf(":", matchIndex);
|
|
@@ -77,9 +137,15 @@ const DEFAULT_GUARD_CONFIG = {
|
|
|
77
137
|
exemptRules: {},
|
|
78
138
|
severity: {}
|
|
79
139
|
};
|
|
80
|
-
const GRACE_PERIOD_KINDS = {
|
|
140
|
+
const GRACE_PERIOD_KINDS = {
|
|
141
|
+
"theme-allow-unscoped": "introduced in the round-4 guard minor \u2014 promote to error one minor later. Every consumer has bare `theme-allow` comments today (basalt itself has ~20), and landing this as an error would fail every one of their builds on an upgrade they took for the fixes.",
|
|
142
|
+
"surface-shadow-override": "introduced in the round-4 guard minor \u2014 promote to error one minor later. Catches the shape a TOKEN-FLUENT consumer writes, so the code it rejects is code that looks correct and passed every previous release.",
|
|
143
|
+
"css-raw-surface": 'introduced in the round-4 guard minor \u2014 promote to error one minor later. The surface kinds reaching CSS at all is the "existing kind, new file type" case the doctrine names explicitly.',
|
|
144
|
+
"inline-font-size": "introduced in the round-4 guard minor \u2014 promote to error one minor later. The oxlint plugin has enforced the same thing as `basalt/no-raw-font-size` for a while, so a consumer running both sees nothing new; one running check-theme alone gets a minor of runway.",
|
|
145
|
+
"hidden-inline-style": "introduced in the round-4 guard minor \u2014 promote by DELETING the kind and folding its two scans into `raw-html-layout`, which is where they belong once the widening has had its minor."
|
|
146
|
+
};
|
|
81
147
|
function severityOf(kind, cfg) {
|
|
82
|
-
return cfg.severity?.[kind] ?? (kind
|
|
148
|
+
return cfg.severity?.[kind] ?? (Object.hasOwn(GRACE_PERIOD_KINDS, kind) ? "warn" : "error");
|
|
83
149
|
}
|
|
84
150
|
const FINDING_TEXT_MAX_LENGTH = 100;
|
|
85
151
|
function textForFinding(sourceLine, fallbackToken) {
|
|
@@ -90,18 +156,241 @@ function textForFinding(sourceLine, fallbackToken) {
|
|
|
90
156
|
function isChartFile(relPath) {
|
|
91
157
|
return relPath.includes("/charts/") && !AXIS_WRAPPER_FILE.test(relPath);
|
|
92
158
|
}
|
|
159
|
+
const EXEMPT_RULE_ALIASES = {
|
|
160
|
+
"hidden-inline-style": "raw-html-layout",
|
|
161
|
+
"surface-shadow-override": "raw-surface",
|
|
162
|
+
"css-raw-surface": "raw-surface"
|
|
163
|
+
};
|
|
164
|
+
function normalizeExemptPattern(pattern) {
|
|
165
|
+
return pattern.trim().replace(/^\.\//, "").replace(/\/+$/, "");
|
|
166
|
+
}
|
|
167
|
+
function exemptGlobToRegExp(pattern) {
|
|
168
|
+
const body = pattern.split(/(\*\*\/|\*\*|\*|\?)/).map((part) => {
|
|
169
|
+
if (part === "**/") return "(?:.*/)?";
|
|
170
|
+
if (part === "**") return ".*";
|
|
171
|
+
if (part === "*") return "[^/]*";
|
|
172
|
+
if (part === "?") return "[^/]";
|
|
173
|
+
return part.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
174
|
+
}).join("");
|
|
175
|
+
return new RegExp(`^${body}$`);
|
|
176
|
+
}
|
|
177
|
+
function exemptPatternMatches(pattern, relPath) {
|
|
178
|
+
const p = normalizeExemptPattern(pattern);
|
|
179
|
+
if (p === "") return false;
|
|
180
|
+
if (relPath === p || relPath.startsWith(`${p}/`)) return true;
|
|
181
|
+
if (/[*?]/.test(p)) {
|
|
182
|
+
const re = exemptGlobToRegExp(p);
|
|
183
|
+
if (re.test(relPath)) return true;
|
|
184
|
+
if (!p.includes("/") && re.test(relPath.split("/").pop() ?? "")) return true;
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
return !p.includes("/") && relPath.split("/").includes(p);
|
|
188
|
+
}
|
|
93
189
|
function isRuleExempt(kind, relPath, exemptRules) {
|
|
190
|
+
const alias = EXEMPT_RULE_ALIASES[kind];
|
|
191
|
+
if (alias !== void 0 && isRuleExempt(alias, relPath, exemptRules)) return true;
|
|
94
192
|
const patterns = exemptRules?.[kind];
|
|
95
193
|
if (patterns === void 0 || patterns.length === 0) return false;
|
|
96
|
-
|
|
97
|
-
return patterns.some((pattern) => segments.includes(pattern.replace(/\/$/, "")));
|
|
194
|
+
return patterns.some((pattern) => exemptPatternMatches(pattern, relPath));
|
|
98
195
|
}
|
|
99
|
-
function
|
|
100
|
-
|
|
196
|
+
function unmatchedExemptPatterns(cfg, scannedRelPaths) {
|
|
197
|
+
const out = [];
|
|
198
|
+
for (const [kind, patterns] of Object.entries(cfg.exemptRules ?? {})) {
|
|
199
|
+
const known = Object.hasOwn(GUARD_RULES, kind);
|
|
200
|
+
for (const pattern of patterns ?? []) {
|
|
201
|
+
if (!known) {
|
|
202
|
+
out.push({ kind, pattern, reason: "unknown-kind" });
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (scannedRelPaths.some((relPath) => exemptPatternMatches(pattern, relPath))) continue;
|
|
206
|
+
out.push({ kind, pattern, reason: "no-match" });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
211
|
+
const PLUGIN_RULE_IDS = /* @__PURE__ */ new Set([
|
|
212
|
+
"no-raw-font-size",
|
|
213
|
+
"raw-size-literal",
|
|
214
|
+
"card-inset",
|
|
215
|
+
"chart-in-raw-surface",
|
|
216
|
+
"hand-rolled-plot",
|
|
217
|
+
"chart-legend-literal",
|
|
218
|
+
"hand-rolled-shell",
|
|
219
|
+
"shadow-basalt-export",
|
|
220
|
+
"raw-scroll-container",
|
|
221
|
+
"visx-boundary",
|
|
222
|
+
"visx-tooltip",
|
|
223
|
+
"token-layer-boundary",
|
|
224
|
+
// These three honour `basalt-agent-allow`, never `theme-allow` — but they are still REAL ids, so
|
|
225
|
+
// a `theme-allow ai-sdk-major` must parse as a (useless) scoped annotation rather than as prose.
|
|
226
|
+
"agent-resume-guard",
|
|
227
|
+
"agent-no-raw-usechat",
|
|
228
|
+
"ai-sdk-major"
|
|
229
|
+
]);
|
|
230
|
+
const MIN_ALLOW_REASON_LENGTH = 4;
|
|
231
|
+
function parseAllowAnnotation(rest, scope) {
|
|
232
|
+
const rules = [];
|
|
233
|
+
const unknownRules = [];
|
|
234
|
+
let remainder = rest.replace(/^[\s,]+/, "");
|
|
235
|
+
let inIdSlot = true;
|
|
236
|
+
for (; ; ) {
|
|
237
|
+
const token = /^(?:basalt\/)?([a-z][a-z0-9-]*)(?=$|[\s,:—–])/.exec(remainder);
|
|
238
|
+
const id = token?.[1];
|
|
239
|
+
if (token === null || id === void 0) break;
|
|
240
|
+
if (!(Object.hasOwn(GUARD_RULES, id) || PLUGIN_RULE_IDS.has(id))) {
|
|
241
|
+
if (inIdSlot) unknownRules.push(id);
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
rules.push(id);
|
|
245
|
+
const after = remainder.slice(token[0].length);
|
|
246
|
+
remainder = after.replace(/^[\s,]+/, "");
|
|
247
|
+
inIdSlot = /^\s*,/.test(after);
|
|
248
|
+
}
|
|
249
|
+
const reason = remainder.replace(/^(?:—|–|-{1,2}|:)\s*/, "").trim();
|
|
250
|
+
return { rules, unknownRules, hasReason: reason.length >= MIN_ALLOW_REASON_LENGTH, scope };
|
|
251
|
+
}
|
|
252
|
+
const MAX_CSS_CONTINUATION_LINES = 8;
|
|
253
|
+
const MAX_COMMENT_BLOCK_LINES = 8;
|
|
254
|
+
const COMMENT_OPENER_BEFORE = /(?:\/\/|\/\*|<!--)\s*$/;
|
|
255
|
+
const COMMENT_ONLY_LINE = /^\s*(?:\{\s*\/\*|\/\/|\/\*|\*|<!--)/;
|
|
256
|
+
const ANNOTATION_PREFIX = /(?:^|\/\/|\/\*|<!--|^\s*\*)\s*$/;
|
|
257
|
+
function escapeRegExp(value) {
|
|
258
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
259
|
+
}
|
|
260
|
+
function allowTokenRe(allowComment) {
|
|
261
|
+
return new RegExp(`${escapeRegExp(allowComment)}(-file)?(?![\\w-])`, "g");
|
|
262
|
+
}
|
|
263
|
+
function jsonAllowKeyRe(allowComment) {
|
|
264
|
+
return new RegExp(`"basalt:${escapeRegExp(allowComment)}(-file)?"\\s*:\\s*"([^"]*)"`, "g");
|
|
265
|
+
}
|
|
266
|
+
function collectAllowAnnotations(lines, codeLines, allowComment, syntax) {
|
|
267
|
+
const byLine = /* @__PURE__ */ new Map();
|
|
268
|
+
const fileScoped = [];
|
|
269
|
+
const declared = [];
|
|
270
|
+
const add = (index, annotation) => {
|
|
271
|
+
const list = byLine.get(index) ?? [];
|
|
272
|
+
list.push(annotation);
|
|
273
|
+
byLine.set(index, list);
|
|
274
|
+
};
|
|
275
|
+
const record = (index, annotation, trailing) => {
|
|
276
|
+
declared.push({ line: index + 1, annotation });
|
|
277
|
+
if (annotation.scope === "file") {
|
|
278
|
+
fileScoped.push(annotation);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
add(index, annotation);
|
|
282
|
+
if (!trailing) {
|
|
283
|
+
for (let ahead = 1; ahead <= MAX_COMMENT_BLOCK_LINES; ahead++) {
|
|
284
|
+
const raw = lines[index + ahead] ?? "";
|
|
285
|
+
add(index + ahead, annotation);
|
|
286
|
+
if (raw.trim() !== "" && (codeLines[index + ahead] ?? "").trim() !== "") break;
|
|
287
|
+
if (raw.trim() === "") break;
|
|
288
|
+
}
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (syntax !== "css") return;
|
|
292
|
+
for (let back = 1; back <= MAX_CSS_CONTINUATION_LINES; back++) {
|
|
293
|
+
const previous = (codeLines[index - back] ?? "").trim();
|
|
294
|
+
if (previous === "" || /[;{}]$/.test(previous)) break;
|
|
295
|
+
add(index - back, annotation);
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
const token = allowTokenRe(allowComment);
|
|
299
|
+
const jsonKey = jsonAllowKeyRe(allowComment);
|
|
300
|
+
for (let i = 0; i < lines.length; i++) {
|
|
301
|
+
const line = lines[i] ?? "";
|
|
302
|
+
const stripped = codeLines[i] ?? "";
|
|
303
|
+
const isCommentOnlyLine = stripped.trim() === "" || COMMENT_ONLY_LINE.test(line);
|
|
304
|
+
if (syntax === "markup") {
|
|
305
|
+
for (const m of line.matchAll(jsonKey)) {
|
|
306
|
+
record(i, parseAllowAnnotation(m[2] ?? "", m[1] === void 0 ? "line" : "file"), false);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
for (const m of line.matchAll(token)) {
|
|
310
|
+
const at = m.index;
|
|
311
|
+
const before = line.slice(0, at);
|
|
312
|
+
if (stripped[at] !== " " && !COMMENT_OPENER_BEFORE.test(before)) continue;
|
|
313
|
+
if (!ANNOTATION_PREFIX.test(before)) continue;
|
|
314
|
+
const rest = line.slice(at + m[0].length);
|
|
315
|
+
record(
|
|
316
|
+
i,
|
|
317
|
+
parseAllowAnnotation(rest, m[1] === void 0 ? "line" : "file"),
|
|
318
|
+
!isCommentOnlyLine
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return { waivers: byLine, fileScoped, declared };
|
|
323
|
+
}
|
|
324
|
+
function annotationCovers(annotation, kind) {
|
|
325
|
+
if (annotation.rules.includes(kind)) return true;
|
|
326
|
+
if (annotation.scope === "file") return false;
|
|
327
|
+
return annotation.rules.length === 0 && annotation.unknownRules.length === 0;
|
|
328
|
+
}
|
|
329
|
+
function annotationsCover(annotations, kind) {
|
|
330
|
+
return (annotations ?? []).some((a) => annotationCovers(a, kind));
|
|
101
331
|
}
|
|
102
332
|
function guardSyntaxFor(relPath) {
|
|
103
|
-
|
|
333
|
+
if (relPath.endsWith(".css")) return "css";
|
|
334
|
+
if (/\.(?:html?|webmanifest|json)$/.test(relPath)) return "markup";
|
|
335
|
+
return "ts";
|
|
336
|
+
}
|
|
337
|
+
const MARKUP_KINDS = /* @__PURE__ */ new Set(["raw-hex", "raw-color-fn", "raw-font-family"]);
|
|
338
|
+
function stripMarkupComments(text) {
|
|
339
|
+
return text.replace(/<!--[\s\S]*?-->/g, (m) => m.replace(/[^\n]/g, " "));
|
|
340
|
+
}
|
|
341
|
+
const GENERATED_HEADER_LINE = "/* @generated basalt-ui \u2014 do not edit; regenerate with the command on the next line */";
|
|
342
|
+
const GENERATED_PROVENANCE_LINE = /^\/\* basalt-ui \d+\.\d+\.\d+[^\s]* — `basalt-ui (?:tokens:css|fonts:css)[^`]*` \*\/$/;
|
|
343
|
+
const GENERATED_HEADER_LINES = 2;
|
|
344
|
+
const GENERATED_COMMENT_LINE = /^\/\*(?:[^*]|\*(?!\/))*\*\/$/;
|
|
345
|
+
const GENERATED_SELECTOR_LINE = /^[^{};]*[,{]$/;
|
|
346
|
+
const GENERATED_DECL_LINE = /^(?:--(?:vx|basalt)-[A-Za-z0-9_-]*\s*:[^{};]*;|\})$/;
|
|
347
|
+
const NO_GENERATED_LINES = /* @__PURE__ */ new Set();
|
|
348
|
+
function braceDelta(line) {
|
|
349
|
+
let delta = 0;
|
|
350
|
+
for (const ch of line) {
|
|
351
|
+
if (ch === "{") delta++;
|
|
352
|
+
else if (ch === "}") delta--;
|
|
353
|
+
}
|
|
354
|
+
return delta;
|
|
355
|
+
}
|
|
356
|
+
function generatedTokenLines(relPath, lines, allowComment) {
|
|
357
|
+
if (!relPath.endsWith(".css")) return NO_GENERATED_LINES;
|
|
358
|
+
if ((lines[0] ?? "").trim() !== GENERATED_HEADER_LINE) return NO_GENERATED_LINES;
|
|
359
|
+
if (!GENERATED_PROVENANCE_LINE.test((lines[1] ?? "").trim())) return NO_GENERATED_LINES;
|
|
360
|
+
const skippable = /* @__PURE__ */ new Set([1, 2]);
|
|
361
|
+
let depth = 0;
|
|
362
|
+
for (let i = GENERATED_HEADER_LINES; i < lines.length; i++) {
|
|
363
|
+
const line = lines[i] ?? "";
|
|
364
|
+
const trimmed = line.trim();
|
|
365
|
+
const shaped = trimmed === "" || GENERATED_COMMENT_LINE.test(trimmed) || (depth === 0 ? GENERATED_SELECTOR_LINE : GENERATED_DECL_LINE).test(trimmed);
|
|
366
|
+
if (shaped && !line.includes(allowComment)) skippable.add(i + 1);
|
|
367
|
+
depth += braceDelta(line);
|
|
368
|
+
if (depth < 0) return NO_GENERATED_LINES;
|
|
369
|
+
}
|
|
370
|
+
return skippable;
|
|
104
371
|
}
|
|
372
|
+
const TOKENS_ONLY_DISABLED_KINDS = /* @__PURE__ */ new Set([
|
|
373
|
+
"localstorage-theme",
|
|
374
|
+
"off-identity-accent",
|
|
375
|
+
"mantine-shade-index",
|
|
376
|
+
"raw-spacing",
|
|
377
|
+
"raw-radius",
|
|
378
|
+
"card-with-border",
|
|
379
|
+
"off-system-surface-var",
|
|
380
|
+
"raw-html-layout",
|
|
381
|
+
// Same remedy as raw-html-layout, word for word ("use a Mantine layout primitive") — it is that
|
|
382
|
+
// kind's widening, so it has to share its gate here exactly as it shares it in
|
|
383
|
+
// EXEMPT_RULE_ALIASES. Splitting them told a Mantine-free consumer to import Box/Flex.
|
|
384
|
+
"hidden-inline-style",
|
|
385
|
+
"inline-spacing",
|
|
386
|
+
"inline-display",
|
|
387
|
+
"raw-visx-axis",
|
|
388
|
+
"raw-motion-value",
|
|
389
|
+
"unframed-chart",
|
|
390
|
+
"chart-missing-aria-label",
|
|
391
|
+
"raw-form-control",
|
|
392
|
+
"sub-16-input-font"
|
|
393
|
+
]);
|
|
105
394
|
const REGEX_PRECEDING_PUNCT = /* @__PURE__ */ new Set([
|
|
106
395
|
"=",
|
|
107
396
|
"(",
|
|
@@ -419,13 +708,63 @@ const GUARD_RULES = {
|
|
|
419
708
|
// object literal — both JSX-shaped, neither appears in CSS text.
|
|
420
709
|
appliesTo: (relPath) => !relPath.endsWith(".css"),
|
|
421
710
|
message: "fontSize below 16 on a form control is dead code \u2014 the styles.css iOS floor is `!important` and always wins. Either drop the override, or be honest about 16px in the design."
|
|
711
|
+
},
|
|
712
|
+
"theme-allow-unscoped": {
|
|
713
|
+
kind: "theme-allow-unscoped",
|
|
714
|
+
pattern: /theme-allow/,
|
|
715
|
+
// handled inline (annotation pass); entry keeps the registry complete
|
|
716
|
+
message: "theme-allow without a usable rule id and a reason. Write `theme-allow <rule-id> \u2014 <why>`: the id scopes the exception to that one kind (a bare comment waives EVERY kind on the line, including ones added later), and the reason is what makes it reviewable in a diff. An id that names no rule is a typo, not a blanket waiver \u2014 it suppresses nothing. `theme-allow-file <rule-id> \u2014 <why>` declares the whole file, and must name its ids: a bare one waives nothing."
|
|
717
|
+
},
|
|
718
|
+
"surface-shadow-override": {
|
|
719
|
+
kind: "surface-shadow-override",
|
|
720
|
+
pattern: SHADOW_DECL,
|
|
721
|
+
// handled inline (value is judged, not just matched)
|
|
722
|
+
enabled: (cfg) => cfg.rawSurface,
|
|
723
|
+
message: "Token-composed boxShadow that REPLACES surface depth \u2014 a card whose shadow drops --vx-shadow-card is the one card in the app with no depth. Compose with the depth token for its tier (`${VX.shadowCard}, <your ring>`; a fixed/floating surface wants --vx-shadow-overlay) rather than instead of it."
|
|
724
|
+
},
|
|
725
|
+
"css-raw-surface": {
|
|
726
|
+
kind: "css-raw-surface",
|
|
727
|
+
pattern: CSS_SURFACE_RADIUS,
|
|
728
|
+
// handled inline (value is judged); entry keeps the registry complete
|
|
729
|
+
enabled: (cfg) => cfg.rawSurface,
|
|
730
|
+
// The kebab dialect only exists in CSS text; the camelCase half is `raw-surface`.
|
|
731
|
+
appliesTo: (relPath) => relPath.endsWith(".css"),
|
|
732
|
+
message: "Surface literal in CSS \u2014 use var(--vx-radius-card|--vx-radius-md) for a corner and var(--vx-shadow-card) for depth, the same tokens the TSX side is held to."
|
|
733
|
+
},
|
|
734
|
+
"hidden-inline-style": {
|
|
735
|
+
kind: "hidden-inline-style",
|
|
736
|
+
pattern: RAW_HTML_LAYOUT_TAG,
|
|
737
|
+
// handled inline (full-text tag-scoped scan + hoisted-const lookup)
|
|
738
|
+
enabled: (cfg) => cfg.rawHtmlLayout,
|
|
739
|
+
appliesTo: (relPath) => !isChartFile(relPath) && !relPath.endsWith(".css"),
|
|
740
|
+
message: "Raw HTML element with inline layout/surface styling that the line scan cannot see \u2014 the tag is formatted across lines, or the style object is hoisted to a const. Same violation as raw-html-layout: use a Mantine layout primitive (Box/Flex/Grid/Stack/Group). Its own kind for one minor so the widening lands as a warning; it merges into raw-html-layout at the promotion."
|
|
741
|
+
},
|
|
742
|
+
"inline-font-size": {
|
|
743
|
+
kind: "inline-font-size",
|
|
744
|
+
pattern: INLINE_FONT_SIZE,
|
|
745
|
+
message: "Raw font-size literal \u2014 route through VX.text.* (numbers) / --vx-text-* (CSS), or the Mantine fz token. Same rule the oxlint plugin enforces as basalt/no-raw-font-size; this is the half a check-theme-only CI can see."
|
|
422
746
|
}
|
|
423
747
|
};
|
|
748
|
+
function guardKindRemedy(kind) {
|
|
749
|
+
return `${kind}: ${GUARD_RULES[kind].message}`;
|
|
750
|
+
}
|
|
751
|
+
const JSON_FAMILY_FILE = /\.(?:webmanifest|json)$/;
|
|
752
|
+
function guardWaiverHint(relPath) {
|
|
753
|
+
if (relPath.endsWith(".webmanifest")) {
|
|
754
|
+
return 'A manifest cannot reference a CSS variable, so a hex here is a hand-copy that drifts: let basaltAppPlugin emit it (`manifest: { \u2026 }` in vite.config.ts) instead of hand-writing it. For a manifest basalt does not generate, declare the exception with a `"basalt:theme-allow-file": "raw-hex \u2014 <why>"` member.';
|
|
755
|
+
}
|
|
756
|
+
if (JSON_FAMILY_FILE.test(relPath)) {
|
|
757
|
+
return 'JSON has no comments \u2014 declare a deliberate exception with a `"basalt:theme-allow-file": "<rule-id> \u2014 <why>"` member.';
|
|
758
|
+
}
|
|
759
|
+
return "Add a `theme-allow <rule-id> \u2014 <why>` comment for a deliberate exception (`theme-allow-file` declares the whole file).";
|
|
760
|
+
}
|
|
424
761
|
function ruleApplies(kind, relPath) {
|
|
425
762
|
const appliesTo = GUARD_RULES[kind].appliesTo;
|
|
426
763
|
return appliesTo === void 0 || appliesTo(relPath);
|
|
427
764
|
}
|
|
428
765
|
function checkSource(text, relPath, cfg) {
|
|
766
|
+
const lines = text.split("\n");
|
|
767
|
+
const generatedLines = generatedTokenLines(relPath, lines, cfg.allowComment);
|
|
429
768
|
const findings = [];
|
|
430
769
|
const forbiddenAccentRe = GUARD_RULES["off-identity-accent"].pattern(cfg);
|
|
431
770
|
const spacingPropRe = GUARD_RULES["raw-spacing"].pattern(cfg);
|
|
@@ -433,101 +772,179 @@ function checkSource(text, relPath, cfg) {
|
|
|
433
772
|
GUARD_RULES["raw-radius"].pattern.source,
|
|
434
773
|
GUARD_RULES["raw-radius"].pattern.flags
|
|
435
774
|
);
|
|
436
|
-
const lines = text.split("\n");
|
|
437
775
|
const syntax = guardSyntaxFor(relPath);
|
|
438
|
-
const codeText = stripComments(text, syntax);
|
|
776
|
+
const codeText = syntax === "markup" ? stripMarkupComments(text) : stripComments(text, syntax);
|
|
439
777
|
const codeLines = codeText.split("\n");
|
|
778
|
+
const { waivers, fileScoped, declared } = collectAllowAnnotations(
|
|
779
|
+
lines,
|
|
780
|
+
codeLines,
|
|
781
|
+
cfg.allowComment,
|
|
782
|
+
syntax
|
|
783
|
+
);
|
|
784
|
+
const fileWaived = /* @__PURE__ */ new Set();
|
|
785
|
+
for (const annotation of fileScoped) {
|
|
786
|
+
for (const rule of annotation.rules) {
|
|
787
|
+
if (Object.hasOwn(GUARD_RULES, rule)) fileWaived.add(rule);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
const isAllowed = (line, kind) => fileWaived.has(kind) || annotationsCover(waivers.get(line - 1), kind);
|
|
791
|
+
const isAllowedInRange = (start, end, kind) => {
|
|
792
|
+
for (let n = start; n <= end; n++) if (isAllowed(n, kind)) return true;
|
|
793
|
+
return false;
|
|
794
|
+
};
|
|
795
|
+
const push = (kind, line, token) => {
|
|
796
|
+
if (isAllowed(line, kind)) return;
|
|
797
|
+
findings.push({ relPath, line, token, kind });
|
|
798
|
+
};
|
|
799
|
+
for (const { line, annotation } of declared) {
|
|
800
|
+
if (annotation.unknownRules.length === 0 && annotation.rules.length > 0 && annotation.hasReason)
|
|
801
|
+
continue;
|
|
802
|
+
const unknown = `unknown rule id '${annotation.unknownRules.join("', '")}'`;
|
|
803
|
+
const missing = annotation.unknownRules.length > 0 ? annotation.rules.length === 0 ? `${unknown} \u2014 waives nothing` : `${unknown} \u2014 not waived` : annotation.rules.length === 0 ? "no rule id" : "no reason";
|
|
804
|
+
findings.push({
|
|
805
|
+
relPath,
|
|
806
|
+
line,
|
|
807
|
+
token: `${cfg.allowComment} (${missing})`,
|
|
808
|
+
kind: "theme-allow-unscoped"
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
const rawHtmlLayoutLines = /* @__PURE__ */ new Set();
|
|
812
|
+
const lineStarts = [];
|
|
813
|
+
for (let n = 0, i = 0; i < codeLines.length; i++) {
|
|
814
|
+
lineStarts.push(n);
|
|
815
|
+
n += (codeLines[i] ?? "").length + 1;
|
|
816
|
+
}
|
|
817
|
+
const styleIdentifiers = new Set(
|
|
818
|
+
[...codeText.matchAll(STYLE_BOUND_IDENTIFIER)].map((m) => m[1] ?? "")
|
|
819
|
+
);
|
|
440
820
|
for (let i = 0; i < lines.length; i++) {
|
|
441
821
|
const line = codeLines[i] ?? "";
|
|
442
|
-
if ((lines[i] ?? "").includes(cfg.allowComment)) continue;
|
|
443
822
|
for (const m of line.matchAll(GUARD_RULES["raw-hex"].pattern)) {
|
|
444
|
-
|
|
823
|
+
push("raw-hex", i + 1, m[0]);
|
|
445
824
|
}
|
|
446
825
|
for (const m of line.matchAll(GUARD_RULES["raw-color-fn"].pattern)) {
|
|
447
|
-
|
|
826
|
+
push("raw-color-fn", i + 1, m[0]);
|
|
448
827
|
}
|
|
449
828
|
if (ruleApplies("localstorage-theme", relPath)) {
|
|
450
829
|
for (const m of line.matchAll(GUARD_RULES["localstorage-theme"].pattern)) {
|
|
451
|
-
|
|
830
|
+
push("localstorage-theme", i + 1, m[0]);
|
|
452
831
|
}
|
|
453
832
|
}
|
|
454
833
|
for (const m of line.matchAll(GUARD_RULES["raw-font-family"].pattern)) {
|
|
455
|
-
|
|
834
|
+
push("raw-font-family", i + 1, m[0]);
|
|
835
|
+
}
|
|
836
|
+
for (const m of line.matchAll(GUARD_RULES["inline-font-size"].pattern)) {
|
|
837
|
+
push("inline-font-size", i + 1, m[0]);
|
|
456
838
|
}
|
|
457
839
|
if (ruleApplies("off-identity-accent", relPath)) {
|
|
458
840
|
for (const m of line.matchAll(forbiddenAccentRe)) {
|
|
459
|
-
|
|
841
|
+
push("off-identity-accent", i + 1, m[1] ?? "");
|
|
460
842
|
}
|
|
461
843
|
}
|
|
462
844
|
if (ruleApplies("raw-spacing", relPath)) {
|
|
463
|
-
for (const m of line.matchAll(spacingPropRe))
|
|
464
|
-
findings.push({ relPath, line: i + 1, token: m[0], kind: "raw-spacing" });
|
|
465
|
-
}
|
|
845
|
+
for (const m of line.matchAll(spacingPropRe)) push("raw-spacing", i + 1, m[0]);
|
|
466
846
|
}
|
|
467
847
|
if (GUARD_RULES["raw-radius"].enabled(cfg) && ruleApplies("raw-radius", relPath)) {
|
|
468
|
-
for (const m of line.matchAll(radiusPropRe))
|
|
469
|
-
findings.push({ relPath, line: i + 1, token: m[0], kind: "raw-radius" });
|
|
470
|
-
}
|
|
848
|
+
for (const m of line.matchAll(radiusPropRe)) push("raw-radius", i + 1, m[0]);
|
|
471
849
|
}
|
|
472
850
|
if (GUARD_RULES["raw-surface"].enabled(cfg)) {
|
|
473
|
-
for (const m of line.matchAll(SURFACE_BORDER))
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
851
|
+
for (const m of line.matchAll(SURFACE_BORDER)) push("raw-surface", i + 1, m[0]);
|
|
852
|
+
for (const m of line.matchAll(SURFACE_RADIUS)) push("raw-surface", i + 1, m[0]);
|
|
853
|
+
if (syntax === "css") {
|
|
854
|
+
for (const m of line.matchAll(CSS_SURFACE_RADIUS)) {
|
|
855
|
+
const value = m[1] ?? "";
|
|
856
|
+
if (!/\d/.test(value) || isAllowedCssRadius(value)) continue;
|
|
857
|
+
push("css-raw-surface", i + 1, m[0].trim());
|
|
858
|
+
}
|
|
859
|
+
for (const m of line.matchAll(CSS_SURFACE_SHADOW)) {
|
|
860
|
+
const value = m[1] ?? "";
|
|
861
|
+
if (SHADOW_IS_COMPOSED.test(value) || /^\s*none\s*$/.test(value)) continue;
|
|
862
|
+
push("css-raw-surface", i + 1, m[0].trim());
|
|
863
|
+
}
|
|
864
|
+
} else {
|
|
865
|
+
for (const m of line.matchAll(SHADOW_DECL)) {
|
|
866
|
+
const value = m[2] ?? "";
|
|
867
|
+
if (!SHADOW_IS_COMPOSED.test(value)) {
|
|
868
|
+
push("raw-surface", i + 1, m[0]);
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
if (SHADOW_KEEPS_CARD_DEPTH.test(value)) continue;
|
|
872
|
+
push("surface-shadow-override", i + 1, m[0]);
|
|
873
|
+
}
|
|
481
874
|
}
|
|
482
875
|
}
|
|
483
876
|
if (GUARD_RULES["off-system-surface-var"].enabled(cfg)) {
|
|
484
877
|
for (const m of line.matchAll(GUARD_RULES["off-system-surface-var"].pattern)) {
|
|
485
|
-
|
|
878
|
+
push("off-system-surface-var", i + 1, m[0]);
|
|
486
879
|
}
|
|
487
880
|
}
|
|
488
881
|
if (GUARD_RULES["mantine-shade-index"].enabled(cfg)) {
|
|
489
882
|
for (const m of line.matchAll(GUARD_RULES["mantine-shade-index"].pattern)) {
|
|
490
|
-
|
|
883
|
+
push("mantine-shade-index", i + 1, m[0]);
|
|
491
884
|
}
|
|
492
885
|
}
|
|
493
886
|
if (GUARD_RULES["raw-html-layout"].enabled(cfg) && ruleApplies("raw-html-layout", relPath) && RAW_HTML_TAG.test(line) && INLINE_STYLE.test(line) && LAYOUT_SURFACE_PROP.test(line)) {
|
|
494
|
-
|
|
887
|
+
rawHtmlLayoutLines.add(i + 1);
|
|
888
|
+
push("raw-html-layout", i + 1, "<raw-html style>");
|
|
495
889
|
}
|
|
496
890
|
if (GUARD_RULES["inline-spacing"].enabled(cfg)) {
|
|
497
891
|
for (const m of line.matchAll(GUARD_RULES["inline-spacing"].pattern)) {
|
|
498
892
|
if (syntax === "css" && isSubScaleCssSpacing(line, m.index)) continue;
|
|
499
|
-
|
|
893
|
+
const hasUnit = /^[a-z%]/.test(line.slice(m.index + m[0].length));
|
|
894
|
+
if (syntax === "ts" && !hasUnit && !isStyleObjectContext(codeText, lineStarts[i] + m.index, styleIdentifiers))
|
|
895
|
+
continue;
|
|
896
|
+
push("inline-spacing", i + 1, m[0]);
|
|
500
897
|
}
|
|
501
898
|
}
|
|
502
899
|
if (GUARD_RULES["inline-display"].enabled(cfg) && ruleApplies("inline-display", relPath)) {
|
|
503
900
|
for (const m of line.matchAll(GUARD_RULES["inline-display"].pattern)) {
|
|
504
|
-
|
|
901
|
+
push("inline-display", i + 1, m[0]);
|
|
505
902
|
}
|
|
506
903
|
}
|
|
507
904
|
if (GUARD_RULES["raw-visx-axis"].enabled(cfg) && GUARD_RULES["raw-visx-axis"].appliesTo(relPath)) {
|
|
508
905
|
for (const m of line.matchAll(GUARD_RULES["raw-visx-axis"].pattern)) {
|
|
509
|
-
|
|
906
|
+
push("raw-visx-axis", i + 1, m[0]);
|
|
510
907
|
}
|
|
511
908
|
}
|
|
512
909
|
if (GUARD_RULES["raw-motion-value"].enabled(cfg) && ruleApplies("raw-motion-value", relPath)) {
|
|
513
|
-
for (const m of line.matchAll(MOTION_TRANSITION_NUMERIC))
|
|
514
|
-
|
|
515
|
-
}
|
|
910
|
+
for (const m of line.matchAll(MOTION_TRANSITION_NUMERIC))
|
|
911
|
+
push("raw-motion-value", i + 1, m[0]);
|
|
516
912
|
for (const m of line.matchAll(MOTION_TRANSITION_EASE_ARRAY)) {
|
|
517
|
-
|
|
913
|
+
push("raw-motion-value", i + 1, m[0]);
|
|
518
914
|
}
|
|
519
915
|
}
|
|
520
916
|
if (GUARD_RULES["raw-form-control"].enabled(cfg) && ruleApplies("raw-form-control", relPath)) {
|
|
521
917
|
for (const m of line.matchAll(GUARD_RULES["raw-form-control"].pattern)) {
|
|
522
|
-
|
|
918
|
+
push("raw-form-control", i + 1, m[0]);
|
|
523
919
|
}
|
|
524
920
|
}
|
|
525
921
|
}
|
|
922
|
+
if (GUARD_RULES["hidden-inline-style"].enabled(cfg) && ruleApplies("hidden-inline-style", relPath)) {
|
|
923
|
+
for (const m of codeText.matchAll(RAW_HTML_LAYOUT_TAG)) {
|
|
924
|
+
const tagText = m[0];
|
|
925
|
+
let styleBody = "";
|
|
926
|
+
if (INLINE_STYLE.test(tagText)) styleBody = tagText;
|
|
927
|
+
else {
|
|
928
|
+
const ident = STYLE_IDENTIFIER.exec(tagText)?.[1];
|
|
929
|
+
if (ident !== void 0) styleBody = hoistedObjectBody(codeText, ident);
|
|
930
|
+
}
|
|
931
|
+
if (styleBody === "" || !LAYOUT_SURFACE_PROP.test(styleBody)) continue;
|
|
932
|
+
const startLine = codeText.slice(0, m.index ?? 0).split("\n").length;
|
|
933
|
+
if (rawHtmlLayoutLines.has(startLine)) continue;
|
|
934
|
+
const endLine = startLine + (tagText.split("\n").length - 1);
|
|
935
|
+
if (isAllowedInRange(startLine, endLine, "hidden-inline-style")) continue;
|
|
936
|
+
findings.push({
|
|
937
|
+
relPath,
|
|
938
|
+
line: startLine,
|
|
939
|
+
token: "<raw-html style>",
|
|
940
|
+
kind: "hidden-inline-style"
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
}
|
|
526
944
|
if (GUARD_RULES["unframed-chart"].enabled(cfg) && ruleApplies("unframed-chart", relPath)) {
|
|
527
945
|
for (const m of codeText.matchAll(RAW_CHART_LEGEND_ARRAY)) {
|
|
528
946
|
const lineNo = codeText.slice(0, (m.index ?? 0) + m[0].length).split("\n").length;
|
|
529
|
-
|
|
530
|
-
findings.push({ relPath, line: lineNo, token: "items={[", kind: "unframed-chart" });
|
|
947
|
+
push("unframed-chart", lineNo, "items={[");
|
|
531
948
|
}
|
|
532
949
|
}
|
|
533
950
|
if (GUARD_RULES["chart-missing-aria-label"].enabled(cfg) && ruleApplies("chart-missing-aria-label", relPath)) {
|
|
@@ -536,14 +953,7 @@ function checkSource(text, relPath, cfg) {
|
|
|
536
953
|
if (HAS_ARIA_LABEL_PROP.test(tagText)) continue;
|
|
537
954
|
const startLine = codeText.slice(0, m.index ?? 0).split("\n").length;
|
|
538
955
|
const endLine = startLine + (tagText.split("\n").length - 1);
|
|
539
|
-
|
|
540
|
-
for (let n = startLine; n <= endLine; n++) {
|
|
541
|
-
if (isSkippedLine(lines[n - 1] ?? "", cfg)) {
|
|
542
|
-
skipped = true;
|
|
543
|
-
break;
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
if (skipped) continue;
|
|
956
|
+
if (isAllowedInRange(startLine, endLine, "chart-missing-aria-label")) continue;
|
|
547
957
|
findings.push({
|
|
548
958
|
relPath,
|
|
549
959
|
line: startLine,
|
|
@@ -557,8 +967,7 @@ function checkSource(text, relPath, cfg) {
|
|
|
557
967
|
const withBorder = WITH_BORDER_PROP.exec(m[0]);
|
|
558
968
|
if (withBorder === null) continue;
|
|
559
969
|
const lineNo = codeText.slice(0, (m.index ?? 0) + withBorder.index).split("\n").length;
|
|
560
|
-
|
|
561
|
-
findings.push({ relPath, line: lineNo, token: "withBorder", kind: "card-with-border" });
|
|
970
|
+
push("card-with-border", lineNo, "withBorder");
|
|
562
971
|
}
|
|
563
972
|
}
|
|
564
973
|
if (GUARD_RULES["sub-16-input-font"].enabled(cfg) && ruleApplies("sub-16-input-font", relPath)) {
|
|
@@ -570,8 +979,7 @@ function checkSource(text, relPath, cfg) {
|
|
|
570
979
|
const value = Number.parseFloat(fontSizeMatch[1] ?? "");
|
|
571
980
|
if (!Number.isFinite(value) || value >= 16) continue;
|
|
572
981
|
const lineNo = codeText.slice(0, (m.index ?? 0) + tagText.length).split("\n").length;
|
|
573
|
-
|
|
574
|
-
findings.push({ relPath, line: lineNo, token: fontSizeMatch[0], kind: "sub-16-input-font" });
|
|
982
|
+
push("sub-16-input-font", lineNo, fontSizeMatch[0]);
|
|
575
983
|
}
|
|
576
984
|
for (const m of codeText.matchAll(STYLES_INPUT_PART)) {
|
|
577
985
|
const inputBody = m[1] ?? "";
|
|
@@ -580,11 +988,10 @@ function checkSource(text, relPath, cfg) {
|
|
|
580
988
|
const value = Number.parseFloat(fontSizeMatch[1] ?? "");
|
|
581
989
|
if (!Number.isFinite(value) || value >= 16) continue;
|
|
582
990
|
const lineNo = codeText.slice(0, (m.index ?? 0) + m[0].length).split("\n").length;
|
|
583
|
-
|
|
584
|
-
findings.push({ relPath, line: lineNo, token: fontSizeMatch[0], kind: "sub-16-input-font" });
|
|
991
|
+
push("sub-16-input-font", lineNo, fontSizeMatch[0]);
|
|
585
992
|
}
|
|
586
993
|
}
|
|
587
|
-
return findings.filter((f) => !isRuleExempt(f.kind, f.relPath, cfg.exemptRules)).map((f) => ({
|
|
994
|
+
return findings.filter((f) => !generatedLines.has(f.line)).filter((f) => !isRuleExempt(f.kind, f.relPath, cfg.exemptRules)).filter((f) => syntax !== "markup" || MARKUP_KINDS.has(f.kind)).filter((f) => cfg.profile !== "tokens-only" || !TOKENS_ONLY_DISABLED_KINDS.has(f.kind)).map((f) => ({
|
|
588
995
|
...f,
|
|
589
996
|
text: textForFinding(lines[f.line - 1], f.token),
|
|
590
997
|
severity: severityOf(f.kind, cfg)
|
|
@@ -592,6 +999,11 @@ function checkSource(text, relPath, cfg) {
|
|
|
592
999
|
}
|
|
593
1000
|
export {
|
|
594
1001
|
DEFAULT_GUARD_CONFIG,
|
|
1002
|
+
GENERATED_HEADER_LINE,
|
|
595
1003
|
GUARD_RULES,
|
|
596
|
-
|
|
1004
|
+
TOKENS_ONLY_DISABLED_KINDS,
|
|
1005
|
+
checkSource,
|
|
1006
|
+
guardKindRemedy,
|
|
1007
|
+
guardWaiverHint,
|
|
1008
|
+
unmatchedExemptPatterns
|
|
597
1009
|
};
|