@fairfox/polly 0.24.0 → 0.25.1

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 (95) hide show
  1. package/dist/cli/polly.js +21 -1
  2. package/dist/cli/polly.js.map +3 -3
  3. package/dist/src/actions/error.d.ts +26 -0
  4. package/dist/src/actions/event-delegation.d.ts +48 -0
  5. package/dist/src/actions/form.d.ts +72 -0
  6. package/dist/src/actions/index.d.ts +13 -0
  7. package/dist/src/actions/index.js +525 -0
  8. package/dist/src/actions/index.js.map +15 -0
  9. package/dist/src/actions/overlay.d.ts +26 -0
  10. package/dist/src/actions/registry.d.ts +25 -0
  11. package/dist/src/actions/store.d.ts +26 -0
  12. package/dist/src/actions/testing.d.ts +26 -0
  13. package/dist/src/background/index.js +26 -1
  14. package/dist/src/background/index.js.map +2 -2
  15. package/dist/src/background/message-router.js +26 -1
  16. package/dist/src/background/message-router.js.map +2 -2
  17. package/dist/src/client/index.js +27 -2
  18. package/dist/src/client/index.js.map +3 -3
  19. package/dist/src/elysia/index.js +62 -35
  20. package/dist/src/elysia/index.js.map +3 -3
  21. package/dist/src/index.js +26 -1
  22. package/dist/src/index.js.map +2 -2
  23. package/dist/src/mesh-node.js +26 -1
  24. package/dist/src/mesh-node.js.map +2 -2
  25. package/dist/src/mesh.js +26 -1
  26. package/dist/src/mesh.js.map +2 -2
  27. package/dist/src/peer.js +32 -5
  28. package/dist/src/peer.js.map +3 -3
  29. package/dist/src/polly-ui/ActionForm.d.ts +21 -0
  30. package/dist/src/polly-ui/ActionInput.d.ts +41 -0
  31. package/dist/src/polly-ui/ConfirmDialog.d.ts +24 -0
  32. package/dist/src/polly-ui/Layout.d.ts +51 -0
  33. package/dist/src/polly-ui/Modal.d.ts +52 -0
  34. package/dist/src/polly-ui/OverlayRoot.d.ts +10 -0
  35. package/dist/src/polly-ui/TextInput.d.ts +31 -0
  36. package/dist/src/polly-ui/Toast.d.ts +19 -0
  37. package/dist/src/polly-ui/index.css +319 -0
  38. package/dist/src/polly-ui/index.d.ts +17 -0
  39. package/dist/src/polly-ui/index.js +953 -0
  40. package/dist/src/polly-ui/index.js.map +22 -0
  41. package/dist/src/polly-ui/internal/focus-trap.d.ts +10 -0
  42. package/dist/src/polly-ui/internal/input-base.d.ts +18 -0
  43. package/dist/src/polly-ui/internal/scroll-lock.d.ts +9 -0
  44. package/dist/src/polly-ui/styles.css +70 -0
  45. package/dist/src/polly-ui/theme.css +163 -0
  46. package/dist/src/shared/adapters/index.js +26 -1
  47. package/dist/src/shared/adapters/index.js.map +2 -2
  48. package/dist/src/shared/lib/context-helpers.js +26 -1
  49. package/dist/src/shared/lib/context-helpers.js.map +2 -2
  50. package/dist/src/shared/lib/errors.js +26 -1
  51. package/dist/src/shared/lib/errors.js.map +2 -2
  52. package/dist/src/shared/lib/message-bus.js +26 -1
  53. package/dist/src/shared/lib/message-bus.js.map +2 -2
  54. package/dist/src/shared/lib/peer-repo-server.d.ts +8 -8
  55. package/dist/src/shared/lib/resource.js +26 -1
  56. package/dist/src/shared/lib/resource.js.map +2 -2
  57. package/dist/src/shared/lib/state.js +26 -1
  58. package/dist/src/shared/lib/state.js.map +2 -2
  59. package/dist/src/shared/lib/test-helpers.js +26 -1
  60. package/dist/src/shared/lib/test-helpers.js.map +2 -2
  61. package/dist/src/shared/state/app-state.js +26 -1
  62. package/dist/src/shared/state/app-state.js.map +2 -2
  63. package/dist/src/shared/types/messages.js +26 -1
  64. package/dist/src/shared/types/messages.js.map +2 -2
  65. package/dist/tools/quality/src/cli.js +640 -28
  66. package/dist/tools/quality/src/cli.js.map +11 -5
  67. package/dist/tools/quality/src/css/check-layout.d.ts +19 -0
  68. package/dist/tools/quality/src/css/check-quality.d.ts +24 -0
  69. package/dist/tools/quality/src/css/check-unused.d.ts +20 -0
  70. package/dist/tools/quality/src/css/check-vars.d.ts +22 -0
  71. package/dist/tools/quality/src/css/shared.d.ts +33 -0
  72. package/dist/tools/quality/src/index.d.ts +12 -0
  73. package/dist/tools/quality/src/index.js +557 -18
  74. package/dist/tools/quality/src/index.js.map +10 -4
  75. package/dist/tools/quality/src/logger.d.ts +26 -0
  76. package/dist/tools/test/src/adapters/index.js +26 -1
  77. package/dist/tools/test/src/adapters/index.js.map +2 -2
  78. package/dist/tools/test/src/browser/index.js +26 -1
  79. package/dist/tools/test/src/browser/index.js.map +2 -2
  80. package/dist/tools/test/src/browser/run.js +238 -0
  81. package/dist/tools/test/src/browser/run.js.map +11 -0
  82. package/dist/tools/test/src/index.js +26 -1
  83. package/dist/tools/test/src/index.js.map +2 -2
  84. package/dist/tools/test/src/test-utils.js +26 -1
  85. package/dist/tools/test/src/test-utils.js.map +2 -2
  86. package/dist/tools/test/src/visual/compare.d.ts +23 -0
  87. package/dist/tools/test/src/visual/harness.d.ts +53 -0
  88. package/dist/tools/test/src/visual/index.d.ts +12 -0
  89. package/dist/tools/test/src/visual/index.js +13968 -0
  90. package/dist/tools/test/src/visual/index.js.map +41 -0
  91. package/dist/tools/verify/src/cli.js +3 -3
  92. package/dist/tools/verify/src/cli.js.map +1 -1
  93. package/dist/tools/verify/src/config.js +26 -1
  94. package/dist/tools/verify/src/config.js.map +2 -2
  95. package/package.json +21 -2
@@ -16,6 +16,539 @@ var __export = (target, all) => {
16
16
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
17
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
18
18
 
19
+ // tools/quality/src/css/shared.ts
20
+ import { readdir } from "node:fs/promises";
21
+ import { join, relative } from "node:path";
22
+
23
+ // tools/quality/src/logger.ts
24
+ function defaultLog(message) {
25
+ console.log(message);
26
+ }
27
+ function defaultError(message) {
28
+ console.error(message);
29
+ }
30
+ function defaultInfo(message) {
31
+ console.info(message);
32
+ }
33
+ function defaultWarn(message) {
34
+ console.warn(message);
35
+ }
36
+ var logger = {
37
+ log: defaultLog,
38
+ error: defaultError,
39
+ info: defaultInfo,
40
+ warn: defaultWarn
41
+ };
42
+ function resetLogger() {
43
+ logger.log = defaultLog;
44
+ logger.error = defaultError;
45
+ logger.info = defaultInfo;
46
+ logger.warn = defaultWarn;
47
+ }
48
+
49
+ // tools/quality/src/css/shared.ts
50
+ var DEFAULT_SKIP_DIRS = ["node_modules", ".git", "dist", "dist-test", "build", "coverage"];
51
+ async function walkDirectory(dir, visit, opts) {
52
+ const skipDirs = new Set(opts.skipDirs ?? DEFAULT_SKIP_DIRS);
53
+ const skipFiles = new Set(opts.skipFiles ?? []);
54
+ const rootDir = opts.rootDir;
55
+ async function walk(current) {
56
+ let entries;
57
+ try {
58
+ entries = await readdir(current, { withFileTypes: true });
59
+ } catch {
60
+ return;
61
+ }
62
+ for (const entry of entries) {
63
+ const full = join(current, entry.name);
64
+ if (entry.isDirectory()) {
65
+ if (skipDirs.has(entry.name))
66
+ continue;
67
+ await walk(full);
68
+ } else if (entry.isFile()) {
69
+ if (skipFiles.has(entry.name))
70
+ continue;
71
+ const rel = relative(rootDir, full);
72
+ await visit(full, rel);
73
+ }
74
+ }
75
+ }
76
+ await walk(dir);
77
+ }
78
+ function formatViolations(kind, violations, rootDir) {
79
+ const lines = [];
80
+ if (violations.length === 0) {
81
+ lines.push(`✅ ${kind}: no violations`);
82
+ return lines;
83
+ }
84
+ lines.push(`❌ ${kind}: ${violations.length} violation(s)`);
85
+ const byFile = new Map;
86
+ for (const v of violations) {
87
+ const bucket = byFile.get(v.file) ?? [];
88
+ bucket.push(v);
89
+ byFile.set(v.file, bucket);
90
+ }
91
+ for (const [file, fileViolations] of byFile) {
92
+ lines.push(` ${relative(rootDir, file)}`);
93
+ for (const v of fileViolations) {
94
+ lines.push(` L${v.line}: ${v.content}`);
95
+ lines.push(` → ${v.suggestion} [${v.rule}]`);
96
+ }
97
+ }
98
+ return lines;
99
+ }
100
+ function makeResult(kind, rootDir, violations) {
101
+ return {
102
+ violations,
103
+ print() {
104
+ for (const line of formatViolations(kind, violations, rootDir)) {
105
+ if (line.startsWith("❌")) {
106
+ logger.error(line);
107
+ } else {
108
+ logger.log(line);
109
+ }
110
+ }
111
+ }
112
+ };
113
+ }
114
+ function isInsideComment(line) {
115
+ const trimmed = line.trim();
116
+ return trimmed.startsWith("/*") || trimmed.startsWith("*") || trimmed.startsWith("//");
117
+ }
118
+ function isInsideKeyframes(lineNum, allLines) {
119
+ for (let i = lineNum - 1;i >= 0; i -= 1) {
120
+ const l = allLines[i]?.trim() ?? "";
121
+ if (l.startsWith("@keyframes"))
122
+ return true;
123
+ if (l === "}" && i < lineNum - 1)
124
+ return false;
125
+ }
126
+ return false;
127
+ }
128
+
129
+ // tools/quality/src/css/check-layout.ts
130
+ var CSS_PATTERNS = [
131
+ { pattern: /display\s*:\s*flex/, kind: "display: flex in CSS" },
132
+ { pattern: /display\s*:\s*grid/, kind: "display: grid in CSS" }
133
+ ];
134
+ var TSX_PATTERNS = [
135
+ {
136
+ pattern: /display\s*:\s*['"]flex['"]/,
137
+ kind: "display: flex in inline style"
138
+ },
139
+ {
140
+ pattern: /display\s*:\s*['"]grid['"]/,
141
+ kind: "display: grid in inline style"
142
+ }
143
+ ];
144
+ var SUPPRESS = "layout-ignore";
145
+ async function checkCssLayout(options) {
146
+ const rootDir = options.rootDir;
147
+ const exempt = options.layoutExemptPaths ?? ["Layout.module.css", "Layout.tsx"];
148
+ const violations = [];
149
+ await walkDirectory(rootDir, async (full) => {
150
+ const isCss = full.endsWith(".module.css");
151
+ const isTsx = full.endsWith(".tsx");
152
+ if (!isCss && !isTsx)
153
+ return;
154
+ if (exempt.some((fragment) => full.includes(fragment)))
155
+ return;
156
+ const patterns = isCss ? CSS_PATTERNS : TSX_PATTERNS;
157
+ const content = await Bun.file(full).text();
158
+ const lines = content.split(`
159
+ `);
160
+ for (let i = 0;i < lines.length; i += 1) {
161
+ const line = lines[i];
162
+ if (!line)
163
+ continue;
164
+ const trimmed = line.trim();
165
+ if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*"))
166
+ continue;
167
+ if (trimmed.includes(SUPPRESS))
168
+ continue;
169
+ const prev = i > 0 ? lines[i - 1]?.trim() ?? "" : "";
170
+ if (prev.includes(SUPPRESS))
171
+ continue;
172
+ for (const rule of patterns) {
173
+ if (rule.pattern.test(line)) {
174
+ violations.push({
175
+ file: full,
176
+ line: i + 1,
177
+ rule: rule.kind,
178
+ content: trimmed,
179
+ suggestion: "Use the <Layout> component instead"
180
+ });
181
+ break;
182
+ }
183
+ }
184
+ }
185
+ }, {
186
+ rootDir,
187
+ skipDirs: options.skipDirs
188
+ });
189
+ return makeResult("css-layout", rootDir, violations);
190
+ }
191
+ // tools/quality/src/css/check-quality.ts
192
+ var DEFAULT_RULES = [
193
+ {
194
+ id: "no-hardcoded-color",
195
+ check: (line) => {
196
+ if (isInsideComment(line))
197
+ return null;
198
+ if (line.includes("var("))
199
+ return null;
200
+ if (/\bcolor:\s*(white|black|#[0-9a-fA-F]{3,8})\b/.test(line)) {
201
+ return "Use a semantic colour token (--polly-text, --polly-text-muted, …)";
202
+ }
203
+ if (/background(-color)?:\s*#[0-9a-fA-F]{3,8}/.test(line) && !line.includes("var(")) {
204
+ return "Use a semantic surface token (--polly-surface, --polly-surface-raised, …)";
205
+ }
206
+ return null;
207
+ }
208
+ },
209
+ {
210
+ id: "no-hardcoded-rgba",
211
+ check: (line) => {
212
+ if (isInsideComment(line))
213
+ return null;
214
+ if (!line.includes("rgba("))
215
+ return null;
216
+ if (line.includes("var("))
217
+ return null;
218
+ if (line.includes("color-mix("))
219
+ return null;
220
+ if (/background.*rgba/.test(line)) {
221
+ return "Use a semantic surface or overlay token";
222
+ }
223
+ if (/box-shadow.*rgba/.test(line)) {
224
+ return "Use a semantic shadow token";
225
+ }
226
+ return "Use a semantic token instead of raw rgba()";
227
+ }
228
+ },
229
+ {
230
+ id: "no-hardcoded-z-index",
231
+ check: (line) => {
232
+ const m = line.match(/z-index:\s*(\d+)/);
233
+ if (!m?.[1])
234
+ return null;
235
+ if (line.includes("var("))
236
+ return null;
237
+ if (Number.parseInt(m[1], 10) < 10)
238
+ return null;
239
+ return "Use a --polly-z-* token";
240
+ }
241
+ },
242
+ {
243
+ id: "no-hardcoded-opacity",
244
+ check: (line) => {
245
+ if (!/opacity:\s*0\.\d/.test(line))
246
+ return null;
247
+ if (line.includes("var("))
248
+ return null;
249
+ const m = line.match(/opacity:\s*(0\.\d+)/);
250
+ if (!m?.[1])
251
+ return null;
252
+ const v = Number.parseFloat(m[1]);
253
+ if (v >= 0.5 && v <= 0.7) {
254
+ return "Use var(--polly-opacity-disabled) for disabled states";
255
+ }
256
+ return null;
257
+ }
258
+ },
259
+ {
260
+ id: "no-hardcoded-transition",
261
+ check: (line, lineNum, allLines) => {
262
+ if (isInsideKeyframes(lineNum, allLines))
263
+ return null;
264
+ if (line.includes("var(--polly-motion"))
265
+ return null;
266
+ if (!/(?:transition|animation)/.test(line))
267
+ return null;
268
+ if (/\d+(\.\d+)?(ms|s)\s+(ease|linear|ease-in|ease-out|ease-in-out)/.test(line) && !line.includes("infinite")) {
269
+ return "Use var(--polly-motion-fast|base|slow)";
270
+ }
271
+ if (/\s\d+(\.\d+)?s[;\s,]/.test(line) && !line.includes("infinite")) {
272
+ return "Use var(--polly-motion-fast|base|slow)";
273
+ }
274
+ return null;
275
+ }
276
+ },
277
+ {
278
+ id: "no-important",
279
+ check: (line) => {
280
+ if (isInsideComment(line))
281
+ return null;
282
+ if (!line.includes("!important"))
283
+ return null;
284
+ return "Refactor specificity instead of using !important";
285
+ }
286
+ },
287
+ {
288
+ id: "no-rem-units",
289
+ check: (line, lineNum, allLines) => {
290
+ if (isInsideComment(line))
291
+ return null;
292
+ if (isInsideKeyframes(lineNum, allLines))
293
+ return null;
294
+ if (/:\s*[^;]*\d+(\.\d+)?rem[;\s]/.test(line)) {
295
+ if (line.includes("calc(") || line.includes("translateY(")) {
296
+ return null;
297
+ }
298
+ return "Use --polly-space-* or --polly-text-* tokens instead of rem";
299
+ }
300
+ return null;
301
+ }
302
+ },
303
+ {
304
+ id: "no-hardcoded-border-width",
305
+ check: (line) => {
306
+ if (isInsideComment(line))
307
+ return null;
308
+ if (line.includes("var(--polly-border-width"))
309
+ return null;
310
+ const m = line.match(/border(?:-(?:top|right|bottom|left))?:\s*(\d+)px\s+solid/);
311
+ if (!m?.[1])
312
+ return null;
313
+ return "Use a --polly-border-width-* token";
314
+ }
315
+ },
316
+ {
317
+ id: "no-hardcoded-border-radius",
318
+ check: (line) => {
319
+ if (isInsideComment(line))
320
+ return null;
321
+ if (line.includes("var("))
322
+ return null;
323
+ const m = line.match(/border-radius:\s*(\d+)px/);
324
+ if (!m?.[1])
325
+ return null;
326
+ if (m[1] === "0")
327
+ return null;
328
+ return "Use a --polly-radius-{sm,md,lg} token";
329
+ }
330
+ },
331
+ {
332
+ id: "no-hardcoded-box-shadow",
333
+ check: (line) => {
334
+ if (isInsideComment(line))
335
+ return null;
336
+ if (!line.includes("box-shadow:"))
337
+ return null;
338
+ if (line.includes("var(--polly-shadow"))
339
+ return null;
340
+ if (line.includes("var(--polly-focus-ring"))
341
+ return null;
342
+ if (line.includes("var("))
343
+ return null;
344
+ if (/box-shadow:\s*none/.test(line))
345
+ return null;
346
+ return "Use var(--polly-shadow-*) or var(--polly-focus-ring)";
347
+ }
348
+ },
349
+ {
350
+ id: "no-hardcoded-font-weight",
351
+ check: (line) => {
352
+ if (!/font-weight:\s*\d{3}/.test(line))
353
+ return null;
354
+ if (line.includes("var("))
355
+ return null;
356
+ return "Use var(--polly-weight-normal|medium|bold)";
357
+ }
358
+ }
359
+ ];
360
+ async function checkCssQuality(options) {
361
+ const rootDir = options.rootDir;
362
+ const extensions = options.extensions ?? [".module.css"];
363
+ const skipFiles = options.skipFiles ?? ["theme.css", "tokens.css"];
364
+ const disabled = new Set(options.disableRules ?? []);
365
+ const rules = DEFAULT_RULES.filter((r) => !disabled.has(r.id));
366
+ const violations = [];
367
+ await walkDirectory(rootDir, async (full) => {
368
+ if (!extensions.some((ext) => full.endsWith(ext)))
369
+ return;
370
+ const content = await Bun.file(full).text();
371
+ const lines = content.split(`
372
+ `);
373
+ const hasFileIgnore = lines[0]?.includes("polly-ignore-all");
374
+ if (hasFileIgnore)
375
+ return;
376
+ for (let i = 0;i < lines.length; i += 1) {
377
+ const line = lines[i];
378
+ if (!line)
379
+ continue;
380
+ const trimmed = line.trim();
381
+ if (trimmed === "" || trimmed === "{" || trimmed === "}")
382
+ continue;
383
+ for (const rule of rules) {
384
+ const suggestion = rule.check(line, i, lines);
385
+ if (suggestion) {
386
+ violations.push({
387
+ file: full,
388
+ line: i + 1,
389
+ rule: rule.id,
390
+ content: trimmed,
391
+ suggestion
392
+ });
393
+ }
394
+ }
395
+ }
396
+ }, {
397
+ rootDir,
398
+ skipDirs: options.skipDirs,
399
+ skipFiles
400
+ });
401
+ return makeResult("css-quality", rootDir, violations);
402
+ }
403
+ // tools/quality/src/css/check-unused.ts
404
+ async function checkCssUnused(options) {
405
+ const rootDir = options.rootDir;
406
+ const alwaysUsed = new Set(options.alwaysUsedClasses ?? []);
407
+ const definitions = [];
408
+ const tsContents = [];
409
+ const cssContents = [];
410
+ await walkDirectory(rootDir, async (full) => {
411
+ if (full.endsWith(".module.css")) {
412
+ const content = await Bun.file(full).text();
413
+ const lines = content.split(`
414
+ `);
415
+ for (let i = 0;i < lines.length; i += 1) {
416
+ const line = lines[i];
417
+ if (!line)
418
+ continue;
419
+ for (const m of line.matchAll(/(?:^|[\s,>+~])\.([\w-]+)/g)) {
420
+ if (m[1]) {
421
+ definitions.push({
422
+ file: full,
423
+ name: m[1],
424
+ line: i + 1,
425
+ type: "class"
426
+ });
427
+ }
428
+ }
429
+ for (const m of line.matchAll(/^\s+--([\w-]+)\s*:/g)) {
430
+ if (m[1]) {
431
+ definitions.push({
432
+ file: full,
433
+ name: `--${m[1]}`,
434
+ line: i + 1,
435
+ type: "variable"
436
+ });
437
+ }
438
+ }
439
+ }
440
+ cssContents.push({ file: full, content });
441
+ } else if (full.endsWith(".css") && !full.endsWith(".css.d.ts")) {
442
+ const content = await Bun.file(full).text();
443
+ cssContents.push({ file: full, content });
444
+ } else if ((full.endsWith(".ts") || full.endsWith(".tsx")) && !full.endsWith(".css.d.ts")) {
445
+ const content = await Bun.file(full).text();
446
+ tsContents.push({ file: full, content });
447
+ }
448
+ }, { rootDir, skipDirs: options.skipDirs });
449
+ const seen = new Set;
450
+ const uniqueDefs = definitions.filter((d) => {
451
+ const key = `${d.file}:${d.type}:${d.name}`;
452
+ if (seen.has(key))
453
+ return false;
454
+ seen.add(key);
455
+ return true;
456
+ });
457
+ function classReferenced(name) {
458
+ if (alwaysUsed.has(name))
459
+ return true;
460
+ for (const { content } of tsContents) {
461
+ if (content.includes(`.${name}`) || content.includes(`['${name}']`) || content.includes(`["${name}"]`)) {
462
+ return true;
463
+ }
464
+ }
465
+ return false;
466
+ }
467
+ function variableReferenced(name, defFile) {
468
+ for (const { file, content } of cssContents) {
469
+ if (file === defFile)
470
+ continue;
471
+ if (content.includes(`var(${name})`))
472
+ return true;
473
+ }
474
+ const self = cssContents.find((f) => f.file === defFile);
475
+ if (self) {
476
+ const occurrences = self.content.split(name).length - 1;
477
+ if (occurrences > 1)
478
+ return true;
479
+ }
480
+ for (const { content } of tsContents) {
481
+ if (content.includes(name))
482
+ return true;
483
+ }
484
+ return false;
485
+ }
486
+ const violations = [];
487
+ for (const def of uniqueDefs) {
488
+ if (def.type === "class" && !classReferenced(def.name)) {
489
+ violations.push({
490
+ file: def.file,
491
+ line: def.line,
492
+ rule: "unused-class",
493
+ content: `.${def.name}`,
494
+ suggestion: "Delete the selector or reference the class from a component"
495
+ });
496
+ } else if (def.type === "variable" && !variableReferenced(def.name, def.file)) {
497
+ violations.push({
498
+ file: def.file,
499
+ line: def.line,
500
+ rule: "unused-variable",
501
+ content: def.name,
502
+ suggestion: "Delete the definition or reference the variable"
503
+ });
504
+ }
505
+ }
506
+ return makeResult("css-unused (advisory)", rootDir, violations);
507
+ }
508
+ // tools/quality/src/css/check-vars.ts
509
+ async function checkCssVars(options) {
510
+ const rootDir = options.rootDir;
511
+ const scanExts = options.scanExtensions ?? [".ts", ".tsx", ".css"];
512
+ const dynamic = new Set(options.dynamicVars ?? []);
513
+ const definitions = new Set(dynamic);
514
+ const violations = [];
515
+ await walkDirectory(rootDir, async (full) => {
516
+ if (!full.endsWith(".css"))
517
+ return;
518
+ const content = await Bun.file(full).text();
519
+ for (const m of content.matchAll(/--([\w-]+)\s*:/g)) {
520
+ if (m[1])
521
+ definitions.add(`--${m[1]}`);
522
+ }
523
+ }, { rootDir, skipDirs: options.skipDirs });
524
+ await walkDirectory(rootDir, async (full) => {
525
+ if (full.endsWith(".css.d.ts"))
526
+ return;
527
+ if (!scanExts.some((ext) => full.endsWith(ext)))
528
+ return;
529
+ const content = await Bun.file(full).text();
530
+ const lines = content.split(`
531
+ `);
532
+ for (let i = 0;i < lines.length; i += 1) {
533
+ const line = lines[i];
534
+ if (!line)
535
+ continue;
536
+ for (const m of line.matchAll(/var\(--([\w-]+)\)/g)) {
537
+ const name = `--${m[1]}`;
538
+ if (!definitions.has(name)) {
539
+ violations.push({
540
+ file: full,
541
+ line: i + 1,
542
+ rule: "undefined-var",
543
+ content: line.trim(),
544
+ suggestion: `Define ${name} in a CSS file or add to dynamicVars`
545
+ });
546
+ }
547
+ }
548
+ }
549
+ }, { rootDir, skipDirs: options.skipDirs });
550
+ return makeResult("css-vars", rootDir, violations);
551
+ }
19
552
  // tools/quality/src/no-as-casting.ts
20
553
  import { readFileSync } from "node:fs";
21
554
  import { Glob } from "bun";
@@ -120,16 +653,16 @@ function suggestFix(line) {
120
653
  }
121
654
  return;
122
655
  }
123
- function isFileExcluded(relative, excludeDirs, excludePackages, excludeFiles) {
124
- const segments = relative.split("/");
656
+ function isFileExcluded(relative2, excludeDirs, excludePackages, excludeFiles) {
657
+ const segments = relative2.split("/");
125
658
  if (segments.some((s) => excludeDirs.has(s)))
126
659
  return true;
127
660
  if (excludePackages.size > 0 && segments.some((s) => excludePackages.has(s)))
128
661
  return true;
129
662
  const basename = segments[segments.length - 1] ?? "";
130
- return excludeFiles.has(basename) || excludeFiles.has(relative);
663
+ return excludeFiles.has(basename) || excludeFiles.has(relative2);
131
664
  }
132
- function findViolations(relative, content) {
665
+ function findViolations(relative2, content) {
133
666
  const results = [];
134
667
  const lines = content.split(`
135
668
  `);
@@ -144,7 +677,7 @@ function findViolations(relative, content) {
144
677
  continue;
145
678
  if (!isLineClean(line)) {
146
679
  results.push({
147
- file: relative,
680
+ file: relative2,
148
681
  line: i + 1,
149
682
  content: line.trim(),
150
683
  advice: suggestFix(line.trim())
@@ -155,20 +688,20 @@ function findViolations(relative, content) {
155
688
  }
156
689
  function printViolations(violations) {
157
690
  if (violations.length === 0) {
158
- console.log("[no-as-casting] ✅ No violations found.");
691
+ logger.log("[no-as-casting] ✅ No violations found.");
159
692
  return;
160
693
  }
161
- console.log(`[no-as-casting] ❌ ${violations.length} violation(s) found:
694
+ logger.log(`[no-as-casting] ❌ ${violations.length} violation(s) found:
162
695
  `);
163
696
  for (const v of violations) {
164
- console.log(` ${v.file}:${v.line}`);
165
- console.log(` ${v.content}`);
697
+ logger.log(` ${v.file}:${v.line}`);
698
+ logger.log(` ${v.content}`);
166
699
  if (v.advice)
167
- console.log(` \uD83D\uDCA1 ${v.advice}`);
168
- console.log();
700
+ logger.log(` \uD83D\uDCA1 ${v.advice}`);
701
+ logger.log("");
169
702
  }
170
- console.log("[no-as-casting] Use type guards, validation, or fix the types at the source.");
171
- console.log('[no-as-casting] Only "as const" and "as unknown as" are allowed.');
703
+ logger.log("[no-as-casting] Use type guards, validation, or fix the types at the source.");
704
+ logger.log('[no-as-casting] Only "as const" and "as unknown as" are allowed.');
172
705
  }
173
706
  async function checkNoAsCasting(options) {
174
707
  const rootDir = options.rootDir;
@@ -179,18 +712,24 @@ async function checkNoAsCasting(options) {
179
712
  const glob = new Glob(pattern);
180
713
  const violations = [];
181
714
  for await (const file of glob.scan({ cwd: rootDir, absolute: true })) {
182
- const relative = file.replace(`${rootDir}/`, "");
183
- if (isFileExcluded(relative, excludeDirs, excludePackages, excludeFiles))
715
+ const relative2 = file.replace(`${rootDir}/`, "");
716
+ if (isFileExcluded(relative2, excludeDirs, excludePackages, excludeFiles))
184
717
  continue;
185
718
  const content = readFileSync(file, "utf-8");
186
- violations.push(...findViolations(relative, content));
719
+ violations.push(...findViolations(relative2, content));
187
720
  }
188
721
  return { violations, print: () => printViolations(violations) };
189
722
  }
190
723
  export {
191
724
  suggestFix,
725
+ resetLogger,
726
+ logger,
192
727
  isLineClean,
193
- checkNoAsCasting
728
+ checkNoAsCasting,
729
+ checkCssVars,
730
+ checkCssUnused,
731
+ checkCssQuality,
732
+ checkCssLayout
194
733
  };
195
734
 
196
- //# debugId=8394B2630EA383EC64756E2164756E21
735
+ //# debugId=99E4430007A4308464756E2164756E21