@bastani/atomic 0.9.0-alpha.3 → 0.9.0-alpha.4

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 (84) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/builtin/cursor/package.json +2 -2
  3. package/dist/builtin/intercom/package.json +1 -1
  4. package/dist/builtin/mcp/package.json +1 -1
  5. package/dist/builtin/subagents/package.json +1 -1
  6. package/dist/builtin/web-access/package.json +1 -1
  7. package/dist/builtin/workflows/CHANGELOG.md +17 -0
  8. package/dist/builtin/workflows/README.md +12 -12
  9. package/dist/builtin/workflows/builtin/goal-prompts.ts +8 -0
  10. package/dist/builtin/workflows/builtin/goal-runner.ts +96 -1
  11. package/dist/builtin/workflows/builtin/goal-types.ts +2 -0
  12. package/dist/builtin/workflows/builtin/goal.d.ts +3 -0
  13. package/dist/builtin/workflows/builtin/goal.ts +12 -1
  14. package/dist/builtin/workflows/builtin/index.d.ts +8 -8
  15. package/dist/builtin/workflows/builtin/open-claude-design-feedback.ts +359 -0
  16. package/dist/builtin/workflows/builtin/open-claude-design-phases.ts +254 -352
  17. package/dist/builtin/workflows/builtin/open-claude-design-runner.ts +256 -414
  18. package/dist/builtin/workflows/builtin/open-claude-design-setup.ts +272 -0
  19. package/dist/builtin/workflows/builtin/open-claude-design-utils.ts +58 -68
  20. package/dist/builtin/workflows/builtin/open-claude-design.d.ts +5 -9
  21. package/dist/builtin/workflows/builtin/open-claude-design.ts +14 -26
  22. package/dist/builtin/workflows/package.json +1 -1
  23. package/dist/builtin/workflows/skills/impeccable/SKILL.md +14 -23
  24. package/dist/builtin/workflows/skills/impeccable/reference/brand.md +2 -2
  25. package/dist/builtin/workflows/skills/impeccable/reference/live.md +25 -4
  26. package/dist/builtin/workflows/skills/impeccable/scripts/context-signals.mjs +1 -1
  27. package/dist/builtin/workflows/skills/impeccable/scripts/context.mjs +724 -29
  28. package/dist/builtin/workflows/skills/impeccable/scripts/critique-storage.mjs +1 -1
  29. package/dist/builtin/workflows/skills/impeccable/scripts/detector/browser/injected/index.mjs +219 -7
  30. package/dist/builtin/workflows/skills/impeccable/scripts/detector/cli/main.mjs +57 -11
  31. package/dist/builtin/workflows/skills/impeccable/scripts/detector/design-system.mjs +750 -0
  32. package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +648 -53
  33. package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns.mjs +7 -0
  34. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/browser/detect-url.mjs +29 -4
  35. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +44 -11
  36. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +29 -0
  37. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +27 -1
  38. package/dist/builtin/workflows/skills/impeccable/scripts/detector/node/file-system.mjs +1 -1
  39. package/dist/builtin/workflows/skills/impeccable/scripts/detector/registry/antipatterns.mjs +29 -0
  40. package/dist/builtin/workflows/skills/impeccable/scripts/detector/rules/checks.mjs +401 -46
  41. package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/inline-ignores.mjs +148 -0
  42. package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/page.mjs +6 -6
  43. package/dist/builtin/workflows/skills/impeccable/scripts/{design-parser.mjs → lib/design-parser.mjs} +8 -1
  44. package/dist/builtin/workflows/skills/impeccable/scripts/lib/impeccable-config.mjs +638 -0
  45. package/dist/builtin/workflows/skills/impeccable/scripts/lib/impeccable-paths.mjs +128 -0
  46. package/dist/builtin/workflows/skills/impeccable/scripts/{is-generated.mjs → lib/is-generated.mjs} +2 -2
  47. package/dist/builtin/workflows/skills/impeccable/scripts/lib/target-args.mjs +42 -0
  48. package/dist/builtin/workflows/skills/impeccable/scripts/live/browser-script-parts.mjs +49 -0
  49. package/dist/builtin/workflows/skills/impeccable/scripts/{live-completion.mjs → live/completion.mjs} +1 -0
  50. package/dist/builtin/workflows/skills/impeccable/scripts/{live-event-validation.mjs → live/event-validation.mjs} +6 -5
  51. package/dist/builtin/workflows/skills/impeccable/scripts/live/manual-apply.mjs +939 -0
  52. package/dist/builtin/workflows/skills/impeccable/scripts/live/manual-edit-routes.mjs +357 -0
  53. package/dist/builtin/workflows/skills/impeccable/scripts/{live-manual-edits-buffer.mjs → live/manual-edits-buffer.mjs} +1 -1
  54. package/dist/builtin/workflows/skills/impeccable/scripts/{live-session-store.mjs → live/session-store.mjs} +21 -3
  55. package/dist/builtin/workflows/skills/impeccable/scripts/live/svelte-component.mjs +835 -0
  56. package/dist/builtin/workflows/skills/impeccable/scripts/live/sveltekit-adapter.mjs +274 -0
  57. package/dist/builtin/workflows/skills/impeccable/scripts/live/ui-core.mjs +180 -0
  58. package/dist/builtin/workflows/skills/impeccable/scripts/live/vocabulary.mjs +36 -0
  59. package/dist/builtin/workflows/skills/impeccable/scripts/live-accept.mjs +185 -60
  60. package/dist/builtin/workflows/skills/impeccable/scripts/live-browser-dom.js +146 -0
  61. package/dist/builtin/workflows/skills/impeccable/scripts/live-browser.js +3369 -1026
  62. package/dist/builtin/workflows/skills/impeccable/scripts/live-commit-manual-edits.mjs +2 -2
  63. package/dist/builtin/workflows/skills/impeccable/scripts/live-complete.mjs +2 -2
  64. package/dist/builtin/workflows/skills/impeccable/scripts/live-discard-manual-edits.mjs +1 -1
  65. package/dist/builtin/workflows/skills/impeccable/scripts/live-inject.mjs +133 -9
  66. package/dist/builtin/workflows/skills/impeccable/scripts/live-insert.mjs +42 -2
  67. package/dist/builtin/workflows/skills/impeccable/scripts/live-manual-edit-evidence.mjs +4 -4
  68. package/dist/builtin/workflows/skills/impeccable/scripts/live-poll.mjs +21 -15
  69. package/dist/builtin/workflows/skills/impeccable/scripts/live-resume.mjs +1 -1
  70. package/dist/builtin/workflows/skills/impeccable/scripts/live-server.mjs +205 -1269
  71. package/dist/builtin/workflows/skills/impeccable/scripts/live-status.mjs +2 -2
  72. package/dist/builtin/workflows/skills/impeccable/scripts/live-target.mjs +30 -0
  73. package/dist/builtin/workflows/skills/impeccable/scripts/live-wrap.mjs +69 -26
  74. package/dist/builtin/workflows/skills/impeccable/scripts/live.mjs +73 -22
  75. package/dist/core/atomic-guide-command.d.ts.map +1 -1
  76. package/dist/core/atomic-guide-command.js +5 -5
  77. package/dist/core/atomic-guide-command.js.map +1 -1
  78. package/docs/index.md +2 -2
  79. package/docs/quickstart.md +9 -9
  80. package/docs/workflows.md +42 -23
  81. package/package.json +2 -2
  82. package/dist/builtin/workflows/skills/impeccable/scripts/cleanup-deprecated.mjs +0 -284
  83. package/dist/builtin/workflows/skills/impeccable/scripts/impeccable-paths.mjs +0 -126
  84. /package/dist/builtin/workflows/skills/impeccable/scripts/{live-insert-ui.mjs → live/insert-ui.mjs} +0 -0
@@ -514,9 +514,13 @@ function checkHtmlPatterns(html) {
514
514
  // --- Motion ---
515
515
 
516
516
  // Bounce/elastic animation names
517
- const bounceRe = /animation(?:-name)?\s*:\s*[^;]*\b(bounce|elastic|wobble|jiggle|spring)\b/gi;
518
- if (bounceRe.test(html)) {
519
- findings.push({ id: 'bounce-easing', snippet: 'Bounce/elastic animation in CSS' });
517
+ const bounceRe = /animation(?:-name)?\s*:\s*([^;{}]*(?:bounce|elastic|wobble|jiggle|spring)[^;{}]*)/gi;
518
+ const bounceMatch = bounceRe.exec(html);
519
+ if (bounceMatch) {
520
+ const animationToken = bounceMatch[1]
521
+ .split(/[,\s]+/)
522
+ .find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
523
+ findings.push({ id: 'bounce-easing', snippet: `animation: ${animationToken || bounceMatch[1].trim()}` });
520
524
  }
521
525
 
522
526
  // Overshoot cubic-bezier
@@ -974,11 +978,16 @@ function parseAnyColor(s) {
974
978
  // OKLCH parser. Tailwind v4's CSS minifier squishes the space after
975
979
  // `%` ("21.5%.02 50"), so the separator between L and C may be absent.
976
980
  // Match L (with optional %), then C and H separated permissively.
977
- m = str.match(/oklch\(\s*([\d.]+)(%?)\s*[\s,]*\s*([\d.]+)\s*[\s,]+\s*([-\d.]+)(?:deg)?\s*\)/i);
981
+ m = str.match(/oklch\(\s*([\d.]+)(%?)\s*[\s,]*\s*([\d.]+)\s*[\s,]+\s*([-\d.]+)(?:deg)?(?:\s*\/\s*([\d.]+)(%)?)?\s*\)/i);
978
982
  if (m) {
979
983
  const Lnum = parseFloat(m[1]);
980
984
  const L = m[2] === '%' ? Lnum / 100 : Lnum;
981
- return oklchToRgb(L, parseFloat(m[3]), parseFloat(m[4]));
985
+ const rgb = oklchToRgb(L, parseFloat(m[3]), parseFloat(m[4]));
986
+ if (m[5] !== undefined) {
987
+ const alpha = parseFloat(m[5]);
988
+ rgb.a = m[6] === '%' ? alpha / 100 : alpha;
989
+ }
990
+ return rgb;
982
991
  }
983
992
  return null;
984
993
  }
@@ -1007,9 +1016,19 @@ const REPEATED_KICKER_SKIP_SELECTOR = [
1007
1016
  '[role="navigation"]',
1008
1017
  '[aria-label*="breadcrumb" i]',
1009
1018
  '[class*="breadcrumb" i]',
1019
+ '[aria-hidden="true"]',
1010
1020
  '[data-impeccable-allow-kickers]',
1011
1021
  ].join(',');
1012
1022
 
1023
+ const REPEATED_KICKER_CARD_CONTEXT_SELECTOR = [
1024
+ 'article',
1025
+ 'button',
1026
+ 'a',
1027
+ 'li',
1028
+ '[role="listitem"]',
1029
+ '[role="option"]',
1030
+ ].join(',');
1031
+
1013
1032
  function cleanInlineText(el) {
1014
1033
  return [...el.childNodes]
1015
1034
  .filter(n => n.nodeType === 3)
@@ -1019,6 +1038,11 @@ function cleanInlineText(el) {
1019
1038
  .trim();
1020
1039
  }
1021
1040
 
1041
+ function isRepeatedKickerCardContext(heading, kicker) {
1042
+ const item = heading.closest?.(REPEATED_KICKER_CARD_CONTEXT_SELECTOR);
1043
+ return Boolean(item && (!item.contains || item.contains(kicker)));
1044
+ }
1045
+
1022
1046
  function isRepeatedKickerCandidate(opts) {
1023
1047
  const {
1024
1048
  headingTag,
@@ -1032,6 +1056,7 @@ function isRepeatedKickerCandidate(opts) {
1032
1056
  } = opts;
1033
1057
  if (!['h2', 'h3', 'h4'].includes(headingTag)) return false;
1034
1058
  if (!headingText || headingText.length < 3) return false;
1059
+ if (/^\/[\w-]+/i.test(headingText.replace(/^"|"$/g, '').trim())) return false;
1035
1060
  if (!(headingFontSize >= 20)) return false;
1036
1061
  if (!kickerTag || HEADING_TAGS.has(kickerTag)) return false;
1037
1062
  if (!['p', 'span', 'div', 'small'].includes(kickerTag)) return false;
@@ -1053,6 +1078,7 @@ function collectRepeatedSectionKickerCandidates(doc, getStyle, resolveLetterSpac
1053
1078
  if (heading.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
1054
1079
  const kicker = heading.previousElementSibling;
1055
1080
  if (!kicker || kicker.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
1081
+ if (isRepeatedKickerCardContext(heading, kicker)) continue;
1056
1082
 
1057
1083
  const headingStyle = getStyle(heading);
1058
1084
  const kickerStyle = getStyle(kicker);
@@ -1235,6 +1261,84 @@ function resolveLengthPx(value, fontSizePx) {
1235
1261
  return num * fontSizePx;
1236
1262
  }
1237
1263
 
1264
+ function cssColorIsTransparent(value) {
1265
+ if (!value) return true;
1266
+ const str = String(value).trim().toLowerCase();
1267
+ if (!str || str === 'transparent' || str === 'rgba(0, 0, 0, 0)') return true;
1268
+ const parsed = parseAnyColor(str);
1269
+ if (parsed) return (parsed.a ?? 1) <= 0.05;
1270
+ return /^rgba\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*0(?:\.0+)?\s*\)$/.test(str);
1271
+ }
1272
+
1273
+ function colorsNearlyMatch(a, b) {
1274
+ const ca = parseAnyColor(a);
1275
+ const cb = parseAnyColor(b);
1276
+ if (!ca || !cb) return false;
1277
+ const alphaDelta = Math.abs((ca.a ?? 1) - (cb.a ?? 1));
1278
+ const channelDelta = Math.max(
1279
+ Math.abs(ca.r - cb.r),
1280
+ Math.abs(ca.g - cb.g),
1281
+ Math.abs(ca.b - cb.b),
1282
+ );
1283
+ return alphaDelta <= 0.03 && channelDelta <= 3;
1284
+ }
1285
+
1286
+ function getComputedStyleFor(win, el) {
1287
+ if (win && typeof win.getComputedStyle === 'function') {
1288
+ try { return win.getComputedStyle(el); } catch {}
1289
+ }
1290
+ if (typeof getComputedStyle === 'function') {
1291
+ try { return getComputedStyle(el); } catch {}
1292
+ }
1293
+ return null;
1294
+ }
1295
+
1296
+ function hasVisibleBackgroundBoundary(style, el, win) {
1297
+ const bg = style?.backgroundColor || '';
1298
+ if (cssColorIsTransparent(bg)) return false;
1299
+
1300
+ let parent = el?.parentElement || null;
1301
+ while (parent) {
1302
+ const parentStyle = getComputedStyleFor(win, parent);
1303
+ const parentBg = parentStyle?.backgroundColor || '';
1304
+ if (!cssColorIsTransparent(parentBg)) {
1305
+ return !colorsNearlyMatch(bg, parentBg);
1306
+ }
1307
+ parent = parent.parentElement;
1308
+ }
1309
+
1310
+ return true;
1311
+ }
1312
+
1313
+ const TEXT_EDGE_TAGS = new Set(['A', 'BUTTON', 'CODE', 'DD', 'DT', 'FIGCAPTION', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'LI', 'P', 'PRE', 'SPAN', 'TD', 'TH']);
1314
+
1315
+ function hasMeaningfulDirectText(node) {
1316
+ if (!node?.childNodes) return false;
1317
+ for (const child of node.childNodes) {
1318
+ if (child.nodeType === 3 && child.textContent.trim().length > 4) return true;
1319
+ }
1320
+ return false;
1321
+ }
1322
+
1323
+ function textDescendantsFlushSides(el, rect) {
1324
+ const flush = { top: false, right: false, bottom: false, left: false };
1325
+ if (!rect || !el?.querySelectorAll) return flush;
1326
+ const TEXT_EDGE_THRESHOLD = 4;
1327
+ const candidates = el.querySelectorAll('a, button, code, dd, dt, figcaption, h1, h2, h3, h4, h5, h6, li, p, pre, span, td, th');
1328
+ for (const node of candidates) {
1329
+ if (!TEXT_EDGE_TAGS.has(node.tagName) || !hasMeaningfulDirectText(node)) continue;
1330
+ let nodeRect = null;
1331
+ try { nodeRect = node.getBoundingClientRect(); } catch {}
1332
+ if (!nodeRect || nodeRect.width <= 0 || nodeRect.height <= 0) continue;
1333
+ if (nodeRect.bottom < rect.top || nodeRect.top > rect.bottom || nodeRect.right < rect.left || nodeRect.left > rect.right) continue;
1334
+ if (nodeRect.top - rect.top <= TEXT_EDGE_THRESHOLD) flush.top = true;
1335
+ if (rect.right - nodeRect.right <= TEXT_EDGE_THRESHOLD) flush.right = true;
1336
+ if (rect.bottom - nodeRect.bottom <= TEXT_EDGE_THRESHOLD) flush.bottom = true;
1337
+ if (nodeRect.left - rect.left <= TEXT_EDGE_THRESHOLD) flush.left = true;
1338
+ }
1339
+ return flush;
1340
+ }
1341
+
1238
1342
  // Pure quality checks. Most run on computed CSS and DOM-only inputs (work in
1239
1343
  // jsdom and the browser). Two checks (line-length, cramped-padding) gate on
1240
1344
  // element rect dimensions, which jsdom can't compute — pass `rect: null` from
@@ -1264,7 +1368,8 @@ function checkQuality(opts) {
1264
1368
  // font-size — bigger text demands proportionally more padding.
1265
1369
  // vertical: max(4px, fontSize × 0.3)
1266
1370
  // horizontal: max(8px, fontSize × 0.5)
1267
- if (rect && hasDirectText && textLen > 20 && rect.width > 100 && rect.height > 30) {
1371
+ const isInlineCode = tag === 'code' && !(el.closest && el.closest('pre'));
1372
+ if (!isInlineCode && rect && hasDirectText && textLen > 20 && rect.width > 100 && rect.height > 30) {
1268
1373
  const borders = {
1269
1374
  top: parseFloat(style.borderTopWidth) || 0,
1270
1375
  right: parseFloat(style.borderRightWidth) || 0,
@@ -1272,7 +1377,7 @@ function checkQuality(opts) {
1272
1377
  left: parseFloat(style.borderLeftWidth) || 0,
1273
1378
  };
1274
1379
  const borderCount = Object.values(borders).filter(w => w > 0).length;
1275
- const hasBg = style.backgroundColor && style.backgroundColor !== 'rgba(0, 0, 0, 0)';
1380
+ const hasBg = hasVisibleBackgroundBoundary(style, el, win);
1276
1381
  if (borderCount >= 2 || hasBg) {
1277
1382
  const vPads = [], hPads = [];
1278
1383
  if (hasBg || borders.top > 0) vPads.push(parseFloat(style.paddingTop) || 0);
@@ -1320,10 +1425,6 @@ function checkQuality(opts) {
1320
1425
  !['fixed', 'absolute'].includes(elPosition) &&
1321
1426
  el.children && el.children.length > 0
1322
1427
  ) {
1323
- const isTransparent = (c) =>
1324
- !c || c === 'transparent' || c === 'rgba(0, 0, 0, 0)' ||
1325
- /^rgba\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*0(?:\.0+)?\s*\)$/.test(c);
1326
-
1327
1428
  const borderW = {
1328
1429
  top: parseFloat(style.borderTopWidth) || 0,
1329
1430
  right: parseFloat(style.borderRightWidth) || 0,
@@ -1331,10 +1432,10 @@ function checkQuality(opts) {
1331
1432
  left: parseFloat(style.borderLeftWidth) || 0,
1332
1433
  };
1333
1434
  const borderVisible = {
1334
- top: borderW.top > 0 && !isTransparent(style.borderTopColor),
1335
- right: borderW.right > 0 && !isTransparent(style.borderRightColor),
1336
- bottom: borderW.bottom > 0 && !isTransparent(style.borderBottomColor),
1337
- left: borderW.left > 0 && !isTransparent(style.borderLeftColor),
1435
+ top: borderW.top > 0 && !cssColorIsTransparent(style.borderTopColor),
1436
+ right: borderW.right > 0 && !cssColorIsTransparent(style.borderRightColor),
1437
+ bottom: borderW.bottom > 0 && !cssColorIsTransparent(style.borderBottomColor),
1438
+ left: borderW.left > 0 && !cssColorIsTransparent(style.borderLeftColor),
1338
1439
  };
1339
1440
  // Outline detection. jsdom decomposes `border` shorthand into
1340
1441
  // border{Top,…}Width/Color but does NOT decompose `outline` —
@@ -1354,8 +1455,8 @@ function checkQuality(opts) {
1354
1455
  if (cMatch) outlineColorVal = cMatch[1];
1355
1456
  }
1356
1457
  }
1357
- const outlineVisible = outlineW > 0 && !isTransparent(outlineColorVal) && outlineStyleVal && outlineStyleVal !== 'none';
1358
- const bgVisible = !isTransparent(style.backgroundColor);
1458
+ const outlineVisible = outlineW > 0 && !cssColorIsTransparent(outlineColorVal) && outlineStyleVal && outlineStyleVal !== 'none';
1459
+ const bgVisible = hasVisibleBackgroundBoundary(style, el, win);
1359
1460
 
1360
1461
  const anyVisible = borderVisible.top || borderVisible.right || borderVisible.bottom || borderVisible.left || outlineVisible || bgVisible;
1361
1462
  if (anyVisible) {
@@ -1383,13 +1484,7 @@ function checkQuality(opts) {
1383
1484
  const CHILD_INSULATE_THRESHOLD = 4;
1384
1485
  const childrenInsulate = { top: false, right: false, bottom: false, left: false };
1385
1486
  for (const child of el.children) {
1386
- let childStyle = null;
1387
- if (win && typeof win.getComputedStyle === 'function') {
1388
- try { childStyle = win.getComputedStyle(child); } catch {}
1389
- }
1390
- if (!childStyle && typeof getComputedStyle === 'function') {
1391
- try { childStyle = getComputedStyle(child); } catch {}
1392
- }
1487
+ let childStyle = getComputedStyleFor(win, child);
1393
1488
  if (!childStyle) continue;
1394
1489
  const childPad = {
1395
1490
  top: resolveLengthPx(childStyle.paddingTop, fontSize) ?? 0,
@@ -1397,15 +1492,37 @@ function checkQuality(opts) {
1397
1492
  bottom: resolveLengthPx(childStyle.paddingBottom, fontSize) ?? 0,
1398
1493
  left: resolveLengthPx(childStyle.paddingLeft, fontSize) ?? 0,
1399
1494
  };
1495
+ const childMargin = {
1496
+ top: resolveLengthPx(childStyle.marginTop, fontSize) ?? 0,
1497
+ right: resolveLengthPx(childStyle.marginRight, fontSize) ?? 0,
1498
+ bottom: resolveLengthPx(childStyle.marginBottom, fontSize) ?? 0,
1499
+ left: resolveLengthPx(childStyle.marginLeft, fontSize) ?? 0,
1500
+ };
1501
+ if (rect && typeof child.getBoundingClientRect === 'function') {
1502
+ try {
1503
+ const childRect = child.getBoundingClientRect();
1504
+ if (childRect && childRect.width > 0 && childRect.height > 0) {
1505
+ if (childRect.top - rect.top >= CHILD_INSULATE_THRESHOLD) childrenInsulate.top = true;
1506
+ if (rect.right - childRect.right >= CHILD_INSULATE_THRESHOLD) childrenInsulate.right = true;
1507
+ if (rect.bottom - childRect.bottom >= CHILD_INSULATE_THRESHOLD) childrenInsulate.bottom = true;
1508
+ if (childRect.left - rect.left >= CHILD_INSULATE_THRESHOLD) childrenInsulate.left = true;
1509
+ }
1510
+ } catch {}
1511
+ }
1400
1512
  for (const s of ['top', 'right', 'bottom', 'left']) {
1401
- if (childPad[s] >= CHILD_INSULATE_THRESHOLD) childrenInsulate[s] = true;
1513
+ if (childPad[s] >= CHILD_INSULATE_THRESHOLD || childMargin[s] >= CHILD_INSULATE_THRESHOLD) {
1514
+ childrenInsulate[s] = true;
1515
+ }
1402
1516
  }
1403
1517
  }
1404
1518
 
1519
+ const textFlush = rect ? textDescendantsFlushSides(el, rect) : null;
1520
+ const fullBleedBgBand = rect && viewportWidth > 0 && rect.width >= viewportWidth * 0.94 && bgVisible && !outlineVisible;
1405
1521
  const flushSides = [];
1406
1522
  for (const side of ['top', 'right', 'bottom', 'left']) {
1407
- const sideBounded = borderVisible[side] || outlineVisible || bgVisible;
1408
- if (sideBounded && pad[side] <= PAD_THRESHOLD && !childrenInsulate[side]) {
1523
+ const bgBoundsSide = bgVisible && !(fullBleedBgBand && (side === 'left' || side === 'right'));
1524
+ const sideBounded = borderVisible[side] || outlineVisible || bgBoundsSide;
1525
+ if (sideBounded && pad[side] <= PAD_THRESHOLD && !childrenInsulate[side] && (!textFlush || textFlush[side])) {
1409
1526
  flushSides.push(side);
1410
1527
  }
1411
1528
  }
@@ -1499,7 +1616,7 @@ function checkQuality(opts) {
1499
1616
  // Only flag actual body content, not UI labels (buttons, tabs, badges, captions, footer text, etc.)
1500
1617
  if (hasDirectText && textLen > 20 && fontSize < 12) {
1501
1618
  const skipTags = ['sub', 'sup', 'code', 'kbd', 'samp', 'var', 'caption', 'figcaption'];
1502
- const inUIContext = el.closest && el.closest('button, a, label, summary, [role="button"], [role="link"], [role="tab"], [role="menuitem"], [role="option"], nav, footer, [class*="badge" i], [class*="chip" i], [class*="pill" i], [class*="tag" i], [class*="label" i], [class*="caption" i]');
1619
+ 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]');
1503
1620
  const isUppercase = style.textTransform === 'uppercase';
1504
1621
  if (!skipTags.includes(tag) && !inUIContext && !isUppercase) {
1505
1622
  findings.push({ id: 'tiny-text', snippet: `${fontSize}px body text` });
@@ -2107,17 +2224,28 @@ function checkCreamPalette(doc, win) {
2107
2224
  }
2108
2225
 
2109
2226
  // ─── Oversized hero headline ────────────────────────────────────────────────
2110
- // Fires when a *long* headline is set at display size, so a full sentence ends
2111
- // up dominating the viewport. A punchy one- or two-word headline at the same
2112
- // size is a legitimate stylistic choice and must pass length, not size
2113
- // alone, is the tell.
2227
+ // Fires when a *long* headline is set at display size and actually dominates
2228
+ // the viewport. A punchy one- or two-word headline at the same size is a
2229
+ // legitimate stylistic choice, and a large-but-contained two-line hero should
2230
+ // pass too — length and viewport share together are the tell.
2114
2231
  const OVERSIZED_H1_FONT_PX = 72;
2115
2232
  const OVERSIZED_H1_MIN_CHARS = 40;
2116
- function checkOversizedH1({ tag, fontSize, headingText }) {
2233
+ const OVERSIZED_H1_MIN_VIEWPORT_HEIGHT_RATIO = 0.28;
2234
+ const OVERSIZED_H1_MIN_VIEWPORT_AREA_RATIO = 0.25;
2235
+ function checkOversizedH1({ tag, fontSize, headingText, rect = null, viewportWidth = 0, viewportHeight = 0 }) {
2117
2236
  if (tag !== 'h1') return [];
2118
2237
  const textLen = headingText.length;
2119
2238
  if (fontSize >= OVERSIZED_H1_FONT_PX && textLen >= OVERSIZED_H1_MIN_CHARS) {
2120
- return [{ id: 'oversized-h1', snippet: `${Math.round(fontSize)}px h1, ${textLen} chars "${headingText.slice(0, 60)}"` }];
2239
+ let viewportDetail = '';
2240
+ if (rect && viewportWidth > 0 && viewportHeight > 0) {
2241
+ const heightRatio = rect.height / viewportHeight;
2242
+ const areaRatio = (rect.width * rect.height) / (viewportWidth * viewportHeight);
2243
+ const dominatesViewport = heightRatio >= OVERSIZED_H1_MIN_VIEWPORT_HEIGHT_RATIO
2244
+ || areaRatio >= OVERSIZED_H1_MIN_VIEWPORT_AREA_RATIO;
2245
+ if (!dominatesViewport) return [];
2246
+ viewportDetail = `, ${Math.round(heightRatio * 100)}vh`;
2247
+ }
2248
+ return [{ id: 'oversized-h1', snippet: `${Math.round(fontSize)}px h1, ${textLen} chars${viewportDetail} "${headingText.slice(0, 60)}"` }];
2121
2249
  }
2122
2250
  return [];
2123
2251
  }
@@ -2135,31 +2263,54 @@ function checkElementOversizedH1DOM(el) {
2135
2263
  const style = getComputedStyle(el);
2136
2264
  const fontSize = parseFloat(style.fontSize) || 0;
2137
2265
  const headingText = (el.textContent || '').trim().replace(/\s+/g, ' ');
2138
- return checkOversizedH1({ tag, fontSize, headingText });
2266
+ const rect = el.getBoundingClientRect();
2267
+ const viewportWidth = (typeof window !== 'undefined' ? window.innerWidth : 0) || 0;
2268
+ const viewportHeight = (typeof window !== 'undefined' ? window.innerHeight : 0) || 0;
2269
+ return checkOversizedH1({ tag, fontSize, headingText, rect, viewportWidth, viewportHeight });
2139
2270
  }
2140
2271
 
2141
2272
  // ─── GPT tell: hairline border + wide diffuse shadow (gated --gpt) ────────────
2142
- function shadowMaxBlurPx(boxShadow) {
2273
+ const CSS_COLOR_TOKEN_RE = /(?:rgba?|hsla?|oklch|oklab|lab|lch|color)\([^)]*\)|#[0-9a-fA-F]{3,8}\b|\b(?:black|white|transparent|currentcolor)\b/gi;
2274
+
2275
+ function shadowLayerAlpha(layer) {
2276
+ CSS_COLOR_TOKEN_RE.lastIndex = 0;
2277
+ const match = CSS_COLOR_TOKEN_RE.exec(layer);
2278
+ if (!match) return 1;
2279
+ if (match[0].toLowerCase() === 'transparent') return 0;
2280
+ const parsed = parseAnyColor(match[0]);
2281
+ return parsed ? (parsed.a ?? 1) : 1;
2282
+ }
2283
+
2284
+ function shadowMaxBlurPx(boxShadow, { minAlpha = 0 } = {}) {
2143
2285
  if (!boxShadow || boxShadow === 'none') return 0;
2144
2286
  let maxBlur = 0;
2145
2287
  // Split into layers on commas not inside parentheses (rgba(...) etc.).
2146
2288
  for (const layer of boxShadow.split(/,(?![^()]*\))/)) {
2289
+ if (shadowLayerAlpha(layer) < minAlpha) continue;
2147
2290
  // Strip colors and keywords (rgba()/hsl()/hex/named/inset/px), leaving the
2148
2291
  // ordered length tokens: offsetX offsetY blur [spread]. Static jsdom keeps
2149
2292
  // unitless zeros ("0 0 24px"); browsers normalize to px ("0px 0px 24px") —
2150
2293
  // both reduce to the same numbers here.
2151
- const cleaned = layer.replace(/rgba?\([^)]*\)|hsla?\([^)]*\)|#[0-9a-f]+|\b[a-z]+\b/gi, ' ');
2294
+ const cleaned = layer.replace(CSS_COLOR_TOKEN_RE, ' ').replace(/\b[a-z]+\b/gi, ' ');
2152
2295
  const nums = [...cleaned.matchAll(/-?\d*\.?\d+/g)].map(m => parseFloat(m[0]));
2153
2296
  if (nums.length >= 3) maxBlur = Math.max(maxBlur, nums[2]);
2154
2297
  }
2155
2298
  return maxBlur;
2156
2299
  }
2157
2300
 
2158
- function checkGptThinBorderWideShadow({ borderWidths, boxShadow }) {
2159
- const maxBorder = Math.max(0, ...borderWidths);
2160
- const hasThinBorder = maxBorder > 0 && maxBorder <= 1.5;
2161
- const blur = shadowMaxBlurPx(boxShadow);
2162
- if (hasThinBorder && blur >= 16) {
2301
+ function cssColorAlpha(value) {
2302
+ if (cssColorIsTransparent(value)) return 0;
2303
+ const parsed = parseAnyColor(value);
2304
+ return parsed ? (parsed.a ?? 1) : 1;
2305
+ }
2306
+
2307
+ function checkGptThinBorderWideShadow({ borderWidths, borderColors, boxShadow }) {
2308
+ const visibleThinBorders = borderWidths
2309
+ .map((width, index) => ({ width, alpha: cssColorAlpha(borderColors?.[index] || '') }))
2310
+ .filter(({ width, alpha }) => width > 0 && width <= 1.5 && alpha >= 0.28);
2311
+ const maxBorder = Math.max(0, ...visibleThinBorders.map(({ width }) => width));
2312
+ const blur = shadowMaxBlurPx(boxShadow, { minAlpha: 0.12 });
2313
+ if (visibleThinBorders.length >= 2 && blur >= 16) {
2163
2314
  return [{ id: 'gpt-thin-border-wide-shadow', snippet: `${maxBorder}px border + ${Math.round(blur)}px shadow blur` }];
2164
2315
  }
2165
2316
  return [];
@@ -2174,13 +2325,22 @@ function borderWidthsFromStyle(style) {
2174
2325
  ];
2175
2326
  }
2176
2327
 
2328
+ function borderColorsFromStyle(style) {
2329
+ return [
2330
+ style.borderTopColor || '',
2331
+ style.borderRightColor || '',
2332
+ style.borderBottomColor || '',
2333
+ style.borderLeftColor || '',
2334
+ ];
2335
+ }
2336
+
2177
2337
  function checkElementGptBorderShadow(el, style) {
2178
- return checkGptThinBorderWideShadow({ borderWidths: borderWidthsFromStyle(style), boxShadow: style.boxShadow || '' });
2338
+ return checkGptThinBorderWideShadow({ borderWidths: borderWidthsFromStyle(style), borderColors: borderColorsFromStyle(style), boxShadow: style.boxShadow || '' });
2179
2339
  }
2180
2340
 
2181
2341
  function checkElementGptBorderShadowDOM(el) {
2182
2342
  const style = getComputedStyle(el);
2183
- return checkGptThinBorderWideShadow({ borderWidths: borderWidthsFromStyle(style), boxShadow: style.boxShadow || '' });
2343
+ return checkGptThinBorderWideShadow({ borderWidths: borderWidthsFromStyle(style), borderColors: borderColorsFromStyle(style), boxShadow: style.boxShadow || '' });
2184
2344
  }
2185
2345
 
2186
2346
  // ─── Clipped overflow container ───────────────────────────────────────────────
@@ -2193,17 +2353,131 @@ function classSelector(el) {
2193
2353
  return tokens.length ? `${tag}.${tokens.join('.')}` : tag;
2194
2354
  }
2195
2355
 
2356
+ function positionedChildIsDecorative(child) {
2357
+ if (!child || typeof child.getAttribute !== 'function') return false;
2358
+ if (child.closest?.('[aria-hidden="true"]')) return true;
2359
+ const role = (child.getAttribute('role') || '').toLowerCase();
2360
+ if (role === 'none' || role === 'presentation') return true;
2361
+ const tag = child.tagName ? child.tagName.toLowerCase() : '';
2362
+ if (['img', 'svg', 'canvas', 'video'].includes(tag)) return true;
2363
+ const ident = `${child.getAttribute('class') || ''} ${child.getAttribute('id') || ''}`;
2364
+ if (
2365
+ /\b(art|bg|background|badge|blob|crop|decor|dot|glow|grain|image|mask|ornament|overlay|photo|scrim|shadow|shine|texture)\b/i.test(ident) &&
2366
+ !positionedChildHasSubstantiveContent(child)
2367
+ ) {
2368
+ return true;
2369
+ }
2370
+ return false;
2371
+ }
2372
+
2373
+ const POSITIONED_CHILD_INTERACTIVE_SELECTOR = [
2374
+ 'a[href]',
2375
+ 'button',
2376
+ 'input',
2377
+ 'select',
2378
+ 'summary',
2379
+ 'textarea',
2380
+ '[tabindex]:not([tabindex="-1"])',
2381
+ '[role="button"]',
2382
+ '[role="dialog"]',
2383
+ '[role="link"]',
2384
+ '[role="listbox"]',
2385
+ '[role="menu"]',
2386
+ '[role="menuitem"]',
2387
+ '[role="option"]',
2388
+ '[role="tooltip"]',
2389
+ ].join(',');
2390
+
2391
+ function positionedChildHasSubstantiveContent(child) {
2392
+ const text = (child.textContent || '').replace(/\s+/g, ' ').trim();
2393
+ if (text.length > 0) return true;
2394
+ if (typeof child.matches === 'function') {
2395
+ try {
2396
+ if (child.matches(POSITIONED_CHILD_INTERACTIVE_SELECTOR)) return true;
2397
+ } catch {}
2398
+ }
2399
+ if (typeof child.querySelector === 'function') {
2400
+ try {
2401
+ if (child.querySelector(POSITIONED_CHILD_INTERACTIVE_SELECTOR)) return true;
2402
+ } catch {}
2403
+ }
2404
+ return false;
2405
+ }
2406
+
2407
+ function clippingContainerIsIntentionalViewport(el) {
2408
+ if (!el || typeof el.getAttribute !== 'function') return false;
2409
+ const roleDescription = (el.getAttribute('aria-roledescription') || '').toLowerCase();
2410
+ if (/\b(carousel|slider)\b/.test(roleDescription)) return true;
2411
+ const ident = `${el.getAttribute('class') || ''} ${el.getAttribute('id') || ''}`.toLowerCase();
2412
+ return /\b(carousel|comparison|compare|fisheye|marquee|preview|scroller|slider|slideshow|split|viewport)\b/.test(ident) ||
2413
+ /\b(demo-area|demo-stage|demo-viewport)\b/.test(ident);
2414
+ }
2415
+
2416
+ function elementRect(el) {
2417
+ if (!el || typeof el.getBoundingClientRect !== 'function') return null;
2418
+ try {
2419
+ const rect = el.getBoundingClientRect();
2420
+ if (!rect) return null;
2421
+ const values = [rect.top, rect.right, rect.bottom, rect.left, rect.width, rect.height];
2422
+ if (!values.every(Number.isFinite)) return null;
2423
+ if (rect.width <= 0 && rect.height <= 0) return null;
2424
+ return rect;
2425
+ } catch {
2426
+ return null;
2427
+ }
2428
+ }
2429
+
2430
+ function positionedStyleImpliesEscape(style) {
2431
+ const values = [
2432
+ style.top,
2433
+ style.right,
2434
+ style.bottom,
2435
+ style.left,
2436
+ style.inset,
2437
+ style.insetBlock,
2438
+ style.insetInline,
2439
+ style.insetBlockStart,
2440
+ style.insetBlockEnd,
2441
+ style.insetInlineStart,
2442
+ style.insetInlineEnd,
2443
+ ].filter(Boolean).map(value => String(value).trim().toLowerCase());
2444
+ for (const value of values) {
2445
+ if (/(^|[\s(])-+(?:\d|\.)/.test(value)) return true;
2446
+ if (/(^|[\s(])100(?:\.0+)?%/.test(value)) return true;
2447
+ }
2448
+ return false;
2449
+ }
2450
+
2451
+ function positionedChildEscapesClip(el, child, clipX, clipY) {
2452
+ const parentRect = elementRect(el);
2453
+ const childRect = elementRect(child);
2454
+ if (!parentRect || !childRect) return null;
2455
+ const threshold = 2;
2456
+ return Boolean(
2457
+ (clipX && (childRect.left < parentRect.left - threshold || childRect.right > parentRect.right + threshold)) ||
2458
+ (clipY && (childRect.top < parentRect.top - threshold || childRect.bottom > parentRect.bottom + threshold))
2459
+ );
2460
+ }
2461
+
2196
2462
  function checkClippedOverflow(el, style, getStyle) {
2197
2463
  const clips = (v) => v === 'hidden' || v === 'clip';
2198
2464
  const scrolls = (v) => v === 'auto' || v === 'scroll';
2199
2465
  const ox = style.overflowX || '', oy = style.overflowY || '', ov = style.overflow || '';
2200
- const anyClip = clips(ox) || clips(oy) || clips(ov);
2466
+ const clipX = clips(ox) || clips(ov);
2467
+ const clipY = clips(oy) || clips(ov);
2468
+ const anyClip = clipX || clipY;
2201
2469
  const anyScroll = scrolls(ox) || scrolls(oy) || scrolls(ov);
2202
2470
  if (!anyClip || anyScroll) return [];
2471
+ if (clippingContainerIsIntentionalViewport(el)) return [];
2203
2472
  if (!el.querySelectorAll) return [];
2204
2473
  for (const child of el.querySelectorAll('*')) {
2205
- const pos = (getStyle(child).position) || '';
2474
+ const childStyle = getStyle(child);
2475
+ const pos = childStyle.position || '';
2206
2476
  if (pos === 'absolute' || pos === 'fixed') {
2477
+ if (positionedChildIsDecorative(child)) continue;
2478
+ const escapes = positionedChildEscapesClip(el, child, clipX, clipY);
2479
+ if (escapes === false) continue;
2480
+ if (escapes === null && !positionedStyleImpliesEscape(childStyle)) continue;
2207
2481
  return [{ id: 'clipped-overflow-container', snippet: `${classSelector(el)} clips a positioned child` }];
2208
2482
  }
2209
2483
  }
@@ -2222,14 +2496,94 @@ function checkElementClippedOverflowDOM(el) {
2222
2496
  // ─── Text overflow (browser-only: needs scrollWidth/clientWidth) ──────────────
2223
2497
  const TEXT_OVERFLOW_SKIP_TAGS = new Set(['pre', 'code', 'textarea', 'svg', 'canvas', 'select', 'option', 'marquee']);
2224
2498
 
2499
+ function metricLengthPx(value, fontSizePx = 16) {
2500
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
2501
+ if (typeof value !== 'string') return null;
2502
+ return resolveLengthPx(value, fontSizePx);
2503
+ }
2504
+
2505
+ function firstMetricLengthPx(fontSizePx, ...values) {
2506
+ for (const value of values) {
2507
+ const parsed = metricLengthPx(value, fontSizePx);
2508
+ if (parsed !== null) return parsed;
2509
+ }
2510
+ return null;
2511
+ }
2512
+
2513
+ function expandBoxShorthand(parts) {
2514
+ if (parts.length === 1) return [parts[0], parts[0], parts[0], parts[0]];
2515
+ if (parts.length === 2) return [parts[0], parts[1], parts[0], parts[1]];
2516
+ if (parts.length === 3) return [parts[0], parts[1], parts[2], parts[1]];
2517
+ return [parts[0], parts[1], parts[2], parts[3]];
2518
+ }
2519
+
2520
+ function clippedByInset(clipPath) {
2521
+ const match = String(clipPath || '').trim().toLowerCase().match(/^inset\s*\(([^)]*)\)$/);
2522
+ if (!match) return false;
2523
+ const beforeRound = match[1].split(/\s+round\s+/)[0].trim();
2524
+ if (!beforeRound) return false;
2525
+ const values = expandBoxShorthand(beforeRound.split(/\s+/).slice(0, 4));
2526
+ const percents = values.map(value => String(value).trim().match(/^(-?\d+(?:\.\d+)?)%$/));
2527
+ if (percents.some(match => !match)) return false;
2528
+ const [top, right, bottom, left] = percents.map(match => parseFloat(match[1]));
2529
+ return top + bottom >= 100 || left + right >= 100;
2530
+ }
2531
+
2532
+ function clippedByRect(clip) {
2533
+ const match = String(clip || '').trim().toLowerCase().match(/^rect\s*\(([^)]*)\)$/);
2534
+ if (!match) return false;
2535
+ const values = match[1].split(/[,\s]+/).map(value => value.trim()).filter(Boolean);
2536
+ if (values.length !== 4) return false;
2537
+ const [top, right, bottom, left] = values.map(value => metricLengthPx(value, 16));
2538
+ if ([top, right, bottom, left].some(value => value === null)) return false;
2539
+ return bottom <= top || right <= left;
2540
+ }
2541
+
2542
+ function isScreenReaderOnlyTextStyle(style, metrics = {}) {
2543
+ if (!style) return false;
2544
+ const overflowValues = [style.overflow, style.overflowX, style.overflowY]
2545
+ .map(value => String(value || '').toLowerCase());
2546
+ const clipsOverflow = overflowValues.some(value => value === 'hidden' || value === 'clip');
2547
+
2548
+ const fontSize = metricLengthPx(style.fontSize, 16) || 16;
2549
+ const width = firstMetricLengthPx(fontSize, metrics.width, metrics.clientWidth, style.width, style.inlineSize);
2550
+ const height = firstMetricLengthPx(fontSize, metrics.height, metrics.clientHeight, style.height, style.blockSize);
2551
+ const isTiny = width !== null && height !== null && width <= 2 && height <= 2;
2552
+ const isAbsolutelyHidden = String(style.position || '').toLowerCase() === 'absolute' && isTiny && clipsOverflow;
2553
+
2554
+ const clipPath = String(style.clipPath || style.webkitClipPath || '').trim();
2555
+ const clip = String(style.clip || '').trim();
2556
+ return isAbsolutelyHidden || clippedByInset(clipPath) || clippedByRect(clip);
2557
+ }
2558
+
2559
+ function isRenderedForBrowserRule(el) {
2560
+ for (let cur = el; cur && cur.nodeType === 1; cur = cur.parentElement) {
2561
+ if (cur.getAttribute?.('aria-hidden') === 'true') return false;
2562
+ const style = getComputedStyle(cur);
2563
+ const visibility = String(style.visibility || '').toLowerCase();
2564
+ if (style.display === 'none' || visibility === 'hidden' || visibility === 'collapse') return false;
2565
+ if ((parseFloat(style.opacity) || 0) <= 0.01) return false;
2566
+ if (String(style.contentVisibility || '').toLowerCase() === 'hidden') return false;
2567
+ }
2568
+ return true;
2569
+ }
2570
+
2225
2571
  function checkElementTextOverflowDOM(el) {
2226
2572
  const tag = el.tagName.toLowerCase();
2227
2573
  if (TEXT_OVERFLOW_SKIP_TAGS.has(tag)) return [];
2574
+ if (!isRenderedForBrowserRule(el)) return [];
2228
2575
  // Only the element that actually owns overflowing text — not its ancestors,
2229
2576
  // which inherit a wider scrollWidth from the spilling descendant.
2230
2577
  const hasDirectText = [...el.childNodes].some(n => n.nodeType === 3 && n.textContent.trim().length > 0);
2231
2578
  if (!hasDirectText) return [];
2232
2579
  const style = getComputedStyle(el);
2580
+ const rect = el.getBoundingClientRect ? el.getBoundingClientRect() : null;
2581
+ if (isScreenReaderOnlyTextStyle(style, {
2582
+ width: rect?.width,
2583
+ height: rect?.height,
2584
+ clientWidth: el.clientWidth,
2585
+ clientHeight: el.clientHeight,
2586
+ })) return [];
2233
2587
  const isScrollRegion = (s) => /(auto|scroll)/.test(s.overflowX || '') || /(auto|scroll)/.test(s.overflow || '');
2234
2588
  if (isScrollRegion(style)) return [];
2235
2589
  // A scrollable ancestor means this overflow is intentional and scrollable.
@@ -2312,5 +2666,6 @@ export {
2312
2666
  checkClippedOverflow,
2313
2667
  checkElementClippedOverflow,
2314
2668
  checkElementClippedOverflowDOM,
2669
+ isScreenReaderOnlyTextStyle,
2315
2670
  checkElementTextOverflowDOM,
2316
2671
  };