@monoes/monodesign 1.2.9 → 1.2.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/engine/browser/injected/index.mjs +16 -18
- package/cli/engine/cli/main.mjs +3 -3
- package/cli/engine/detect-antipatterns-browser.js +69 -49
- package/cli/engine/engines/regex/detect-text.mjs +6 -6
- package/cli/engine/engines/static-html/css-cascade.mjs +6 -5
- package/cli/engine/engines/static-html/detect-html.mjs +6 -6
- package/cli/engine/fix/index.mjs +3 -3
- package/cli/engine/node/file-system.mjs +1 -1
- package/cli/engine/registry/antipatterns.mjs +1 -1
- package/cli/engine/rules/checks.mjs +26 -27
- package/cli/engine/shared/color.mjs +2 -2
- package/cli/engine/shared/constants.mjs +1 -1
- package/cli/engine/shared/inline-ignores.mjs +2 -2
- package/cli/lib/monodesign-config.mjs +5 -5
- package/package.json +10 -9
- package/skill/scripts/context.mjs +5 -5
- package/skill/scripts/critique-storage.mjs +1 -1
- package/skill/scripts/detect-csp.mjs +1 -6
- package/skill/scripts/hook-admin.mjs +6 -6
- package/skill/scripts/hook-before-edit.mjs +4 -4
- package/skill/scripts/hook-lib.mjs +20 -11
- package/skill/scripts/hook.mjs +1 -1
- package/skill/scripts/lib/design-parser.mjs +9 -42
- package/skill/scripts/lib/is-generated.mjs +1 -1
- package/skill/scripts/lib/monodesign-paths.mjs +1 -1
- package/skill/scripts/live/event-validation.mjs +13 -13
- package/skill/scripts/live/manual-apply.mjs +4 -4
- package/skill/scripts/live/manual-edits-buffer.mjs +1 -1
- package/skill/scripts/live/session-store.mjs +5 -5
- package/skill/scripts/live/svelte-component.mjs +13 -13
- package/skill/scripts/live/sveltekit-adapter.mjs +4 -4
- package/skill/scripts/live/ui-core.mjs +1 -1
- package/skill/scripts/live-accept.mjs +24 -24
- package/skill/scripts/live-browser-dom.js +8 -7
- package/skill/scripts/live-browser-session.js +8 -7
- package/skill/scripts/live-browser.js +406 -433
- package/skill/scripts/live-commit-manual-edits.mjs +6 -6
- package/skill/scripts/live-copy-edit-agent.mjs +2 -2
- package/skill/scripts/live-discard-manual-edits.mjs +1 -1
- package/skill/scripts/live-inject.mjs +7 -7
- package/skill/scripts/live-insert.mjs +11 -11
- package/skill/scripts/live-manual-edit-evidence.mjs +4 -4
- package/skill/scripts/live-poll.mjs +5 -5
- package/skill/scripts/live-server.mjs +10 -10
- package/skill/scripts/live-wrap.mjs +31 -41
- package/skill/scripts/live.mjs +5 -5
- package/skill/scripts/palette.mjs +3 -1
- package/skill/scripts/pin.mjs +1 -1
|
@@ -112,7 +112,7 @@ if (IS_BROWSER) {
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
function showSpotlight(target) {
|
|
115
|
-
if (!target
|
|
115
|
+
if (!target?.getBoundingClientRect) return;
|
|
116
116
|
// Respect the spotlightBlur setting: if disabled, don't show the backdrop
|
|
117
117
|
if (window.__MONODESIGN_CONFIG__?.spotlightBlur === false) {
|
|
118
118
|
spotlightTarget = target;
|
|
@@ -258,10 +258,8 @@ if (IS_BROWSER) {
|
|
|
258
258
|
}
|
|
259
259
|
});
|
|
260
260
|
|
|
261
|
-
const highlight =
|
|
261
|
+
const highlight = (el, findings) => {
|
|
262
262
|
if (el._monodesignOverlay) detachOverlay(el._monodesignOverlay);
|
|
263
|
-
const hasSlop = findings.some(f => RULE_CATEGORY[f.type || f.id] === 'slop');
|
|
264
|
-
|
|
265
263
|
const fixed = isInFixedContext(el);
|
|
266
264
|
const rect = el.getBoundingClientRect();
|
|
267
265
|
const outline = document.createElement('div');
|
|
@@ -405,7 +403,7 @@ if (IS_BROWSER) {
|
|
|
405
403
|
overlays.push(outline);
|
|
406
404
|
};
|
|
407
405
|
|
|
408
|
-
const showPageBanner =
|
|
406
|
+
const showPageBanner = (findings) => {
|
|
409
407
|
if (!findings.length) return;
|
|
410
408
|
const banner = document.createElement('div');
|
|
411
409
|
banner.className = 'monodesign-overlay monodesign-banner';
|
|
@@ -505,7 +503,7 @@ if (IS_BROWSER) {
|
|
|
505
503
|
.filter(c => !c.startsWith('monodesign-') && !isLikelyHashedClass(c))
|
|
506
504
|
.slice(0, 2);
|
|
507
505
|
if (classes.length > 0) {
|
|
508
|
-
sel +=
|
|
506
|
+
sel += `.${classes.map(c => CSS.escape(c)).join('.')}`;
|
|
509
507
|
}
|
|
510
508
|
}
|
|
511
509
|
|
|
@@ -513,7 +511,7 @@ if (IS_BROWSER) {
|
|
|
513
511
|
const parent = el.parentElement;
|
|
514
512
|
if (parent) {
|
|
515
513
|
try {
|
|
516
|
-
const matching = parent.querySelectorAll(
|
|
514
|
+
const matching = parent.querySelectorAll(`:scope > ${sel}`);
|
|
517
515
|
if (matching.length > 1) {
|
|
518
516
|
const sameType = [...parent.children].filter(c => c.tagName === el.tagName);
|
|
519
517
|
const idx = sameType.indexOf(el) + 1;
|
|
@@ -530,7 +528,7 @@ if (IS_BROWSER) {
|
|
|
530
528
|
function generateSelector(el) {
|
|
531
529
|
if (el === document.body) return 'body';
|
|
532
530
|
if (el === document.documentElement) return 'html';
|
|
533
|
-
if (el.id) return
|
|
531
|
+
if (el.id) return `#${CSS.escape(el.id)}`;
|
|
534
532
|
|
|
535
533
|
const parts = [];
|
|
536
534
|
let current = el;
|
|
@@ -542,7 +540,7 @@ if (IS_BROWSER) {
|
|
|
542
540
|
|
|
543
541
|
// Anchor on an ancestor's ID and stop walking up
|
|
544
542
|
if (current.id) {
|
|
545
|
-
parts[0] =
|
|
543
|
+
parts[0] = `#${CSS.escape(current.id)}`;
|
|
546
544
|
break;
|
|
547
545
|
}
|
|
548
546
|
|
|
@@ -682,7 +680,7 @@ if (IS_BROWSER) {
|
|
|
682
680
|
|
|
683
681
|
const textColor = parseRgb(style.color);
|
|
684
682
|
const fontSize = parseFloat(style.fontSize) || 16;
|
|
685
|
-
const fontWeight = parseInt(style.fontWeight) || 400;
|
|
683
|
+
const fontWeight = parseInt(style.fontWeight, 10) || 400;
|
|
686
684
|
const isLargeText = fontSize >= WCAG_LARGE_TEXT_PX || (fontSize >= WCAG_LARGE_BOLD_TEXT_PX && fontWeight >= 700);
|
|
687
685
|
const threshold = isLargeText ? 3.0 : 4.5;
|
|
688
686
|
const clip = {
|
|
@@ -900,7 +898,7 @@ if (IS_BROWSER) {
|
|
|
900
898
|
function sampleDrawablePixel(drawable, sourcePoint) {
|
|
901
899
|
if (visualContrastRasterCache.has(drawable)) {
|
|
902
900
|
const cached = visualContrastRasterCache.get(drawable);
|
|
903
|
-
if (!cached
|
|
901
|
+
if (!cached?.ctx) return { status: 'unresolved', reason: cached?.reason || 'image sample failed' };
|
|
904
902
|
try {
|
|
905
903
|
const x = Math.max(0, Math.min(cached.width - 1, Math.floor(sourcePoint.x * cached.scaleX)));
|
|
906
904
|
const y = Math.max(0, Math.min(cached.height - 1, Math.floor(sourcePoint.y * cached.scaleY)));
|
|
@@ -1035,7 +1033,7 @@ if (IS_BROWSER) {
|
|
|
1035
1033
|
const unresolved = [];
|
|
1036
1034
|
|
|
1037
1035
|
for (const node of nodes) {
|
|
1038
|
-
if (
|
|
1036
|
+
if (node?.nodeType !== 1) continue;
|
|
1039
1037
|
if (node.closest?.('.monodesign-overlay, .monodesign-label, .monodesign-banner, .monodesign-tooltip')) continue;
|
|
1040
1038
|
const tag = node.tagName?.toLowerCase();
|
|
1041
1039
|
if (tag === 'img') {
|
|
@@ -1232,7 +1230,7 @@ if (IS_BROWSER) {
|
|
|
1232
1230
|
}));
|
|
1233
1231
|
}
|
|
1234
1232
|
|
|
1235
|
-
const printSummary =
|
|
1233
|
+
const printSummary = (allFindings) => {
|
|
1236
1234
|
if (allFindings.length === 0) {
|
|
1237
1235
|
console.log('%c[monodesign] No anti-patterns found.', 'color: #22c55e; font-weight: bold');
|
|
1238
1236
|
return;
|
|
@@ -1559,7 +1557,7 @@ if (IS_BROWSER) {
|
|
|
1559
1557
|
try { rules = sheet.cssRules; } catch { continue; }
|
|
1560
1558
|
if (!rules) continue;
|
|
1561
1559
|
for (const rule of Array.from(rules)) {
|
|
1562
|
-
try { stylesheetCssText += rule.cssText
|
|
1560
|
+
try { stylesheetCssText += `${rule.cssText}\n`; } catch { /* ignore */ }
|
|
1563
1561
|
}
|
|
1564
1562
|
}
|
|
1565
1563
|
const stylesheetFindings = [
|
|
@@ -1832,7 +1830,7 @@ if (IS_BROWSER) {
|
|
|
1832
1830
|
}
|
|
1833
1831
|
|
|
1834
1832
|
let firstScanDone = false;
|
|
1835
|
-
const scan =
|
|
1833
|
+
const scan = (options = {}) => {
|
|
1836
1834
|
clearOverlays();
|
|
1837
1835
|
const generation = scanGeneration;
|
|
1838
1836
|
const collected = collectBrowserFindings();
|
|
@@ -1849,7 +1847,7 @@ if (IS_BROWSER) {
|
|
|
1849
1847
|
return allFindings;
|
|
1850
1848
|
};
|
|
1851
1849
|
|
|
1852
|
-
const scanAsync = async
|
|
1850
|
+
const scanAsync = async (options = {}) => {
|
|
1853
1851
|
clearOverlays();
|
|
1854
1852
|
const generation = scanGeneration;
|
|
1855
1853
|
if (shouldRunVisualContrast(options)) {
|
|
@@ -1861,13 +1859,13 @@ if (IS_BROWSER) {
|
|
|
1861
1859
|
return renderBrowserFindings(collectBrowserFindings(), options);
|
|
1862
1860
|
};
|
|
1863
1861
|
|
|
1864
|
-
const detect =
|
|
1862
|
+
const detect = (options = {}) => {
|
|
1865
1863
|
lastVisualContrastAnalyses = [];
|
|
1866
1864
|
const { allFindings } = collectBrowserFindings();
|
|
1867
1865
|
return options.serialize === false ? allFindings : serializeFindings(allFindings);
|
|
1868
1866
|
};
|
|
1869
1867
|
|
|
1870
|
-
const detectAsync = async
|
|
1868
|
+
const detectAsync = async (options = {}) => {
|
|
1871
1869
|
if (shouldRunVisualContrast(options)) {
|
|
1872
1870
|
const { allFindings } = await collectBrowserFindingsAsync(options);
|
|
1873
1871
|
return options.serialize === false ? allFindings : serializeFindings(allFindings);
|
package/cli/engine/cli/main.mjs
CHANGED
|
@@ -309,9 +309,9 @@ async function detectCli() {
|
|
|
309
309
|
allFindings = filterByScopes(allFindings, scopes);
|
|
310
310
|
|
|
311
311
|
if (allFindings.length > 0) {
|
|
312
|
-
if (jsonMode) process.stdout.write(formatFindings(allFindings, true)
|
|
313
|
-
else if (quietMode) process.stderr.write(formatFindingSummary(allFindings.length)
|
|
314
|
-
else process.stderr.write(formatFindings(allFindings, false)
|
|
312
|
+
if (jsonMode) process.stdout.write(`${formatFindings(allFindings, true)}\n`);
|
|
313
|
+
else if (quietMode) process.stderr.write(`${formatFindingSummary(allFindings.length)}\n`);
|
|
314
|
+
else process.stderr.write(`${formatFindings(allFindings, false)}\n`);
|
|
315
315
|
process.exit(2);
|
|
316
316
|
}
|
|
317
317
|
if (jsonMode) process.stdout.write('[]\n');
|
|
@@ -67,7 +67,7 @@ function isBrandFontOnOwnDomain(font) {
|
|
|
67
67
|
const allowed = BRAND_FONT_DOMAINS[font];
|
|
68
68
|
if (!allowed) return false;
|
|
69
69
|
const host = location.hostname.toLowerCase();
|
|
70
|
-
return allowed.some(suffix => host === suffix || host.endsWith(
|
|
70
|
+
return allowed.some(suffix => host === suffix || host.endsWith(`.${suffix}`));
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
const GENERIC_FONTS = new Set([
|
|
@@ -672,7 +672,7 @@ function contrastRatio(c1, c2) {
|
|
|
672
672
|
}
|
|
673
673
|
|
|
674
674
|
function parseGradientColors(bgImage) {
|
|
675
|
-
if (!bgImage
|
|
675
|
+
if (!bgImage?.includes('gradient')) return [];
|
|
676
676
|
const colors = [];
|
|
677
677
|
for (const m of bgImage.matchAll(/rgba?\([^)]+\)/g)) {
|
|
678
678
|
const c = parseRgb(m[0]);
|
|
@@ -709,7 +709,7 @@ function getHue(c) {
|
|
|
709
709
|
|
|
710
710
|
function colorToHex(c) {
|
|
711
711
|
if (!c) return '?';
|
|
712
|
-
return
|
|
712
|
+
return `#${[c.r, c.g, c.b].map(v => v.toString(16).padStart(2, '0')).join('')}`;
|
|
713
713
|
}
|
|
714
714
|
|
|
715
715
|
// --- cli/engine/shared/fonts.mjs ---
|
|
@@ -778,8 +778,8 @@ function checkBorders(tag, widths, colors, radius) {
|
|
|
778
778
|
// (plus whitespace / variation selectors). Emojis render as multicolor glyphs
|
|
779
779
|
// regardless of CSS `color`, so contrast checks against the element's text
|
|
780
780
|
// color are meaningless for these nodes.
|
|
781
|
-
const EMOJI_CHAR_RE = /[\u{1F1E6}-\u{1F1FF}\u{1F300}-\u{1F9FF}\u{1FA00}-\u{1FAFF}\u{2600}-\u{27BF}\u{2300}-\u{23FF}
|
|
782
|
-
const EMOJI_CHARS_GLOBAL = /[\u{1F1E6}-\u{1F1FF}\u{1F300}-\u{1F9FF}\u{1FA00}-\u{1FAFF}\u{2600}-\u{27BF}\u{2300}-\u{23FF}
|
|
781
|
+
const EMOJI_CHAR_RE = /(?:[\u{1F1E6}-\u{1F1FF}\u{1F300}-\u{1F9FF}\u{1FA00}-\u{1FAFF}\u{2600}-\u{27BF}\u{2300}-\u{23FF}]|\u{FE0F}|\u{200D}|[\u{1F3FB}-\u{1F3FF}])/u;
|
|
782
|
+
const EMOJI_CHARS_GLOBAL = /(?:[\u{1F1E6}-\u{1F1FF}\u{1F300}-\u{1F9FF}\u{1FA00}-\u{1FAFF}\u{2600}-\u{27BF}\u{2300}-\u{23FF}]|\u{FE0F}|\u{200D}|[\u{1F3FB}-\u{1F3FF}])/gu;
|
|
783
783
|
function isEmojiOnlyText(text) {
|
|
784
784
|
if (!text) return false;
|
|
785
785
|
if (!EMOJI_CHAR_RE.test(text)) return false;
|
|
@@ -805,7 +805,7 @@ function checkColors(opts) {
|
|
|
805
805
|
if (hasDirectText && textColor && !isEmojiOnly) {
|
|
806
806
|
// Run background-dependent checks against either a solid bg or, if the
|
|
807
807
|
// ancestor is a gradient, against every gradient stop (use the worst case).
|
|
808
|
-
const bgs = effectiveBg ? [effectiveBg] : (effectiveBgStops
|
|
808
|
+
const bgs = effectiveBg ? [effectiveBg] : (effectiveBgStops?.length ? effectiveBgStops : null);
|
|
809
809
|
if (bgs) {
|
|
810
810
|
// Gray on colored background — flag if every stop is chromatic
|
|
811
811
|
const textLum = relativeLuminance(textColor);
|
|
@@ -986,7 +986,6 @@ function isAccentColor(cssColor) {
|
|
|
986
986
|
if (!cssColor) return false;
|
|
987
987
|
const s = String(cssColor).trim();
|
|
988
988
|
// rgb / rgba — direct channel-distance check.
|
|
989
|
-
const rgbM = /rgba?\(\s*(\d+)\s*,?\s+|\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/.exec(s.replace(/rgba?\(\s*/, 'rgb(').replace(/,/g, ', '));
|
|
990
989
|
const rgbStrict = /rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/.exec(s);
|
|
991
990
|
if (rgbStrict) {
|
|
992
991
|
const r = +rgbStrict[1], g = +rgbStrict[2], b = +rgbStrict[3];
|
|
@@ -1031,7 +1030,7 @@ function isAccentColor(cssColor) {
|
|
|
1031
1030
|
// uppercase eyebrow OR the modern accent-colored bold eyebrow.
|
|
1032
1031
|
function checkHeroEyebrow(opts) {
|
|
1033
1032
|
const {
|
|
1034
|
-
headingTag, headingText,
|
|
1033
|
+
headingTag, headingText,
|
|
1035
1034
|
siblingTag, siblingText, siblingTextTransform,
|
|
1036
1035
|
siblingFontSize, siblingLetterSpacing,
|
|
1037
1036
|
siblingFontWeight, siblingColor,
|
|
@@ -1502,7 +1501,7 @@ function parseRadiusToPx(value, widthPx) {
|
|
|
1502
1501
|
return num;
|
|
1503
1502
|
}
|
|
1504
1503
|
|
|
1505
|
-
function resolveBorderRadiusPx(
|
|
1504
|
+
function resolveBorderRadiusPx(_el, style, widthPx, _win) {
|
|
1506
1505
|
const fromComputed = parseRadiusToPx(style.borderRadius, widthPx);
|
|
1507
1506
|
if (fromComputed !== null) return fromComputed;
|
|
1508
1507
|
return 0;
|
|
@@ -1545,7 +1544,7 @@ function checkElementColorsDOM(el) {
|
|
|
1545
1544
|
effectiveBg,
|
|
1546
1545
|
effectiveBgStops: effectiveBg ? null : resolveGradientStops(el),
|
|
1547
1546
|
fontSize: parseFloat(style.fontSize) || 16,
|
|
1548
|
-
fontWeight: parseInt(style.fontWeight) || 400,
|
|
1547
|
+
fontWeight: parseInt(style.fontWeight, 10) || 400,
|
|
1549
1548
|
hasDirectText,
|
|
1550
1549
|
isEmojiOnly: isEmojiOnlyText(directText),
|
|
1551
1550
|
bgClip: style.webkitBackgroundClip || style.backgroundClip || '',
|
|
@@ -1653,7 +1652,7 @@ function buildCustomPropMap(document) {
|
|
|
1653
1652
|
if (!style) continue;
|
|
1654
1653
|
for (let i = 0; i < style.length; i++) {
|
|
1655
1654
|
const prop = style[i];
|
|
1656
|
-
if (!prop
|
|
1655
|
+
if (!prop?.startsWith('--')) continue;
|
|
1657
1656
|
const val = style.getPropertyValue(prop).trim();
|
|
1658
1657
|
if (val) map.set(prop, val);
|
|
1659
1658
|
}
|
|
@@ -1695,7 +1694,7 @@ function oklchToRgb(L, C, H) {
|
|
|
1695
1694
|
const bLin = -0.0041960863 * lc - 0.7034186147 * mc + 1.7076147010 * sc;
|
|
1696
1695
|
const enc = (x) => {
|
|
1697
1696
|
const c = Math.max(0, Math.min(1, x));
|
|
1698
|
-
return c <= 0.0031308 ? 12.92 * c : 1.055 *
|
|
1697
|
+
return c <= 0.0031308 ? 12.92 * c : 1.055 * c ** (1 / 2.4) - 0.055;
|
|
1699
1698
|
};
|
|
1700
1699
|
return {
|
|
1701
1700
|
r: Math.round(enc(rLin) * 255),
|
|
@@ -1997,7 +1996,7 @@ function resolveFontSizePx(el, win) {
|
|
|
1997
1996
|
const v = chain[i];
|
|
1998
1997
|
if (!v || v === 'inherit') continue;
|
|
1999
1998
|
const num = parseFloat(v);
|
|
2000
|
-
if (isNaN(num)) continue;
|
|
1999
|
+
if (Number.isNaN(num)) continue;
|
|
2001
2000
|
if (v.endsWith('px')) px = num;
|
|
2002
2001
|
else if (v.endsWith('rem')) px = num * 16;
|
|
2003
2002
|
else if (v.endsWith('em')) px = num * px;
|
|
@@ -2012,7 +2011,7 @@ function resolveFontSizePx(el, win) {
|
|
|
2012
2011
|
function resolveLengthPx(value, fontSizePx) {
|
|
2013
2012
|
if (!value || value === 'normal' || value === 'auto' || value === 'inherit') return null;
|
|
2014
2013
|
const num = parseFloat(value);
|
|
2015
|
-
if (isNaN(num)) return null;
|
|
2014
|
+
if (Number.isNaN(num)) return null;
|
|
2016
2015
|
if (value.endsWith('px')) return num;
|
|
2017
2016
|
if (value.endsWith('rem')) return num * 16;
|
|
2018
2017
|
if (value.endsWith('em')) return num * fontSizePx;
|
|
@@ -2128,7 +2127,7 @@ function checkQuality(opts) {
|
|
|
2128
2127
|
// font-size — bigger text demands proportionally more padding.
|
|
2129
2128
|
// vertical: max(4px, fontSize × 0.3)
|
|
2130
2129
|
// horizontal: max(8px, fontSize × 0.5)
|
|
2131
|
-
const isInlineCode = tag === 'code' && !
|
|
2130
|
+
const isInlineCode = tag === 'code' && !el.closest?.('pre');
|
|
2132
2131
|
if (!isInlineCode && rect && hasDirectText && textLen > 20 && rect.width > 100 && rect.height > 30) {
|
|
2133
2132
|
const borders = {
|
|
2134
2133
|
top: parseFloat(style.borderTopWidth) || 0,
|
|
@@ -2244,7 +2243,7 @@ function checkQuality(opts) {
|
|
|
2244
2243
|
const CHILD_INSULATE_THRESHOLD = 4;
|
|
2245
2244
|
const childrenInsulate = { top: false, right: false, bottom: false, left: false };
|
|
2246
2245
|
for (const child of el.children) {
|
|
2247
|
-
|
|
2246
|
+
const childStyle = getComputedStyleFor(win, child);
|
|
2248
2247
|
if (!childStyle) continue;
|
|
2249
2248
|
const childPad = {
|
|
2250
2249
|
top: resolveLengthPx(childStyle.paddingTop, fontSize) ?? 0,
|
|
@@ -2376,7 +2375,7 @@ function checkQuality(opts) {
|
|
|
2376
2375
|
// Only flag actual body content, not UI labels (buttons, tabs, badges, captions, footer text, etc.)
|
|
2377
2376
|
if (hasDirectText && textLen > 20 && fontSize < 12) {
|
|
2378
2377
|
const skipTags = ['sub', 'sup', 'code', 'kbd', 'samp', 'var', 'caption', 'figcaption'];
|
|
2379
|
-
const inUIContext = el.closest
|
|
2378
|
+
const inUIContext = el.closest?.('button, a, label, summary, pre, [role="button"], [role="link"], [role="tab"], [role="menuitem"], [role="option"], nav, footer, [aria-hidden="true"], [class*="badge" i], [class*="caption" i], [class*="chip" i], [class*="code" i], [class*="console" i], [class*="diff" i], [class*="label" i], [class*="meta" i], [class*="mock" i], [class*="pill" i], [class*="preview" i], [class*="tag" i], [class*="terminal" i], [class*="writes" i]');
|
|
2380
2379
|
const isUppercase = style.textTransform === 'uppercase';
|
|
2381
2380
|
if (!skipTags.includes(tag) && !inUIContext && !isUppercase) {
|
|
2382
2381
|
findings.push({ id: 'tiny-text', snippet: `${fontSize}px body text` });
|
|
@@ -2441,7 +2440,7 @@ function checkPageQualityFromDoc(doc) {
|
|
|
2441
2440
|
let prevLevel = 0;
|
|
2442
2441
|
let prevText = '';
|
|
2443
2442
|
for (const h of headings) {
|
|
2444
|
-
const level = parseInt(h.tagName[1]);
|
|
2443
|
+
const level = parseInt(h.tagName[1], 10);
|
|
2445
2444
|
const text = (h.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 60);
|
|
2446
2445
|
if (prevLevel > 0 && level > prevLevel + 1) {
|
|
2447
2446
|
findings.push({
|
|
@@ -2489,7 +2488,7 @@ function checkElementBorders(tag, style, overrides, resolvedRadius) {
|
|
|
2489
2488
|
if (widths[s] === 0 && overrides && overrides[s]) {
|
|
2490
2489
|
widths[s] = overrides[s].width;
|
|
2491
2490
|
colors[s] = overrides[s].color;
|
|
2492
|
-
} else if (colors[s]
|
|
2491
|
+
} else if (colors[s]?.startsWith('var(') && overrides?.[s]) {
|
|
2493
2492
|
// Longhand case: jsdom kept the width but left the color as the
|
|
2494
2493
|
// literal `var(...)` string. Substitute the resolved color.
|
|
2495
2494
|
colors[s] = overrides[s].color;
|
|
@@ -2550,7 +2549,7 @@ function checkElementColors(el, style, tag, window, customPropMap, hasAnchorInhe
|
|
|
2550
2549
|
effectiveBg,
|
|
2551
2550
|
effectiveBgStops: effectiveBg ? null : resolveGradientStops(el, window),
|
|
2552
2551
|
fontSize: parseFloat(style.fontSize) || 16,
|
|
2553
|
-
fontWeight: parseInt(style.fontWeight) || 400,
|
|
2552
|
+
fontWeight: parseInt(style.fontWeight, 10) || 400,
|
|
2554
2553
|
hasDirectText,
|
|
2555
2554
|
isEmojiOnly: isEmojiOnlyText(directText),
|
|
2556
2555
|
bgClip: style.webkitBackgroundClip || style.backgroundClip || '',
|
|
@@ -2677,7 +2676,7 @@ function checkTypography() {
|
|
|
2677
2676
|
let totalTextElements = 0;
|
|
2678
2677
|
for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span')) {
|
|
2679
2678
|
// Skip monodesign's own elements
|
|
2680
|
-
if (el.closest
|
|
2679
|
+
if (el.closest?.('.monodesign-overlay, .monodesign-label, .monodesign-banner, .monodesign-tooltip')) continue;
|
|
2681
2680
|
// Only count elements that actually have visible direct text
|
|
2682
2681
|
const hasText = [...el.childNodes].some(n => n.nodeType === 3 && n.textContent.trim().length > 0);
|
|
2683
2682
|
if (!hasText) continue;
|
|
@@ -2718,7 +2717,7 @@ function checkTypography() {
|
|
|
2718
2717
|
const sorted = [...sizes].sort((a, b) => a - b);
|
|
2719
2718
|
const ratio = sorted[sorted.length - 1] / sorted[0];
|
|
2720
2719
|
if (ratio < 2.0) {
|
|
2721
|
-
findings.push({ type: 'flat-type-hierarchy', detail: `Sizes: ${sorted.map(s => s
|
|
2720
|
+
findings.push({ type: 'flat-type-hierarchy', detail: `Sizes: ${sorted.map(s => `${s}px`).join(', ')} (ratio ${ratio.toFixed(1)}:1)` });
|
|
2722
2721
|
}
|
|
2723
2722
|
}
|
|
2724
2723
|
|
|
@@ -2837,7 +2836,7 @@ function checkPageTypography(doc, win) {
|
|
|
2837
2836
|
const sorted = [...sizes].sort((a, b) => a - b);
|
|
2838
2837
|
const ratio = sorted[sorted.length - 1] / sorted[0];
|
|
2839
2838
|
if (ratio < 2.0) {
|
|
2840
|
-
findings.push({ id: 'flat-type-hierarchy', snippet: `Sizes: ${sorted.map(s => s
|
|
2839
|
+
findings.push({ id: 'flat-type-hierarchy', snippet: `Sizes: ${sorted.map(s => `${s}px`).join(', ')} (ratio ${ratio.toFixed(1)}:1)` });
|
|
2841
2840
|
}
|
|
2842
2841
|
}
|
|
2843
2842
|
|
|
@@ -2969,7 +2968,7 @@ function checkCreamPalette(doc, win) {
|
|
|
2969
2968
|
// 2. Tailwind class fallback — for the static path, where utility classes
|
|
2970
2969
|
// never resolve to computed CSS.
|
|
2971
2970
|
for (const el of [body, html]) {
|
|
2972
|
-
const tok = creamFromClassList(el
|
|
2971
|
+
const tok = creamFromClassList(el?.getAttribute ? el.getAttribute('class') : '');
|
|
2973
2972
|
if (tok) {
|
|
2974
2973
|
findings.push({ id: 'cream-palette', snippet: `cream/beige page background (Tailwind ${tok})` });
|
|
2975
2974
|
break;
|
|
@@ -3005,7 +3004,7 @@ function checkOversizedH1({ tag, fontSize, headingText, rect = null, viewportWid
|
|
|
3005
3004
|
return [];
|
|
3006
3005
|
}
|
|
3007
3006
|
|
|
3008
|
-
function checkElementOversizedH1(el,
|
|
3007
|
+
function checkElementOversizedH1(el, _style, tag, window) {
|
|
3009
3008
|
if (tag !== 'h1') return [];
|
|
3010
3009
|
const fontSize = resolveFontSizePx(el, window);
|
|
3011
3010
|
const headingText = (el.textContent || '').trim().replace(/\s+/g, ' ');
|
|
@@ -3089,7 +3088,7 @@ function borderColorsFromStyle(style) {
|
|
|
3089
3088
|
];
|
|
3090
3089
|
}
|
|
3091
3090
|
|
|
3092
|
-
function checkElementGptBorderShadow(
|
|
3091
|
+
function checkElementGptBorderShadow(_el, style) {
|
|
3093
3092
|
return checkGptThinBorderWideShadow({ borderWidths: borderWidthsFromStyle(style), borderColors: borderColorsFromStyle(style), boxShadow: style.boxShadow || '' });
|
|
3094
3093
|
}
|
|
3095
3094
|
|
|
@@ -3239,7 +3238,7 @@ function checkClippedOverflow(el, style, getStyle) {
|
|
|
3239
3238
|
return [];
|
|
3240
3239
|
}
|
|
3241
3240
|
|
|
3242
|
-
function checkElementClippedOverflow(el, style,
|
|
3241
|
+
function checkElementClippedOverflow(el, style, _tag, window) {
|
|
3243
3242
|
return checkClippedOverflow(el, style, (n) => window.getComputedStyle(n));
|
|
3244
3243
|
}
|
|
3245
3244
|
|
|
@@ -3358,6 +3357,29 @@ function checkElementTextOverflowDOM(el) {
|
|
|
3358
3357
|
// conditions can't be read off a resting element. Shared by the regex, static
|
|
3359
3358
|
// and browser engines (each hands over its own CSS text).
|
|
3360
3359
|
|
|
3360
|
+
// Split a selector list on top-level commas only — i.e. commas inside a
|
|
3361
|
+
// functional pseudo-class's parens (`:is(a, button)`, `:where(a, b)`,
|
|
3362
|
+
// `:not(a, b)`, `:has(x, y)`) don't split. A naive `str.split(',')` breaks
|
|
3363
|
+
// `:is(a, button):focus { outline: none }` into `:is(a` and `button):focus`,
|
|
3364
|
+
// which both selectorTargetsInteractive() and the dark/light selector maps
|
|
3365
|
+
// below then treat as two unrelated (and malformed) selectors — producing
|
|
3366
|
+
// false-positive missing-focus-visible findings and double-counted rules.
|
|
3367
|
+
function splitTopLevelCommas(str) {
|
|
3368
|
+
const parts = [];
|
|
3369
|
+
let depth = 0, start = 0;
|
|
3370
|
+
for (let k = 0; k < str.length; k++) {
|
|
3371
|
+
const ch = str[k];
|
|
3372
|
+
if (ch === '(') depth++;
|
|
3373
|
+
else if (ch === ')') depth = Math.max(0, depth - 1);
|
|
3374
|
+
else if (ch === ',' && depth === 0) {
|
|
3375
|
+
parts.push(str.slice(start, k));
|
|
3376
|
+
start = k + 1;
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
parts.push(str.slice(start));
|
|
3380
|
+
return parts;
|
|
3381
|
+
}
|
|
3382
|
+
|
|
3361
3383
|
// Parse flat CSS into { selector, body, dark } tuples, tracking one level of
|
|
3362
3384
|
// dark-scheme context (a `@media (prefers-color-scheme: dark)` ancestor).
|
|
3363
3385
|
// @supports / @layer / @container / @scope are transparently descended.
|
|
@@ -3387,7 +3409,7 @@ function collectCssRules(css, dark = false, depth = 0) {
|
|
|
3387
3409
|
}
|
|
3388
3410
|
continue;
|
|
3389
3411
|
}
|
|
3390
|
-
for (const sel of prelude
|
|
3412
|
+
for (const sel of splitTopLevelCommas(prelude).map(s => s.trim()).filter(Boolean)) {
|
|
3391
3413
|
out.push({ selector: sel, body, dark });
|
|
3392
3414
|
}
|
|
3393
3415
|
}
|
|
@@ -3562,7 +3584,7 @@ function checkElementImageDimensions(el, style) {
|
|
|
3562
3584
|
const parts = [el.getAttribute?.('style') || el.attribs?.style || ''];
|
|
3563
3585
|
const ar = style && (style.aspectRatio || style['aspect-ratio']);
|
|
3564
3586
|
if (ar && ar !== 'auto') parts.push(`aspect-ratio:${ar}`);
|
|
3565
|
-
const h = style
|
|
3587
|
+
const h = style?.height;
|
|
3566
3588
|
if (h && h !== 'auto' && h !== '') parts.push(`height:${h}`);
|
|
3567
3589
|
return checkImageDimensions({ hasWidthAttr, hasHeightAttr, style: parts.join(';') });
|
|
3568
3590
|
}
|
|
@@ -3622,7 +3644,7 @@ function checkElementSmallTouchTarget(el, style, tag) {
|
|
|
3622
3644
|
// (button-like). jsdom returns '' for the default inline display, so key
|
|
3623
3645
|
// off "not blockish" rather than "== inline".
|
|
3624
3646
|
const isBlockish = /^(block|flex|grid|inline-block|inline-flex|table)/.test(style.display || '');
|
|
3625
|
-
const inProse = !!
|
|
3647
|
+
const inProse = !!el.closest?.(PROSE_ANCESTOR_SELECTOR);
|
|
3626
3648
|
const isInlineLinkInProse = tag === 'a' && !isBlockish && inProse;
|
|
3627
3649
|
return checkSmallTouchTarget({ tag, role, href, inputType, widthPx, heightPx, isInlineLinkInProse });
|
|
3628
3650
|
}
|
|
@@ -3636,7 +3658,7 @@ function checkElementSmallTouchTargetDOM(el) {
|
|
|
3636
3658
|
const inputType = el.getAttribute('type') || '';
|
|
3637
3659
|
const style = getComputedStyle(el);
|
|
3638
3660
|
const isBlockish = /^(block|flex|grid|inline-block|inline-flex|table)/.test(style.display || '');
|
|
3639
|
-
const inProse = !!
|
|
3661
|
+
const inProse = !!el.closest?.(PROSE_ANCESTOR_SELECTOR);
|
|
3640
3662
|
const isInlineLinkInProse = tag === 'a' && !isBlockish && inProse;
|
|
3641
3663
|
return checkSmallTouchTarget({ tag, role, href, inputType, widthPx: rect.width, heightPx: rect.height, isInlineLinkInProse });
|
|
3642
3664
|
}
|
|
@@ -3756,7 +3778,7 @@ if (IS_BROWSER) {
|
|
|
3756
3778
|
}
|
|
3757
3779
|
|
|
3758
3780
|
function showSpotlight(target) {
|
|
3759
|
-
if (!target
|
|
3781
|
+
if (!target?.getBoundingClientRect) return;
|
|
3760
3782
|
// Respect the spotlightBlur setting: if disabled, don't show the backdrop
|
|
3761
3783
|
if (window.__MONODESIGN_CONFIG__?.spotlightBlur === false) {
|
|
3762
3784
|
spotlightTarget = target;
|
|
@@ -3902,10 +3924,8 @@ if (IS_BROWSER) {
|
|
|
3902
3924
|
}
|
|
3903
3925
|
});
|
|
3904
3926
|
|
|
3905
|
-
const highlight =
|
|
3927
|
+
const highlight = (el, findings) => {
|
|
3906
3928
|
if (el._monodesignOverlay) detachOverlay(el._monodesignOverlay);
|
|
3907
|
-
const hasSlop = findings.some(f => RULE_CATEGORY[f.type || f.id] === 'slop');
|
|
3908
|
-
|
|
3909
3929
|
const fixed = isInFixedContext(el);
|
|
3910
3930
|
const rect = el.getBoundingClientRect();
|
|
3911
3931
|
const outline = document.createElement('div');
|
|
@@ -4049,7 +4069,7 @@ if (IS_BROWSER) {
|
|
|
4049
4069
|
overlays.push(outline);
|
|
4050
4070
|
};
|
|
4051
4071
|
|
|
4052
|
-
const showPageBanner =
|
|
4072
|
+
const showPageBanner = (findings) => {
|
|
4053
4073
|
if (!findings.length) return;
|
|
4054
4074
|
const banner = document.createElement('div');
|
|
4055
4075
|
banner.className = 'monodesign-overlay monodesign-banner';
|
|
@@ -4149,7 +4169,7 @@ if (IS_BROWSER) {
|
|
|
4149
4169
|
.filter(c => !c.startsWith('monodesign-') && !isLikelyHashedClass(c))
|
|
4150
4170
|
.slice(0, 2);
|
|
4151
4171
|
if (classes.length > 0) {
|
|
4152
|
-
sel +=
|
|
4172
|
+
sel += `.${classes.map(c => CSS.escape(c)).join('.')}`;
|
|
4153
4173
|
}
|
|
4154
4174
|
}
|
|
4155
4175
|
|
|
@@ -4157,7 +4177,7 @@ if (IS_BROWSER) {
|
|
|
4157
4177
|
const parent = el.parentElement;
|
|
4158
4178
|
if (parent) {
|
|
4159
4179
|
try {
|
|
4160
|
-
const matching = parent.querySelectorAll(
|
|
4180
|
+
const matching = parent.querySelectorAll(`:scope > ${sel}`);
|
|
4161
4181
|
if (matching.length > 1) {
|
|
4162
4182
|
const sameType = [...parent.children].filter(c => c.tagName === el.tagName);
|
|
4163
4183
|
const idx = sameType.indexOf(el) + 1;
|
|
@@ -4174,7 +4194,7 @@ if (IS_BROWSER) {
|
|
|
4174
4194
|
function generateSelector(el) {
|
|
4175
4195
|
if (el === document.body) return 'body';
|
|
4176
4196
|
if (el === document.documentElement) return 'html';
|
|
4177
|
-
if (el.id) return
|
|
4197
|
+
if (el.id) return `#${CSS.escape(el.id)}`;
|
|
4178
4198
|
|
|
4179
4199
|
const parts = [];
|
|
4180
4200
|
let current = el;
|
|
@@ -4186,7 +4206,7 @@ if (IS_BROWSER) {
|
|
|
4186
4206
|
|
|
4187
4207
|
// Anchor on an ancestor's ID and stop walking up
|
|
4188
4208
|
if (current.id) {
|
|
4189
|
-
parts[0] =
|
|
4209
|
+
parts[0] = `#${CSS.escape(current.id)}`;
|
|
4190
4210
|
break;
|
|
4191
4211
|
}
|
|
4192
4212
|
|
|
@@ -4326,7 +4346,7 @@ if (IS_BROWSER) {
|
|
|
4326
4346
|
|
|
4327
4347
|
const textColor = parseRgb(style.color);
|
|
4328
4348
|
const fontSize = parseFloat(style.fontSize) || 16;
|
|
4329
|
-
const fontWeight = parseInt(style.fontWeight) || 400;
|
|
4349
|
+
const fontWeight = parseInt(style.fontWeight, 10) || 400;
|
|
4330
4350
|
const isLargeText = fontSize >= WCAG_LARGE_TEXT_PX || (fontSize >= WCAG_LARGE_BOLD_TEXT_PX && fontWeight >= 700);
|
|
4331
4351
|
const threshold = isLargeText ? 3.0 : 4.5;
|
|
4332
4352
|
const clip = {
|
|
@@ -4544,7 +4564,7 @@ if (IS_BROWSER) {
|
|
|
4544
4564
|
function sampleDrawablePixel(drawable, sourcePoint) {
|
|
4545
4565
|
if (visualContrastRasterCache.has(drawable)) {
|
|
4546
4566
|
const cached = visualContrastRasterCache.get(drawable);
|
|
4547
|
-
if (!cached
|
|
4567
|
+
if (!cached?.ctx) return { status: 'unresolved', reason: cached?.reason || 'image sample failed' };
|
|
4548
4568
|
try {
|
|
4549
4569
|
const x = Math.max(0, Math.min(cached.width - 1, Math.floor(sourcePoint.x * cached.scaleX)));
|
|
4550
4570
|
const y = Math.max(0, Math.min(cached.height - 1, Math.floor(sourcePoint.y * cached.scaleY)));
|
|
@@ -4679,7 +4699,7 @@ if (IS_BROWSER) {
|
|
|
4679
4699
|
const unresolved = [];
|
|
4680
4700
|
|
|
4681
4701
|
for (const node of nodes) {
|
|
4682
|
-
if (
|
|
4702
|
+
if (node?.nodeType !== 1) continue;
|
|
4683
4703
|
if (node.closest?.('.monodesign-overlay, .monodesign-label, .monodesign-banner, .monodesign-tooltip')) continue;
|
|
4684
4704
|
const tag = node.tagName?.toLowerCase();
|
|
4685
4705
|
if (tag === 'img') {
|
|
@@ -4876,7 +4896,7 @@ if (IS_BROWSER) {
|
|
|
4876
4896
|
}));
|
|
4877
4897
|
}
|
|
4878
4898
|
|
|
4879
|
-
const printSummary =
|
|
4899
|
+
const printSummary = (allFindings) => {
|
|
4880
4900
|
if (allFindings.length === 0) {
|
|
4881
4901
|
console.log('%c[monodesign] No anti-patterns found.', 'color: #22c55e; font-weight: bold');
|
|
4882
4902
|
return;
|
|
@@ -5203,7 +5223,7 @@ if (IS_BROWSER) {
|
|
|
5203
5223
|
try { rules = sheet.cssRules; } catch { continue; }
|
|
5204
5224
|
if (!rules) continue;
|
|
5205
5225
|
for (const rule of Array.from(rules)) {
|
|
5206
|
-
try { stylesheetCssText += rule.cssText
|
|
5226
|
+
try { stylesheetCssText += `${rule.cssText}\n`; } catch { /* ignore */ }
|
|
5207
5227
|
}
|
|
5208
5228
|
}
|
|
5209
5229
|
const stylesheetFindings = [
|
|
@@ -5476,7 +5496,7 @@ if (IS_BROWSER) {
|
|
|
5476
5496
|
}
|
|
5477
5497
|
|
|
5478
5498
|
let firstScanDone = false;
|
|
5479
|
-
const scan =
|
|
5499
|
+
const scan = (options = {}) => {
|
|
5480
5500
|
clearOverlays();
|
|
5481
5501
|
const generation = scanGeneration;
|
|
5482
5502
|
const collected = collectBrowserFindings();
|
|
@@ -5493,7 +5513,7 @@ if (IS_BROWSER) {
|
|
|
5493
5513
|
return allFindings;
|
|
5494
5514
|
};
|
|
5495
5515
|
|
|
5496
|
-
const scanAsync = async
|
|
5516
|
+
const scanAsync = async (options = {}) => {
|
|
5497
5517
|
clearOverlays();
|
|
5498
5518
|
const generation = scanGeneration;
|
|
5499
5519
|
if (shouldRunVisualContrast(options)) {
|
|
@@ -5505,13 +5525,13 @@ if (IS_BROWSER) {
|
|
|
5505
5525
|
return renderBrowserFindings(collectBrowserFindings(), options);
|
|
5506
5526
|
};
|
|
5507
5527
|
|
|
5508
|
-
const detect =
|
|
5528
|
+
const detect = (options = {}) => {
|
|
5509
5529
|
lastVisualContrastAnalyses = [];
|
|
5510
5530
|
const { allFindings } = collectBrowserFindings();
|
|
5511
5531
|
return options.serialize === false ? allFindings : serializeFindings(allFindings);
|
|
5512
5532
|
};
|
|
5513
5533
|
|
|
5514
|
-
const detectAsync = async
|
|
5534
|
+
const detectAsync = async (options = {}) => {
|
|
5515
5535
|
if (shouldRunVisualContrast(options)) {
|
|
5516
5536
|
const { allFindings } = await collectBrowserFindingsAsync(options);
|
|
5517
5537
|
return options.serialize === false ? allFindings : serializeFindings(allFindings);
|
|
@@ -106,22 +106,22 @@ const REGEX_MATCHERS = [
|
|
|
106
106
|
fmt: (m) => `Google Fonts: ${m.overusedGoogleFont || firstOverusedGoogleFont(m[0])}` },
|
|
107
107
|
// --- Gradient text ---
|
|
108
108
|
{ id: 'gradient-text', regex: /background-clip\s*:\s*text|-webkit-background-clip\s*:\s*text/gi,
|
|
109
|
-
test: (
|
|
109
|
+
test: (_m, line) => /gradient/i.test(line),
|
|
110
110
|
fmt: () => 'background-clip: text + gradient' },
|
|
111
111
|
// --- Gradient text (Tailwind) ---
|
|
112
112
|
{ id: 'gradient-text', regex: /\bbg-clip-text\b/g,
|
|
113
|
-
test: (
|
|
113
|
+
test: (_m, line) => /\bbg-gradient-to-/i.test(line),
|
|
114
114
|
fmt: () => 'bg-clip-text + bg-gradient' },
|
|
115
115
|
// --- Tailwind gray on colored bg ---
|
|
116
116
|
{ id: 'gray-on-color', regex: /\btext-(?:gray|slate|zinc|neutral|stone)-(\d+)\b/g,
|
|
117
|
-
test: (
|
|
117
|
+
test: (_m, line) => /\bbg-(?:red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-\d+\b/.test(line),
|
|
118
118
|
fmt: (m, line) => { const bg = line.match(/\bbg-(?:red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-\d+\b/); return `${m[0]} on ${bg?.[0] || '?'}`; } },
|
|
119
119
|
// --- Tailwind AI palette ---
|
|
120
120
|
{ id: 'ai-color-palette', regex: /\btext-(?:purple|violet|indigo)-(\d+)\b/g,
|
|
121
|
-
test: (
|
|
121
|
+
test: (_m, line) => /\btext-(?:[2-9]xl|[3-9]xl)\b|<h[1-3]/i.test(line),
|
|
122
122
|
fmt: (m) => `${m[0]} on heading` },
|
|
123
123
|
{ id: 'ai-color-palette', regex: /\bfrom-(?:purple|violet|indigo)-(\d+)\b/g,
|
|
124
|
-
test: (
|
|
124
|
+
test: (_m, line) => /\bto-(?:purple|violet|indigo|blue|cyan|pink|fuchsia)-\d+\b/.test(line),
|
|
125
125
|
fmt: (m) => `${m[0]} gradient` },
|
|
126
126
|
// --- Bounce/elastic easing ---
|
|
127
127
|
{ id: 'bounce-easing', regex: /\banimate-bounce\b/g,
|
|
@@ -226,7 +226,7 @@ const REGEX_ANALYZERS = [
|
|
|
226
226
|
const lines = content.split('\n');
|
|
227
227
|
let line = 1;
|
|
228
228
|
for (let i = 0; i < lines.length; i++) { if (/font-size/i.test(lines[i]) || /\btext-(?:xs|sm|base|lg|xl|\d)/i.test(lines[i])) { line = i + 1; break; } }
|
|
229
|
-
return [finding('flat-type-hierarchy', filePath, `Sizes: ${sorted.map(s => s
|
|
229
|
+
return [finding('flat-type-hierarchy', filePath, `Sizes: ${sorted.map(s => `${s}px`).join(', ')} (ratio ${ratio.toFixed(1)}:1)`, line)];
|
|
230
230
|
},
|
|
231
231
|
// Monotonous spacing (regex)
|
|
232
232
|
(content, filePath) => {
|