@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.
Files changed (48) hide show
  1. package/cli/engine/browser/injected/index.mjs +16 -18
  2. package/cli/engine/cli/main.mjs +3 -3
  3. package/cli/engine/detect-antipatterns-browser.js +69 -49
  4. package/cli/engine/engines/regex/detect-text.mjs +6 -6
  5. package/cli/engine/engines/static-html/css-cascade.mjs +6 -5
  6. package/cli/engine/engines/static-html/detect-html.mjs +6 -6
  7. package/cli/engine/fix/index.mjs +3 -3
  8. package/cli/engine/node/file-system.mjs +1 -1
  9. package/cli/engine/registry/antipatterns.mjs +1 -1
  10. package/cli/engine/rules/checks.mjs +26 -27
  11. package/cli/engine/shared/color.mjs +2 -2
  12. package/cli/engine/shared/constants.mjs +1 -1
  13. package/cli/engine/shared/inline-ignores.mjs +2 -2
  14. package/cli/lib/monodesign-config.mjs +5 -5
  15. package/package.json +10 -9
  16. package/skill/scripts/context.mjs +5 -5
  17. package/skill/scripts/critique-storage.mjs +1 -1
  18. package/skill/scripts/detect-csp.mjs +1 -6
  19. package/skill/scripts/hook-admin.mjs +6 -6
  20. package/skill/scripts/hook-before-edit.mjs +4 -4
  21. package/skill/scripts/hook-lib.mjs +20 -11
  22. package/skill/scripts/hook.mjs +1 -1
  23. package/skill/scripts/lib/design-parser.mjs +9 -42
  24. package/skill/scripts/lib/is-generated.mjs +1 -1
  25. package/skill/scripts/lib/monodesign-paths.mjs +1 -1
  26. package/skill/scripts/live/event-validation.mjs +13 -13
  27. package/skill/scripts/live/manual-apply.mjs +4 -4
  28. package/skill/scripts/live/manual-edits-buffer.mjs +1 -1
  29. package/skill/scripts/live/session-store.mjs +5 -5
  30. package/skill/scripts/live/svelte-component.mjs +13 -13
  31. package/skill/scripts/live/sveltekit-adapter.mjs +4 -4
  32. package/skill/scripts/live/ui-core.mjs +1 -1
  33. package/skill/scripts/live-accept.mjs +24 -24
  34. package/skill/scripts/live-browser-dom.js +8 -7
  35. package/skill/scripts/live-browser-session.js +8 -7
  36. package/skill/scripts/live-browser.js +406 -433
  37. package/skill/scripts/live-commit-manual-edits.mjs +6 -6
  38. package/skill/scripts/live-copy-edit-agent.mjs +2 -2
  39. package/skill/scripts/live-discard-manual-edits.mjs +1 -1
  40. package/skill/scripts/live-inject.mjs +7 -7
  41. package/skill/scripts/live-insert.mjs +11 -11
  42. package/skill/scripts/live-manual-edit-evidence.mjs +4 -4
  43. package/skill/scripts/live-poll.mjs +5 -5
  44. package/skill/scripts/live-server.mjs +10 -10
  45. package/skill/scripts/live-wrap.mjs +31 -41
  46. package/skill/scripts/live.mjs +5 -5
  47. package/skill/scripts/palette.mjs +3 -1
  48. package/skill/scripts/pin.mjs +1 -1
@@ -118,17 +118,17 @@ function buildBorderOverrideMap(document, window) {
118
118
 
119
119
  for (const [prop, side] of SIDE_PROPS) {
120
120
  const val = rule.style[prop];
121
- if (!val || !val.includes('var(')) continue;
121
+ if (!val?.includes('var(')) continue;
122
122
  const parsed = parseShorthand(resolveVar(val));
123
- if (parsed && parsed.color) perSide[side] = parsed;
123
+ if (parsed?.color) perSide[side] = parsed;
124
124
  }
125
125
 
126
126
  // Uniform `border: <w> <style> var(...)` applies to every side the
127
127
  // per-side map didn't already claim.
128
128
  const borderAll = rule.style.border;
129
- if (borderAll && borderAll.includes('var(')) {
129
+ if (borderAll?.includes('var(')) {
130
130
  const parsed = parseShorthand(resolveVar(borderAll));
131
- if (parsed && parsed.color) {
131
+ if (parsed?.color) {
132
132
  for (const s of ['Top', 'Right', 'Bottom', 'Left']) {
133
133
  if (!perSide[s]) perSide[s] = parsed;
134
134
  }
@@ -144,7 +144,7 @@ function buildBorderOverrideMap(document, window) {
144
144
  ['borderBottomColor', 'Bottom'],
145
145
  ]) {
146
146
  const val = rule.style[prop];
147
- if (!val || !val.includes('var(')) continue;
147
+ if (!val?.includes('var(')) continue;
148
148
  const resolved = resolveVar(val).trim();
149
149
  if (!resolved) continue;
150
150
  // Width may or may not come from this rule — that's fine; the
@@ -185,6 +185,7 @@ function buildBorderOverrideMap(document, window) {
185
185
  // styles. We walk the source character-by-character, balancing braces
186
186
  // so we correctly handle nested style rules inside the layer block.
187
187
  function unwrapCssAtLayer(source) {
188
+ // biome-ignore lint/complexity/useOptionalChain: exported helper with no typed callers; `source?.includes` would throw for a falsy non-string (0/false) that this guard currently returns unchanged
188
189
  if (!source || !source.includes('@layer')) return source;
189
190
  // Find `@layer <name>? {` openers. The match starts at the @, and
190
191
  // we then balance braces from the opening { onward.
@@ -74,14 +74,14 @@ function checkStaticPageTypography(document, window) {
74
74
  const sorted = [...sizes].sort((a, b) => a - b);
75
75
  const ratio = sorted[sorted.length - 1] / sorted[0];
76
76
  if (ratio < 2.0) {
77
- findings.push({ id: 'flat-type-hierarchy', snippet: `Sizes: ${sorted.map(s => s + 'px').join(', ')} (ratio ${ratio.toFixed(1)}:1)` });
77
+ findings.push({ id: 'flat-type-hierarchy', snippet: `Sizes: ${sorted.map(s => `${s}px`).join(', ')} (ratio ${ratio.toFixed(1)}:1)` });
78
78
  }
79
79
  }
80
80
  return findings;
81
81
  }
82
82
 
83
83
  function checkElementBrokenImage(el) {
84
- const src = (el.getAttribute && el.getAttribute('src')) ?? el.attribs?.src;
84
+ const src = el.getAttribute?.('src') ?? el.attribs?.src;
85
85
  // Missing src attribute entirely
86
86
  if (src === undefined || src === null) {
87
87
  return [{ id: 'broken-image', snippet: '<img> with no src attribute' }];
@@ -95,10 +95,10 @@ function checkElementBrokenImage(el) {
95
95
  }
96
96
 
97
97
  const STATIC_ELEMENT_RULES = [
98
- { id: 'border-rules', selector: '*', run: (el, tag, style, window, customPropMap) => checkElementBorders(tag, style, null, resolveBorderRadiusPx(el, style, parseFloat(style.width) || 0, window)) },
98
+ { id: 'border-rules', selector: '*', run: (el, tag, style, window, _customPropMap) => checkElementBorders(tag, style, null, resolveBorderRadiusPx(el, style, parseFloat(style.width) || 0, window)) },
99
99
  { id: 'color-rules', selector: '*', run: (el, tag, style, window, customPropMap) => checkElementColors(el, style, tag, window, customPropMap, false) },
100
100
  { id: 'dark-glow', selector: '*', run: (el, tag, style, window, customPropMap) => checkElementGlow(tag, style, resolveBackground(el.parentElement || el, window, customPropMap)) },
101
- { id: 'motion-rules', selector: '*', run: (el, tag, style) => checkElementMotion(tag, style) },
101
+ { id: 'motion-rules', selector: '*', run: (_el, tag, style) => checkElementMotion(tag, style) },
102
102
  { id: 'icon-tile-stack', selector: 'h1,h2,h3,h4,h5,h6', run: (el, tag, _style, window) => checkElementIconTile(el, tag, window) },
103
103
  { id: 'italic-serif-display', selector: 'h1,h2', run: (el, tag, style) => checkElementItalicSerif(el, style, tag) },
104
104
  { id: 'hero-eyebrow-chip', selector: 'h1', run: (el, tag, style, window, customPropMap) => checkElementHeroEyebrow(el, style, tag, window, customPropMap) },
@@ -106,8 +106,8 @@ const STATIC_ELEMENT_RULES = [
106
106
  { id: 'quality-rules', selector: '*', run: (el, tag, style, window) => checkElementQuality(el, style, tag, window) },
107
107
  { id: 'oversized-h1', selector: 'h1', run: (el, tag, style, window) => checkElementOversizedH1(el, style, tag, window) },
108
108
  { id: 'clipped-overflow-container', selector: '*', run: (el, tag, style, window) => checkElementClippedOverflow(el, style, tag, window) },
109
- { id: 'gpt-thin-border-wide-shadow', selector: '*', run: (el, tag, style) => checkElementGptBorderShadow(el, style) },
110
- { id: 'image-missing-dimensions', selector: 'img', run: (el, tag, style) => checkElementImageDimensions(el, style) },
109
+ { id: 'gpt-thin-border-wide-shadow', selector: '*', run: (el, _tag, style) => checkElementGptBorderShadow(el, style) },
110
+ { id: 'image-missing-dimensions', selector: 'img', run: (el, _tag, style) => checkElementImageDimensions(el, style) },
111
111
  { id: 'small-touch-target', selector: 'a,button,input,select,summary,[role=button],[role=link],[role=menuitem],[role=tab],[role=switch]', run: (el, tag, style) => checkElementSmallTouchTarget(el, style, tag) },
112
112
  ];
113
113
 
@@ -130,7 +130,7 @@ function groupByRule(findings) {
130
130
  async function runFix(targets, options = {}) {
131
131
  const cwd = options.cwd || process.cwd();
132
132
  const dryRun = Boolean(options.dryRun);
133
- const ruleFilter = options.rules && options.rules.length ? new Set(options.rules) : null;
133
+ const ruleFilter = options.rules?.length ? new Set(options.rules) : null;
134
134
  const config = options.noConfig
135
135
  ? { ignoreRules: [], ignoreFiles: [], ignoreValues: [] }
136
136
  : readDetectionConfig(cwd);
@@ -409,8 +409,8 @@ async function runFixCli(args) {
409
409
  const report = await runFix(targets, options);
410
410
  for (const warning of report.warnings) process.stderr.write(`${warning}\n`);
411
411
  const formatted = formatFixReport(report, jsonMode);
412
- if (jsonMode || report.dryRun) process.stdout.write(formatted + '\n');
413
- else process.stderr.write(formatted + '\n');
412
+ if (jsonMode || report.dryRun) process.stdout.write(`${formatted}\n`);
413
+ else process.stderr.write(`${formatted}\n`);
414
414
  process.exit(!report.dryRun && report.remaining.length > 0 ? 2 : 0);
415
415
  }
416
416
 
@@ -46,7 +46,7 @@ function resolveImport(specifier, fromDir, fileSet) {
46
46
  }
47
47
  // index file convention
48
48
  for (const ext of SCANNABLE_EXTENSIONS) {
49
- const indexFile = path.join(base, 'index' + ext);
49
+ const indexFile = path.join(base, `index${ext}`);
50
50
  if (fileSet.has(indexFile)) return indexFile;
51
51
  }
52
52
  return null;
@@ -527,7 +527,7 @@ function filterByProviders(findings, providers = []) {
527
527
  if (!GATED_PROVIDERS.size) return findings;
528
528
  return findings.filter(f => {
529
529
  const rule = getAntipattern(f.antipattern);
530
- if (!rule || !rule.gated) return true;
530
+ if (!rule?.gated) return true;
531
531
  return enabled.has(rule.gated);
532
532
  });
533
533
  }
@@ -55,8 +55,8 @@ function checkBorders(tag, widths, colors, radius) {
55
55
  // (plus whitespace / variation selectors). Emojis render as multicolor glyphs
56
56
  // regardless of CSS `color`, so contrast checks against the element's text
57
57
  // color are meaningless for these nodes.
58
- 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;
59
- 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;
58
+ 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;
59
+ 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;
60
60
  function isEmojiOnlyText(text) {
61
61
  if (!text) return false;
62
62
  if (!EMOJI_CHAR_RE.test(text)) return false;
@@ -82,7 +82,7 @@ function checkColors(opts) {
82
82
  if (hasDirectText && textColor && !isEmojiOnly) {
83
83
  // Run background-dependent checks against either a solid bg or, if the
84
84
  // ancestor is a gradient, against every gradient stop (use the worst case).
85
- const bgs = effectiveBg ? [effectiveBg] : (effectiveBgStops && effectiveBgStops.length ? effectiveBgStops : null);
85
+ const bgs = effectiveBg ? [effectiveBg] : (effectiveBgStops?.length ? effectiveBgStops : null);
86
86
  if (bgs) {
87
87
  // Gray on colored background — flag if every stop is chromatic
88
88
  const textLum = relativeLuminance(textColor);
@@ -263,7 +263,6 @@ function isAccentColor(cssColor) {
263
263
  if (!cssColor) return false;
264
264
  const s = String(cssColor).trim();
265
265
  // rgb / rgba — direct channel-distance check.
266
- const rgbM = /rgba?\(\s*(\d+)\s*,?\s+|\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/.exec(s.replace(/rgba?\(\s*/, 'rgb(').replace(/,/g, ', '));
267
266
  const rgbStrict = /rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/.exec(s);
268
267
  if (rgbStrict) {
269
268
  const r = +rgbStrict[1], g = +rgbStrict[2], b = +rgbStrict[3];
@@ -308,7 +307,7 @@ function isAccentColor(cssColor) {
308
307
  // uppercase eyebrow OR the modern accent-colored bold eyebrow.
309
308
  function checkHeroEyebrow(opts) {
310
309
  const {
311
- headingTag, headingText, headingFontSize,
310
+ headingTag, headingText,
312
311
  siblingTag, siblingText, siblingTextTransform,
313
312
  siblingFontSize, siblingLetterSpacing,
314
313
  siblingFontWeight, siblingColor,
@@ -779,7 +778,7 @@ function parseRadiusToPx(value, widthPx) {
779
778
  return num;
780
779
  }
781
780
 
782
- function resolveBorderRadiusPx(el, style, widthPx, win) {
781
+ function resolveBorderRadiusPx(_el, style, widthPx, _win) {
783
782
  const fromComputed = parseRadiusToPx(style.borderRadius, widthPx);
784
783
  if (fromComputed !== null) return fromComputed;
785
784
  return 0;
@@ -822,7 +821,7 @@ function checkElementColorsDOM(el) {
822
821
  effectiveBg,
823
822
  effectiveBgStops: effectiveBg ? null : resolveGradientStops(el),
824
823
  fontSize: parseFloat(style.fontSize) || 16,
825
- fontWeight: parseInt(style.fontWeight) || 400,
824
+ fontWeight: parseInt(style.fontWeight, 10) || 400,
826
825
  hasDirectText,
827
826
  isEmojiOnly: isEmojiOnlyText(directText),
828
827
  bgClip: style.webkitBackgroundClip || style.backgroundClip || '',
@@ -930,7 +929,7 @@ function buildCustomPropMap(document) {
930
929
  if (!style) continue;
931
930
  for (let i = 0; i < style.length; i++) {
932
931
  const prop = style[i];
933
- if (!prop || !prop.startsWith('--')) continue;
932
+ if (!prop?.startsWith('--')) continue;
934
933
  const val = style.getPropertyValue(prop).trim();
935
934
  if (val) map.set(prop, val);
936
935
  }
@@ -972,7 +971,7 @@ function oklchToRgb(L, C, H) {
972
971
  const bLin = -0.0041960863 * lc - 0.7034186147 * mc + 1.7076147010 * sc;
973
972
  const enc = (x) => {
974
973
  const c = Math.max(0, Math.min(1, x));
975
- return c <= 0.0031308 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
974
+ return c <= 0.0031308 ? 12.92 * c : 1.055 * c ** (1 / 2.4) - 0.055;
976
975
  };
977
976
  return {
978
977
  r: Math.round(enc(rLin) * 255),
@@ -1274,7 +1273,7 @@ function resolveFontSizePx(el, win) {
1274
1273
  const v = chain[i];
1275
1274
  if (!v || v === 'inherit') continue;
1276
1275
  const num = parseFloat(v);
1277
- if (isNaN(num)) continue;
1276
+ if (Number.isNaN(num)) continue;
1278
1277
  if (v.endsWith('px')) px = num;
1279
1278
  else if (v.endsWith('rem')) px = num * 16;
1280
1279
  else if (v.endsWith('em')) px = num * px;
@@ -1289,7 +1288,7 @@ function resolveFontSizePx(el, win) {
1289
1288
  function resolveLengthPx(value, fontSizePx) {
1290
1289
  if (!value || value === 'normal' || value === 'auto' || value === 'inherit') return null;
1291
1290
  const num = parseFloat(value);
1292
- if (isNaN(num)) return null;
1291
+ if (Number.isNaN(num)) return null;
1293
1292
  if (value.endsWith('px')) return num;
1294
1293
  if (value.endsWith('rem')) return num * 16;
1295
1294
  if (value.endsWith('em')) return num * fontSizePx;
@@ -1405,7 +1404,7 @@ function checkQuality(opts) {
1405
1404
  // font-size — bigger text demands proportionally more padding.
1406
1405
  // vertical: max(4px, fontSize × 0.3)
1407
1406
  // horizontal: max(8px, fontSize × 0.5)
1408
- const isInlineCode = tag === 'code' && !(el.closest && el.closest('pre'));
1407
+ const isInlineCode = tag === 'code' && !el.closest?.('pre');
1409
1408
  if (!isInlineCode && rect && hasDirectText && textLen > 20 && rect.width > 100 && rect.height > 30) {
1410
1409
  const borders = {
1411
1410
  top: parseFloat(style.borderTopWidth) || 0,
@@ -1521,7 +1520,7 @@ function checkQuality(opts) {
1521
1520
  const CHILD_INSULATE_THRESHOLD = 4;
1522
1521
  const childrenInsulate = { top: false, right: false, bottom: false, left: false };
1523
1522
  for (const child of el.children) {
1524
- let childStyle = getComputedStyleFor(win, child);
1523
+ const childStyle = getComputedStyleFor(win, child);
1525
1524
  if (!childStyle) continue;
1526
1525
  const childPad = {
1527
1526
  top: resolveLengthPx(childStyle.paddingTop, fontSize) ?? 0,
@@ -1653,7 +1652,7 @@ function checkQuality(opts) {
1653
1652
  // Only flag actual body content, not UI labels (buttons, tabs, badges, captions, footer text, etc.)
1654
1653
  if (hasDirectText && textLen > 20 && fontSize < 12) {
1655
1654
  const skipTags = ['sub', 'sup', 'code', 'kbd', 'samp', 'var', 'caption', 'figcaption'];
1656
- const inUIContext = el.closest && 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]');
1655
+ 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]');
1657
1656
  const isUppercase = style.textTransform === 'uppercase';
1658
1657
  if (!skipTags.includes(tag) && !inUIContext && !isUppercase) {
1659
1658
  findings.push({ id: 'tiny-text', snippet: `${fontSize}px body text` });
@@ -1718,7 +1717,7 @@ function checkPageQualityFromDoc(doc) {
1718
1717
  let prevLevel = 0;
1719
1718
  let prevText = '';
1720
1719
  for (const h of headings) {
1721
- const level = parseInt(h.tagName[1]);
1720
+ const level = parseInt(h.tagName[1], 10);
1722
1721
  const text = (h.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 60);
1723
1722
  if (prevLevel > 0 && level > prevLevel + 1) {
1724
1723
  findings.push({
@@ -1766,7 +1765,7 @@ function checkElementBorders(tag, style, overrides, resolvedRadius) {
1766
1765
  if (widths[s] === 0 && overrides && overrides[s]) {
1767
1766
  widths[s] = overrides[s].width;
1768
1767
  colors[s] = overrides[s].color;
1769
- } else if (colors[s] && colors[s].startsWith('var(') && overrides && overrides[s]) {
1768
+ } else if (colors[s]?.startsWith('var(') && overrides?.[s]) {
1770
1769
  // Longhand case: jsdom kept the width but left the color as the
1771
1770
  // literal `var(...)` string. Substitute the resolved color.
1772
1771
  colors[s] = overrides[s].color;
@@ -1827,7 +1826,7 @@ function checkElementColors(el, style, tag, window, customPropMap, hasAnchorInhe
1827
1826
  effectiveBg,
1828
1827
  effectiveBgStops: effectiveBg ? null : resolveGradientStops(el, window),
1829
1828
  fontSize: parseFloat(style.fontSize) || 16,
1830
- fontWeight: parseInt(style.fontWeight) || 400,
1829
+ fontWeight: parseInt(style.fontWeight, 10) || 400,
1831
1830
  hasDirectText,
1832
1831
  isEmojiOnly: isEmojiOnlyText(directText),
1833
1832
  bgClip: style.webkitBackgroundClip || style.backgroundClip || '',
@@ -1954,7 +1953,7 @@ function checkTypography() {
1954
1953
  let totalTextElements = 0;
1955
1954
  for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span')) {
1956
1955
  // Skip monodesign's own elements
1957
- if (el.closest && el.closest('.monodesign-overlay, .monodesign-label, .monodesign-banner, .monodesign-tooltip')) continue;
1956
+ if (el.closest?.('.monodesign-overlay, .monodesign-label, .monodesign-banner, .monodesign-tooltip')) continue;
1958
1957
  // Only count elements that actually have visible direct text
1959
1958
  const hasText = [...el.childNodes].some(n => n.nodeType === 3 && n.textContent.trim().length > 0);
1960
1959
  if (!hasText) continue;
@@ -1995,7 +1994,7 @@ function checkTypography() {
1995
1994
  const sorted = [...sizes].sort((a, b) => a - b);
1996
1995
  const ratio = sorted[sorted.length - 1] / sorted[0];
1997
1996
  if (ratio < 2.0) {
1998
- findings.push({ type: 'flat-type-hierarchy', detail: `Sizes: ${sorted.map(s => s + 'px').join(', ')} (ratio ${ratio.toFixed(1)}:1)` });
1997
+ findings.push({ type: 'flat-type-hierarchy', detail: `Sizes: ${sorted.map(s => `${s}px`).join(', ')} (ratio ${ratio.toFixed(1)}:1)` });
1999
1998
  }
2000
1999
  }
2001
2000
 
@@ -2114,7 +2113,7 @@ function checkPageTypography(doc, win) {
2114
2113
  const sorted = [...sizes].sort((a, b) => a - b);
2115
2114
  const ratio = sorted[sorted.length - 1] / sorted[0];
2116
2115
  if (ratio < 2.0) {
2117
- findings.push({ id: 'flat-type-hierarchy', snippet: `Sizes: ${sorted.map(s => s + 'px').join(', ')} (ratio ${ratio.toFixed(1)}:1)` });
2116
+ findings.push({ id: 'flat-type-hierarchy', snippet: `Sizes: ${sorted.map(s => `${s}px`).join(', ')} (ratio ${ratio.toFixed(1)}:1)` });
2118
2117
  }
2119
2118
  }
2120
2119
 
@@ -2246,7 +2245,7 @@ function checkCreamPalette(doc, win) {
2246
2245
  // 2. Tailwind class fallback — for the static path, where utility classes
2247
2246
  // never resolve to computed CSS.
2248
2247
  for (const el of [body, html]) {
2249
- const tok = creamFromClassList(el && el.getAttribute ? el.getAttribute('class') : '');
2248
+ const tok = creamFromClassList(el?.getAttribute ? el.getAttribute('class') : '');
2250
2249
  if (tok) {
2251
2250
  findings.push({ id: 'cream-palette', snippet: `cream/beige page background (Tailwind ${tok})` });
2252
2251
  break;
@@ -2282,7 +2281,7 @@ function checkOversizedH1({ tag, fontSize, headingText, rect = null, viewportWid
2282
2281
  return [];
2283
2282
  }
2284
2283
 
2285
- function checkElementOversizedH1(el, style, tag, window) {
2284
+ function checkElementOversizedH1(el, _style, tag, window) {
2286
2285
  if (tag !== 'h1') return [];
2287
2286
  const fontSize = resolveFontSizePx(el, window);
2288
2287
  const headingText = (el.textContent || '').trim().replace(/\s+/g, ' ');
@@ -2366,7 +2365,7 @@ function borderColorsFromStyle(style) {
2366
2365
  ];
2367
2366
  }
2368
2367
 
2369
- function checkElementGptBorderShadow(el, style) {
2368
+ function checkElementGptBorderShadow(_el, style) {
2370
2369
  return checkGptThinBorderWideShadow({ borderWidths: borderWidthsFromStyle(style), borderColors: borderColorsFromStyle(style), boxShadow: style.boxShadow || '' });
2371
2370
  }
2372
2371
 
@@ -2516,7 +2515,7 @@ function checkClippedOverflow(el, style, getStyle) {
2516
2515
  return [];
2517
2516
  }
2518
2517
 
2519
- function checkElementClippedOverflow(el, style, tag, window) {
2518
+ function checkElementClippedOverflow(el, style, _tag, window) {
2520
2519
  return checkClippedOverflow(el, style, (n) => window.getComputedStyle(n));
2521
2520
  }
2522
2521
 
@@ -2862,7 +2861,7 @@ function checkElementImageDimensions(el, style) {
2862
2861
  const parts = [el.getAttribute?.('style') || el.attribs?.style || ''];
2863
2862
  const ar = style && (style.aspectRatio || style['aspect-ratio']);
2864
2863
  if (ar && ar !== 'auto') parts.push(`aspect-ratio:${ar}`);
2865
- const h = style && style.height;
2864
+ const h = style?.height;
2866
2865
  if (h && h !== 'auto' && h !== '') parts.push(`height:${h}`);
2867
2866
  return checkImageDimensions({ hasWidthAttr, hasHeightAttr, style: parts.join(';') });
2868
2867
  }
@@ -2922,7 +2921,7 @@ function checkElementSmallTouchTarget(el, style, tag) {
2922
2921
  // (button-like). jsdom returns '' for the default inline display, so key
2923
2922
  // off "not blockish" rather than "== inline".
2924
2923
  const isBlockish = /^(block|flex|grid|inline-block|inline-flex|table)/.test(style.display || '');
2925
- const inProse = !!(el.closest && el.closest(PROSE_ANCESTOR_SELECTOR));
2924
+ const inProse = !!el.closest?.(PROSE_ANCESTOR_SELECTOR);
2926
2925
  const isInlineLinkInProse = tag === 'a' && !isBlockish && inProse;
2927
2926
  return checkSmallTouchTarget({ tag, role, href, inputType, widthPx, heightPx, isInlineLinkInProse });
2928
2927
  }
@@ -2936,7 +2935,7 @@ function checkElementSmallTouchTargetDOM(el) {
2936
2935
  const inputType = el.getAttribute('type') || '';
2937
2936
  const style = getComputedStyle(el);
2938
2937
  const isBlockish = /^(block|flex|grid|inline-block|inline-flex|table)/.test(style.display || '');
2939
- const inProse = !!(el.closest && el.closest(PROSE_ANCESTOR_SELECTOR));
2938
+ const inProse = !!el.closest?.(PROSE_ANCESTOR_SELECTOR);
2940
2939
  const isInlineLinkInProse = tag === 'a' && !isBlockish && inProse;
2941
2940
  return checkSmallTouchTarget({ tag, role, href, inputType, widthPx: rect.width, heightPx: rect.height, isInlineLinkInProse });
2942
2941
  }
@@ -72,7 +72,7 @@ function contrastRatio(c1, c2) {
72
72
  }
73
73
 
74
74
  function parseGradientColors(bgImage) {
75
- if (!bgImage || !bgImage.includes('gradient')) return [];
75
+ if (!bgImage?.includes('gradient')) return [];
76
76
  const colors = [];
77
77
  for (const m of bgImage.matchAll(/rgba?\([^)]+\)/g)) {
78
78
  const c = parseRgb(m[0]);
@@ -109,7 +109,7 @@ function getHue(c) {
109
109
 
110
110
  function colorToHex(c) {
111
111
  if (!c) return '?';
112
- return '#' + [c.r, c.g, c.b].map(v => v.toString(16).padStart(2, '0')).join('');
112
+ return `#${[c.r, c.g, c.b].map(v => v.toString(16).padStart(2, '0')).join('')}`;
113
113
  }
114
114
 
115
115
  export {
@@ -53,7 +53,7 @@ function isBrandFontOnOwnDomain(font) {
53
53
  const allowed = BRAND_FONT_DOMAINS[font];
54
54
  if (!allowed) return false;
55
55
  const host = location.hostname.toLowerCase();
56
- return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
56
+ return allowed.some(suffix => host === suffix || host.endsWith(`.${suffix}`));
57
57
  }
58
58
 
59
59
  const GENERIC_FONTS = new Set([
@@ -117,10 +117,10 @@ function setMatches(set, rule) {
117
117
  }
118
118
 
119
119
  function isInlineIgnored(finding, directives) {
120
- const rule = normalizeRule(finding && finding.antipattern);
120
+ const rule = normalizeRule(finding?.antipattern);
121
121
  if (!rule) return false;
122
122
  if (setMatches(directives.file, rule)) return true;
123
- const line = Number(finding && finding.line) || 0;
123
+ const line = Number(finding?.line) || 0;
124
124
  if (line > 0) {
125
125
  if (setMatches(directives.line.get(line), rule)) return true;
126
126
  if (setMatches(directives.nextLine.get(line), rule)) return true;
@@ -36,11 +36,11 @@ function safeReadJson(filePath) {
36
36
  }
37
37
 
38
38
  function hookSection(raw) {
39
- return raw && raw.hook && typeof raw.hook === 'object' && !Array.isArray(raw.hook) ? raw.hook : null;
39
+ return raw?.hook && typeof raw.hook === 'object' && !Array.isArray(raw.hook) ? raw.hook : null;
40
40
  }
41
41
 
42
42
  function detectorSection(raw) {
43
- return raw && raw.detector && typeof raw.detector === 'object' && !Array.isArray(raw.detector) ? raw.detector : null;
43
+ return raw?.detector && typeof raw.detector === 'object' && !Array.isArray(raw.detector) ? raw.detector : null;
44
44
  }
45
45
 
46
46
  const DETECTOR_CONFIG_KEYS = new Set(['ignoreRules', 'ignoreFiles', 'ignoreValues', 'designSystem']);
@@ -74,7 +74,7 @@ function applyDetectionConfigSource(config, raw) {
74
74
  if (raw.designSystem && typeof raw.designSystem === 'object' && !Array.isArray(raw.designSystem)) {
75
75
  config.designSystem = {
76
76
  ...config.designSystem,
77
- enabled: raw.designSystem.enabled === false ? false : true,
77
+ enabled: raw.designSystem.enabled !== false,
78
78
  };
79
79
  }
80
80
  if (Array.isArray(raw.ignoreRules)) {
@@ -153,7 +153,7 @@ function normalizeDetectionConfigForWrite(config) {
153
153
  out.ignoreValues = normalizeIgnoreValueEntries(config?.ignoreValues || []);
154
154
  if (config?.designSystem && typeof config.designSystem === 'object' && !Array.isArray(config.designSystem)) {
155
155
  out.designSystem = {
156
- enabled: config.designSystem.enabled === false ? false : true,
156
+ enabled: config.designSystem.enabled !== false,
157
157
  };
158
158
  }
159
159
  return out;
@@ -242,7 +242,7 @@ function splitColorArgs(body) {
242
242
  if (text.includes(',')) {
243
243
  const parts = text.split(',').map((part) => part.trim()).filter(Boolean);
244
244
  const last = parts[parts.length - 1];
245
- if (last && last.includes('/')) {
245
+ if (last?.includes('/')) {
246
246
  const split = last.split('/').map((part) => part.trim()).filter(Boolean);
247
247
  return [...parts.slice(0, -1), ...split];
248
248
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monoes/monodesign",
3
- "version": "1.2.9",
3
+ "version": "1.2.12",
4
4
  "description": "Frontend design intelligence for AI agents — design tokens, antipattern detection, and the monodesign skill",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -24,6 +24,13 @@
24
24
  "engines": {
25
25
  "node": ">=22.12.0"
26
26
  },
27
+ "scripts": {
28
+ "prebuild": "node ../../../scripts/build-fs.mjs clean dist tsconfig.tsbuildinfo",
29
+ "build": "tsc",
30
+ "prepublishOnly": "npm run build",
31
+ "test": "node scripts/run-tests.mjs",
32
+ "sync": "node scripts/sync-skill.mjs"
33
+ },
27
34
  "keywords": [
28
35
  "design-system",
29
36
  "design-tokens",
@@ -44,13 +51,7 @@
44
51
  "marked": "^18.0.0"
45
52
  },
46
53
  "optionalDependencies": {
47
- "@monoes/monobrowse": "^1.0.7",
54
+ "@monoes/monobrowse": "workspace:*",
48
55
  "puppeteer": "^25.1.0"
49
- },
50
- "scripts": {
51
- "prebuild": "node ../../../scripts/build-fs.mjs clean dist tsconfig.tsbuildinfo",
52
- "build": "tsc",
53
- "test": "node scripts/run-tests.mjs",
54
- "sync": "node scripts/sync-skill.mjs"
55
56
  }
56
- }
57
+ }
@@ -205,7 +205,7 @@ function resolveLocalContextDir(root) {
205
205
 
206
206
  function resolveEnvContextDir(cwd) {
207
207
  const envDir = process.env.MONODESIGN_CONTEXT_DIR;
208
- if (!envDir || !envDir.trim()) return null;
208
+ if (!envDir?.trim()) return null;
209
209
  const trimmed = envDir.trim();
210
210
  return path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed);
211
211
  }
@@ -880,7 +880,7 @@ async function computeUpdateDirective(now = Date.now()) {
880
880
  // there is no separate registry endpoint to poll. Kept as a stub so the boot
881
881
  // path (and MONODESIGN_NO_UPDATE_CHECK-aware tests) stays unchanged.
882
882
  return null;
883
- // eslint-disable-next-line no-unreachable
883
+ // biome-ignore lint/correctness/noUnreachable: disabled on purpose (see above); kept to re-enable
884
884
  try {
885
885
  if (process.env.MONODESIGN_NO_UPDATE_CHECK) return null;
886
886
  if (updateCheckDisabledByConfig()) return null;
@@ -930,7 +930,7 @@ async function cli() {
930
930
  const targetExists = targetProvided ? pathExistsForTarget(process.cwd(), cliOptions.targetPath) : null;
931
931
  const selection = resolveTargetSelection(process.cwd(), cliOptions);
932
932
  if (selection) {
933
- process.stdout.write(buildTargetSelectionDirective(selection) + '\n');
933
+ process.stdout.write(`${buildTargetSelectionDirective(selection)}\n`);
934
934
  process.exit(0);
935
935
  }
936
936
  const ctx = loadContext(process.cwd(), cliOptions);
@@ -952,7 +952,7 @@ async function cli() {
952
952
  parts.push(buildMissingTargetDirective());
953
953
  }
954
954
  if (updateDirective) parts.push(updateDirective);
955
- process.stdout.write(parts.join('\n\n---\n\n') + '\n');
955
+ process.stdout.write(`${parts.join('\n\n---\n\n')}\n`);
956
956
  process.exit(0);
957
957
  }
958
958
  const parts = [`# PRODUCT.md\n\n${ctx.product.trim()}`];
@@ -989,7 +989,7 @@ async function cli() {
989
989
  }
990
990
  }
991
991
  if (updateDirective) parts.push(updateDirective);
992
- process.stdout.write(parts.join('\n\n---\n\n') + '\n');
992
+ process.stdout.write(`${parts.join('\n\n---\n\n')}\n`);
993
993
  }
994
994
 
995
995
  function parseCliOptions(args) {
@@ -381,7 +381,7 @@ function main(argv) {
381
381
  }
382
382
  case 'trend': {
383
383
  const rows = readTrend(args[0], { limit: args[1] ? Number(args[1]) : 5 });
384
- process.stdout.write(JSON.stringify(rows, null, 2) + '\n');
384
+ process.stdout.write(`${JSON.stringify(rows, null, 2)}\n`);
385
385
  return;
386
386
  }
387
387
  case 'recall': {
@@ -71,11 +71,6 @@ const INLINE_HEADER_SIGNALS = [
71
71
  /\bscript-src\b/,
72
72
  /\bconnect-src\b/,
73
73
  ];
74
- const NUXT_ROUTE_RULES_SIGNALS = [
75
- /\brouteRules\b/,
76
- /Content-Security-Policy/i,
77
- /\bscript-src\b/,
78
- ];
79
74
 
80
75
  const MIDDLEWARE_HINT = /headers\.set\(\s*["']Content-Security-Policy["']/i;
81
76
  const META_TAG_HINT = /http-equiv\s*=\s*["']Content-Security-Policy["']/i;
@@ -91,7 +86,7 @@ export function detectCsp(cwd = process.cwd()) {
91
86
  const ext = path.extname(absPath);
92
87
  const base = path.basename(absPath).toLowerCase();
93
88
  const isConfig = (name) =>
94
- new RegExp('(^|/)' + name + '\\.config\\.').test(relPath);
89
+ new RegExp(`(^|/)${name}\\.config\\.`).test(relPath);
95
90
 
96
91
  // === append-arrays candidates ===
97
92
 
@@ -184,7 +184,7 @@ function writeHookConfig(cwd, hookConfig, opts = {}) {
184
184
  // (consent, quiet, auditLog) survive an Monodesign hooks edit.
185
185
  const next = { ...existing, hook: { ...existingHook, ...hookConfig } };
186
186
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
187
- fs.writeFileSync(filePath, JSON.stringify(next, null, 2) + '\n');
187
+ fs.writeFileSync(filePath, `${JSON.stringify(next, null, 2)}\n`);
188
188
  return filePath;
189
189
  }
190
190
 
@@ -202,14 +202,14 @@ function writeDetectorConfig(cwd, detectorConfig, opts = {}) {
202
202
  if (Object.keys(nextHook).length > 0) next.hook = nextHook;
203
203
  else delete next.hook;
204
204
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
205
- fs.writeFileSync(filePath, JSON.stringify(next, null, 2) + '\n');
205
+ fs.writeFileSync(filePath, `${JSON.stringify(next, null, 2)}\n`);
206
206
  return filePath;
207
207
  }
208
208
 
209
209
  function mergeHookConfig(existing) {
210
210
  const base = existing && typeof existing === 'object' ? existing : {};
211
211
  return {
212
- enabled: base.enabled === false ? false : true,
212
+ enabled: base.enabled !== false,
213
213
  limits: {
214
214
  maxFindings: Number.isFinite(base?.limits?.maxFindings) ? base.limits.maxFindings : DEFAULT_CONFIG.limits.maxFindings,
215
215
  maxChars: Number.isFinite(base?.limits?.maxChars) ? base.limits.maxChars : DEFAULT_CONFIG.limits.maxChars,
@@ -234,7 +234,7 @@ function mergeDetectorConfig(existing, seed = null) {
234
234
  if (base.designSystem && typeof base.designSystem === 'object' && !Array.isArray(base.designSystem)) {
235
235
  out.designSystem = {
236
236
  ...(out.designSystem || {}),
237
- enabled: base.designSystem.enabled === false ? false : true,
237
+ enabled: base.designSystem.enabled !== false,
238
238
  };
239
239
  }
240
240
  if (Array.isArray(base.ignoreRules)) {
@@ -644,7 +644,7 @@ function reset(cwd) {
644
644
  if (Object.keys(rest).length === 0) {
645
645
  fs.unlinkSync(filePath);
646
646
  } else {
647
- fs.writeFileSync(filePath, JSON.stringify(rest, null, 2) + '\n');
647
+ fs.writeFileSync(filePath, `${JSON.stringify(rest, null, 2)}\n`);
648
648
  }
649
649
  removed.push(path.relative(cwd, filePath).split(path.sep).join('/') || filePath);
650
650
  } catch { /* ignore */ }
@@ -684,7 +684,7 @@ function main() {
684
684
  case 'ignore-value': out = addIgnoreValue(cwd, rest); break;
685
685
  case 'reset': out = reset(cwd); break;
686
686
  }
687
- process.stdout.write(out + '\n');
687
+ process.stdout.write(`${out}\n`);
688
688
  } catch (err) {
689
689
  process.stderr.write(`Error: ${err.message || err}\n`);
690
690
  process.exit(1);
@@ -17,11 +17,11 @@ import path from 'node:path';
17
17
  import {
18
18
  ALLOWED_EXTS,
19
19
  EDIT_COUNT_THRESHOLD,
20
- GENERATED_PATH,
21
20
  SENSITIVE_PATH,
22
21
  appendDesignSystemNote,
23
22
  designSystemOptions,
24
23
  filterFindings,
24
+ isGeneratedPath,
25
25
  isNativePlatform,
26
26
  loadDetector,
27
27
  matchConfiguredExtension,
@@ -162,7 +162,7 @@ function replaceOnce(original, oldString, newString) {
162
162
 
163
163
  function readExistingProjectFile(filePath, cwd) {
164
164
  if (!isInsideProject(filePath, cwd)) return null;
165
- if (SENSITIVE_PATH.test(filePath) || GENERATED_PATH.test(filePath)) return null;
165
+ if (SENSITIVE_PATH.test(filePath) || isGeneratedPath(filePath, cwd)) return null;
166
166
  try {
167
167
  const stat = fs.statSync(filePath);
168
168
  if (!stat.isFile() || stat.size > 1024 * 1024) return null;
@@ -233,7 +233,7 @@ function shellCopiedFileContent(command, cwd) {
233
233
  if (!source) return '';
234
234
  const sourcePath = path.isAbsolute(source) ? source : path.resolve(cwd, source);
235
235
  if (!isInsideProject(sourcePath, cwd)) return '';
236
- if (SENSITIVE_PATH.test(sourcePath) || GENERATED_PATH.test(sourcePath)) return '';
236
+ if (SENSITIVE_PATH.test(sourcePath) || isGeneratedPath(sourcePath, cwd)) return '';
237
237
  try {
238
238
  const stat = fs.statSync(sourcePath);
239
239
  if (!stat.isFile() || stat.size > 1024 * 1024) return '';
@@ -416,7 +416,7 @@ async function main() {
416
416
  if (!filePath) return allow({ ...audit, skipped: 'no-file-path', durationMs: Date.now() - started });
417
417
  if (!isInsideProject(filePath, cwd)) return allow({ ...audit, skipped: 'outside-project', durationMs: Date.now() - started });
418
418
  if (SENSITIVE_PATH.test(filePath)) return allow({ ...audit, skipped: 'sensitive', durationMs: Date.now() - started });
419
- if (GENERATED_PATH.test(filePath)) return allow({ ...audit, skipped: 'generated', durationMs: Date.now() - started });
419
+ if (isGeneratedPath(filePath, cwd)) return allow({ ...audit, skipped: 'generated', durationMs: Date.now() - started });
420
420
 
421
421
  // Config is read before the extension gate so `detector.extensions` entries
422
422
  // (e.g. `.blade.php` template files, issue #316) can widen it.