@bastani/atomic 0.9.0-alpha.3 → 0.9.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 (89) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/builtin/cursor/CHANGELOG.md +7 -0
  3. package/dist/builtin/cursor/package.json +2 -2
  4. package/dist/builtin/intercom/CHANGELOG.md +8 -0
  5. package/dist/builtin/intercom/package.json +1 -1
  6. package/dist/builtin/mcp/CHANGELOG.md +12 -0
  7. package/dist/builtin/mcp/package.json +1 -1
  8. package/dist/builtin/subagents/CHANGELOG.md +17 -0
  9. package/dist/builtin/subagents/package.json +1 -1
  10. package/dist/builtin/web-access/CHANGELOG.md +8 -0
  11. package/dist/builtin/web-access/package.json +1 -1
  12. package/dist/builtin/workflows/CHANGELOG.md +50 -0
  13. package/dist/builtin/workflows/README.md +12 -12
  14. package/dist/builtin/workflows/builtin/goal-prompts.ts +8 -0
  15. package/dist/builtin/workflows/builtin/goal-runner.ts +96 -1
  16. package/dist/builtin/workflows/builtin/goal-types.ts +2 -0
  17. package/dist/builtin/workflows/builtin/goal.d.ts +3 -0
  18. package/dist/builtin/workflows/builtin/goal.ts +12 -1
  19. package/dist/builtin/workflows/builtin/index.d.ts +8 -8
  20. package/dist/builtin/workflows/builtin/open-claude-design-feedback.ts +359 -0
  21. package/dist/builtin/workflows/builtin/open-claude-design-phases.ts +254 -352
  22. package/dist/builtin/workflows/builtin/open-claude-design-runner.ts +256 -414
  23. package/dist/builtin/workflows/builtin/open-claude-design-setup.ts +272 -0
  24. package/dist/builtin/workflows/builtin/open-claude-design-utils.ts +58 -68
  25. package/dist/builtin/workflows/builtin/open-claude-design.d.ts +5 -9
  26. package/dist/builtin/workflows/builtin/open-claude-design.ts +14 -26
  27. package/dist/builtin/workflows/package.json +1 -1
  28. package/dist/builtin/workflows/skills/impeccable/SKILL.md +14 -23
  29. package/dist/builtin/workflows/skills/impeccable/reference/brand.md +2 -2
  30. package/dist/builtin/workflows/skills/impeccable/reference/live.md +25 -4
  31. package/dist/builtin/workflows/skills/impeccable/scripts/context-signals.mjs +1 -1
  32. package/dist/builtin/workflows/skills/impeccable/scripts/context.mjs +724 -29
  33. package/dist/builtin/workflows/skills/impeccable/scripts/critique-storage.mjs +1 -1
  34. package/dist/builtin/workflows/skills/impeccable/scripts/detector/browser/injected/index.mjs +219 -7
  35. package/dist/builtin/workflows/skills/impeccable/scripts/detector/cli/main.mjs +57 -11
  36. package/dist/builtin/workflows/skills/impeccable/scripts/detector/design-system.mjs +750 -0
  37. package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +648 -53
  38. package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns.mjs +7 -0
  39. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/browser/detect-url.mjs +29 -4
  40. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +44 -11
  41. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +29 -0
  42. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +27 -1
  43. package/dist/builtin/workflows/skills/impeccable/scripts/detector/node/file-system.mjs +1 -1
  44. package/dist/builtin/workflows/skills/impeccable/scripts/detector/registry/antipatterns.mjs +29 -0
  45. package/dist/builtin/workflows/skills/impeccable/scripts/detector/rules/checks.mjs +401 -46
  46. package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/inline-ignores.mjs +148 -0
  47. package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/page.mjs +6 -6
  48. package/dist/builtin/workflows/skills/impeccable/scripts/{design-parser.mjs → lib/design-parser.mjs} +8 -1
  49. package/dist/builtin/workflows/skills/impeccable/scripts/lib/impeccable-config.mjs +638 -0
  50. package/dist/builtin/workflows/skills/impeccable/scripts/lib/impeccable-paths.mjs +128 -0
  51. package/dist/builtin/workflows/skills/impeccable/scripts/{is-generated.mjs → lib/is-generated.mjs} +2 -2
  52. package/dist/builtin/workflows/skills/impeccable/scripts/lib/target-args.mjs +42 -0
  53. package/dist/builtin/workflows/skills/impeccable/scripts/live/browser-script-parts.mjs +49 -0
  54. package/dist/builtin/workflows/skills/impeccable/scripts/{live-completion.mjs → live/completion.mjs} +1 -0
  55. package/dist/builtin/workflows/skills/impeccable/scripts/{live-event-validation.mjs → live/event-validation.mjs} +6 -5
  56. package/dist/builtin/workflows/skills/impeccable/scripts/live/manual-apply.mjs +939 -0
  57. package/dist/builtin/workflows/skills/impeccable/scripts/live/manual-edit-routes.mjs +357 -0
  58. package/dist/builtin/workflows/skills/impeccable/scripts/{live-manual-edits-buffer.mjs → live/manual-edits-buffer.mjs} +1 -1
  59. package/dist/builtin/workflows/skills/impeccable/scripts/{live-session-store.mjs → live/session-store.mjs} +21 -3
  60. package/dist/builtin/workflows/skills/impeccable/scripts/live/svelte-component.mjs +835 -0
  61. package/dist/builtin/workflows/skills/impeccable/scripts/live/sveltekit-adapter.mjs +274 -0
  62. package/dist/builtin/workflows/skills/impeccable/scripts/live/ui-core.mjs +180 -0
  63. package/dist/builtin/workflows/skills/impeccable/scripts/live/vocabulary.mjs +36 -0
  64. package/dist/builtin/workflows/skills/impeccable/scripts/live-accept.mjs +185 -60
  65. package/dist/builtin/workflows/skills/impeccable/scripts/live-browser-dom.js +146 -0
  66. package/dist/builtin/workflows/skills/impeccable/scripts/live-browser.js +3369 -1026
  67. package/dist/builtin/workflows/skills/impeccable/scripts/live-commit-manual-edits.mjs +2 -2
  68. package/dist/builtin/workflows/skills/impeccable/scripts/live-complete.mjs +2 -2
  69. package/dist/builtin/workflows/skills/impeccable/scripts/live-discard-manual-edits.mjs +1 -1
  70. package/dist/builtin/workflows/skills/impeccable/scripts/live-inject.mjs +133 -9
  71. package/dist/builtin/workflows/skills/impeccable/scripts/live-insert.mjs +42 -2
  72. package/dist/builtin/workflows/skills/impeccable/scripts/live-manual-edit-evidence.mjs +4 -4
  73. package/dist/builtin/workflows/skills/impeccable/scripts/live-poll.mjs +21 -15
  74. package/dist/builtin/workflows/skills/impeccable/scripts/live-resume.mjs +1 -1
  75. package/dist/builtin/workflows/skills/impeccable/scripts/live-server.mjs +205 -1269
  76. package/dist/builtin/workflows/skills/impeccable/scripts/live-status.mjs +2 -2
  77. package/dist/builtin/workflows/skills/impeccable/scripts/live-target.mjs +30 -0
  78. package/dist/builtin/workflows/skills/impeccable/scripts/live-wrap.mjs +69 -26
  79. package/dist/builtin/workflows/skills/impeccable/scripts/live.mjs +73 -22
  80. package/dist/core/atomic-guide-command.d.ts.map +1 -1
  81. package/dist/core/atomic-guide-command.js +5 -5
  82. package/dist/core/atomic-guide-command.js.map +1 -1
  83. package/docs/index.md +2 -2
  84. package/docs/quickstart.md +9 -9
  85. package/docs/workflows.md +42 -23
  86. package/package.json +2 -2
  87. package/dist/builtin/workflows/skills/impeccable/scripts/cleanup-deprecated.mjs +0 -284
  88. package/dist/builtin/workflows/skills/impeccable/scripts/impeccable-paths.mjs +0 -126
  89. /package/dist/builtin/workflows/skills/impeccable/scripts/{live-insert-ui.mjs → live/insert-ui.mjs} +0 -0
@@ -23,6 +23,13 @@ export {
23
23
  checkHtmlPatterns,
24
24
  } from './rules/checks.mjs';
25
25
  export { createDetectorProfile, summarizeDetectorProfile } from './profile/profiler.mjs';
26
+ export {
27
+ parseFrontmatter as parseDesignFrontmatter,
28
+ normalizeDesignSystem,
29
+ loadDesignSystemForCwd,
30
+ checkSourceDesignSystem,
31
+ collectStaticDesignSystemFindings,
32
+ } from './design-system.mjs';
26
33
  export { detectHtml } from './engines/static-html/detect-html.mjs';
27
34
  export { detectUrl, createBrowserDetector } from './engines/browser/detect-url.mjs';
28
35
  export { detectText, extractStyleBlocks, extractCSSinJS } from './engines/regex/detect-text.mjs';
@@ -7,6 +7,25 @@ import { filterByProviders } from '../../registry/antipatterns.mjs';
7
7
  import { profileFindingsAsync, profileStep, profileStepAsync } from '../../profile/profiler.mjs';
8
8
  import { captureVisualContrastCandidate } from '../visual/screenshot-contrast.mjs';
9
9
 
10
+ function serializeDesignSystemForBrowser(designSystem) {
11
+ if (!designSystem?.present) return null;
12
+ return {
13
+ present: true,
14
+ hasFonts: designSystem.hasFonts === true,
15
+ allowedFonts: Array.from(designSystem.allowedFonts || []),
16
+ hasColors: designSystem.hasColors === true,
17
+ allowedColors: Array.from(designSystem.allowedColorKeys?.values?.() || [])
18
+ .map(entry => entry?.color)
19
+ .filter(color => color && Number.isFinite(color.r) && Number.isFinite(color.g) && Number.isFinite(color.b))
20
+ .map(color => ({ r: color.r, g: color.g, b: color.b })),
21
+ hasRadii: designSystem.hasRadii === true,
22
+ allowedRadii: (designSystem.allowedRadii || [])
23
+ .map(entry => Number(entry?.px))
24
+ .filter(px => Number.isFinite(px)),
25
+ hasPillRadius: designSystem.hasPillRadius === true,
26
+ };
27
+ }
28
+
10
29
  async function runVisualContrastFallback(page, serializedGroups, options, profile, target) {
11
30
  if (options?.visualContrast === false) return [];
12
31
  const maxCandidates = Number.isFinite(options?.visualContrastMaxCandidates)
@@ -163,17 +182,19 @@ async function detectUrl(url, options = {}) {
163
182
  }
164
183
 
165
184
  // Inject the browser detection script and collect results
185
+ const browserDesignSystem = serializeDesignSystemForBrowser(options?.designSystem);
166
186
  await profileStepAsync(profile, {
167
187
  engine: 'browser',
168
188
  phase: 'scan',
169
189
  ruleId: 'configure-pure-detect',
170
190
  target: url,
171
- }, () => page.evaluate(() => {
191
+ }, () => page.evaluate((designSystem) => {
172
192
  window.__IMPECCABLE_CONFIG__ = {
173
193
  ...(window.__IMPECCABLE_CONFIG__ || {}),
174
194
  autoScan: false,
195
+ ...(designSystem ? { designSystem } : {}),
175
196
  };
176
- }));
197
+ }, browserDesignSystem));
177
198
  await profileStepAsync(profile, {
178
199
  engine: 'browser',
179
200
  phase: 'scan',
@@ -192,7 +213,7 @@ async function detectUrl(url, options = {}) {
192
213
  return window.impeccableDetect({ decorate: false, serialize: true });
193
214
  });
194
215
  return serializedGroups.flatMap(({ findings }) =>
195
- findings.map(f => ({ id: f.type, snippet: f.detail }))
216
+ findings.map(f => ({ id: f.type, snippet: f.detail, ignoreValue: f.ignoreValue || '' }))
196
217
  );
197
218
  });
198
219
  const visualFindings = await runVisualContrastFallback(page, serializedGroups, options, profile, url);
@@ -213,7 +234,11 @@ async function detectUrl(url, options = {}) {
213
234
  }, () => browser.close());
214
235
  }
215
236
  }
216
- return filterByProviders(results.map(f => finding(f.id, url, f.snippet)), options.providers);
237
+ return filterByProviders(results.map(f => {
238
+ const item = finding(f.id, url, f.snippet);
239
+ if (f.ignoreValue) item.ignoreValue = f.ignoreValue;
240
+ return item;
241
+ }), options.providers);
217
242
  }
218
243
 
219
244
  async function createBrowserDetector(options = {}) {
@@ -1,5 +1,8 @@
1
1
  import { GENERIC_FONTS } from '../../shared/constants.mjs';
2
+ import { isNeutralColor } from '../../shared/color.mjs';
3
+ import { checkSourceDesignSystem } from '../../design-system.mjs';
2
4
  import { isFullPage } from '../../shared/page.mjs';
5
+ import { applyInlineIgnores } from '../../shared/inline-ignores.mjs';
3
6
  import { finding } from '../../findings.mjs';
4
7
  import { filterByProviders } from '../../registry/antipatterns.mjs';
5
8
  import { profileFindings, profileStep } from '../../profile/profiler.mjs';
@@ -23,11 +26,24 @@ function stripHtmlToText(html) {
23
26
  .replace(/\s+/g, ' ');
24
27
  }
25
28
 
29
+ const PAGE_ANALYZER_EXTS = new Set(['.html', '.htm', '.astro', '.vue', '.svelte']);
30
+
31
+ function extFromFilePath(filePath) {
32
+ return filePath ? (filePath.match(/\.\w+$/)?.[0] || '').toLowerCase() : '';
33
+ }
34
+
35
+ function shouldRunPageAnalyzers(content, filePath) {
36
+ if (!isFullPage(content)) return false;
37
+ const ext = extFromFilePath(filePath);
38
+ return !ext || PAGE_ANALYZER_EXTS.has(ext);
39
+ }
40
+
26
41
  function isNeutralBorderColor(str) {
27
- const m = str.match(/solid\s+(#[0-9a-f]{3,8}|rgba?\([^)]+\)|\w+)/i);
42
+ const m = str.match(/solid\s+((?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color)\([^)]*\)|#[0-9a-f]{3,8}\b|[a-z]+)/i);
28
43
  if (!m) return false;
29
44
  const c = m[1].toLowerCase();
30
45
  if (['gray', 'grey', 'silver', 'white', 'black', 'transparent', 'currentcolor'].includes(c)) return true;
46
+ if (/^(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb)\(/i.test(c)) return isNeutralColor(c);
31
47
  const hex = c.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/);
32
48
  if (hex) {
33
49
  const [r, g, b] = [parseInt(hex[1], 16), parseInt(hex[2], 16), parseInt(hex[3], 16)];
@@ -44,10 +60,10 @@ function isNeutralBorderColor(str) {
44
60
  const REGEX_MATCHERS = [
45
61
  // --- Side-tab ---
46
62
  { id: 'side-tab', regex: /\bborder-[lrse]-(\d+)\b/g,
47
- test: (m, line) => { const n = +m[1]; return hasRounded(line) ? n >= 1 : n >= 4; },
63
+ test: (m, line) => { const n = +m[1]; return hasRounded(line) ? n >= 2 : n >= 4; },
48
64
  fmt: (m) => m[0] },
49
65
  { id: 'side-tab', regex: /border-(?:left|right)\s*:\s*(\d+)px\s+solid[^;]*/gi,
50
- test: (m, line) => { if (isSafeElement(line)) return false; if (isNeutralBorderColor(m[0])) return false; const n = +m[1]; return hasBorderRadius(line) ? n >= 1 : n >= 3; },
66
+ test: (m, line) => { if (isSafeElement(line)) return false; if (isNeutralBorderColor(m[0])) return false; const n = +m[1]; return hasBorderRadius(line) ? n >= 2 : n >= 3; },
51
67
  fmt: (m) => m[0].replace(/\s*;?\s*$/, '') },
52
68
  { id: 'side-tab', regex: /border-(?:left|right)-width\s*:\s*(\d+)px/gi,
53
69
  test: (m, line) => !isSafeElement(line) && +m[1] >= 3,
@@ -98,9 +114,14 @@ const REGEX_MATCHERS = [
98
114
  { id: 'bounce-easing', regex: /\banimate-bounce\b/g,
99
115
  test: () => true,
100
116
  fmt: () => 'animate-bounce (Tailwind)' },
101
- { id: 'bounce-easing', regex: /animation(?:-name)?\s*:\s*[^;]*\b(bounce|elastic|wobble|jiggle|spring)\b/gi,
117
+ { id: 'bounce-easing', regex: /animation(?:-name)?\s*:\s*([^;{}]*(?:bounce|elastic|wobble|jiggle|spring)[^;{}]*)/gi,
102
118
  test: () => true,
103
- fmt: (m) => m[0] },
119
+ fmt: (m) => {
120
+ const token = m[1]
121
+ .split(/[,\s]+/)
122
+ .find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
123
+ return `animation: ${token || m[1].trim()}`;
124
+ } },
104
125
  { id: 'bounce-easing', regex: /cubic-bezier\(\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*\)/g,
105
126
  test: (m) => {
106
127
  const y1 = parseFloat(m[2]), y2 = parseFloat(m[4]);
@@ -326,7 +347,7 @@ function extractStyleBlocks(content, ext) {
326
347
  ext = ext.toLowerCase();
327
348
  if (ext !== '.vue' && ext !== '.svelte') return [];
328
349
  const blocks = [];
329
- const re = /<style[^>]*>([\s\S]*?)<\/style>/gi;
350
+ const re = /<style[^>]*>([\s\S]*?)<\/style[^>]*>/gi;
330
351
  let m;
331
352
  while ((m = re.exec(content)) !== null) {
332
353
  const before = content.substring(0, m.index);
@@ -422,7 +443,7 @@ const TEXT_CONTENT_ANALYZER_IDS = [
422
443
 
423
444
  function runTextContentAnalyzers(content, filePath, options = {}) {
424
445
  const profile = options?.profile;
425
- if (!isFullPage(content)) return [];
446
+ if (!shouldRunPageAnalyzers(content, filePath)) return [];
426
447
  // The 4 text-content analyzers are at indices 3-6 in REGEX_ANALYZERS.
427
448
  const findings = [];
428
449
  for (let i = 0; i < TEXT_CONTENT_ANALYZER_IDS.length; i++) {
@@ -442,11 +463,11 @@ function detectText(content, filePath, options = {}) {
442
463
  const profile = options?.profile;
443
464
  const findings = [];
444
465
  const lines = content.split('\n');
445
- const ext = filePath ? (filePath.match(/\.\w+$/)?.[0] || '').toLowerCase() : '';
466
+ const ext = extFromFilePath(filePath);
446
467
 
447
468
  // Run regex matchers on the full file content (catches Tailwind classes, inline styles)
448
469
  // Enable block context for CSS files where related properties span multiple lines
449
- const cssLike = new Set(['.css', '.scss', '.less']);
470
+ const cssLike = new Set(['.css', '.scss', '.sass', '.less']);
450
471
  findings.push(...runRegexMatchers(lines, filePath, 0, cssLike.has(ext) || null, {
451
472
  profile,
452
473
  phase: 'source',
@@ -486,6 +507,15 @@ function detectText(content, filePath, options = {}) {
486
507
  }));
487
508
  }
488
509
 
510
+ if (options?.designSystem) {
511
+ findings.push(...profileFindings(profile, {
512
+ engine: 'regex',
513
+ phase: 'source',
514
+ ruleId: 'design-system',
515
+ target: filePath,
516
+ }, () => checkSourceDesignSystem(content, filePath, { designSystem: options.designSystem })));
517
+ }
518
+
489
519
  // Deduplicate findings (same antipattern + similar snippet, within 2 lines)
490
520
  const deduped = [];
491
521
  for (const f of findings) {
@@ -498,7 +528,7 @@ function detectText(content, filePath, options = {}) {
498
528
  }
499
529
 
500
530
  // Page-level analyzers only run on full pages
501
- if (isFullPage(content)) {
531
+ if (shouldRunPageAnalyzers(content, filePath)) {
502
532
  const analyzerIds = [
503
533
  'single-font',
504
534
  'flat-type-hierarchy',
@@ -520,7 +550,10 @@ function detectText(content, filePath, options = {}) {
520
550
  }
521
551
  }
522
552
 
523
- return filterByProviders(deduped, options?.providers);
553
+ const byProvider = filterByProviders(deduped, options?.providers);
554
+ // Inline `impeccable-disable*` waivers travel with the file; honor them unless
555
+ // explicitly bypassed (`--no-config` / `--no-inline-ignores`).
556
+ return options?.inlineIgnores === false ? byProvider : applyInlineIgnores(byProvider, content);
524
557
  }
525
558
 
526
559
  export {
@@ -267,7 +267,17 @@ const STATIC_DEFAULT_STYLE = {
267
267
  paddingRight: '0px',
268
268
  paddingBottom: '0px',
269
269
  paddingLeft: '0px',
270
+ marginTop: '0px',
271
+ marginRight: '0px',
272
+ marginBottom: '0px',
273
+ marginLeft: '0px',
270
274
  position: 'static',
275
+ visibility: 'visible',
276
+ top: 'auto',
277
+ right: 'auto',
278
+ bottom: 'auto',
279
+ left: 'auto',
280
+ inset: '',
271
281
  display: '',
272
282
  overflow: 'visible',
273
283
  overflowX: 'visible',
@@ -312,7 +322,17 @@ const STATIC_PROP_MAP = {
312
322
  'padding-right': 'paddingRight',
313
323
  'padding-bottom': 'paddingBottom',
314
324
  'padding-left': 'paddingLeft',
325
+ 'margin-top': 'marginTop',
326
+ 'margin-right': 'marginRight',
327
+ 'margin-bottom': 'marginBottom',
328
+ 'margin-left': 'marginLeft',
315
329
  'position': 'position',
330
+ 'visibility': 'visibility',
331
+ 'top': 'top',
332
+ 'right': 'right',
333
+ 'bottom': 'bottom',
334
+ 'left': 'left',
335
+ 'inset': 'inset',
316
336
  'display': 'display',
317
337
  'overflow': 'overflow',
318
338
  'overflow-x': 'overflowX',
@@ -579,6 +599,15 @@ function expandStaticDeclaration(prop, value) {
579
599
  ['paddingLeft', vals[3]],
580
600
  ];
581
601
  }
602
+ if (p === 'margin') {
603
+ const vals = expandStaticBoxValues(splitCssTokens(v));
604
+ return [
605
+ ['marginTop', vals[0]],
606
+ ['marginRight', vals[1]],
607
+ ['marginBottom', vals[2]],
608
+ ['marginLeft', vals[3]],
609
+ ];
610
+ }
582
611
  if (p === 'font') return parseStaticFont(v);
583
612
  if (p === 'transition') {
584
613
  const parsed = parseStaticTransition(v);
@@ -2,7 +2,13 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
 
4
4
  import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
5
+ import {
6
+ checkSourceDesignSystem,
7
+ collectStaticDesignSystemFindings,
8
+ mergeDesignSystemFindings,
9
+ } from '../../design-system.mjs';
5
10
  import { isFullPage } from '../../shared/page.mjs';
11
+ import { applyInlineIgnores } from '../../shared/inline-ignores.mjs';
6
12
  import { finding } from '../../findings.mjs';
7
13
  import { profileFindings, profileStep, profileStepAsync } from '../../profile/profiler.mjs';
8
14
  import {
@@ -168,6 +174,22 @@ async function detectHtml(filePath, options = {}) {
168
174
  }
169
175
  }
170
176
 
177
+ if (options?.designSystem) {
178
+ const sourceDesignFindings = profileFindings(profile, {
179
+ engine: 'static-html',
180
+ phase: 'source',
181
+ ruleId: 'design-system',
182
+ target: filePath,
183
+ }, () => checkSourceDesignSystem(html, filePath, { designSystem: options.designSystem }));
184
+ const staticDesignFindings = profileFindings(profile, {
185
+ engine: 'static-html',
186
+ phase: 'page',
187
+ ruleId: 'design-system',
188
+ target: filePath,
189
+ }, () => collectStaticDesignSystemFindings(document, window, filePath, options.designSystem));
190
+ findings.push(...mergeDesignSystemFindings(staticDesignFindings, sourceDesignFindings));
191
+ }
192
+
171
193
  if (isFullPage(html)) {
172
194
  const runPageCheck = (ruleId, callback) => profile
173
195
  ? profileFindings(profile, { engine: 'static-html', phase: 'page', ruleId, target: filePath }, callback)
@@ -202,7 +224,11 @@ async function detectHtml(filePath, options = {}) {
202
224
  }
203
225
  }
204
226
 
205
- return filterByProviders(findings, options.providers);
227
+ const byProvider = filterByProviders(findings, options.providers);
228
+ // Static-HTML findings carry no line number, so only whole-file
229
+ // `impeccable-disable` directives apply here — exactly the standalone-document
230
+ // waiver this primitive targets. Bypassed by `--no-config` / `--no-inline-ignores`.
231
+ return options?.inlineIgnores === false ? byProvider : applyInlineIgnores(byProvider, html);
206
232
  }
207
233
 
208
234
  export { checkStaticPageTypography, STATIC_ELEMENT_RULES, detectHtml };
@@ -11,7 +11,7 @@ const SKIP_DIRS = new Set([
11
11
  ]);
12
12
 
13
13
  const SCANNABLE_EXTENSIONS = new Set([
14
- '.html', '.htm', '.css', '.scss', '.less',
14
+ '.html', '.htm', '.css', '.scss', '.sass', '.less',
15
15
  '.jsx', '.tsx', '.js', '.ts',
16
16
  '.vue', '.svelte', '.astro',
17
17
  ]);
@@ -323,6 +323,35 @@ const ANTIPATTERNS = [
323
323
  skillSection: 'Layout & Space',
324
324
  skillGuideline: 'overflow container clipping positioned children',
325
325
  },
326
+ {
327
+ id: 'design-system-font',
328
+ category: 'quality',
329
+ name: 'Font outside DESIGN.md',
330
+ description:
331
+ 'A font is used that is not declared in DESIGN.md typography. Use the documented type system or update DESIGN.md if this is an intentional brand addition.',
332
+ skillSection: 'Typography',
333
+ skillGuideline: 'font family outside the project design system',
334
+ },
335
+ {
336
+ id: 'design-system-color',
337
+ category: 'quality',
338
+ severity: 'advisory',
339
+ name: 'Color outside DESIGN.md',
340
+ description:
341
+ 'A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.',
342
+ skillSection: 'Color & Contrast',
343
+ skillGuideline: 'literal color outside the project design system',
344
+ },
345
+ {
346
+ id: 'design-system-radius',
347
+ category: 'quality',
348
+ severity: 'advisory',
349
+ name: 'Radius outside DESIGN.md',
350
+ description:
351
+ 'A border-radius value is outside the DESIGN.md rounded scale. Use a documented radius token or update the design system if the new shape is intentional.',
352
+ skillSection: 'Visual Details',
353
+ skillGuideline: 'border radius outside the project design system',
354
+ },
326
355
 
327
356
  // ── Provider tells: opt-in via --gpt / --gemini (gated off by default) ──
328
357
  {