@monoes/monodesign 1.2.18 → 1.2.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/README.md +3 -3
  2. package/cli/engine/design-system-checks.mjs +381 -0
  3. package/cli/engine/design-system-parse.mjs +433 -0
  4. package/cli/engine/design-system.mjs +20 -736
  5. package/cli/engine/detect-antipatterns-browser.js +12 -1
  6. package/cli/engine/engines/regex/detect-text-matchers.mjs +371 -0
  7. package/cli/engine/engines/regex/detect-text-run.mjs +186 -0
  8. package/cli/engine/engines/regex/detect-text.mjs +11 -523
  9. package/cli/engine/engines/static-html/css-cascade-color.mjs +219 -0
  10. package/cli/engine/engines/static-html/css-cascade-static-data.mjs +138 -0
  11. package/cli/engine/engines/static-html/css-cascade-static-dom.mjs +132 -0
  12. package/cli/engine/engines/static-html/css-cascade-static-parse.mjs +393 -0
  13. package/cli/engine/engines/static-html/css-cascade.mjs +24 -837
  14. package/cli/engine/registry/antipatterns-provider.mjs +60 -0
  15. package/cli/engine/registry/antipatterns-quality.mjs +218 -0
  16. package/cli/engine/registry/antipatterns-slop.mjs +226 -0
  17. package/cli/engine/registry/antipatterns.mjs +7 -494
  18. package/cli/engine/rules/checks-background.mjs +155 -0
  19. package/cli/engine/rules/checks-css-values.mjs +137 -0
  20. package/cli/engine/rules/checks-dom-effects.mjs +243 -0
  21. package/cli/engine/rules/checks-dom.mjs +148 -0
  22. package/cli/engine/rules/checks-element.mjs +286 -0
  23. package/cli/engine/rules/checks-html.mjs +204 -0
  24. package/cli/engine/rules/checks-overflow.mjs +263 -0
  25. package/cli/engine/rules/checks-page.mjs +465 -0
  26. package/cli/engine/rules/checks-pure.mjs +453 -0
  27. package/cli/engine/rules/checks-quality.mjs +447 -0
  28. package/cli/engine/rules/checks-stylesheet.mjs +324 -0
  29. package/cli/engine/rules/checks.mjs +98 -2939
  30. package/cli/lib/monodesign-config-consent.mjs +93 -0
  31. package/cli/lib/monodesign-config-core.mjs +37 -0
  32. package/cli/lib/monodesign-config-detection.mjs +133 -0
  33. package/cli/lib/monodesign-config-filter.mjs +198 -0
  34. package/cli/lib/monodesign-config-ignore-value.mjs +212 -0
  35. package/cli/lib/monodesign-config.mjs +21 -623
  36. package/package.json +2 -2
  37. package/skill/scripts/context-fields.mjs +65 -0
  38. package/skill/scripts/context-resolve.mjs +425 -0
  39. package/skill/scripts/context-update.mjs +151 -0
  40. package/skill/scripts/context-utils.mjs +68 -0
  41. package/skill/scripts/context-workspace.mjs +245 -0
  42. package/skill/scripts/context.mjs +17 -891
  43. package/skill/scripts/hook-admin-config.mjs +152 -0
  44. package/skill/scripts/hook-admin-ignore.mjs +131 -0
  45. package/skill/scripts/hook-admin-manifest.mjs +292 -0
  46. package/skill/scripts/hook-admin.mjs +8 -541
  47. package/skill/scripts/hook-before-edit-shell.mjs +184 -0
  48. package/skill/scripts/hook-before-edit.mjs +9 -165
  49. package/skill/scripts/hook-lib-cache.mjs +145 -0
  50. package/skill/scripts/hook-lib-config.mjs +292 -0
  51. package/skill/scripts/hook-lib-detector.mjs +72 -0
  52. package/skill/scripts/hook-lib-events.mjs +182 -0
  53. package/skill/scripts/hook-lib-findings.mjs +201 -0
  54. package/skill/scripts/hook-lib-ignore.mjs +205 -0
  55. package/skill/scripts/hook-lib-render.mjs +273 -0
  56. package/skill/scripts/hook-lib-run.mjs +281 -0
  57. package/skill/scripts/hook-lib-scan-targets.mjs +128 -0
  58. package/skill/scripts/hook-lib.mjs +71 -1747
  59. package/skill/scripts/lib/design-parser-colors.mjs +182 -0
  60. package/skill/scripts/lib/design-parser-extract.mjs +333 -0
  61. package/skill/scripts/lib/design-parser-sections.mjs +214 -0
  62. package/skill/scripts/lib/design-parser-yaml.mjs +135 -0
  63. package/skill/scripts/lib/design-parser.mjs +10 -783
  64. package/skill/scripts/live/manual-apply-chunk.mjs +151 -0
  65. package/skill/scripts/live/manual-apply-compact.mjs +122 -0
  66. package/skill/scripts/live/manual-apply-evidence.mjs +115 -0
  67. package/skill/scripts/live/manual-apply-log.mjs +179 -0
  68. package/skill/scripts/live/manual-apply-transaction.mjs +122 -0
  69. package/skill/scripts/live/manual-apply.mjs +65 -640
  70. package/skill/scripts/live/svelte-component-accept.mjs +248 -0
  71. package/skill/scripts/live/svelte-component-css.mjs +230 -0
  72. package/skill/scripts/live/svelte-component-markup.mjs +104 -0
  73. package/skill/scripts/live/svelte-component-scaffold.mjs +118 -0
  74. package/skill/scripts/live/svelte-component-session.mjs +158 -0
  75. package/skill/scripts/live/svelte-component.mjs +32 -792
  76. package/skill/scripts/live-accept-mutate.mjs +152 -0
  77. package/skill/scripts/live-accept-parse.mjs +370 -0
  78. package/skill/scripts/live-accept-scrub.mjs +59 -0
  79. package/skill/scripts/live-accept-search.mjs +58 -0
  80. package/skill/scripts/live-accept.mjs +15 -631
  81. package/skill/scripts/live-commit-manual-edits-paths.mjs +27 -0
  82. package/skill/scripts/live-commit-manual-edits-repair.mjs +166 -0
  83. package/skill/scripts/live-commit-manual-edits-rollback.mjs +231 -0
  84. package/skill/scripts/live-commit-manual-edits-utils.mjs +167 -0
  85. package/skill/scripts/live-commit-manual-edits-verify.mjs +356 -0
  86. package/skill/scripts/live-commit-manual-edits.mjs +24 -881
  87. package/skill/scripts/live-copy-edit-agent.mjs +16 -551
  88. package/skill/scripts/live-copy-edit-compact.mjs +137 -0
  89. package/skill/scripts/live-copy-edit-post-apply.mjs +175 -0
  90. package/skill/scripts/live-copy-edit-runners.mjs +297 -0
  91. package/skill/scripts/live-inject-csp.mjs +132 -0
  92. package/skill/scripts/live-inject-files.mjs +229 -0
  93. package/skill/scripts/live-inject-tag.mjs +114 -0
  94. package/skill/scripts/live-inject.mjs +13 -440
  95. package/skill/scripts/live-server-poll.mjs +232 -0
  96. package/skill/scripts/live-server-routes.mjs +449 -0
  97. package/skill/scripts/live-server-state.mjs +361 -0
  98. package/skill/scripts/live-server.mjs +10 -946
  99. package/skill/scripts/live-wrap-css.mjs +132 -0
  100. package/skill/scripts/live-wrap-manual-edit.mjs +153 -0
  101. package/skill/scripts/live-wrap-search.mjs +247 -0
  102. package/skill/scripts/live-wrap.mjs +21 -484
  103. package/skill/scripts/palette-seeds.mjs +401 -0
  104. package/skill/scripts/palette.mjs +1 -395
@@ -0,0 +1,324 @@
1
+ // ─── Section 7: Stylesheet-level accessibility rules ─────────────────────────
2
+ // These operate on raw CSS text (concatenated <style> blocks + linked sheets),
3
+ // not per-element computed style — because :hover / :focus-visible / @media
4
+ // conditions can't be read off a resting element. Shared by the regex, static
5
+ // and browser engines (each hands over its own CSS text).
6
+
7
+ // Split a selector list on top-level commas only — i.e. commas inside a
8
+ // functional pseudo-class's parens (`:is(a, button)`, `:where(a, b)`,
9
+ // `:not(a, b)`, `:has(x, y)`) don't split. A naive `str.split(',')` breaks
10
+ // `:is(a, button):focus { outline: none }` into `:is(a` and `button):focus`,
11
+ // which both selectorTargetsInteractive() and the dark/light selector maps
12
+ // below then treat as two unrelated (and malformed) selectors — producing
13
+ // false-positive missing-focus-visible findings and double-counted rules.
14
+ function splitTopLevelCommas(str) {
15
+ const parts = [];
16
+ let depth = 0, start = 0;
17
+ for (let k = 0; k < str.length; k++) {
18
+ const ch = str[k];
19
+ if (ch === '(') depth++;
20
+ else if (ch === ')') depth = Math.max(0, depth - 1);
21
+ else if (ch === ',' && depth === 0) {
22
+ parts.push(str.slice(start, k));
23
+ start = k + 1;
24
+ }
25
+ }
26
+ parts.push(str.slice(start));
27
+ return parts;
28
+ }
29
+
30
+ // Parse flat CSS into { selector, body, dark } tuples, tracking one level of
31
+ // dark-scheme context (a `@media (prefers-color-scheme: dark)` ancestor).
32
+ // @supports / @layer / @container / @scope are transparently descended.
33
+ // @font-face / @keyframes and other at-rules are skipped.
34
+ function collectCssRules(css, dark = false, depth = 0) {
35
+ const out = [];
36
+ if (!css || depth > 6) return out;
37
+ const src = css.replace(/\/\*[\s\S]*?\*\//g, '');
38
+ let i = 0;
39
+ while (i < src.length) {
40
+ const braceStart = src.indexOf('{', i);
41
+ if (braceStart === -1) break;
42
+ const prelude = src.slice(i, braceStart).trim();
43
+ let d = 1, j = braceStart + 1;
44
+ for (; j < src.length; j++) {
45
+ if (src[j] === '{') d++;
46
+ else if (src[j] === '}') { d--; if (d === 0) break; }
47
+ }
48
+ const body = src.slice(braceStart + 1, j);
49
+ i = j + 1;
50
+ if (!prelude) continue;
51
+ if (prelude.startsWith('@')) {
52
+ const atName = prelude.slice(1).split(/[\s(]/)[0].toLowerCase();
53
+ if (['media', 'supports', 'layer', 'container', 'scope'].includes(atName)) {
54
+ const isDarkMedia = atName === 'media' && /prefers-color-scheme\s*:\s*dark/i.test(prelude);
55
+ out.push(...collectCssRules(body, dark || isDarkMedia, depth + 1));
56
+ }
57
+ continue;
58
+ }
59
+ for (const sel of splitTopLevelCommas(prelude).map(s => s.trim()).filter(Boolean)) {
60
+ out.push({ selector: sel, body, dark });
61
+ }
62
+ }
63
+ return out;
64
+ }
65
+
66
+ // --- missing-focus-visible ---
67
+ function selectorTargetsInteractive(sel) {
68
+ return /(^|[\s>+~(,])(a|button|input|select|textarea|summary)(\b|[.:#[])/i.test(sel)
69
+ || /:focus\b/i.test(sel)
70
+ || /\[role\s*=\s*["']?button["']?\]/i.test(sel);
71
+ }
72
+
73
+ function checkFocusVisible(cssText) {
74
+ if (!cssText) return [];
75
+ const rules = collectCssRules(cssText);
76
+ let suppressor = null;
77
+ let hasFocusVisible = false;
78
+ let hasFocusReplacement = false;
79
+ for (const r of rules) {
80
+ const sel = r.selector.toLowerCase();
81
+ const body = r.body.toLowerCase();
82
+ if (/:focus-visible/.test(sel)) {
83
+ // Any :focus-visible styling counts as deliberate focus handling.
84
+ hasFocusVisible = true;
85
+ }
86
+ const suppresses = /outline\s*:\s*(none|0(?:px)?)\b/.test(body)
87
+ || /outline-style\s*:\s*none/.test(body)
88
+ || /outline-width\s*:\s*0(?:px)?\b/.test(body);
89
+ if (suppresses && selectorTargetsInteractive(sel) && !/:focus-visible/.test(sel)) {
90
+ suppressor = suppressor || r;
91
+ }
92
+ // A plain :focus rule that draws a visible affordance is a valid fallback.
93
+ if (/:focus\b/.test(sel) && !/:focus-visible/.test(sel)) {
94
+ const olVal = /outline\s*:\s*([^;}]+)/.exec(body);
95
+ const outlineReplaces = olVal && !/^\s*(none|0(?:px)?|inherit|initial|unset)\s*$/.test(olVal[1]);
96
+ if (/box-shadow\s*:/.test(body)
97
+ || outlineReplaces
98
+ || /outline-(?:color|width|style)\s*:\s*(?!none|0(?:px)?\b)/.test(body)
99
+ || /border(?:-\w+)?(?:-color)?\s*:/.test(body)) {
100
+ hasFocusReplacement = true;
101
+ }
102
+ }
103
+ }
104
+ if (suppressor && !hasFocusVisible && !hasFocusReplacement) {
105
+ return [{ id: 'missing-focus-visible', snippet: `"${suppressor.selector}" removes outline with no :focus-visible replacement` }];
106
+ }
107
+ return [];
108
+ }
109
+
110
+ // --- hover-only-affordance ---
111
+ function lastCompound(sel) {
112
+ const parts = sel.split(/\s*[>+~]\s*|\s+/).filter(Boolean);
113
+ const last = parts[parts.length - 1] || sel;
114
+ return last.replace(/:{1,2}[a-z-]+(\([^)]*\))?/gi, '').trim().toLowerCase();
115
+ }
116
+ function bodyRevealsVisible(body) {
117
+ const b = body.toLowerCase();
118
+ if (/display\s*:\s*(block|flex|grid|inline(?:-\w+)?|table|list-item)/.test(b)) return true;
119
+ if (/visibility\s*:\s*visible/.test(b)) return true;
120
+ if (/opacity\s*:\s*(?:1(?:\.0+)?|0?\.[1-9]\d*|\.\d*[1-9])\b/.test(b)) return true;
121
+ return false;
122
+ }
123
+ function bodyHidesByDefault(body) {
124
+ const b = body.toLowerCase();
125
+ return /display\s*:\s*none/.test(b)
126
+ || /visibility\s*:\s*hidden/.test(b)
127
+ || /opacity\s*:\s*0(?:\.0+)?\b/.test(b);
128
+ }
129
+ function checkHoverOnlyAffordance(cssText) {
130
+ if (!cssText) return [];
131
+ const rules = collectCssRules(cssText);
132
+ const focusTargets = new Set();
133
+ const hiddenTargets = new Set();
134
+ for (const r of rules) {
135
+ const sel = r.selector;
136
+ if (/:(focus|focus-within|focus-visible|active)\b/i.test(sel) && bodyRevealsVisible(r.body)) {
137
+ focusTargets.add(lastCompound(sel));
138
+ }
139
+ if (!/:hover\b/i.test(sel) && bodyHidesByDefault(r.body)) {
140
+ hiddenTargets.add(lastCompound(sel));
141
+ }
142
+ }
143
+ const findings = [];
144
+ const seen = new Set();
145
+ for (const r of rules) {
146
+ const sel = r.selector;
147
+ if (!/:hover\b/i.test(sel)) continue;
148
+ if (!bodyRevealsVisible(r.body)) continue;
149
+ const parts = sel.split(/\s*[>+~]\s*|\s+/).filter(Boolean);
150
+ if (parts.length < 2) continue; // hover on the subject itself — can't hide+reveal
151
+ if (/:hover\b/i.test(parts[parts.length - 1])) continue; // :hover is on the revealed subject
152
+ const target = lastCompound(sel);
153
+ if (!target || !hiddenTargets.has(target)) continue;
154
+ if (focusTargets.has(target)) continue;
155
+ if (seen.has(target)) continue;
156
+ seen.add(target);
157
+ findings.push({ id: 'hover-only-affordance', snippet: `"${target}" is revealed on :hover with no focus/active equivalent` });
158
+ }
159
+ return findings;
160
+ }
161
+
162
+ // --- dark-scheme-contrast-blindspot ---
163
+ const DARK_SCOPE_RE = /(?:^|[\s>+~(,])(?:html|:root|body)?(?:\.dark|\.theme-dark|\.dark-mode)\b|\[data-theme\s*[~|]?=\s*["']?dark["']?\]|\[data-mode\s*=\s*["']?dark["']?\]|\[data-color-scheme\s*=\s*["']?dark["']?\]/i;
164
+ function stripDarkScope(sel) {
165
+ return sel
166
+ .replace(/(?:html|:root|body)?(?:\.dark|\.theme-dark|\.dark-mode)\b/gi, '')
167
+ .replace(/\[data-(?:theme|mode|color-scheme)\s*[~|]?=\s*["']?dark["']?\]/gi, '')
168
+ .replace(/\s*[>+~]\s*/g, ' ')
169
+ .replace(/\s+/g, ' ')
170
+ .trim();
171
+ }
172
+ function normalizeSelKey(sel) {
173
+ return sel.replace(/\s*[>+~]\s*/g, ' ').replace(/\s+/g, ' ').trim().toLowerCase();
174
+ }
175
+ function declHasColor(body) { return /(?:^|[;{\s])color\s*:/i.test(body); }
176
+ function declHasBg(body) { return /(?:^|[;{\s])background(?:-color)?\s*:/i.test(body); }
177
+ function checkDarkSchemeContrast(cssText) {
178
+ if (!cssText) return [];
179
+ const rules = collectCssRules(cssText);
180
+ const hasDarkMedia = /@media[^{]*prefers-color-scheme\s*:\s*dark/i.test(cssText);
181
+ const hasDarkSelectors = rules.some(r => DARK_SCOPE_RE.test(r.selector));
182
+ if (!hasDarkMedia && !hasDarkSelectors) return [];
183
+
184
+ const light = new Map(); // base selector → { color, bg }
185
+ const darkAgg = new Map();
186
+ for (const r of rules) {
187
+ const scopedDark = DARK_SCOPE_RE.test(r.selector);
188
+ const isDark = r.dark || scopedDark;
189
+ if (isDark) {
190
+ const base = normalizeSelKey(scopedDark ? stripDarkScope(r.selector) : r.selector);
191
+ if (!base) continue;
192
+ const e = darkAgg.get(base) || { color: false, bg: false };
193
+ if (declHasColor(r.body)) e.color = true;
194
+ if (declHasBg(r.body)) e.bg = true;
195
+ darkAgg.set(base, e);
196
+ } else {
197
+ const base = normalizeSelKey(r.selector);
198
+ if (!base) continue;
199
+ const e = light.get(base) || { color: false, bg: false };
200
+ if (declHasColor(r.body)) e.color = true;
201
+ if (declHasBg(r.body)) e.bg = true;
202
+ light.set(base, e);
203
+ }
204
+ }
205
+
206
+ const findings = [];
207
+ for (const [base, d] of darkAgg) {
208
+ if (d.color === d.bg) continue; // sets both or neither — not a split pair
209
+ const lp = light.get(base);
210
+ if (!lp || !(lp.color && lp.bg)) continue; // light scheme must pair both
211
+ const changed = d.bg ? 'background' : 'text color';
212
+ const missing = d.bg ? 'text color' : 'background';
213
+ findings.push({ id: 'dark-scheme-contrast-blindspot', snippet: `"${base}" overrides ${changed} in dark scheme but not ${missing}` });
214
+ }
215
+ return findings;
216
+ }
217
+
218
+ // --- image-missing-dimensions ---
219
+ function checkImageDimensions({ hasWidthAttr, hasHeightAttr, style }) {
220
+ const s = String(style || '').toLowerCase();
221
+ if (hasWidthAttr && hasHeightAttr) return [];
222
+ if (/aspect-?ratio\s*:/.test(s)) return [];
223
+ const heightM = /(?:^|[;{\s])height\s*:\s*([^;]+)/.exec(s);
224
+ if (heightM && !/^\s*(auto|inherit|initial|unset|0(?:px)?)\s*$/.test(heightM[1])) return [];
225
+ return [{ id: 'image-missing-dimensions', snippet: '<img> without width/height attributes or CSS aspect-ratio (layout shift risk)' }];
226
+ }
227
+
228
+ function checkElementImageDimensions(el, style) {
229
+ const hasWidthAttr = (el.getAttribute?.('width') ?? el.attribs?.width) != null;
230
+ const hasHeightAttr = (el.getAttribute?.('height') ?? el.attribs?.height) != null;
231
+ const parts = [el.getAttribute?.('style') || el.attribs?.style || ''];
232
+ const ar = style && (style.aspectRatio || style['aspect-ratio']);
233
+ if (ar && ar !== 'auto') parts.push(`aspect-ratio:${ar}`);
234
+ const h = style?.height;
235
+ if (h && h !== 'auto' && h !== '') parts.push(`height:${h}`);
236
+ return checkImageDimensions({ hasWidthAttr, hasHeightAttr, style: parts.join(';') });
237
+ }
238
+
239
+ function checkElementImageDimensionsDOM(el) {
240
+ // Only <img> can have this problem. The static engine gets this for free —
241
+ // it registers the rule with `selector: 'img'` (engines/static-html/
242
+ // detect-html.mjs) — but the DOM walker calls every check against every
243
+ // element, so the tag filter has to live here. Without it this rule fired on
244
+ // <head>, <meta>, <title> and <style>: a clean 84-line fixture reported 43
245
+ // findings, all of them "an <img> ships without width and height".
246
+ if (el.tagName !== 'IMG') return [];
247
+ const hasWidthAttr = el.getAttribute('width') != null;
248
+ const hasHeightAttr = el.getAttribute('height') != null;
249
+ // Browser computed `height` always resolves to a used px value after layout,
250
+ // so it can't tell a reserved height from a rendered one — only use inline
251
+ // style + the computed aspect-ratio (which stays 'auto' unless authored).
252
+ const parts = [el.getAttribute('style') || ''];
253
+ const style = getComputedStyle(el);
254
+ const ar = style.aspectRatio || '';
255
+ if (ar && ar !== 'auto') parts.push(`aspect-ratio:${ar}`);
256
+ return checkImageDimensions({ hasWidthAttr, hasHeightAttr, style: parts.join(';') });
257
+ }
258
+
259
+ // --- small-touch-target ---
260
+ const CLICKABLE_INPUT_TYPES = new Set(['button', 'submit', 'reset', 'checkbox', 'radio', 'image']);
261
+ const CLICKABLE_ROLES = new Set(['button', 'link', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'tab', 'switch', 'checkbox', 'radio', 'option']);
262
+ function isClickableControl(tag, role, href, inputType) {
263
+ if (tag === 'button' || tag === 'select' || tag === 'summary') return true;
264
+ if (tag === 'a') return href != null && href !== '';
265
+ if (tag === 'input') return CLICKABLE_INPUT_TYPES.has(String(inputType || '').toLowerCase());
266
+ if (role && CLICKABLE_ROLES.has(role)) return true;
267
+ return false;
268
+ }
269
+ function checkSmallTouchTarget({ tag, role, href, inputType, widthPx, heightPx, isInlineLinkInProse }) {
270
+ if (!isClickableControl(tag, role, href, inputType)) return [];
271
+ if (isInlineLinkInProse) return [];
272
+ if (!(widthPx > 0) || !(heightPx > 0)) return [];
273
+ if (widthPx >= 44 && heightPx >= 44) return [];
274
+ const roleLabel = role && !['button', 'a', 'input', 'select'].includes(tag) ? `[role=${role}]` : '';
275
+ return [{ id: 'small-touch-target', snippet: `<${tag}>${roleLabel} renders ${Math.round(widthPx)}x${Math.round(heightPx)}px (min 44x44)` }];
276
+ }
277
+
278
+ const PROSE_ANCESTOR_SELECTOR = 'p,li,blockquote,figcaption,dd,dt';
279
+ function checkElementSmallTouchTarget(el, style, tag) {
280
+ // Static path: only provable when width AND height are explicit px lengths
281
+ // (jsdom performs no layout; rem/%/auto can't be resolved to a real box).
282
+ const wRaw = style.width || '';
283
+ const hRaw = style.height || '';
284
+ if (!/px\s*$/.test(wRaw) || !/px\s*$/.test(hRaw)) return [];
285
+ const widthPx = parseFloat(wRaw);
286
+ const heightPx = parseFloat(hRaw);
287
+ const href = el.getAttribute?.('href');
288
+ const role = (el.getAttribute?.('role') || '').toLowerCase();
289
+ const inputType = el.getAttribute?.('type') || '';
290
+ // An <a> is a prose text link unless it is explicitly laid out as a block
291
+ // (button-like). jsdom returns '' for the default inline display, so key
292
+ // off "not blockish" rather than "== inline".
293
+ const isBlockish = /^(block|flex|grid|inline-block|inline-flex|table)/.test(style.display || '');
294
+ const inProse = !!el.closest?.(PROSE_ANCESTOR_SELECTOR);
295
+ const isInlineLinkInProse = tag === 'a' && !isBlockish && inProse;
296
+ return checkSmallTouchTarget({ tag, role, href, inputType, widthPx, heightPx, isInlineLinkInProse });
297
+ }
298
+
299
+ function checkElementSmallTouchTargetDOM(el) {
300
+ const tag = el.tagName.toLowerCase();
301
+ const rect = el.getBoundingClientRect();
302
+ if (!(rect.width > 0) || !(rect.height > 0)) return [];
303
+ const href = el.getAttribute('href');
304
+ const role = (el.getAttribute('role') || '').toLowerCase();
305
+ const inputType = el.getAttribute('type') || '';
306
+ const style = getComputedStyle(el);
307
+ const isBlockish = /^(block|flex|grid|inline-block|inline-flex|table)/.test(style.display || '');
308
+ const inProse = !!el.closest?.(PROSE_ANCESTOR_SELECTOR);
309
+ const isInlineLinkInProse = tag === 'a' && !isBlockish && inProse;
310
+ return checkSmallTouchTarget({ tag, role, href, inputType, widthPx: rect.width, heightPx: rect.height, isInlineLinkInProse });
311
+ }
312
+
313
+ export {
314
+ collectCssRules,
315
+ checkFocusVisible,
316
+ checkHoverOnlyAffordance,
317
+ checkDarkSchemeContrast,
318
+ checkImageDimensions,
319
+ checkElementImageDimensions,
320
+ checkElementImageDimensionsDOM,
321
+ checkSmallTouchTarget,
322
+ checkElementSmallTouchTarget,
323
+ checkElementSmallTouchTargetDOM,
324
+ };