@monoes/monodesign 1.2.18 → 1.2.22

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 (104) hide show
  1. package/README.md +3 -3
  2. package/cli/engine/design-system-checks.mjs +381 -0
  3. package/cli/engine/design-system-parse.mjs +433 -0
  4. package/cli/engine/design-system.mjs +20 -736
  5. package/cli/engine/detect-antipatterns-browser.js +12 -1
  6. package/cli/engine/engines/regex/detect-text-matchers.mjs +371 -0
  7. package/cli/engine/engines/regex/detect-text-run.mjs +186 -0
  8. package/cli/engine/engines/regex/detect-text.mjs +11 -523
  9. package/cli/engine/engines/static-html/css-cascade-color.mjs +219 -0
  10. package/cli/engine/engines/static-html/css-cascade-static-data.mjs +138 -0
  11. package/cli/engine/engines/static-html/css-cascade-static-dom.mjs +132 -0
  12. package/cli/engine/engines/static-html/css-cascade-static-parse.mjs +393 -0
  13. package/cli/engine/engines/static-html/css-cascade.mjs +24 -837
  14. package/cli/engine/registry/antipatterns-provider.mjs +60 -0
  15. package/cli/engine/registry/antipatterns-quality.mjs +218 -0
  16. package/cli/engine/registry/antipatterns-slop.mjs +226 -0
  17. package/cli/engine/registry/antipatterns.mjs +7 -494
  18. package/cli/engine/rules/checks-background.mjs +155 -0
  19. package/cli/engine/rules/checks-css-values.mjs +137 -0
  20. package/cli/engine/rules/checks-dom-effects.mjs +243 -0
  21. package/cli/engine/rules/checks-dom.mjs +148 -0
  22. package/cli/engine/rules/checks-element.mjs +286 -0
  23. package/cli/engine/rules/checks-html.mjs +204 -0
  24. package/cli/engine/rules/checks-overflow.mjs +263 -0
  25. package/cli/engine/rules/checks-page.mjs +465 -0
  26. package/cli/engine/rules/checks-pure.mjs +453 -0
  27. package/cli/engine/rules/checks-quality.mjs +447 -0
  28. package/cli/engine/rules/checks-stylesheet.mjs +324 -0
  29. package/cli/engine/rules/checks.mjs +98 -2939
  30. package/cli/lib/monodesign-config-consent.mjs +93 -0
  31. package/cli/lib/monodesign-config-core.mjs +37 -0
  32. package/cli/lib/monodesign-config-detection.mjs +133 -0
  33. package/cli/lib/monodesign-config-filter.mjs +198 -0
  34. package/cli/lib/monodesign-config-ignore-value.mjs +212 -0
  35. package/cli/lib/monodesign-config.mjs +21 -623
  36. package/package.json +2 -2
  37. package/skill/scripts/context-fields.mjs +65 -0
  38. package/skill/scripts/context-resolve.mjs +425 -0
  39. package/skill/scripts/context-update.mjs +151 -0
  40. package/skill/scripts/context-utils.mjs +68 -0
  41. package/skill/scripts/context-workspace.mjs +245 -0
  42. package/skill/scripts/context.mjs +17 -891
  43. package/skill/scripts/hook-admin-config.mjs +152 -0
  44. package/skill/scripts/hook-admin-ignore.mjs +131 -0
  45. package/skill/scripts/hook-admin-manifest.mjs +292 -0
  46. package/skill/scripts/hook-admin.mjs +8 -541
  47. package/skill/scripts/hook-before-edit-shell.mjs +184 -0
  48. package/skill/scripts/hook-before-edit.mjs +9 -165
  49. package/skill/scripts/hook-lib-cache.mjs +145 -0
  50. package/skill/scripts/hook-lib-config.mjs +292 -0
  51. package/skill/scripts/hook-lib-detector.mjs +72 -0
  52. package/skill/scripts/hook-lib-events.mjs +182 -0
  53. package/skill/scripts/hook-lib-findings.mjs +201 -0
  54. package/skill/scripts/hook-lib-ignore.mjs +205 -0
  55. package/skill/scripts/hook-lib-render.mjs +273 -0
  56. package/skill/scripts/hook-lib-run.mjs +281 -0
  57. package/skill/scripts/hook-lib-scan-targets.mjs +128 -0
  58. package/skill/scripts/hook-lib.mjs +71 -1747
  59. package/skill/scripts/lib/design-parser-colors.mjs +182 -0
  60. package/skill/scripts/lib/design-parser-extract.mjs +333 -0
  61. package/skill/scripts/lib/design-parser-sections.mjs +214 -0
  62. package/skill/scripts/lib/design-parser-yaml.mjs +135 -0
  63. package/skill/scripts/lib/design-parser.mjs +10 -783
  64. package/skill/scripts/live/manual-apply-chunk.mjs +151 -0
  65. package/skill/scripts/live/manual-apply-compact.mjs +122 -0
  66. package/skill/scripts/live/manual-apply-evidence.mjs +115 -0
  67. package/skill/scripts/live/manual-apply-log.mjs +179 -0
  68. package/skill/scripts/live/manual-apply-transaction.mjs +122 -0
  69. package/skill/scripts/live/manual-apply.mjs +65 -640
  70. package/skill/scripts/live/svelte-component-accept.mjs +248 -0
  71. package/skill/scripts/live/svelte-component-css.mjs +230 -0
  72. package/skill/scripts/live/svelte-component-markup.mjs +104 -0
  73. package/skill/scripts/live/svelte-component-scaffold.mjs +118 -0
  74. package/skill/scripts/live/svelte-component-session.mjs +158 -0
  75. package/skill/scripts/live/svelte-component.mjs +32 -792
  76. package/skill/scripts/live-accept-mutate.mjs +152 -0
  77. package/skill/scripts/live-accept-parse.mjs +370 -0
  78. package/skill/scripts/live-accept-scrub.mjs +59 -0
  79. package/skill/scripts/live-accept-search.mjs +58 -0
  80. package/skill/scripts/live-accept.mjs +15 -631
  81. package/skill/scripts/live-commit-manual-edits-paths.mjs +27 -0
  82. package/skill/scripts/live-commit-manual-edits-repair.mjs +166 -0
  83. package/skill/scripts/live-commit-manual-edits-rollback.mjs +231 -0
  84. package/skill/scripts/live-commit-manual-edits-utils.mjs +167 -0
  85. package/skill/scripts/live-commit-manual-edits-verify.mjs +356 -0
  86. package/skill/scripts/live-commit-manual-edits.mjs +24 -881
  87. package/skill/scripts/live-copy-edit-agent.mjs +16 -551
  88. package/skill/scripts/live-copy-edit-compact.mjs +137 -0
  89. package/skill/scripts/live-copy-edit-post-apply.mjs +175 -0
  90. package/skill/scripts/live-copy-edit-runners.mjs +297 -0
  91. package/skill/scripts/live-inject-csp.mjs +132 -0
  92. package/skill/scripts/live-inject-files.mjs +229 -0
  93. package/skill/scripts/live-inject-tag.mjs +114 -0
  94. package/skill/scripts/live-inject.mjs +13 -440
  95. package/skill/scripts/live-server-poll.mjs +232 -0
  96. package/skill/scripts/live-server-routes.mjs +449 -0
  97. package/skill/scripts/live-server-state.mjs +361 -0
  98. package/skill/scripts/live-server.mjs +10 -946
  99. package/skill/scripts/live-wrap-css.mjs +132 -0
  100. package/skill/scripts/live-wrap-manual-edit.mjs +153 -0
  101. package/skill/scripts/live-wrap-search.mjs +247 -0
  102. package/skill/scripts/live-wrap.mjs +21 -484
  103. package/skill/scripts/palette-seeds.mjs +401 -0
  104. package/skill/scripts/palette.mjs +1 -395
@@ -0,0 +1,137 @@
1
+ function buildCustomPropMap(document) {
2
+ const map = new Map();
3
+ let sheets;
4
+ try { sheets = Array.from(document.styleSheets || []); }
5
+ catch { return map; }
6
+ for (const sheet of sheets) {
7
+ let rules;
8
+ try { rules = Array.from(sheet.cssRules || []); }
9
+ catch { continue; }
10
+ for (const rule of rules) {
11
+ // Style rules only (type 1). Walk @media / @supports if present.
12
+ if (rule.type === 4 /* MEDIA_RULE */ || rule.type === 12 /* SUPPORTS_RULE */) {
13
+ try { rules.push(...Array.from(rule.cssRules || [])); } catch { /* ignore */ }
14
+ continue;
15
+ }
16
+ if (rule.type !== 1 /* STYLE_RULE */) continue;
17
+ const sel = rule.selectorText || '';
18
+ if (!/(^|,\s*)(:root|html|:host)\b/i.test(sel)) continue;
19
+ const style = rule.style;
20
+ if (!style) continue;
21
+ for (let i = 0; i < style.length; i++) {
22
+ const prop = style[i];
23
+ if (!prop?.startsWith('--')) continue;
24
+ const val = style.getPropertyValue(prop).trim();
25
+ if (val) map.set(prop, val);
26
+ }
27
+ }
28
+ }
29
+ return map;
30
+ }
31
+
32
+ // Resolve var(--X[, fallback]) refs in a computed-style value string.
33
+ // Recurses up to 8 levels for chained refs (--a: var(--b)). Returns
34
+ // the original string when no refs are present or the chain doesn't
35
+ // resolve. Safe to call on already-resolved values.
36
+ function resolveVarRefs(raw, customPropMap, depth = 0) {
37
+ if (typeof raw !== 'string' || !raw.includes('var(')) return raw;
38
+ if (depth > 8) return raw;
39
+ return raw.replace(/var\(\s*(--[a-zA-Z0-9_-]+)\s*(?:,\s*([^)]+))?\)/g, (_m, name, fallback) => {
40
+ const v = customPropMap.get(name);
41
+ if (v != null) return resolveVarRefs(v, customPropMap, depth + 1);
42
+ return fallback ? resolveVarRefs(fallback.trim(), customPropMap, depth + 1) : _m;
43
+ });
44
+ }
45
+
46
+ // OKLCH → sRGB conversion (Björn Ottosson's matrices). L in 0..1 (or %),
47
+ // C in 0..~0.4 typical, H in degrees. Returns clamped {r,g,b,a:1} in 0..255.
48
+ // Needed because jsdom doesn't compute oklch() values — getComputedStyle
49
+ // returns the literal "oklch(...)" string. Without this, the entire
50
+ // Tailwind v4 color palette (which is OKLCH-based) is invisible to the
51
+ // detector's contrast / color checks.
52
+ function oklchToRgb(L, C, H) {
53
+ const hRad = (H * Math.PI) / 180;
54
+ const a = C * Math.cos(hRad);
55
+ const b = C * Math.sin(hRad);
56
+ const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
57
+ const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
58
+ const s_ = L - 0.0894841775 * a - 1.2914855480 * b;
59
+ const lc = l_ * l_ * l_, mc = m_ * m_ * m_, sc = s_ * s_ * s_;
60
+ const rLin = 4.0767416621 * lc - 3.3077115913 * mc + 0.2309699292 * sc;
61
+ const gLin = -1.2684380046 * lc + 2.6097574011 * mc - 0.3413193965 * sc;
62
+ const bLin = -0.0041960863 * lc - 0.7034186147 * mc + 1.7076147010 * sc;
63
+ const enc = (x) => {
64
+ const c = Math.max(0, Math.min(1, x));
65
+ return c <= 0.0031308 ? 12.92 * c : 1.055 * c ** (1 / 2.4) - 0.055;
66
+ };
67
+ return {
68
+ r: Math.round(enc(rLin) * 255),
69
+ g: Math.round(enc(gLin) * 255),
70
+ b: Math.round(enc(bLin) * 255),
71
+ a: 1,
72
+ };
73
+ }
74
+
75
+ // Extended color parser: rgb/rgba/hex/oklch. Returns null on no match.
76
+ // Use this when the input might be any CSS color form; use plain parseRgb
77
+ // when you only expect computed rgb() values from real browsers.
78
+ function parseAnyColor(s) {
79
+ if (!s || typeof s !== 'string') return null;
80
+ const str = s.trim();
81
+ if (str === 'transparent' || str === 'currentcolor' || str === 'inherit') return null;
82
+ let m;
83
+ m = str.match(/rgba?\(\s*(\d+(?:\.\d+)?)\s*,?\s*(\d+(?:\.\d+)?)\s*,?\s*(\d+(?:\.\d+)?)(?:\s*[,/]\s*([\d.]+))?\s*\)/);
84
+ if (m) return { r: Math.round(+m[1]), g: Math.round(+m[2]), b: Math.round(+m[3]), a: m[4] !== undefined ? +m[4] : 1 };
85
+ m = str.match(/^#([0-9a-f]{3,8})$/i);
86
+ if (m) {
87
+ const h = m[1];
88
+ if (h.length === 3 || h.length === 4) {
89
+ return {
90
+ r: parseInt(h[0] + h[0], 16),
91
+ g: parseInt(h[1] + h[1], 16),
92
+ b: parseInt(h[2] + h[2], 16),
93
+ a: h.length === 4 ? parseInt(h[3] + h[3], 16) / 255 : 1,
94
+ };
95
+ }
96
+ if (h.length === 6 || h.length === 8) {
97
+ return {
98
+ r: parseInt(h.slice(0, 2), 16),
99
+ g: parseInt(h.slice(2, 4), 16),
100
+ b: parseInt(h.slice(4, 6), 16),
101
+ a: h.length === 8 ? parseInt(h.slice(6, 8), 16) / 255 : 1,
102
+ };
103
+ }
104
+ }
105
+ // OKLCH parser. Tailwind v4's CSS minifier squishes the space after
106
+ // `%` ("21.5%.02 50"), so the separator between L and C may be absent.
107
+ // Match L (with optional %), then C and H separated permissively.
108
+ m = str.match(/oklch\(\s*([\d.]+)(%?)\s*[\s,]*\s*([\d.]+)\s*[\s,]+\s*([-\d.]+)(?:deg)?(?:\s*\/\s*([\d.]+)(%)?)?\s*\)/i);
109
+ if (m) {
110
+ const Lnum = parseFloat(m[1]);
111
+ const L = m[2] === '%' ? Lnum / 100 : Lnum;
112
+ const rgb = oklchToRgb(L, parseFloat(m[3]), parseFloat(m[4]));
113
+ if (m[5] !== undefined) {
114
+ const alpha = parseFloat(m[5]);
115
+ rgb.a = m[6] === '%' ? alpha / 100 : alpha;
116
+ }
117
+ return rgb;
118
+ }
119
+ return null;
120
+ }
121
+
122
+ // Resolve var() refs in a color string (via customPropMap), then parse.
123
+ // Returns null on any failure. Used in jsdom-mode paths where
124
+ // getComputedStyle returns literal "var(--X)" or "oklch(...)" strings.
125
+ function parseColorResolved(str, customPropMap) {
126
+ if (!str) return null;
127
+ const resolved = customPropMap ? resolveVarRefs(str, customPropMap) : str;
128
+ return parseAnyColor(resolved);
129
+ }
130
+
131
+ export {
132
+ buildCustomPropMap,
133
+ resolveVarRefs,
134
+ oklchToRgb,
135
+ parseAnyColor,
136
+ parseColorResolved,
137
+ };
@@ -0,0 +1,243 @@
1
+ import { SAFE_TAGS } from '../shared/constants.mjs';
2
+ import {
3
+ getHue,
4
+ hasChroma,
5
+ parseGradientColors,
6
+ parseRgb,
7
+ relativeLuminance,
8
+ } from '../shared/color.mjs';
9
+ import {
10
+ HEADING_TAGS,
11
+ checkRepeatedSectionKickers,
12
+ checkMotion,
13
+ checkGlow,
14
+ } from './checks-pure.mjs';
15
+ import { resolveBackground } from './checks-background.mjs';
16
+ import { resolveLengthPx } from './checks-quality.mjs';
17
+
18
+ const REPEATED_KICKER_SKIP_SELECTOR = [
19
+ 'nav',
20
+ 'form',
21
+ 'table',
22
+ 'thead',
23
+ 'tbody',
24
+ 'tfoot',
25
+ 'figure',
26
+ 'figcaption',
27
+ 'ol',
28
+ 'ul',
29
+ 'li',
30
+ '[role="navigation"]',
31
+ '[aria-label*="breadcrumb" i]',
32
+ '[class*="breadcrumb" i]',
33
+ '[aria-hidden="true"]',
34
+ '[data-monodesign-allow-kickers]',
35
+ ].join(',');
36
+
37
+ const REPEATED_KICKER_CARD_CONTEXT_SELECTOR = [
38
+ 'article',
39
+ 'button',
40
+ 'a',
41
+ 'li',
42
+ '[role="listitem"]',
43
+ '[role="option"]',
44
+ ].join(',');
45
+
46
+ function cleanInlineText(el) {
47
+ return [...el.childNodes]
48
+ .filter(n => n.nodeType === 3)
49
+ .map(n => n.textContent)
50
+ .join(' ')
51
+ .replace(/\s+/g, ' ')
52
+ .trim();
53
+ }
54
+
55
+ function isRepeatedKickerCardContext(heading, kicker) {
56
+ const item = heading.closest?.(REPEATED_KICKER_CARD_CONTEXT_SELECTOR);
57
+ return Boolean(item && (!item.contains || item.contains(kicker)));
58
+ }
59
+
60
+ function isRepeatedKickerCandidate(opts) {
61
+ const {
62
+ headingTag,
63
+ headingText,
64
+ headingFontSize,
65
+ kickerTag,
66
+ kickerText,
67
+ kickerTextTransform,
68
+ kickerFontSize,
69
+ kickerLetterSpacing,
70
+ } = opts;
71
+ if (!['h2', 'h3', 'h4'].includes(headingTag)) return false;
72
+ if (!headingText || headingText.length < 3) return false;
73
+ if (/^\/[\w-]+/i.test(headingText.replace(/^"|"$/g, '').trim())) return false;
74
+ if (!(headingFontSize >= 20)) return false;
75
+ if (!kickerTag || HEADING_TAGS.has(kickerTag)) return false;
76
+ if (!['p', 'span', 'div', 'small'].includes(kickerTag)) return false;
77
+ if (!kickerText || kickerText.length < 2 || kickerText.length > 34) return false;
78
+ if (/^step\s*\d+/i.test(kickerText) || /^\d{1,2}$/.test(kickerText)) return false;
79
+
80
+ const isUppercased = kickerTextTransform === 'uppercase'
81
+ || (/[A-Z]/.test(kickerText) && !/[a-z]/.test(kickerText));
82
+ if (!isUppercased) return false;
83
+ if (!(kickerFontSize > 0 && kickerFontSize <= 14)) return false;
84
+ const minTrackedSpacing = Math.max(1, kickerFontSize * 0.08);
85
+ if (!(kickerLetterSpacing >= minTrackedSpacing)) return false;
86
+ return true;
87
+ }
88
+
89
+ function collectRepeatedSectionKickerCandidates(doc, getStyle, resolveLetterSpacing) {
90
+ const candidates = [];
91
+ for (const heading of doc.querySelectorAll('h2, h3, h4')) {
92
+ if (heading.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
93
+ const kicker = heading.previousElementSibling;
94
+ if (!kicker || kicker.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
95
+ if (isRepeatedKickerCardContext(heading, kicker)) continue;
96
+
97
+ const headingStyle = getStyle(heading);
98
+ const kickerStyle = getStyle(kicker);
99
+ const headingText = (heading.textContent || '').replace(/\s+/g, ' ').trim();
100
+ const kickerText = cleanInlineText(kicker) || (kicker.textContent || '').replace(/\s+/g, ' ').trim();
101
+ const headingFontSize = resolveLetterSpacing(headingStyle.fontSize || '', 16) || parseFloat(headingStyle.fontSize) || 0;
102
+ const kickerFontSize = resolveLetterSpacing(kickerStyle.fontSize || '', 16) || parseFloat(kickerStyle.fontSize) || 0;
103
+ const kickerLetterSpacing = resolveLetterSpacing(kickerStyle.letterSpacing || '', kickerFontSize);
104
+
105
+ if (!isRepeatedKickerCandidate({
106
+ headingTag: heading.tagName.toLowerCase(),
107
+ headingText,
108
+ headingFontSize,
109
+ kickerTag: kicker.tagName.toLowerCase(),
110
+ kickerText,
111
+ kickerTextTransform: kickerStyle.textTransform || '',
112
+ kickerFontSize,
113
+ kickerLetterSpacing,
114
+ })) {
115
+ continue;
116
+ }
117
+
118
+ candidates.push({
119
+ headingTag: heading.tagName.toLowerCase(),
120
+ headingText: headingText.replace(/^"|"$/g, '').slice(0, 60),
121
+ kickerText: kickerText.slice(0, 40),
122
+ });
123
+ }
124
+ return candidates;
125
+ }
126
+
127
+ function checkRepeatedSectionKickersDOM() {
128
+ const candidates = collectRepeatedSectionKickerCandidates(
129
+ document,
130
+ (el) => getComputedStyle(el),
131
+ (value, fontSize) => resolveLengthPx(value, fontSize) || 0,
132
+ );
133
+ return checkRepeatedSectionKickers({ candidates });
134
+ }
135
+
136
+ function checkElementMotionDOM(el) {
137
+ const tag = el.tagName.toLowerCase();
138
+ if (SAFE_TAGS.has(tag)) return [];
139
+ const style = getComputedStyle(el);
140
+ return checkMotion({
141
+ tag,
142
+ transitionProperty: style.transitionProperty || '',
143
+ animationName: style.animationName || '',
144
+ timingFunctions: [style.animationTimingFunction, style.transitionTimingFunction].filter(Boolean).join(' '),
145
+ classList: el.getAttribute('class') || '',
146
+ });
147
+ }
148
+
149
+ function checkElementGlowDOM(el) {
150
+ const tag = el.tagName.toLowerCase();
151
+ const style = getComputedStyle(el);
152
+ if (!style.boxShadow || style.boxShadow === 'none') return [];
153
+ // Use parent's background — glow radiates outward, so the surrounding context matters
154
+ // If resolveBackground returns null (gradient), try to infer from the gradient colors
155
+ let parentBg = el.parentElement ? resolveBackground(el.parentElement) : resolveBackground(el);
156
+ if (!parentBg) {
157
+ // Gradient background — sample its colors to determine if it's dark
158
+ let cur = el.parentElement;
159
+ while (cur && cur.nodeType === 1) {
160
+ const bgImage = getComputedStyle(cur).backgroundImage || '';
161
+ const gradColors = parseGradientColors(bgImage);
162
+ if (gradColors.length > 0) {
163
+ // Average the gradient colors
164
+ const avg = { r: 0, g: 0, b: 0 };
165
+ for (const c of gradColors) { avg.r += c.r; avg.g += c.g; avg.b += c.b; }
166
+ avg.r = Math.round(avg.r / gradColors.length);
167
+ avg.g = Math.round(avg.g / gradColors.length);
168
+ avg.b = Math.round(avg.b / gradColors.length);
169
+ parentBg = avg;
170
+ break;
171
+ }
172
+ cur = cur.parentElement;
173
+ }
174
+ }
175
+ return checkGlow({ tag, boxShadow: style.boxShadow, effectiveBg: parentBg });
176
+ }
177
+
178
+ function checkElementAIPaletteDOM(el) {
179
+ const style = getComputedStyle(el);
180
+ const findings = [];
181
+
182
+ // Check gradient backgrounds for purple/violet or cyan
183
+ const bgImage = style.backgroundImage || '';
184
+ const gradColors = parseGradientColors(bgImage);
185
+ for (const c of gradColors) {
186
+ if (hasChroma(c, 50)) {
187
+ const hue = getHue(c);
188
+ if (hue >= 260 && hue <= 310) {
189
+ findings.push({ id: 'ai-color-palette', snippet: 'Purple/violet gradient background' });
190
+ break;
191
+ }
192
+ if (hue >= 160 && hue <= 200) {
193
+ findings.push({ id: 'ai-color-palette', snippet: 'Cyan gradient background' });
194
+ break;
195
+ }
196
+ }
197
+ }
198
+
199
+ // Check for neon text (vivid cyan/purple color on dark background)
200
+ const textColor = parseRgb(style.color);
201
+ if (textColor && hasChroma(textColor, 80)) {
202
+ const hue = getHue(textColor);
203
+ const isAIPalette = (hue >= 160 && hue <= 200) || (hue >= 260 && hue <= 310);
204
+ if (isAIPalette) {
205
+ const parentBg = el.parentElement ? resolveBackground(el.parentElement) : null;
206
+ // Also check gradient parents
207
+ let effectiveBg = parentBg;
208
+ if (!effectiveBg) {
209
+ let cur = el.parentElement;
210
+ while (cur && cur.nodeType === 1) {
211
+ const gi = getComputedStyle(cur).backgroundImage || '';
212
+ const gc = parseGradientColors(gi);
213
+ if (gc.length > 0) {
214
+ const avg = { r: 0, g: 0, b: 0 };
215
+ for (const c of gc) { avg.r += c.r; avg.g += c.g; avg.b += c.b; }
216
+ avg.r = Math.round(avg.r / gc.length);
217
+ avg.g = Math.round(avg.g / gc.length);
218
+ avg.b = Math.round(avg.b / gc.length);
219
+ effectiveBg = avg;
220
+ break;
221
+ }
222
+ cur = cur.parentElement;
223
+ }
224
+ }
225
+ if (effectiveBg && relativeLuminance(effectiveBg) < 0.1) {
226
+ const label = hue >= 260 ? 'Purple/violet' : 'Cyan';
227
+ findings.push({ id: 'ai-color-palette', snippet: `${label} neon text on dark background` });
228
+ }
229
+ }
230
+ }
231
+
232
+ return findings;
233
+ }
234
+
235
+ export {
236
+ cleanInlineText,
237
+ isRepeatedKickerCandidate,
238
+ collectRepeatedSectionKickerCandidates,
239
+ checkRepeatedSectionKickersDOM,
240
+ checkElementMotionDOM,
241
+ checkElementGlowDOM,
242
+ checkElementAIPaletteDOM,
243
+ };
@@ -0,0 +1,148 @@
1
+ import { BORDER_SAFE_TAGS } from '../shared/constants.mjs';
2
+ import { parseRgb } from '../shared/color.mjs';
3
+ import {
4
+ checkBorders,
5
+ isEmojiOnlyText,
6
+ checkColors,
7
+ HEADING_TAGS,
8
+ checkIconTile,
9
+ checkItalicSerif,
10
+ checkHeroEyebrow,
11
+ } from './checks-pure.mjs';
12
+ import {
13
+ readOwnBackgroundColor,
14
+ resolveBackground,
15
+ resolveGradientStops,
16
+ } from './checks-background.mjs';
17
+
18
+ // ─── Section 5: Element Adapters ────────────────────────────────────────────
19
+
20
+ // Browser adapters — call getComputedStyle/getBoundingClientRect on live DOM
21
+
22
+ function checkElementBordersDOM(el) {
23
+ const tag = el.tagName.toLowerCase();
24
+ if (BORDER_SAFE_TAGS.has(tag)) return [];
25
+ const rect = el.getBoundingClientRect();
26
+ if (rect.width < 20 || rect.height < 20) return [];
27
+ const style = getComputedStyle(el);
28
+ const sides = ['Top', 'Right', 'Bottom', 'Left'];
29
+ const widths = {}, colors = {};
30
+ for (const s of sides) {
31
+ widths[s] = parseFloat(style[`border${s}Width`]) || 0;
32
+ colors[s] = style[`border${s}Color`] || '';
33
+ }
34
+ return checkBorders(tag, widths, colors, parseFloat(style.borderRadius) || 0);
35
+ }
36
+
37
+ function checkElementColorsDOM(el) {
38
+ const tag = el.tagName.toLowerCase();
39
+ // No early SAFE_TAGS bail here — checkColors() does its own gating that
40
+ // includes the styled-button exception for <a> / <button> with their own
41
+ // opaque background. Bailing here would prevent that exception from firing.
42
+ const rect = el.getBoundingClientRect();
43
+ if (rect.width < 10 || rect.height < 10) return [];
44
+ const style = getComputedStyle(el);
45
+ const directText = [...el.childNodes].filter(n => n.nodeType === 3).map(n => n.textContent).join('');
46
+ const hasDirectText = directText.trim().length > 0;
47
+ const effectiveBg = resolveBackground(el);
48
+ return checkColors({
49
+ tag,
50
+ textColor: parseRgb(style.color),
51
+ bgColor: readOwnBackgroundColor(el, style),
52
+ effectiveBg,
53
+ effectiveBgStops: effectiveBg ? null : resolveGradientStops(el),
54
+ fontSize: parseFloat(style.fontSize) || 16,
55
+ fontWeight: parseInt(style.fontWeight, 10) || 400,
56
+ hasDirectText,
57
+ isEmojiOnly: isEmojiOnlyText(directText),
58
+ bgClip: style.webkitBackgroundClip || style.backgroundClip || '',
59
+ bgImage: style.backgroundImage || '',
60
+ classList: el.getAttribute('class') || '',
61
+ });
62
+ }
63
+
64
+ function checkElementIconTileDOM(el) {
65
+ const tag = el.tagName.toLowerCase();
66
+ if (!HEADING_TAGS.has(tag)) return [];
67
+ const sibling = el.previousElementSibling;
68
+ if (!sibling) return [];
69
+
70
+ const sibRect = sibling.getBoundingClientRect();
71
+ const headRect = el.getBoundingClientRect();
72
+ const sibStyle = getComputedStyle(sibling);
73
+
74
+ // The tile may either contain an <svg>/<i> icon child, OR the tile itself
75
+ // may contain an emoji/symbol character directly as its only text content
76
+ // (the "card-icon" pattern from many AI-generated demos).
77
+ const iconChild = sibling.querySelector('svg, i[data-lucide], i[class*="fa-"], i[class*="icon"]');
78
+ const iconRect = iconChild?.getBoundingClientRect();
79
+ const sibDirectText = [...sibling.childNodes].filter(n => n.nodeType === 3).map(n => n.textContent).join('');
80
+ const hasInlineEmojiIcon = sibling.children.length === 0 && isEmojiOnlyText(sibDirectText);
81
+
82
+ return checkIconTile({
83
+ headingTag: tag,
84
+ headingText: el.textContent || '',
85
+ headingTop: headRect.top,
86
+ siblingTag: sibling.tagName.toLowerCase(),
87
+ siblingWidth: sibRect.width,
88
+ siblingHeight: sibRect.height,
89
+ siblingBottom: sibRect.bottom,
90
+ siblingBgColor: parseRgb(sibStyle.backgroundColor),
91
+ siblingBgImage: sibStyle.backgroundImage || '',
92
+ siblingBorderWidth: parseFloat(sibStyle.borderTopWidth) || 0,
93
+ siblingBorderRadius: parseFloat(sibStyle.borderRadius) || 0,
94
+ hasIconChild: !!iconChild || hasInlineEmojiIcon,
95
+ iconChildWidth: iconRect?.width || 0,
96
+ });
97
+ }
98
+
99
+ function checkElementItalicSerifDOM(el) {
100
+ const tag = el.tagName.toLowerCase();
101
+ if (tag !== 'h1' && tag !== 'h2') return [];
102
+ const style = getComputedStyle(el);
103
+ return checkItalicSerif({
104
+ tag,
105
+ fontStyle: style.fontStyle || '',
106
+ fontFamily: style.fontFamily || '',
107
+ fontSize: parseFloat(style.fontSize) || 0,
108
+ headingText: el.textContent || '',
109
+ });
110
+ }
111
+
112
+ function checkElementHeroEyebrowDOM(el) {
113
+ const tag = el.tagName.toLowerCase();
114
+ if (tag !== 'h1') return [];
115
+ const sibling = el.previousElementSibling;
116
+ if (!sibling) return [];
117
+ const headStyle = getComputedStyle(el);
118
+ const sibStyle = getComputedStyle(sibling);
119
+ return checkHeroEyebrow({
120
+ headingTag: tag,
121
+ headingText: el.textContent || '',
122
+ headingFontSize: parseFloat(headStyle.fontSize) || 0,
123
+ siblingTag: sibling.tagName.toLowerCase(),
124
+ siblingText: sibling.textContent || '',
125
+ siblingTextTransform: sibStyle.textTransform || '',
126
+ siblingFontSize: parseFloat(sibStyle.fontSize) || 0,
127
+ siblingLetterSpacing: parseFloat(sibStyle.letterSpacing) || 0,
128
+ siblingFontWeight: sibStyle.fontWeight || '',
129
+ siblingColor: sibStyle.color || '',
130
+ });
131
+ }
132
+
133
+ // Build a map of CSS custom properties declared on :root / :host / html.
134
+ // Used to resolve var(--X) refs that jsdom returns verbatim in
135
+ // getComputedStyle. Tailwind v4 routes every utility class through
136
+ // CSS vars (font-weight: var(--font-weight-bold), font-size:
137
+ // var(--text-xs), letter-spacing: var(--tracking-widest)), so without
138
+ // resolution every style-based check silently fails on Tailwind v4
139
+ // builds — the values come back as literal "var(--font-weight-bold)"
140
+ // strings and parseFloat returns NaN.
141
+
142
+ export {
143
+ checkElementBordersDOM,
144
+ checkElementColorsDOM,
145
+ checkElementIconTileDOM,
146
+ checkElementItalicSerifDOM,
147
+ checkElementHeroEyebrowDOM,
148
+ };