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.
Files changed (129) hide show
  1. package/AGENTS.md +19 -1
  2. package/CHANGELOG.md +39 -0
  3. package/MIGRATING.md +326 -0
  4. package/README.md +87 -9
  5. package/agent/rules/basalt-app.md +84 -0
  6. package/agent/rules/basalt-charts.md +32 -17
  7. package/agent/rules/basalt-content.md +4 -0
  8. package/agent/rules/basalt-mantine.md +25 -10
  9. package/agent/rules/basalt-router.md +79 -15
  10. package/agent/rules/basalt-state.md +21 -3
  11. package/agent/rules/basalt-tokens.md +132 -6
  12. package/agent/skills/basalt-app/SKILL.md +45 -6
  13. package/agent/skills/basalt-charts/SKILL.md +5 -3
  14. package/agent/templates/CLAUDE-block.md.tpl +11 -5
  15. package/configs/check.yml +11 -4
  16. package/configs/lefthook.yml +59 -7
  17. package/configs/oxlint-plugin.js +633 -30
  18. package/configs/oxlint-plugin.test.ts +594 -15
  19. package/configs/oxlint.json +10 -1
  20. package/dist/agent/stick-to-bottom.js +1 -1
  21. package/dist/agent-chat/thread-detail-panel.js +1 -1
  22. package/dist/agent-chat/thread-message.js +2 -2
  23. package/dist/agent-chat/tool-chip.js +1 -1
  24. package/dist/charts/kinds/Bars.d.ts +1 -1
  25. package/dist/charts/kinds/Bars.d.ts.map +1 -1
  26. package/dist/charts/kinds/Donut.d.ts +1 -1
  27. package/dist/charts/kinds/Donut.d.ts.map +1 -1
  28. package/dist/charts/kinds/Donut.js +1 -1
  29. package/dist/charts/kinds/DualPanel.d.ts +1 -1
  30. package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
  31. package/dist/charts/kinds/Heatmap.d.ts +1 -1
  32. package/dist/charts/kinds/Heatmap.d.ts.map +1 -1
  33. package/dist/charts/kinds/MultiLine.d.ts +1 -1
  34. package/dist/charts/kinds/MultiLine.d.ts.map +1 -1
  35. package/dist/charts/kinds/StackedArea.d.ts +1 -1
  36. package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
  37. package/dist/charts/kinds/ZonedLine.d.ts +1 -1
  38. package/dist/charts/kinds/ZonedLine.d.ts.map +1 -1
  39. package/dist/charts/primitives/ChartLegend.js +2 -2
  40. package/dist/charts/primitives/ChartTooltip.js +1 -1
  41. package/dist/cli/index.d.ts +146 -20
  42. package/dist/cli/index.d.ts.map +1 -1
  43. package/dist/cli/index.js +901 -101
  44. package/dist/cli/toolchain-wiring.test.d.ts +2 -0
  45. package/dist/cli/toolchain-wiring.test.d.ts.map +1 -0
  46. package/dist/cli/waiver-audit.test.d.ts +2 -0
  47. package/dist/cli/waiver-audit.test.d.ts.map +1 -0
  48. package/dist/dashboard/delta-badge.js +2 -2
  49. package/dist/dashboard/stat-card.js +1 -1
  50. package/dist/data/virtual-list.js +5 -5
  51. package/dist/guard/index.d.ts +101 -2
  52. package/dist/guard/index.d.ts.map +1 -1
  53. package/dist/guard/index.js +474 -62
  54. package/dist/guard/types.d.ts +25 -5
  55. package/dist/guard/types.d.ts.map +1 -1
  56. package/dist/register.d.ts +25 -0
  57. package/dist/register.d.ts.map +1 -1
  58. package/dist/router-tanstack/multi-search-param-store.d.ts +13 -0
  59. package/dist/router-tanstack/multi-search-param-store.d.ts.map +1 -1
  60. package/dist/router-tanstack/multi-search-param-store.js +4 -1
  61. package/dist/router-tanstack/search-param-store.d.ts +29 -5
  62. package/dist/router-tanstack/search-param-store.d.ts.map +1 -1
  63. package/dist/router-tanstack/search-param-store.js +31 -3
  64. package/dist/router-tanstack/search-param-store.test.d.ts +2 -0
  65. package/dist/router-tanstack/search-param-store.test.d.ts.map +1 -0
  66. package/dist/shell/app-mobile-nav.module.css +11 -3
  67. package/dist/shell/index.d.ts +12 -5
  68. package/dist/shell/index.d.ts.map +1 -1
  69. package/dist/shell/index.js +24 -6
  70. package/dist/surfaces.d.ts +1 -1
  71. package/dist/surfaces.js +1 -1
  72. package/dist/tokens/index.d.ts +5 -3
  73. package/dist/tokens/index.d.ts.map +1 -1
  74. package/dist/tokens/index.js +7 -1
  75. package/dist/tokens/palette.d.ts +35 -27
  76. package/dist/tokens/palette.d.ts.map +1 -1
  77. package/dist/tokens/palette.js +24 -18
  78. package/dist/tokens/text-scale.test.d.ts +2 -0
  79. package/dist/tokens/text-scale.test.d.ts.map +1 -0
  80. package/dist/tokens.css +21 -17
  81. package/dist/vite-color-mix.js +2 -2
  82. package/dist/vite.d.ts +24 -2
  83. package/dist/vite.d.ts.map +1 -1
  84. package/dist/vite.js +54 -13
  85. package/llms.txt +3 -3
  86. package/package.json +4 -3
  87. package/src/agent/stick-to-bottom.tsx +1 -1
  88. package/src/agent-chat/thread-detail-panel.tsx +1 -1
  89. package/src/agent-chat/thread-message.tsx +3 -3
  90. package/src/agent-chat/tool-chip.tsx +1 -1
  91. package/src/charts/kinds/Bars.tsx +1 -1
  92. package/src/charts/kinds/Donut.tsx +3 -2
  93. package/src/charts/kinds/DualPanel.tsx +2 -2
  94. package/src/charts/kinds/Heatmap.tsx +1 -1
  95. package/src/charts/kinds/MultiLine.tsx +1 -1
  96. package/src/charts/kinds/StackedArea.tsx +1 -1
  97. package/src/charts/kinds/ZonedLine.tsx +1 -1
  98. package/src/charts/primitives/ChartLegend.tsx +2 -2
  99. package/src/charts/primitives/ChartTooltip.tsx +1 -1
  100. package/src/cli/index.ts +1614 -164
  101. package/src/cli/init-sync.test.ts +21 -3
  102. package/src/cli/template-ledger.test.ts +1 -1
  103. package/src/cli/tokens-css.test.ts +27 -14
  104. package/src/cli/toolchain-wiring.test.ts +856 -0
  105. package/src/cli/waiver-audit.test.ts +131 -0
  106. package/src/dashboard/delta-badge.tsx +3 -3
  107. package/src/dashboard/stat-card.tsx +1 -1
  108. package/src/data/virtual-list.tsx +8 -8
  109. package/src/guard/check-source.test.ts +1104 -12
  110. package/src/guard/index.ts +1106 -103
  111. package/src/guard/types.ts +29 -4
  112. package/src/register.ts +32 -0
  113. package/src/router-tanstack/multi-search-param-store.ts +16 -1
  114. package/src/router-tanstack/search-param-store.test.ts +135 -0
  115. package/src/router-tanstack/search-param-store.ts +90 -8
  116. package/src/shell/app-mobile-nav.module.css +11 -3
  117. package/src/shell/app-mobile-nav.tsx +1 -1
  118. package/src/shell/index.test.tsx +52 -0
  119. package/src/shell/index.tsx +64 -13
  120. package/src/surfaces.ts +1 -1
  121. package/src/theme/density-relations.test.ts +8 -5
  122. package/src/theme/spacing.test.ts +2 -0
  123. package/src/theme-toggle/index.tsx +2 -2
  124. package/src/tokens/index.ts +28 -3
  125. package/src/tokens/palette.ts +24 -18
  126. package/src/tokens/text-scale.test.ts +51 -0
  127. package/src/vite-color-mix.ts +2 -2
  128. package/src/vite.test.ts +125 -6
  129. package/src/vite.ts +148 -20
@@ -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*--(?:basalt-font-|mantine-font-family-)/.source;
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 SURFACE_SHADOW = /\bboxShadow\s*:\s*(?!['"`]?[^'"`]*(?:var\(|\$\{))['"`][^'"`]+['"`]/g;
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 RAW_HTML_TAG = /<(?:div|span|section|header|nav|footer|aside|main|article|ul|ol)\b/;
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 in GRACE_PERIOD_KINDS ? "warn" : "error");
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
- const segments = relPath.split("/");
97
- return patterns.some((pattern) => segments.includes(pattern.replace(/\/$/, "")));
194
+ return patterns.some((pattern) => exemptPatternMatches(pattern, relPath));
98
195
  }
99
- function isSkippedLine(line, cfg) {
100
- return line.includes(cfg.allowComment);
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
- return relPath.endsWith(".css") ? "css" : "ts";
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
- findings.push({ relPath, line: i + 1, token: m[0], kind: "raw-hex" });
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
- findings.push({ relPath, line: i + 1, token: m[0], kind: "raw-color-fn" });
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
- findings.push({ relPath, line: i + 1, token: m[0], kind: "localstorage-theme" });
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
- findings.push({ relPath, line: i + 1, token: m[0], kind: "raw-font-family" });
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
- findings.push({ relPath, line: i + 1, token: m[1] ?? "", kind: "off-identity-accent" });
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
- findings.push({ relPath, line: i + 1, token: m[0], kind: "raw-surface" });
475
- }
476
- for (const m of line.matchAll(SURFACE_RADIUS)) {
477
- findings.push({ relPath, line: i + 1, token: m[0], kind: "raw-surface" });
478
- }
479
- for (const m of line.matchAll(SURFACE_SHADOW)) {
480
- findings.push({ relPath, line: i + 1, token: m[0], kind: "raw-surface" });
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
- findings.push({ relPath, line: i + 1, token: m[0], kind: "off-system-surface-var" });
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
- findings.push({ relPath, line: i + 1, token: m[0], kind: "mantine-shade-index" });
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
- findings.push({ relPath, line: i + 1, token: "<raw-html style>", kind: "raw-html-layout" });
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
- findings.push({ relPath, line: i + 1, token: m[0], kind: "inline-spacing" });
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
- findings.push({ relPath, line: i + 1, token: m[0], kind: "inline-display" });
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
- findings.push({ relPath, line: i + 1, token: m[0], kind: "raw-visx-axis" });
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
- findings.push({ relPath, line: i + 1, token: m[0], kind: "raw-motion-value" });
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
- findings.push({ relPath, line: i + 1, token: m[0], kind: "raw-motion-value" });
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
- findings.push({ relPath, line: i + 1, token: m[0], kind: "raw-form-control" });
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
- if (isSkippedLine(lines[lineNo - 1] ?? "", cfg)) continue;
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
- let skipped = false;
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
- if (isSkippedLine(lines[lineNo - 1] ?? "", cfg)) continue;
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
- if (isSkippedLine(lines[lineNo - 1] ?? "", cfg)) continue;
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
- if (isSkippedLine(lines[lineNo - 1] ?? "", cfg)) continue;
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
- checkSource
1004
+ TOKENS_ONLY_DISABLED_KINDS,
1005
+ checkSource,
1006
+ guardKindRemedy,
1007
+ guardWaiverHint,
1008
+ unmatchedExemptPatterns
597
1009
  };