From 61a2d230fdb98df38a85a3cadfbbaa29e9823e00 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2026 23:21:27 +0000 Subject: [PATCH] Sync generated provider output --- .../detector/detect-antipatterns-browser.js | 37 ++++++++++++------- .../engines/static-html/detect-html.mjs | 6 +-- .../scripts/detector/rules/checks.mjs | 18 ++++----- .../scripts/detector/shared/constants.mjs | 21 +++++++++-- .../detector/detect-antipatterns-browser.js | 37 ++++++++++++------- .../engines/static-html/detect-html.mjs | 6 +-- .../scripts/detector/rules/checks.mjs | 18 ++++----- .../scripts/detector/shared/constants.mjs | 21 +++++++++-- .../detector/detect-antipatterns-browser.js | 37 ++++++++++++------- .../engines/static-html/detect-html.mjs | 6 +-- .../scripts/detector/rules/checks.mjs | 18 ++++----- .../scripts/detector/shared/constants.mjs | 21 +++++++++-- .../detector/detect-antipatterns-browser.js | 37 ++++++++++++------- .../engines/static-html/detect-html.mjs | 6 +-- .../scripts/detector/rules/checks.mjs | 18 ++++----- .../scripts/detector/shared/constants.mjs | 21 +++++++++-- .../detector/detect-antipatterns-browser.js | 37 ++++++++++++------- .../engines/static-html/detect-html.mjs | 6 +-- .../scripts/detector/rules/checks.mjs | 18 ++++----- .../scripts/detector/shared/constants.mjs | 21 +++++++++-- .../detector/detect-antipatterns-browser.js | 37 ++++++++++++------- .../engines/static-html/detect-html.mjs | 6 +-- .../scripts/detector/rules/checks.mjs | 18 ++++----- .../scripts/detector/shared/constants.mjs | 21 +++++++++-- .../detector/detect-antipatterns-browser.js | 37 ++++++++++++------- .../engines/static-html/detect-html.mjs | 6 +-- .../scripts/detector/rules/checks.mjs | 18 ++++----- .../scripts/detector/shared/constants.mjs | 21 +++++++++-- .../detector/detect-antipatterns-browser.js | 37 ++++++++++++------- .../engines/static-html/detect-html.mjs | 6 +-- .../scripts/detector/rules/checks.mjs | 18 ++++----- .../scripts/detector/shared/constants.mjs | 21 +++++++++-- .../detector/detect-antipatterns-browser.js | 37 ++++++++++++------- .../engines/static-html/detect-html.mjs | 6 +-- .../scripts/detector/rules/checks.mjs | 18 ++++----- .../scripts/detector/shared/constants.mjs | 21 +++++++++-- .../detector/detect-antipatterns-browser.js | 37 ++++++++++++------- .../engines/static-html/detect-html.mjs | 6 +-- .../scripts/detector/rules/checks.mjs | 18 ++++----- .../scripts/detector/shared/constants.mjs | 21 +++++++++-- .../detector/detect-antipatterns-browser.js | 37 ++++++++++++------- .../engines/static-html/detect-html.mjs | 6 +-- .../scripts/detector/rules/checks.mjs | 18 ++++----- .../scripts/detector/shared/constants.mjs | 21 +++++++++-- .../detector/detect-antipatterns-browser.js | 37 ++++++++++++------- .../engines/static-html/detect-html.mjs | 6 +-- .../scripts/detector/rules/checks.mjs | 18 ++++----- .../scripts/detector/shared/constants.mjs | 21 +++++++++-- .../detector/detect-antipatterns-browser.js | 37 ++++++++++++------- .../engines/static-html/detect-html.mjs | 6 +-- .../scripts/detector/rules/checks.mjs | 18 ++++----- .../scripts/detector/shared/constants.mjs | 21 +++++++++-- .../detector/detect-antipatterns-browser.js | 37 ++++++++++++------- .../engines/static-html/detect-html.mjs | 6 +-- .../scripts/detector/rules/checks.mjs | 18 ++++----- .../scripts/detector/shared/constants.mjs | 21 +++++++++-- .../detector/detect-antipatterns-browser.js | 37 ++++++++++++------- .../engines/static-html/detect-html.mjs | 6 +-- .../scripts/detector/rules/checks.mjs | 18 ++++----- .../scripts/detector/shared/constants.mjs | 21 +++++++++-- .../detector/detect-antipatterns-browser.js | 37 ++++++++++++------- .../engines/static-html/detect-html.mjs | 6 +-- .../scripts/detector/rules/checks.mjs | 18 ++++----- .../scripts/detector/shared/constants.mjs | 21 +++++++++-- 64 files changed, 832 insertions(+), 480 deletions(-) diff --git a/.agents/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.agents/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 62f5b87f1..3f5abd0a8 100644 --- a/.agents/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.agents/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -70,13 +70,27 @@ function isBrandFontOnOwnDomain(font) { return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix)); } -const GENERIC_FONTS = new Set([ +// Overused-font primary selection skips only CSS generics so a system stack +// keeps the system face as primary; GENERIC_FONTS still includes platform +// faces for design-system/serif resolution. +const CSS_GENERIC_FONTS = new Set([ 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', - 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', - '-apple-system', 'blinkmacsystemfont', 'segoe ui', 'inherit', 'initial', 'unset', 'revert', ]); +const GENERIC_FONTS = new Set([ + ...CSS_GENERIC_FONTS, + 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', + '-apple-system', 'blinkmacsystemfont', 'segoe ui', +]); + +function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) { + return String(fontFamily || '') + .split(',') + .map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()) + .find(f => f && !skip.has(f)) || null; +} + // WCAG large text thresholds are defined in points: 18pt normal text and // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch. const WCAG_LARGE_TEXT_PX = 18 * (96 / 72); @@ -1591,7 +1605,7 @@ function checkIconTile(opts) { function resolveSerif(fontFamily) { if (!fontFamily) return { primary: null, isSerif: false }; const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null; + const primary = primaryFontFace(fontFamily, GENERIC_FONTS); if (!primary) return { primary: null, isSerif: false }; if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true }; if (tokens.includes('serif')) return { primary, isSerif: true }; @@ -5190,8 +5204,7 @@ function checkTypography() { const style = getComputedStyle(el); const ff = style.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (!primary) continue; fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1); totalTextElements++; @@ -5436,8 +5449,7 @@ function checkPageTypography(doc, win) { if (rule.type !== 1) continue; const ff = rule.style?.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (primary) { fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); @@ -5456,11 +5468,10 @@ function checkPageTypography(doc, win) { const ffRe = /font-family\s*:\s*([^;}]+)/gi; let fm; while ((fm = ffRe.exec(html)) !== null) { - for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) { - if (f && !GENERIC_FONTS.has(f)) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + const primary = primaryFontFace(fm[1]); + if (primary) { + fonts.add(primary); + if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); } } diff --git a/.agents/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs b/.agents/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs index b4efa84ac..51c34224b 100644 --- a/.agents/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +++ b/.agents/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs @@ -1,7 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; -import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs'; +import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs'; import { checkSourceDesignSystem, collectStaticDesignSystemFindings, @@ -51,9 +51,7 @@ function checkStaticPageTypography(document, window) { for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) { const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0); if (!hasText) continue; - const ff = window.getComputedStyle(el).fontFamily || ''; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(window.getComputedStyle(el).fontFamily); if (!primary) continue; fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); diff --git a/.agents/skills/impeccable/scripts/detector/rules/checks.mjs b/.agents/skills/impeccable/scripts/detector/rules/checks.mjs index f15957b92..8bfb2b1df 100644 --- a/.agents/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.agents/skills/impeccable/scripts/detector/rules/checks.mjs @@ -9,6 +9,7 @@ import { WCAG_LARGE_BOLD_TEXT_PX, WCAG_LARGE_TEXT_PX, isBrandFontOnOwnDomain, + primaryFontFace, } from '../shared/constants.mjs'; import { CSS_NAMED_COLORS, @@ -331,7 +332,7 @@ function checkIconTile(opts) { function resolveSerif(fontFamily) { if (!fontFamily) return { primary: null, isSerif: false }; const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null; + const primary = primaryFontFace(fontFamily, GENERIC_FONTS); if (!primary) return { primary: null, isSerif: false }; if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true }; if (tokens.includes('serif')) return { primary, isSerif: true }; @@ -3930,8 +3931,7 @@ function checkTypography() { const style = getComputedStyle(el); const ff = style.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (!primary) continue; fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1); totalTextElements++; @@ -4176,8 +4176,7 @@ function checkPageTypography(doc, win) { if (rule.type !== 1) continue; const ff = rule.style?.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (primary) { fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); @@ -4196,11 +4195,10 @@ function checkPageTypography(doc, win) { const ffRe = /font-family\s*:\s*([^;}]+)/gi; let fm; while ((fm = ffRe.exec(html)) !== null) { - for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) { - if (f && !GENERIC_FONTS.has(f)) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + const primary = primaryFontFace(fm[1]); + if (primary) { + fonts.add(primary); + if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); } } diff --git a/.agents/skills/impeccable/scripts/detector/shared/constants.mjs b/.agents/skills/impeccable/scripts/detector/shared/constants.mjs index b9152939a..ea9bb361a 100644 --- a/.agents/skills/impeccable/scripts/detector/shared/constants.mjs +++ b/.agents/skills/impeccable/scripts/detector/shared/constants.mjs @@ -56,13 +56,27 @@ function isBrandFontOnOwnDomain(font) { return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix)); } -const GENERIC_FONTS = new Set([ +// Overused-font primary selection skips only CSS generics so a system stack +// keeps the system face as primary; GENERIC_FONTS still includes platform +// faces for design-system/serif resolution. +const CSS_GENERIC_FONTS = new Set([ 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', - 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', - '-apple-system', 'blinkmacsystemfont', 'segoe ui', 'inherit', 'initial', 'unset', 'revert', ]); +const GENERIC_FONTS = new Set([ + ...CSS_GENERIC_FONTS, + 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', + '-apple-system', 'blinkmacsystemfont', 'segoe ui', +]); + +function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) { + return String(fontFamily || '') + .split(',') + .map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()) + .find(f => f && !skip.has(f)) || null; +} + // WCAG large text thresholds are defined in points: 18pt normal text and // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch. const WCAG_LARGE_TEXT_PX = 18 * (96 / 72); @@ -104,6 +118,7 @@ export { BRAND_FONT_DOMAINS, isBrandFontOnOwnDomain, GENERIC_FONTS, + primaryFontFace, WCAG_LARGE_TEXT_PX, WCAG_LARGE_BOLD_TEXT_PX, EM_DASH_FLOOR, diff --git a/.claude/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.claude/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 62f5b87f1..3f5abd0a8 100644 --- a/.claude/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.claude/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -70,13 +70,27 @@ function isBrandFontOnOwnDomain(font) { return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix)); } -const GENERIC_FONTS = new Set([ +// Overused-font primary selection skips only CSS generics so a system stack +// keeps the system face as primary; GENERIC_FONTS still includes platform +// faces for design-system/serif resolution. +const CSS_GENERIC_FONTS = new Set([ 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', - 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', - '-apple-system', 'blinkmacsystemfont', 'segoe ui', 'inherit', 'initial', 'unset', 'revert', ]); +const GENERIC_FONTS = new Set([ + ...CSS_GENERIC_FONTS, + 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', + '-apple-system', 'blinkmacsystemfont', 'segoe ui', +]); + +function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) { + return String(fontFamily || '') + .split(',') + .map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()) + .find(f => f && !skip.has(f)) || null; +} + // WCAG large text thresholds are defined in points: 18pt normal text and // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch. const WCAG_LARGE_TEXT_PX = 18 * (96 / 72); @@ -1591,7 +1605,7 @@ function checkIconTile(opts) { function resolveSerif(fontFamily) { if (!fontFamily) return { primary: null, isSerif: false }; const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null; + const primary = primaryFontFace(fontFamily, GENERIC_FONTS); if (!primary) return { primary: null, isSerif: false }; if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true }; if (tokens.includes('serif')) return { primary, isSerif: true }; @@ -5190,8 +5204,7 @@ function checkTypography() { const style = getComputedStyle(el); const ff = style.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (!primary) continue; fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1); totalTextElements++; @@ -5436,8 +5449,7 @@ function checkPageTypography(doc, win) { if (rule.type !== 1) continue; const ff = rule.style?.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (primary) { fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); @@ -5456,11 +5468,10 @@ function checkPageTypography(doc, win) { const ffRe = /font-family\s*:\s*([^;}]+)/gi; let fm; while ((fm = ffRe.exec(html)) !== null) { - for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) { - if (f && !GENERIC_FONTS.has(f)) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + const primary = primaryFontFace(fm[1]); + if (primary) { + fonts.add(primary); + if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); } } diff --git a/.claude/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs b/.claude/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs index b4efa84ac..51c34224b 100644 --- a/.claude/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +++ b/.claude/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs @@ -1,7 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; -import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs'; +import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs'; import { checkSourceDesignSystem, collectStaticDesignSystemFindings, @@ -51,9 +51,7 @@ function checkStaticPageTypography(document, window) { for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) { const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0); if (!hasText) continue; - const ff = window.getComputedStyle(el).fontFamily || ''; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(window.getComputedStyle(el).fontFamily); if (!primary) continue; fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); diff --git a/.claude/skills/impeccable/scripts/detector/rules/checks.mjs b/.claude/skills/impeccable/scripts/detector/rules/checks.mjs index f15957b92..8bfb2b1df 100644 --- a/.claude/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.claude/skills/impeccable/scripts/detector/rules/checks.mjs @@ -9,6 +9,7 @@ import { WCAG_LARGE_BOLD_TEXT_PX, WCAG_LARGE_TEXT_PX, isBrandFontOnOwnDomain, + primaryFontFace, } from '../shared/constants.mjs'; import { CSS_NAMED_COLORS, @@ -331,7 +332,7 @@ function checkIconTile(opts) { function resolveSerif(fontFamily) { if (!fontFamily) return { primary: null, isSerif: false }; const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null; + const primary = primaryFontFace(fontFamily, GENERIC_FONTS); if (!primary) return { primary: null, isSerif: false }; if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true }; if (tokens.includes('serif')) return { primary, isSerif: true }; @@ -3930,8 +3931,7 @@ function checkTypography() { const style = getComputedStyle(el); const ff = style.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (!primary) continue; fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1); totalTextElements++; @@ -4176,8 +4176,7 @@ function checkPageTypography(doc, win) { if (rule.type !== 1) continue; const ff = rule.style?.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (primary) { fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); @@ -4196,11 +4195,10 @@ function checkPageTypography(doc, win) { const ffRe = /font-family\s*:\s*([^;}]+)/gi; let fm; while ((fm = ffRe.exec(html)) !== null) { - for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) { - if (f && !GENERIC_FONTS.has(f)) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + const primary = primaryFontFace(fm[1]); + if (primary) { + fonts.add(primary); + if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); } } diff --git a/.claude/skills/impeccable/scripts/detector/shared/constants.mjs b/.claude/skills/impeccable/scripts/detector/shared/constants.mjs index b9152939a..ea9bb361a 100644 --- a/.claude/skills/impeccable/scripts/detector/shared/constants.mjs +++ b/.claude/skills/impeccable/scripts/detector/shared/constants.mjs @@ -56,13 +56,27 @@ function isBrandFontOnOwnDomain(font) { return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix)); } -const GENERIC_FONTS = new Set([ +// Overused-font primary selection skips only CSS generics so a system stack +// keeps the system face as primary; GENERIC_FONTS still includes platform +// faces for design-system/serif resolution. +const CSS_GENERIC_FONTS = new Set([ 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', - 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', - '-apple-system', 'blinkmacsystemfont', 'segoe ui', 'inherit', 'initial', 'unset', 'revert', ]); +const GENERIC_FONTS = new Set([ + ...CSS_GENERIC_FONTS, + 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', + '-apple-system', 'blinkmacsystemfont', 'segoe ui', +]); + +function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) { + return String(fontFamily || '') + .split(',') + .map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()) + .find(f => f && !skip.has(f)) || null; +} + // WCAG large text thresholds are defined in points: 18pt normal text and // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch. const WCAG_LARGE_TEXT_PX = 18 * (96 / 72); @@ -104,6 +118,7 @@ export { BRAND_FONT_DOMAINS, isBrandFontOnOwnDomain, GENERIC_FONTS, + primaryFontFace, WCAG_LARGE_TEXT_PX, WCAG_LARGE_BOLD_TEXT_PX, EM_DASH_FLOOR, diff --git a/.cursor/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.cursor/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 62f5b87f1..3f5abd0a8 100644 --- a/.cursor/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.cursor/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -70,13 +70,27 @@ function isBrandFontOnOwnDomain(font) { return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix)); } -const GENERIC_FONTS = new Set([ +// Overused-font primary selection skips only CSS generics so a system stack +// keeps the system face as primary; GENERIC_FONTS still includes platform +// faces for design-system/serif resolution. +const CSS_GENERIC_FONTS = new Set([ 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', - 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', - '-apple-system', 'blinkmacsystemfont', 'segoe ui', 'inherit', 'initial', 'unset', 'revert', ]); +const GENERIC_FONTS = new Set([ + ...CSS_GENERIC_FONTS, + 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', + '-apple-system', 'blinkmacsystemfont', 'segoe ui', +]); + +function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) { + return String(fontFamily || '') + .split(',') + .map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()) + .find(f => f && !skip.has(f)) || null; +} + // WCAG large text thresholds are defined in points: 18pt normal text and // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch. const WCAG_LARGE_TEXT_PX = 18 * (96 / 72); @@ -1591,7 +1605,7 @@ function checkIconTile(opts) { function resolveSerif(fontFamily) { if (!fontFamily) return { primary: null, isSerif: false }; const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null; + const primary = primaryFontFace(fontFamily, GENERIC_FONTS); if (!primary) return { primary: null, isSerif: false }; if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true }; if (tokens.includes('serif')) return { primary, isSerif: true }; @@ -5190,8 +5204,7 @@ function checkTypography() { const style = getComputedStyle(el); const ff = style.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (!primary) continue; fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1); totalTextElements++; @@ -5436,8 +5449,7 @@ function checkPageTypography(doc, win) { if (rule.type !== 1) continue; const ff = rule.style?.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (primary) { fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); @@ -5456,11 +5468,10 @@ function checkPageTypography(doc, win) { const ffRe = /font-family\s*:\s*([^;}]+)/gi; let fm; while ((fm = ffRe.exec(html)) !== null) { - for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) { - if (f && !GENERIC_FONTS.has(f)) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + const primary = primaryFontFace(fm[1]); + if (primary) { + fonts.add(primary); + if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); } } diff --git a/.cursor/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs b/.cursor/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs index b4efa84ac..51c34224b 100644 --- a/.cursor/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +++ b/.cursor/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs @@ -1,7 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; -import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs'; +import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs'; import { checkSourceDesignSystem, collectStaticDesignSystemFindings, @@ -51,9 +51,7 @@ function checkStaticPageTypography(document, window) { for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) { const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0); if (!hasText) continue; - const ff = window.getComputedStyle(el).fontFamily || ''; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(window.getComputedStyle(el).fontFamily); if (!primary) continue; fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); diff --git a/.cursor/skills/impeccable/scripts/detector/rules/checks.mjs b/.cursor/skills/impeccable/scripts/detector/rules/checks.mjs index f15957b92..8bfb2b1df 100644 --- a/.cursor/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.cursor/skills/impeccable/scripts/detector/rules/checks.mjs @@ -9,6 +9,7 @@ import { WCAG_LARGE_BOLD_TEXT_PX, WCAG_LARGE_TEXT_PX, isBrandFontOnOwnDomain, + primaryFontFace, } from '../shared/constants.mjs'; import { CSS_NAMED_COLORS, @@ -331,7 +332,7 @@ function checkIconTile(opts) { function resolveSerif(fontFamily) { if (!fontFamily) return { primary: null, isSerif: false }; const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null; + const primary = primaryFontFace(fontFamily, GENERIC_FONTS); if (!primary) return { primary: null, isSerif: false }; if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true }; if (tokens.includes('serif')) return { primary, isSerif: true }; @@ -3930,8 +3931,7 @@ function checkTypography() { const style = getComputedStyle(el); const ff = style.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (!primary) continue; fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1); totalTextElements++; @@ -4176,8 +4176,7 @@ function checkPageTypography(doc, win) { if (rule.type !== 1) continue; const ff = rule.style?.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (primary) { fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); @@ -4196,11 +4195,10 @@ function checkPageTypography(doc, win) { const ffRe = /font-family\s*:\s*([^;}]+)/gi; let fm; while ((fm = ffRe.exec(html)) !== null) { - for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) { - if (f && !GENERIC_FONTS.has(f)) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + const primary = primaryFontFace(fm[1]); + if (primary) { + fonts.add(primary); + if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); } } diff --git a/.cursor/skills/impeccable/scripts/detector/shared/constants.mjs b/.cursor/skills/impeccable/scripts/detector/shared/constants.mjs index b9152939a..ea9bb361a 100644 --- a/.cursor/skills/impeccable/scripts/detector/shared/constants.mjs +++ b/.cursor/skills/impeccable/scripts/detector/shared/constants.mjs @@ -56,13 +56,27 @@ function isBrandFontOnOwnDomain(font) { return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix)); } -const GENERIC_FONTS = new Set([ +// Overused-font primary selection skips only CSS generics so a system stack +// keeps the system face as primary; GENERIC_FONTS still includes platform +// faces for design-system/serif resolution. +const CSS_GENERIC_FONTS = new Set([ 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', - 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', - '-apple-system', 'blinkmacsystemfont', 'segoe ui', 'inherit', 'initial', 'unset', 'revert', ]); +const GENERIC_FONTS = new Set([ + ...CSS_GENERIC_FONTS, + 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', + '-apple-system', 'blinkmacsystemfont', 'segoe ui', +]); + +function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) { + return String(fontFamily || '') + .split(',') + .map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()) + .find(f => f && !skip.has(f)) || null; +} + // WCAG large text thresholds are defined in points: 18pt normal text and // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch. const WCAG_LARGE_TEXT_PX = 18 * (96 / 72); @@ -104,6 +118,7 @@ export { BRAND_FONT_DOMAINS, isBrandFontOnOwnDomain, GENERIC_FONTS, + primaryFontFace, WCAG_LARGE_TEXT_PX, WCAG_LARGE_BOLD_TEXT_PX, EM_DASH_FLOOR, diff --git a/.gemini/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.gemini/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 62f5b87f1..3f5abd0a8 100644 --- a/.gemini/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.gemini/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -70,13 +70,27 @@ function isBrandFontOnOwnDomain(font) { return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix)); } -const GENERIC_FONTS = new Set([ +// Overused-font primary selection skips only CSS generics so a system stack +// keeps the system face as primary; GENERIC_FONTS still includes platform +// faces for design-system/serif resolution. +const CSS_GENERIC_FONTS = new Set([ 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', - 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', - '-apple-system', 'blinkmacsystemfont', 'segoe ui', 'inherit', 'initial', 'unset', 'revert', ]); +const GENERIC_FONTS = new Set([ + ...CSS_GENERIC_FONTS, + 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', + '-apple-system', 'blinkmacsystemfont', 'segoe ui', +]); + +function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) { + return String(fontFamily || '') + .split(',') + .map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()) + .find(f => f && !skip.has(f)) || null; +} + // WCAG large text thresholds are defined in points: 18pt normal text and // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch. const WCAG_LARGE_TEXT_PX = 18 * (96 / 72); @@ -1591,7 +1605,7 @@ function checkIconTile(opts) { function resolveSerif(fontFamily) { if (!fontFamily) return { primary: null, isSerif: false }; const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null; + const primary = primaryFontFace(fontFamily, GENERIC_FONTS); if (!primary) return { primary: null, isSerif: false }; if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true }; if (tokens.includes('serif')) return { primary, isSerif: true }; @@ -5190,8 +5204,7 @@ function checkTypography() { const style = getComputedStyle(el); const ff = style.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (!primary) continue; fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1); totalTextElements++; @@ -5436,8 +5449,7 @@ function checkPageTypography(doc, win) { if (rule.type !== 1) continue; const ff = rule.style?.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (primary) { fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); @@ -5456,11 +5468,10 @@ function checkPageTypography(doc, win) { const ffRe = /font-family\s*:\s*([^;}]+)/gi; let fm; while ((fm = ffRe.exec(html)) !== null) { - for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) { - if (f && !GENERIC_FONTS.has(f)) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + const primary = primaryFontFace(fm[1]); + if (primary) { + fonts.add(primary); + if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); } } diff --git a/.gemini/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs b/.gemini/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs index b4efa84ac..51c34224b 100644 --- a/.gemini/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +++ b/.gemini/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs @@ -1,7 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; -import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs'; +import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs'; import { checkSourceDesignSystem, collectStaticDesignSystemFindings, @@ -51,9 +51,7 @@ function checkStaticPageTypography(document, window) { for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) { const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0); if (!hasText) continue; - const ff = window.getComputedStyle(el).fontFamily || ''; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(window.getComputedStyle(el).fontFamily); if (!primary) continue; fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); diff --git a/.gemini/skills/impeccable/scripts/detector/rules/checks.mjs b/.gemini/skills/impeccable/scripts/detector/rules/checks.mjs index f15957b92..8bfb2b1df 100644 --- a/.gemini/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.gemini/skills/impeccable/scripts/detector/rules/checks.mjs @@ -9,6 +9,7 @@ import { WCAG_LARGE_BOLD_TEXT_PX, WCAG_LARGE_TEXT_PX, isBrandFontOnOwnDomain, + primaryFontFace, } from '../shared/constants.mjs'; import { CSS_NAMED_COLORS, @@ -331,7 +332,7 @@ function checkIconTile(opts) { function resolveSerif(fontFamily) { if (!fontFamily) return { primary: null, isSerif: false }; const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null; + const primary = primaryFontFace(fontFamily, GENERIC_FONTS); if (!primary) return { primary: null, isSerif: false }; if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true }; if (tokens.includes('serif')) return { primary, isSerif: true }; @@ -3930,8 +3931,7 @@ function checkTypography() { const style = getComputedStyle(el); const ff = style.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (!primary) continue; fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1); totalTextElements++; @@ -4176,8 +4176,7 @@ function checkPageTypography(doc, win) { if (rule.type !== 1) continue; const ff = rule.style?.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (primary) { fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); @@ -4196,11 +4195,10 @@ function checkPageTypography(doc, win) { const ffRe = /font-family\s*:\s*([^;}]+)/gi; let fm; while ((fm = ffRe.exec(html)) !== null) { - for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) { - if (f && !GENERIC_FONTS.has(f)) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + const primary = primaryFontFace(fm[1]); + if (primary) { + fonts.add(primary); + if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); } } diff --git a/.gemini/skills/impeccable/scripts/detector/shared/constants.mjs b/.gemini/skills/impeccable/scripts/detector/shared/constants.mjs index b9152939a..ea9bb361a 100644 --- a/.gemini/skills/impeccable/scripts/detector/shared/constants.mjs +++ b/.gemini/skills/impeccable/scripts/detector/shared/constants.mjs @@ -56,13 +56,27 @@ function isBrandFontOnOwnDomain(font) { return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix)); } -const GENERIC_FONTS = new Set([ +// Overused-font primary selection skips only CSS generics so a system stack +// keeps the system face as primary; GENERIC_FONTS still includes platform +// faces for design-system/serif resolution. +const CSS_GENERIC_FONTS = new Set([ 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', - 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', - '-apple-system', 'blinkmacsystemfont', 'segoe ui', 'inherit', 'initial', 'unset', 'revert', ]); +const GENERIC_FONTS = new Set([ + ...CSS_GENERIC_FONTS, + 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', + '-apple-system', 'blinkmacsystemfont', 'segoe ui', +]); + +function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) { + return String(fontFamily || '') + .split(',') + .map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()) + .find(f => f && !skip.has(f)) || null; +} + // WCAG large text thresholds are defined in points: 18pt normal text and // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch. const WCAG_LARGE_TEXT_PX = 18 * (96 / 72); @@ -104,6 +118,7 @@ export { BRAND_FONT_DOMAINS, isBrandFontOnOwnDomain, GENERIC_FONTS, + primaryFontFace, WCAG_LARGE_TEXT_PX, WCAG_LARGE_BOLD_TEXT_PX, EM_DASH_FLOOR, diff --git a/.github/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.github/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 62f5b87f1..3f5abd0a8 100644 --- a/.github/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.github/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -70,13 +70,27 @@ function isBrandFontOnOwnDomain(font) { return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix)); } -const GENERIC_FONTS = new Set([ +// Overused-font primary selection skips only CSS generics so a system stack +// keeps the system face as primary; GENERIC_FONTS still includes platform +// faces for design-system/serif resolution. +const CSS_GENERIC_FONTS = new Set([ 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', - 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', - '-apple-system', 'blinkmacsystemfont', 'segoe ui', 'inherit', 'initial', 'unset', 'revert', ]); +const GENERIC_FONTS = new Set([ + ...CSS_GENERIC_FONTS, + 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', + '-apple-system', 'blinkmacsystemfont', 'segoe ui', +]); + +function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) { + return String(fontFamily || '') + .split(',') + .map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()) + .find(f => f && !skip.has(f)) || null; +} + // WCAG large text thresholds are defined in points: 18pt normal text and // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch. const WCAG_LARGE_TEXT_PX = 18 * (96 / 72); @@ -1591,7 +1605,7 @@ function checkIconTile(opts) { function resolveSerif(fontFamily) { if (!fontFamily) return { primary: null, isSerif: false }; const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null; + const primary = primaryFontFace(fontFamily, GENERIC_FONTS); if (!primary) return { primary: null, isSerif: false }; if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true }; if (tokens.includes('serif')) return { primary, isSerif: true }; @@ -5190,8 +5204,7 @@ function checkTypography() { const style = getComputedStyle(el); const ff = style.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (!primary) continue; fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1); totalTextElements++; @@ -5436,8 +5449,7 @@ function checkPageTypography(doc, win) { if (rule.type !== 1) continue; const ff = rule.style?.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (primary) { fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); @@ -5456,11 +5468,10 @@ function checkPageTypography(doc, win) { const ffRe = /font-family\s*:\s*([^;}]+)/gi; let fm; while ((fm = ffRe.exec(html)) !== null) { - for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) { - if (f && !GENERIC_FONTS.has(f)) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + const primary = primaryFontFace(fm[1]); + if (primary) { + fonts.add(primary); + if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); } } diff --git a/.github/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs b/.github/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs index b4efa84ac..51c34224b 100644 --- a/.github/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +++ b/.github/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs @@ -1,7 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; -import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs'; +import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs'; import { checkSourceDesignSystem, collectStaticDesignSystemFindings, @@ -51,9 +51,7 @@ function checkStaticPageTypography(document, window) { for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) { const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0); if (!hasText) continue; - const ff = window.getComputedStyle(el).fontFamily || ''; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(window.getComputedStyle(el).fontFamily); if (!primary) continue; fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); diff --git a/.github/skills/impeccable/scripts/detector/rules/checks.mjs b/.github/skills/impeccable/scripts/detector/rules/checks.mjs index f15957b92..8bfb2b1df 100644 --- a/.github/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.github/skills/impeccable/scripts/detector/rules/checks.mjs @@ -9,6 +9,7 @@ import { WCAG_LARGE_BOLD_TEXT_PX, WCAG_LARGE_TEXT_PX, isBrandFontOnOwnDomain, + primaryFontFace, } from '../shared/constants.mjs'; import { CSS_NAMED_COLORS, @@ -331,7 +332,7 @@ function checkIconTile(opts) { function resolveSerif(fontFamily) { if (!fontFamily) return { primary: null, isSerif: false }; const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null; + const primary = primaryFontFace(fontFamily, GENERIC_FONTS); if (!primary) return { primary: null, isSerif: false }; if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true }; if (tokens.includes('serif')) return { primary, isSerif: true }; @@ -3930,8 +3931,7 @@ function checkTypography() { const style = getComputedStyle(el); const ff = style.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (!primary) continue; fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1); totalTextElements++; @@ -4176,8 +4176,7 @@ function checkPageTypography(doc, win) { if (rule.type !== 1) continue; const ff = rule.style?.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (primary) { fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); @@ -4196,11 +4195,10 @@ function checkPageTypography(doc, win) { const ffRe = /font-family\s*:\s*([^;}]+)/gi; let fm; while ((fm = ffRe.exec(html)) !== null) { - for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) { - if (f && !GENERIC_FONTS.has(f)) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + const primary = primaryFontFace(fm[1]); + if (primary) { + fonts.add(primary); + if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); } } diff --git a/.github/skills/impeccable/scripts/detector/shared/constants.mjs b/.github/skills/impeccable/scripts/detector/shared/constants.mjs index b9152939a..ea9bb361a 100644 --- a/.github/skills/impeccable/scripts/detector/shared/constants.mjs +++ b/.github/skills/impeccable/scripts/detector/shared/constants.mjs @@ -56,13 +56,27 @@ function isBrandFontOnOwnDomain(font) { return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix)); } -const GENERIC_FONTS = new Set([ +// Overused-font primary selection skips only CSS generics so a system stack +// keeps the system face as primary; GENERIC_FONTS still includes platform +// faces for design-system/serif resolution. +const CSS_GENERIC_FONTS = new Set([ 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', - 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', - '-apple-system', 'blinkmacsystemfont', 'segoe ui', 'inherit', 'initial', 'unset', 'revert', ]); +const GENERIC_FONTS = new Set([ + ...CSS_GENERIC_FONTS, + 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', + '-apple-system', 'blinkmacsystemfont', 'segoe ui', +]); + +function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) { + return String(fontFamily || '') + .split(',') + .map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()) + .find(f => f && !skip.has(f)) || null; +} + // WCAG large text thresholds are defined in points: 18pt normal text and // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch. const WCAG_LARGE_TEXT_PX = 18 * (96 / 72); @@ -104,6 +118,7 @@ export { BRAND_FONT_DOMAINS, isBrandFontOnOwnDomain, GENERIC_FONTS, + primaryFontFace, WCAG_LARGE_TEXT_PX, WCAG_LARGE_BOLD_TEXT_PX, EM_DASH_FLOOR, diff --git a/.grok/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.grok/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 62f5b87f1..3f5abd0a8 100644 --- a/.grok/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.grok/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -70,13 +70,27 @@ function isBrandFontOnOwnDomain(font) { return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix)); } -const GENERIC_FONTS = new Set([ +// Overused-font primary selection skips only CSS generics so a system stack +// keeps the system face as primary; GENERIC_FONTS still includes platform +// faces for design-system/serif resolution. +const CSS_GENERIC_FONTS = new Set([ 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', - 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', - '-apple-system', 'blinkmacsystemfont', 'segoe ui', 'inherit', 'initial', 'unset', 'revert', ]); +const GENERIC_FONTS = new Set([ + ...CSS_GENERIC_FONTS, + 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', + '-apple-system', 'blinkmacsystemfont', 'segoe ui', +]); + +function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) { + return String(fontFamily || '') + .split(',') + .map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()) + .find(f => f && !skip.has(f)) || null; +} + // WCAG large text thresholds are defined in points: 18pt normal text and // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch. const WCAG_LARGE_TEXT_PX = 18 * (96 / 72); @@ -1591,7 +1605,7 @@ function checkIconTile(opts) { function resolveSerif(fontFamily) { if (!fontFamily) return { primary: null, isSerif: false }; const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null; + const primary = primaryFontFace(fontFamily, GENERIC_FONTS); if (!primary) return { primary: null, isSerif: false }; if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true }; if (tokens.includes('serif')) return { primary, isSerif: true }; @@ -5190,8 +5204,7 @@ function checkTypography() { const style = getComputedStyle(el); const ff = style.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (!primary) continue; fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1); totalTextElements++; @@ -5436,8 +5449,7 @@ function checkPageTypography(doc, win) { if (rule.type !== 1) continue; const ff = rule.style?.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (primary) { fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); @@ -5456,11 +5468,10 @@ function checkPageTypography(doc, win) { const ffRe = /font-family\s*:\s*([^;}]+)/gi; let fm; while ((fm = ffRe.exec(html)) !== null) { - for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) { - if (f && !GENERIC_FONTS.has(f)) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + const primary = primaryFontFace(fm[1]); + if (primary) { + fonts.add(primary); + if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); } } diff --git a/.grok/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs b/.grok/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs index b4efa84ac..51c34224b 100644 --- a/.grok/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +++ b/.grok/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs @@ -1,7 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; -import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs'; +import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs'; import { checkSourceDesignSystem, collectStaticDesignSystemFindings, @@ -51,9 +51,7 @@ function checkStaticPageTypography(document, window) { for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) { const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0); if (!hasText) continue; - const ff = window.getComputedStyle(el).fontFamily || ''; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(window.getComputedStyle(el).fontFamily); if (!primary) continue; fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); diff --git a/.grok/skills/impeccable/scripts/detector/rules/checks.mjs b/.grok/skills/impeccable/scripts/detector/rules/checks.mjs index f15957b92..8bfb2b1df 100644 --- a/.grok/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.grok/skills/impeccable/scripts/detector/rules/checks.mjs @@ -9,6 +9,7 @@ import { WCAG_LARGE_BOLD_TEXT_PX, WCAG_LARGE_TEXT_PX, isBrandFontOnOwnDomain, + primaryFontFace, } from '../shared/constants.mjs'; import { CSS_NAMED_COLORS, @@ -331,7 +332,7 @@ function checkIconTile(opts) { function resolveSerif(fontFamily) { if (!fontFamily) return { primary: null, isSerif: false }; const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null; + const primary = primaryFontFace(fontFamily, GENERIC_FONTS); if (!primary) return { primary: null, isSerif: false }; if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true }; if (tokens.includes('serif')) return { primary, isSerif: true }; @@ -3930,8 +3931,7 @@ function checkTypography() { const style = getComputedStyle(el); const ff = style.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (!primary) continue; fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1); totalTextElements++; @@ -4176,8 +4176,7 @@ function checkPageTypography(doc, win) { if (rule.type !== 1) continue; const ff = rule.style?.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (primary) { fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); @@ -4196,11 +4195,10 @@ function checkPageTypography(doc, win) { const ffRe = /font-family\s*:\s*([^;}]+)/gi; let fm; while ((fm = ffRe.exec(html)) !== null) { - for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) { - if (f && !GENERIC_FONTS.has(f)) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + const primary = primaryFontFace(fm[1]); + if (primary) { + fonts.add(primary); + if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); } } diff --git a/.grok/skills/impeccable/scripts/detector/shared/constants.mjs b/.grok/skills/impeccable/scripts/detector/shared/constants.mjs index b9152939a..ea9bb361a 100644 --- a/.grok/skills/impeccable/scripts/detector/shared/constants.mjs +++ b/.grok/skills/impeccable/scripts/detector/shared/constants.mjs @@ -56,13 +56,27 @@ function isBrandFontOnOwnDomain(font) { return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix)); } -const GENERIC_FONTS = new Set([ +// Overused-font primary selection skips only CSS generics so a system stack +// keeps the system face as primary; GENERIC_FONTS still includes platform +// faces for design-system/serif resolution. +const CSS_GENERIC_FONTS = new Set([ 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', - 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', - '-apple-system', 'blinkmacsystemfont', 'segoe ui', 'inherit', 'initial', 'unset', 'revert', ]); +const GENERIC_FONTS = new Set([ + ...CSS_GENERIC_FONTS, + 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', + '-apple-system', 'blinkmacsystemfont', 'segoe ui', +]); + +function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) { + return String(fontFamily || '') + .split(',') + .map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()) + .find(f => f && !skip.has(f)) || null; +} + // WCAG large text thresholds are defined in points: 18pt normal text and // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch. const WCAG_LARGE_TEXT_PX = 18 * (96 / 72); @@ -104,6 +118,7 @@ export { BRAND_FONT_DOMAINS, isBrandFontOnOwnDomain, GENERIC_FONTS, + primaryFontFace, WCAG_LARGE_TEXT_PX, WCAG_LARGE_BOLD_TEXT_PX, EM_DASH_FLOOR, diff --git a/.hermes/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.hermes/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 62f5b87f1..3f5abd0a8 100644 --- a/.hermes/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.hermes/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -70,13 +70,27 @@ function isBrandFontOnOwnDomain(font) { return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix)); } -const GENERIC_FONTS = new Set([ +// Overused-font primary selection skips only CSS generics so a system stack +// keeps the system face as primary; GENERIC_FONTS still includes platform +// faces for design-system/serif resolution. +const CSS_GENERIC_FONTS = new Set([ 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', - 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', - '-apple-system', 'blinkmacsystemfont', 'segoe ui', 'inherit', 'initial', 'unset', 'revert', ]); +const GENERIC_FONTS = new Set([ + ...CSS_GENERIC_FONTS, + 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', + '-apple-system', 'blinkmacsystemfont', 'segoe ui', +]); + +function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) { + return String(fontFamily || '') + .split(',') + .map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()) + .find(f => f && !skip.has(f)) || null; +} + // WCAG large text thresholds are defined in points: 18pt normal text and // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch. const WCAG_LARGE_TEXT_PX = 18 * (96 / 72); @@ -1591,7 +1605,7 @@ function checkIconTile(opts) { function resolveSerif(fontFamily) { if (!fontFamily) return { primary: null, isSerif: false }; const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null; + const primary = primaryFontFace(fontFamily, GENERIC_FONTS); if (!primary) return { primary: null, isSerif: false }; if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true }; if (tokens.includes('serif')) return { primary, isSerif: true }; @@ -5190,8 +5204,7 @@ function checkTypography() { const style = getComputedStyle(el); const ff = style.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (!primary) continue; fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1); totalTextElements++; @@ -5436,8 +5449,7 @@ function checkPageTypography(doc, win) { if (rule.type !== 1) continue; const ff = rule.style?.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (primary) { fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); @@ -5456,11 +5468,10 @@ function checkPageTypography(doc, win) { const ffRe = /font-family\s*:\s*([^;}]+)/gi; let fm; while ((fm = ffRe.exec(html)) !== null) { - for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) { - if (f && !GENERIC_FONTS.has(f)) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + const primary = primaryFontFace(fm[1]); + if (primary) { + fonts.add(primary); + if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); } } diff --git a/.hermes/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs b/.hermes/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs index b4efa84ac..51c34224b 100644 --- a/.hermes/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +++ b/.hermes/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs @@ -1,7 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; -import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs'; +import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs'; import { checkSourceDesignSystem, collectStaticDesignSystemFindings, @@ -51,9 +51,7 @@ function checkStaticPageTypography(document, window) { for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) { const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0); if (!hasText) continue; - const ff = window.getComputedStyle(el).fontFamily || ''; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(window.getComputedStyle(el).fontFamily); if (!primary) continue; fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); diff --git a/.hermes/skills/impeccable/scripts/detector/rules/checks.mjs b/.hermes/skills/impeccable/scripts/detector/rules/checks.mjs index f15957b92..8bfb2b1df 100644 --- a/.hermes/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.hermes/skills/impeccable/scripts/detector/rules/checks.mjs @@ -9,6 +9,7 @@ import { WCAG_LARGE_BOLD_TEXT_PX, WCAG_LARGE_TEXT_PX, isBrandFontOnOwnDomain, + primaryFontFace, } from '../shared/constants.mjs'; import { CSS_NAMED_COLORS, @@ -331,7 +332,7 @@ function checkIconTile(opts) { function resolveSerif(fontFamily) { if (!fontFamily) return { primary: null, isSerif: false }; const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null; + const primary = primaryFontFace(fontFamily, GENERIC_FONTS); if (!primary) return { primary: null, isSerif: false }; if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true }; if (tokens.includes('serif')) return { primary, isSerif: true }; @@ -3930,8 +3931,7 @@ function checkTypography() { const style = getComputedStyle(el); const ff = style.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (!primary) continue; fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1); totalTextElements++; @@ -4176,8 +4176,7 @@ function checkPageTypography(doc, win) { if (rule.type !== 1) continue; const ff = rule.style?.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (primary) { fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); @@ -4196,11 +4195,10 @@ function checkPageTypography(doc, win) { const ffRe = /font-family\s*:\s*([^;}]+)/gi; let fm; while ((fm = ffRe.exec(html)) !== null) { - for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) { - if (f && !GENERIC_FONTS.has(f)) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + const primary = primaryFontFace(fm[1]); + if (primary) { + fonts.add(primary); + if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); } } diff --git a/.hermes/skills/impeccable/scripts/detector/shared/constants.mjs b/.hermes/skills/impeccable/scripts/detector/shared/constants.mjs index b9152939a..ea9bb361a 100644 --- a/.hermes/skills/impeccable/scripts/detector/shared/constants.mjs +++ b/.hermes/skills/impeccable/scripts/detector/shared/constants.mjs @@ -56,13 +56,27 @@ function isBrandFontOnOwnDomain(font) { return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix)); } -const GENERIC_FONTS = new Set([ +// Overused-font primary selection skips only CSS generics so a system stack +// keeps the system face as primary; GENERIC_FONTS still includes platform +// faces for design-system/serif resolution. +const CSS_GENERIC_FONTS = new Set([ 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', - 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', - '-apple-system', 'blinkmacsystemfont', 'segoe ui', 'inherit', 'initial', 'unset', 'revert', ]); +const GENERIC_FONTS = new Set([ + ...CSS_GENERIC_FONTS, + 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', + '-apple-system', 'blinkmacsystemfont', 'segoe ui', +]); + +function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) { + return String(fontFamily || '') + .split(',') + .map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()) + .find(f => f && !skip.has(f)) || null; +} + // WCAG large text thresholds are defined in points: 18pt normal text and // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch. const WCAG_LARGE_TEXT_PX = 18 * (96 / 72); @@ -104,6 +118,7 @@ export { BRAND_FONT_DOMAINS, isBrandFontOnOwnDomain, GENERIC_FONTS, + primaryFontFace, WCAG_LARGE_TEXT_PX, WCAG_LARGE_BOLD_TEXT_PX, EM_DASH_FLOOR, diff --git a/.kiro/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.kiro/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 62f5b87f1..3f5abd0a8 100644 --- a/.kiro/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.kiro/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -70,13 +70,27 @@ function isBrandFontOnOwnDomain(font) { return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix)); } -const GENERIC_FONTS = new Set([ +// Overused-font primary selection skips only CSS generics so a system stack +// keeps the system face as primary; GENERIC_FONTS still includes platform +// faces for design-system/serif resolution. +const CSS_GENERIC_FONTS = new Set([ 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', - 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', - '-apple-system', 'blinkmacsystemfont', 'segoe ui', 'inherit', 'initial', 'unset', 'revert', ]); +const GENERIC_FONTS = new Set([ + ...CSS_GENERIC_FONTS, + 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', + '-apple-system', 'blinkmacsystemfont', 'segoe ui', +]); + +function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) { + return String(fontFamily || '') + .split(',') + .map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()) + .find(f => f && !skip.has(f)) || null; +} + // WCAG large text thresholds are defined in points: 18pt normal text and // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch. const WCAG_LARGE_TEXT_PX = 18 * (96 / 72); @@ -1591,7 +1605,7 @@ function checkIconTile(opts) { function resolveSerif(fontFamily) { if (!fontFamily) return { primary: null, isSerif: false }; const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null; + const primary = primaryFontFace(fontFamily, GENERIC_FONTS); if (!primary) return { primary: null, isSerif: false }; if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true }; if (tokens.includes('serif')) return { primary, isSerif: true }; @@ -5190,8 +5204,7 @@ function checkTypography() { const style = getComputedStyle(el); const ff = style.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (!primary) continue; fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1); totalTextElements++; @@ -5436,8 +5449,7 @@ function checkPageTypography(doc, win) { if (rule.type !== 1) continue; const ff = rule.style?.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (primary) { fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); @@ -5456,11 +5468,10 @@ function checkPageTypography(doc, win) { const ffRe = /font-family\s*:\s*([^;}]+)/gi; let fm; while ((fm = ffRe.exec(html)) !== null) { - for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) { - if (f && !GENERIC_FONTS.has(f)) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + const primary = primaryFontFace(fm[1]); + if (primary) { + fonts.add(primary); + if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); } } diff --git a/.kiro/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs b/.kiro/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs index b4efa84ac..51c34224b 100644 --- a/.kiro/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +++ b/.kiro/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs @@ -1,7 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; -import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs'; +import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs'; import { checkSourceDesignSystem, collectStaticDesignSystemFindings, @@ -51,9 +51,7 @@ function checkStaticPageTypography(document, window) { for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) { const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0); if (!hasText) continue; - const ff = window.getComputedStyle(el).fontFamily || ''; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(window.getComputedStyle(el).fontFamily); if (!primary) continue; fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); diff --git a/.kiro/skills/impeccable/scripts/detector/rules/checks.mjs b/.kiro/skills/impeccable/scripts/detector/rules/checks.mjs index f15957b92..8bfb2b1df 100644 --- a/.kiro/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.kiro/skills/impeccable/scripts/detector/rules/checks.mjs @@ -9,6 +9,7 @@ import { WCAG_LARGE_BOLD_TEXT_PX, WCAG_LARGE_TEXT_PX, isBrandFontOnOwnDomain, + primaryFontFace, } from '../shared/constants.mjs'; import { CSS_NAMED_COLORS, @@ -331,7 +332,7 @@ function checkIconTile(opts) { function resolveSerif(fontFamily) { if (!fontFamily) return { primary: null, isSerif: false }; const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null; + const primary = primaryFontFace(fontFamily, GENERIC_FONTS); if (!primary) return { primary: null, isSerif: false }; if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true }; if (tokens.includes('serif')) return { primary, isSerif: true }; @@ -3930,8 +3931,7 @@ function checkTypography() { const style = getComputedStyle(el); const ff = style.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (!primary) continue; fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1); totalTextElements++; @@ -4176,8 +4176,7 @@ function checkPageTypography(doc, win) { if (rule.type !== 1) continue; const ff = rule.style?.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (primary) { fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); @@ -4196,11 +4195,10 @@ function checkPageTypography(doc, win) { const ffRe = /font-family\s*:\s*([^;}]+)/gi; let fm; while ((fm = ffRe.exec(html)) !== null) { - for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) { - if (f && !GENERIC_FONTS.has(f)) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + const primary = primaryFontFace(fm[1]); + if (primary) { + fonts.add(primary); + if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); } } diff --git a/.kiro/skills/impeccable/scripts/detector/shared/constants.mjs b/.kiro/skills/impeccable/scripts/detector/shared/constants.mjs index b9152939a..ea9bb361a 100644 --- a/.kiro/skills/impeccable/scripts/detector/shared/constants.mjs +++ b/.kiro/skills/impeccable/scripts/detector/shared/constants.mjs @@ -56,13 +56,27 @@ function isBrandFontOnOwnDomain(font) { return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix)); } -const GENERIC_FONTS = new Set([ +// Overused-font primary selection skips only CSS generics so a system stack +// keeps the system face as primary; GENERIC_FONTS still includes platform +// faces for design-system/serif resolution. +const CSS_GENERIC_FONTS = new Set([ 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', - 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', - '-apple-system', 'blinkmacsystemfont', 'segoe ui', 'inherit', 'initial', 'unset', 'revert', ]); +const GENERIC_FONTS = new Set([ + ...CSS_GENERIC_FONTS, + 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', + '-apple-system', 'blinkmacsystemfont', 'segoe ui', +]); + +function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) { + return String(fontFamily || '') + .split(',') + .map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()) + .find(f => f && !skip.has(f)) || null; +} + // WCAG large text thresholds are defined in points: 18pt normal text and // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch. const WCAG_LARGE_TEXT_PX = 18 * (96 / 72); @@ -104,6 +118,7 @@ export { BRAND_FONT_DOMAINS, isBrandFontOnOwnDomain, GENERIC_FONTS, + primaryFontFace, WCAG_LARGE_TEXT_PX, WCAG_LARGE_BOLD_TEXT_PX, EM_DASH_FLOOR, diff --git a/.opencode/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.opencode/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 62f5b87f1..3f5abd0a8 100644 --- a/.opencode/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.opencode/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -70,13 +70,27 @@ function isBrandFontOnOwnDomain(font) { return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix)); } -const GENERIC_FONTS = new Set([ +// Overused-font primary selection skips only CSS generics so a system stack +// keeps the system face as primary; GENERIC_FONTS still includes platform +// faces for design-system/serif resolution. +const CSS_GENERIC_FONTS = new Set([ 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', - 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', - '-apple-system', 'blinkmacsystemfont', 'segoe ui', 'inherit', 'initial', 'unset', 'revert', ]); +const GENERIC_FONTS = new Set([ + ...CSS_GENERIC_FONTS, + 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', + '-apple-system', 'blinkmacsystemfont', 'segoe ui', +]); + +function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) { + return String(fontFamily || '') + .split(',') + .map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()) + .find(f => f && !skip.has(f)) || null; +} + // WCAG large text thresholds are defined in points: 18pt normal text and // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch. const WCAG_LARGE_TEXT_PX = 18 * (96 / 72); @@ -1591,7 +1605,7 @@ function checkIconTile(opts) { function resolveSerif(fontFamily) { if (!fontFamily) return { primary: null, isSerif: false }; const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null; + const primary = primaryFontFace(fontFamily, GENERIC_FONTS); if (!primary) return { primary: null, isSerif: false }; if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true }; if (tokens.includes('serif')) return { primary, isSerif: true }; @@ -5190,8 +5204,7 @@ function checkTypography() { const style = getComputedStyle(el); const ff = style.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (!primary) continue; fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1); totalTextElements++; @@ -5436,8 +5449,7 @@ function checkPageTypography(doc, win) { if (rule.type !== 1) continue; const ff = rule.style?.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (primary) { fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); @@ -5456,11 +5468,10 @@ function checkPageTypography(doc, win) { const ffRe = /font-family\s*:\s*([^;}]+)/gi; let fm; while ((fm = ffRe.exec(html)) !== null) { - for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) { - if (f && !GENERIC_FONTS.has(f)) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + const primary = primaryFontFace(fm[1]); + if (primary) { + fonts.add(primary); + if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); } } diff --git a/.opencode/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs b/.opencode/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs index b4efa84ac..51c34224b 100644 --- a/.opencode/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +++ b/.opencode/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs @@ -1,7 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; -import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs'; +import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs'; import { checkSourceDesignSystem, collectStaticDesignSystemFindings, @@ -51,9 +51,7 @@ function checkStaticPageTypography(document, window) { for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) { const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0); if (!hasText) continue; - const ff = window.getComputedStyle(el).fontFamily || ''; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(window.getComputedStyle(el).fontFamily); if (!primary) continue; fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); diff --git a/.opencode/skills/impeccable/scripts/detector/rules/checks.mjs b/.opencode/skills/impeccable/scripts/detector/rules/checks.mjs index f15957b92..8bfb2b1df 100644 --- a/.opencode/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.opencode/skills/impeccable/scripts/detector/rules/checks.mjs @@ -9,6 +9,7 @@ import { WCAG_LARGE_BOLD_TEXT_PX, WCAG_LARGE_TEXT_PX, isBrandFontOnOwnDomain, + primaryFontFace, } from '../shared/constants.mjs'; import { CSS_NAMED_COLORS, @@ -331,7 +332,7 @@ function checkIconTile(opts) { function resolveSerif(fontFamily) { if (!fontFamily) return { primary: null, isSerif: false }; const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null; + const primary = primaryFontFace(fontFamily, GENERIC_FONTS); if (!primary) return { primary: null, isSerif: false }; if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true }; if (tokens.includes('serif')) return { primary, isSerif: true }; @@ -3930,8 +3931,7 @@ function checkTypography() { const style = getComputedStyle(el); const ff = style.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (!primary) continue; fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1); totalTextElements++; @@ -4176,8 +4176,7 @@ function checkPageTypography(doc, win) { if (rule.type !== 1) continue; const ff = rule.style?.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (primary) { fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); @@ -4196,11 +4195,10 @@ function checkPageTypography(doc, win) { const ffRe = /font-family\s*:\s*([^;}]+)/gi; let fm; while ((fm = ffRe.exec(html)) !== null) { - for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) { - if (f && !GENERIC_FONTS.has(f)) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + const primary = primaryFontFace(fm[1]); + if (primary) { + fonts.add(primary); + if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); } } diff --git a/.opencode/skills/impeccable/scripts/detector/shared/constants.mjs b/.opencode/skills/impeccable/scripts/detector/shared/constants.mjs index b9152939a..ea9bb361a 100644 --- a/.opencode/skills/impeccable/scripts/detector/shared/constants.mjs +++ b/.opencode/skills/impeccable/scripts/detector/shared/constants.mjs @@ -56,13 +56,27 @@ function isBrandFontOnOwnDomain(font) { return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix)); } -const GENERIC_FONTS = new Set([ +// Overused-font primary selection skips only CSS generics so a system stack +// keeps the system face as primary; GENERIC_FONTS still includes platform +// faces for design-system/serif resolution. +const CSS_GENERIC_FONTS = new Set([ 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', - 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', - '-apple-system', 'blinkmacsystemfont', 'segoe ui', 'inherit', 'initial', 'unset', 'revert', ]); +const GENERIC_FONTS = new Set([ + ...CSS_GENERIC_FONTS, + 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', + '-apple-system', 'blinkmacsystemfont', 'segoe ui', +]); + +function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) { + return String(fontFamily || '') + .split(',') + .map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()) + .find(f => f && !skip.has(f)) || null; +} + // WCAG large text thresholds are defined in points: 18pt normal text and // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch. const WCAG_LARGE_TEXT_PX = 18 * (96 / 72); @@ -104,6 +118,7 @@ export { BRAND_FONT_DOMAINS, isBrandFontOnOwnDomain, GENERIC_FONTS, + primaryFontFace, WCAG_LARGE_TEXT_PX, WCAG_LARGE_BOLD_TEXT_PX, EM_DASH_FLOOR, diff --git a/.pi/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.pi/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 62f5b87f1..3f5abd0a8 100644 --- a/.pi/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.pi/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -70,13 +70,27 @@ function isBrandFontOnOwnDomain(font) { return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix)); } -const GENERIC_FONTS = new Set([ +// Overused-font primary selection skips only CSS generics so a system stack +// keeps the system face as primary; GENERIC_FONTS still includes platform +// faces for design-system/serif resolution. +const CSS_GENERIC_FONTS = new Set([ 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', - 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', - '-apple-system', 'blinkmacsystemfont', 'segoe ui', 'inherit', 'initial', 'unset', 'revert', ]); +const GENERIC_FONTS = new Set([ + ...CSS_GENERIC_FONTS, + 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', + '-apple-system', 'blinkmacsystemfont', 'segoe ui', +]); + +function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) { + return String(fontFamily || '') + .split(',') + .map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()) + .find(f => f && !skip.has(f)) || null; +} + // WCAG large text thresholds are defined in points: 18pt normal text and // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch. const WCAG_LARGE_TEXT_PX = 18 * (96 / 72); @@ -1591,7 +1605,7 @@ function checkIconTile(opts) { function resolveSerif(fontFamily) { if (!fontFamily) return { primary: null, isSerif: false }; const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null; + const primary = primaryFontFace(fontFamily, GENERIC_FONTS); if (!primary) return { primary: null, isSerif: false }; if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true }; if (tokens.includes('serif')) return { primary, isSerif: true }; @@ -5190,8 +5204,7 @@ function checkTypography() { const style = getComputedStyle(el); const ff = style.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (!primary) continue; fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1); totalTextElements++; @@ -5436,8 +5449,7 @@ function checkPageTypography(doc, win) { if (rule.type !== 1) continue; const ff = rule.style?.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (primary) { fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); @@ -5456,11 +5468,10 @@ function checkPageTypography(doc, win) { const ffRe = /font-family\s*:\s*([^;}]+)/gi; let fm; while ((fm = ffRe.exec(html)) !== null) { - for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) { - if (f && !GENERIC_FONTS.has(f)) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + const primary = primaryFontFace(fm[1]); + if (primary) { + fonts.add(primary); + if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); } } diff --git a/.pi/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs b/.pi/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs index b4efa84ac..51c34224b 100644 --- a/.pi/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +++ b/.pi/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs @@ -1,7 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; -import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs'; +import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs'; import { checkSourceDesignSystem, collectStaticDesignSystemFindings, @@ -51,9 +51,7 @@ function checkStaticPageTypography(document, window) { for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) { const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0); if (!hasText) continue; - const ff = window.getComputedStyle(el).fontFamily || ''; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(window.getComputedStyle(el).fontFamily); if (!primary) continue; fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); diff --git a/.pi/skills/impeccable/scripts/detector/rules/checks.mjs b/.pi/skills/impeccable/scripts/detector/rules/checks.mjs index f15957b92..8bfb2b1df 100644 --- a/.pi/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.pi/skills/impeccable/scripts/detector/rules/checks.mjs @@ -9,6 +9,7 @@ import { WCAG_LARGE_BOLD_TEXT_PX, WCAG_LARGE_TEXT_PX, isBrandFontOnOwnDomain, + primaryFontFace, } from '../shared/constants.mjs'; import { CSS_NAMED_COLORS, @@ -331,7 +332,7 @@ function checkIconTile(opts) { function resolveSerif(fontFamily) { if (!fontFamily) return { primary: null, isSerif: false }; const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null; + const primary = primaryFontFace(fontFamily, GENERIC_FONTS); if (!primary) return { primary: null, isSerif: false }; if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true }; if (tokens.includes('serif')) return { primary, isSerif: true }; @@ -3930,8 +3931,7 @@ function checkTypography() { const style = getComputedStyle(el); const ff = style.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (!primary) continue; fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1); totalTextElements++; @@ -4176,8 +4176,7 @@ function checkPageTypography(doc, win) { if (rule.type !== 1) continue; const ff = rule.style?.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (primary) { fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); @@ -4196,11 +4195,10 @@ function checkPageTypography(doc, win) { const ffRe = /font-family\s*:\s*([^;}]+)/gi; let fm; while ((fm = ffRe.exec(html)) !== null) { - for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) { - if (f && !GENERIC_FONTS.has(f)) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + const primary = primaryFontFace(fm[1]); + if (primary) { + fonts.add(primary); + if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); } } diff --git a/.pi/skills/impeccable/scripts/detector/shared/constants.mjs b/.pi/skills/impeccable/scripts/detector/shared/constants.mjs index b9152939a..ea9bb361a 100644 --- a/.pi/skills/impeccable/scripts/detector/shared/constants.mjs +++ b/.pi/skills/impeccable/scripts/detector/shared/constants.mjs @@ -56,13 +56,27 @@ function isBrandFontOnOwnDomain(font) { return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix)); } -const GENERIC_FONTS = new Set([ +// Overused-font primary selection skips only CSS generics so a system stack +// keeps the system face as primary; GENERIC_FONTS still includes platform +// faces for design-system/serif resolution. +const CSS_GENERIC_FONTS = new Set([ 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', - 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', - '-apple-system', 'blinkmacsystemfont', 'segoe ui', 'inherit', 'initial', 'unset', 'revert', ]); +const GENERIC_FONTS = new Set([ + ...CSS_GENERIC_FONTS, + 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', + '-apple-system', 'blinkmacsystemfont', 'segoe ui', +]); + +function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) { + return String(fontFamily || '') + .split(',') + .map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()) + .find(f => f && !skip.has(f)) || null; +} + // WCAG large text thresholds are defined in points: 18pt normal text and // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch. const WCAG_LARGE_TEXT_PX = 18 * (96 / 72); @@ -104,6 +118,7 @@ export { BRAND_FONT_DOMAINS, isBrandFontOnOwnDomain, GENERIC_FONTS, + primaryFontFace, WCAG_LARGE_TEXT_PX, WCAG_LARGE_BOLD_TEXT_PX, EM_DASH_FLOOR, diff --git a/.qoder/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.qoder/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 62f5b87f1..3f5abd0a8 100644 --- a/.qoder/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.qoder/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -70,13 +70,27 @@ function isBrandFontOnOwnDomain(font) { return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix)); } -const GENERIC_FONTS = new Set([ +// Overused-font primary selection skips only CSS generics so a system stack +// keeps the system face as primary; GENERIC_FONTS still includes platform +// faces for design-system/serif resolution. +const CSS_GENERIC_FONTS = new Set([ 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', - 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', - '-apple-system', 'blinkmacsystemfont', 'segoe ui', 'inherit', 'initial', 'unset', 'revert', ]); +const GENERIC_FONTS = new Set([ + ...CSS_GENERIC_FONTS, + 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', + '-apple-system', 'blinkmacsystemfont', 'segoe ui', +]); + +function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) { + return String(fontFamily || '') + .split(',') + .map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()) + .find(f => f && !skip.has(f)) || null; +} + // WCAG large text thresholds are defined in points: 18pt normal text and // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch. const WCAG_LARGE_TEXT_PX = 18 * (96 / 72); @@ -1591,7 +1605,7 @@ function checkIconTile(opts) { function resolveSerif(fontFamily) { if (!fontFamily) return { primary: null, isSerif: false }; const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null; + const primary = primaryFontFace(fontFamily, GENERIC_FONTS); if (!primary) return { primary: null, isSerif: false }; if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true }; if (tokens.includes('serif')) return { primary, isSerif: true }; @@ -5190,8 +5204,7 @@ function checkTypography() { const style = getComputedStyle(el); const ff = style.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (!primary) continue; fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1); totalTextElements++; @@ -5436,8 +5449,7 @@ function checkPageTypography(doc, win) { if (rule.type !== 1) continue; const ff = rule.style?.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (primary) { fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); @@ -5456,11 +5468,10 @@ function checkPageTypography(doc, win) { const ffRe = /font-family\s*:\s*([^;}]+)/gi; let fm; while ((fm = ffRe.exec(html)) !== null) { - for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) { - if (f && !GENERIC_FONTS.has(f)) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + const primary = primaryFontFace(fm[1]); + if (primary) { + fonts.add(primary); + if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); } } diff --git a/.qoder/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs b/.qoder/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs index b4efa84ac..51c34224b 100644 --- a/.qoder/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +++ b/.qoder/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs @@ -1,7 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; -import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs'; +import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs'; import { checkSourceDesignSystem, collectStaticDesignSystemFindings, @@ -51,9 +51,7 @@ function checkStaticPageTypography(document, window) { for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) { const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0); if (!hasText) continue; - const ff = window.getComputedStyle(el).fontFamily || ''; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(window.getComputedStyle(el).fontFamily); if (!primary) continue; fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); diff --git a/.qoder/skills/impeccable/scripts/detector/rules/checks.mjs b/.qoder/skills/impeccable/scripts/detector/rules/checks.mjs index f15957b92..8bfb2b1df 100644 --- a/.qoder/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.qoder/skills/impeccable/scripts/detector/rules/checks.mjs @@ -9,6 +9,7 @@ import { WCAG_LARGE_BOLD_TEXT_PX, WCAG_LARGE_TEXT_PX, isBrandFontOnOwnDomain, + primaryFontFace, } from '../shared/constants.mjs'; import { CSS_NAMED_COLORS, @@ -331,7 +332,7 @@ function checkIconTile(opts) { function resolveSerif(fontFamily) { if (!fontFamily) return { primary: null, isSerif: false }; const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null; + const primary = primaryFontFace(fontFamily, GENERIC_FONTS); if (!primary) return { primary: null, isSerif: false }; if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true }; if (tokens.includes('serif')) return { primary, isSerif: true }; @@ -3930,8 +3931,7 @@ function checkTypography() { const style = getComputedStyle(el); const ff = style.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (!primary) continue; fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1); totalTextElements++; @@ -4176,8 +4176,7 @@ function checkPageTypography(doc, win) { if (rule.type !== 1) continue; const ff = rule.style?.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (primary) { fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); @@ -4196,11 +4195,10 @@ function checkPageTypography(doc, win) { const ffRe = /font-family\s*:\s*([^;}]+)/gi; let fm; while ((fm = ffRe.exec(html)) !== null) { - for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) { - if (f && !GENERIC_FONTS.has(f)) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + const primary = primaryFontFace(fm[1]); + if (primary) { + fonts.add(primary); + if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); } } diff --git a/.qoder/skills/impeccable/scripts/detector/shared/constants.mjs b/.qoder/skills/impeccable/scripts/detector/shared/constants.mjs index b9152939a..ea9bb361a 100644 --- a/.qoder/skills/impeccable/scripts/detector/shared/constants.mjs +++ b/.qoder/skills/impeccable/scripts/detector/shared/constants.mjs @@ -56,13 +56,27 @@ function isBrandFontOnOwnDomain(font) { return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix)); } -const GENERIC_FONTS = new Set([ +// Overused-font primary selection skips only CSS generics so a system stack +// keeps the system face as primary; GENERIC_FONTS still includes platform +// faces for design-system/serif resolution. +const CSS_GENERIC_FONTS = new Set([ 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', - 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', - '-apple-system', 'blinkmacsystemfont', 'segoe ui', 'inherit', 'initial', 'unset', 'revert', ]); +const GENERIC_FONTS = new Set([ + ...CSS_GENERIC_FONTS, + 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', + '-apple-system', 'blinkmacsystemfont', 'segoe ui', +]); + +function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) { + return String(fontFamily || '') + .split(',') + .map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()) + .find(f => f && !skip.has(f)) || null; +} + // WCAG large text thresholds are defined in points: 18pt normal text and // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch. const WCAG_LARGE_TEXT_PX = 18 * (96 / 72); @@ -104,6 +118,7 @@ export { BRAND_FONT_DOMAINS, isBrandFontOnOwnDomain, GENERIC_FONTS, + primaryFontFace, WCAG_LARGE_TEXT_PX, WCAG_LARGE_BOLD_TEXT_PX, EM_DASH_FLOOR, diff --git a/.rovodev/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.rovodev/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 62f5b87f1..3f5abd0a8 100644 --- a/.rovodev/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.rovodev/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -70,13 +70,27 @@ function isBrandFontOnOwnDomain(font) { return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix)); } -const GENERIC_FONTS = new Set([ +// Overused-font primary selection skips only CSS generics so a system stack +// keeps the system face as primary; GENERIC_FONTS still includes platform +// faces for design-system/serif resolution. +const CSS_GENERIC_FONTS = new Set([ 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', - 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', - '-apple-system', 'blinkmacsystemfont', 'segoe ui', 'inherit', 'initial', 'unset', 'revert', ]); +const GENERIC_FONTS = new Set([ + ...CSS_GENERIC_FONTS, + 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', + '-apple-system', 'blinkmacsystemfont', 'segoe ui', +]); + +function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) { + return String(fontFamily || '') + .split(',') + .map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()) + .find(f => f && !skip.has(f)) || null; +} + // WCAG large text thresholds are defined in points: 18pt normal text and // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch. const WCAG_LARGE_TEXT_PX = 18 * (96 / 72); @@ -1591,7 +1605,7 @@ function checkIconTile(opts) { function resolveSerif(fontFamily) { if (!fontFamily) return { primary: null, isSerif: false }; const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null; + const primary = primaryFontFace(fontFamily, GENERIC_FONTS); if (!primary) return { primary: null, isSerif: false }; if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true }; if (tokens.includes('serif')) return { primary, isSerif: true }; @@ -5190,8 +5204,7 @@ function checkTypography() { const style = getComputedStyle(el); const ff = style.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (!primary) continue; fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1); totalTextElements++; @@ -5436,8 +5449,7 @@ function checkPageTypography(doc, win) { if (rule.type !== 1) continue; const ff = rule.style?.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (primary) { fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); @@ -5456,11 +5468,10 @@ function checkPageTypography(doc, win) { const ffRe = /font-family\s*:\s*([^;}]+)/gi; let fm; while ((fm = ffRe.exec(html)) !== null) { - for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) { - if (f && !GENERIC_FONTS.has(f)) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + const primary = primaryFontFace(fm[1]); + if (primary) { + fonts.add(primary); + if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); } } diff --git a/.rovodev/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs b/.rovodev/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs index b4efa84ac..51c34224b 100644 --- a/.rovodev/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +++ b/.rovodev/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs @@ -1,7 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; -import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs'; +import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs'; import { checkSourceDesignSystem, collectStaticDesignSystemFindings, @@ -51,9 +51,7 @@ function checkStaticPageTypography(document, window) { for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) { const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0); if (!hasText) continue; - const ff = window.getComputedStyle(el).fontFamily || ''; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(window.getComputedStyle(el).fontFamily); if (!primary) continue; fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); diff --git a/.rovodev/skills/impeccable/scripts/detector/rules/checks.mjs b/.rovodev/skills/impeccable/scripts/detector/rules/checks.mjs index f15957b92..8bfb2b1df 100644 --- a/.rovodev/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.rovodev/skills/impeccable/scripts/detector/rules/checks.mjs @@ -9,6 +9,7 @@ import { WCAG_LARGE_BOLD_TEXT_PX, WCAG_LARGE_TEXT_PX, isBrandFontOnOwnDomain, + primaryFontFace, } from '../shared/constants.mjs'; import { CSS_NAMED_COLORS, @@ -331,7 +332,7 @@ function checkIconTile(opts) { function resolveSerif(fontFamily) { if (!fontFamily) return { primary: null, isSerif: false }; const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null; + const primary = primaryFontFace(fontFamily, GENERIC_FONTS); if (!primary) return { primary: null, isSerif: false }; if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true }; if (tokens.includes('serif')) return { primary, isSerif: true }; @@ -3930,8 +3931,7 @@ function checkTypography() { const style = getComputedStyle(el); const ff = style.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (!primary) continue; fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1); totalTextElements++; @@ -4176,8 +4176,7 @@ function checkPageTypography(doc, win) { if (rule.type !== 1) continue; const ff = rule.style?.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (primary) { fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); @@ -4196,11 +4195,10 @@ function checkPageTypography(doc, win) { const ffRe = /font-family\s*:\s*([^;}]+)/gi; let fm; while ((fm = ffRe.exec(html)) !== null) { - for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) { - if (f && !GENERIC_FONTS.has(f)) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + const primary = primaryFontFace(fm[1]); + if (primary) { + fonts.add(primary); + if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); } } diff --git a/.rovodev/skills/impeccable/scripts/detector/shared/constants.mjs b/.rovodev/skills/impeccable/scripts/detector/shared/constants.mjs index b9152939a..ea9bb361a 100644 --- a/.rovodev/skills/impeccable/scripts/detector/shared/constants.mjs +++ b/.rovodev/skills/impeccable/scripts/detector/shared/constants.mjs @@ -56,13 +56,27 @@ function isBrandFontOnOwnDomain(font) { return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix)); } -const GENERIC_FONTS = new Set([ +// Overused-font primary selection skips only CSS generics so a system stack +// keeps the system face as primary; GENERIC_FONTS still includes platform +// faces for design-system/serif resolution. +const CSS_GENERIC_FONTS = new Set([ 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', - 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', - '-apple-system', 'blinkmacsystemfont', 'segoe ui', 'inherit', 'initial', 'unset', 'revert', ]); +const GENERIC_FONTS = new Set([ + ...CSS_GENERIC_FONTS, + 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', + '-apple-system', 'blinkmacsystemfont', 'segoe ui', +]); + +function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) { + return String(fontFamily || '') + .split(',') + .map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()) + .find(f => f && !skip.has(f)) || null; +} + // WCAG large text thresholds are defined in points: 18pt normal text and // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch. const WCAG_LARGE_TEXT_PX = 18 * (96 / 72); @@ -104,6 +118,7 @@ export { BRAND_FONT_DOMAINS, isBrandFontOnOwnDomain, GENERIC_FONTS, + primaryFontFace, WCAG_LARGE_TEXT_PX, WCAG_LARGE_BOLD_TEXT_PX, EM_DASH_FLOOR, diff --git a/.trae-cn/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.trae-cn/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 62f5b87f1..3f5abd0a8 100644 --- a/.trae-cn/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.trae-cn/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -70,13 +70,27 @@ function isBrandFontOnOwnDomain(font) { return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix)); } -const GENERIC_FONTS = new Set([ +// Overused-font primary selection skips only CSS generics so a system stack +// keeps the system face as primary; GENERIC_FONTS still includes platform +// faces for design-system/serif resolution. +const CSS_GENERIC_FONTS = new Set([ 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', - 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', - '-apple-system', 'blinkmacsystemfont', 'segoe ui', 'inherit', 'initial', 'unset', 'revert', ]); +const GENERIC_FONTS = new Set([ + ...CSS_GENERIC_FONTS, + 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', + '-apple-system', 'blinkmacsystemfont', 'segoe ui', +]); + +function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) { + return String(fontFamily || '') + .split(',') + .map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()) + .find(f => f && !skip.has(f)) || null; +} + // WCAG large text thresholds are defined in points: 18pt normal text and // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch. const WCAG_LARGE_TEXT_PX = 18 * (96 / 72); @@ -1591,7 +1605,7 @@ function checkIconTile(opts) { function resolveSerif(fontFamily) { if (!fontFamily) return { primary: null, isSerif: false }; const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null; + const primary = primaryFontFace(fontFamily, GENERIC_FONTS); if (!primary) return { primary: null, isSerif: false }; if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true }; if (tokens.includes('serif')) return { primary, isSerif: true }; @@ -5190,8 +5204,7 @@ function checkTypography() { const style = getComputedStyle(el); const ff = style.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (!primary) continue; fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1); totalTextElements++; @@ -5436,8 +5449,7 @@ function checkPageTypography(doc, win) { if (rule.type !== 1) continue; const ff = rule.style?.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (primary) { fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); @@ -5456,11 +5468,10 @@ function checkPageTypography(doc, win) { const ffRe = /font-family\s*:\s*([^;}]+)/gi; let fm; while ((fm = ffRe.exec(html)) !== null) { - for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) { - if (f && !GENERIC_FONTS.has(f)) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + const primary = primaryFontFace(fm[1]); + if (primary) { + fonts.add(primary); + if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); } } diff --git a/.trae-cn/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs b/.trae-cn/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs index b4efa84ac..51c34224b 100644 --- a/.trae-cn/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +++ b/.trae-cn/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs @@ -1,7 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; -import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs'; +import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs'; import { checkSourceDesignSystem, collectStaticDesignSystemFindings, @@ -51,9 +51,7 @@ function checkStaticPageTypography(document, window) { for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) { const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0); if (!hasText) continue; - const ff = window.getComputedStyle(el).fontFamily || ''; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(window.getComputedStyle(el).fontFamily); if (!primary) continue; fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); diff --git a/.trae-cn/skills/impeccable/scripts/detector/rules/checks.mjs b/.trae-cn/skills/impeccable/scripts/detector/rules/checks.mjs index f15957b92..8bfb2b1df 100644 --- a/.trae-cn/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.trae-cn/skills/impeccable/scripts/detector/rules/checks.mjs @@ -9,6 +9,7 @@ import { WCAG_LARGE_BOLD_TEXT_PX, WCAG_LARGE_TEXT_PX, isBrandFontOnOwnDomain, + primaryFontFace, } from '../shared/constants.mjs'; import { CSS_NAMED_COLORS, @@ -331,7 +332,7 @@ function checkIconTile(opts) { function resolveSerif(fontFamily) { if (!fontFamily) return { primary: null, isSerif: false }; const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null; + const primary = primaryFontFace(fontFamily, GENERIC_FONTS); if (!primary) return { primary: null, isSerif: false }; if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true }; if (tokens.includes('serif')) return { primary, isSerif: true }; @@ -3930,8 +3931,7 @@ function checkTypography() { const style = getComputedStyle(el); const ff = style.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (!primary) continue; fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1); totalTextElements++; @@ -4176,8 +4176,7 @@ function checkPageTypography(doc, win) { if (rule.type !== 1) continue; const ff = rule.style?.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (primary) { fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); @@ -4196,11 +4195,10 @@ function checkPageTypography(doc, win) { const ffRe = /font-family\s*:\s*([^;}]+)/gi; let fm; while ((fm = ffRe.exec(html)) !== null) { - for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) { - if (f && !GENERIC_FONTS.has(f)) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + const primary = primaryFontFace(fm[1]); + if (primary) { + fonts.add(primary); + if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); } } diff --git a/.trae-cn/skills/impeccable/scripts/detector/shared/constants.mjs b/.trae-cn/skills/impeccable/scripts/detector/shared/constants.mjs index b9152939a..ea9bb361a 100644 --- a/.trae-cn/skills/impeccable/scripts/detector/shared/constants.mjs +++ b/.trae-cn/skills/impeccable/scripts/detector/shared/constants.mjs @@ -56,13 +56,27 @@ function isBrandFontOnOwnDomain(font) { return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix)); } -const GENERIC_FONTS = new Set([ +// Overused-font primary selection skips only CSS generics so a system stack +// keeps the system face as primary; GENERIC_FONTS still includes platform +// faces for design-system/serif resolution. +const CSS_GENERIC_FONTS = new Set([ 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', - 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', - '-apple-system', 'blinkmacsystemfont', 'segoe ui', 'inherit', 'initial', 'unset', 'revert', ]); +const GENERIC_FONTS = new Set([ + ...CSS_GENERIC_FONTS, + 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', + '-apple-system', 'blinkmacsystemfont', 'segoe ui', +]); + +function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) { + return String(fontFamily || '') + .split(',') + .map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()) + .find(f => f && !skip.has(f)) || null; +} + // WCAG large text thresholds are defined in points: 18pt normal text and // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch. const WCAG_LARGE_TEXT_PX = 18 * (96 / 72); @@ -104,6 +118,7 @@ export { BRAND_FONT_DOMAINS, isBrandFontOnOwnDomain, GENERIC_FONTS, + primaryFontFace, WCAG_LARGE_TEXT_PX, WCAG_LARGE_BOLD_TEXT_PX, EM_DASH_FLOOR, diff --git a/.trae/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.trae/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 62f5b87f1..3f5abd0a8 100644 --- a/.trae/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.trae/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -70,13 +70,27 @@ function isBrandFontOnOwnDomain(font) { return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix)); } -const GENERIC_FONTS = new Set([ +// Overused-font primary selection skips only CSS generics so a system stack +// keeps the system face as primary; GENERIC_FONTS still includes platform +// faces for design-system/serif resolution. +const CSS_GENERIC_FONTS = new Set([ 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', - 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', - '-apple-system', 'blinkmacsystemfont', 'segoe ui', 'inherit', 'initial', 'unset', 'revert', ]); +const GENERIC_FONTS = new Set([ + ...CSS_GENERIC_FONTS, + 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', + '-apple-system', 'blinkmacsystemfont', 'segoe ui', +]); + +function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) { + return String(fontFamily || '') + .split(',') + .map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()) + .find(f => f && !skip.has(f)) || null; +} + // WCAG large text thresholds are defined in points: 18pt normal text and // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch. const WCAG_LARGE_TEXT_PX = 18 * (96 / 72); @@ -1591,7 +1605,7 @@ function checkIconTile(opts) { function resolveSerif(fontFamily) { if (!fontFamily) return { primary: null, isSerif: false }; const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null; + const primary = primaryFontFace(fontFamily, GENERIC_FONTS); if (!primary) return { primary: null, isSerif: false }; if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true }; if (tokens.includes('serif')) return { primary, isSerif: true }; @@ -5190,8 +5204,7 @@ function checkTypography() { const style = getComputedStyle(el); const ff = style.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (!primary) continue; fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1); totalTextElements++; @@ -5436,8 +5449,7 @@ function checkPageTypography(doc, win) { if (rule.type !== 1) continue; const ff = rule.style?.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (primary) { fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); @@ -5456,11 +5468,10 @@ function checkPageTypography(doc, win) { const ffRe = /font-family\s*:\s*([^;}]+)/gi; let fm; while ((fm = ffRe.exec(html)) !== null) { - for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) { - if (f && !GENERIC_FONTS.has(f)) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + const primary = primaryFontFace(fm[1]); + if (primary) { + fonts.add(primary); + if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); } } diff --git a/.trae/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs b/.trae/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs index b4efa84ac..51c34224b 100644 --- a/.trae/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +++ b/.trae/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs @@ -1,7 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; -import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs'; +import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs'; import { checkSourceDesignSystem, collectStaticDesignSystemFindings, @@ -51,9 +51,7 @@ function checkStaticPageTypography(document, window) { for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) { const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0); if (!hasText) continue; - const ff = window.getComputedStyle(el).fontFamily || ''; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(window.getComputedStyle(el).fontFamily); if (!primary) continue; fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); diff --git a/.trae/skills/impeccable/scripts/detector/rules/checks.mjs b/.trae/skills/impeccable/scripts/detector/rules/checks.mjs index f15957b92..8bfb2b1df 100644 --- a/.trae/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.trae/skills/impeccable/scripts/detector/rules/checks.mjs @@ -9,6 +9,7 @@ import { WCAG_LARGE_BOLD_TEXT_PX, WCAG_LARGE_TEXT_PX, isBrandFontOnOwnDomain, + primaryFontFace, } from '../shared/constants.mjs'; import { CSS_NAMED_COLORS, @@ -331,7 +332,7 @@ function checkIconTile(opts) { function resolveSerif(fontFamily) { if (!fontFamily) return { primary: null, isSerif: false }; const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null; + const primary = primaryFontFace(fontFamily, GENERIC_FONTS); if (!primary) return { primary: null, isSerif: false }; if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true }; if (tokens.includes('serif')) return { primary, isSerif: true }; @@ -3930,8 +3931,7 @@ function checkTypography() { const style = getComputedStyle(el); const ff = style.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (!primary) continue; fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1); totalTextElements++; @@ -4176,8 +4176,7 @@ function checkPageTypography(doc, win) { if (rule.type !== 1) continue; const ff = rule.style?.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (primary) { fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); @@ -4196,11 +4195,10 @@ function checkPageTypography(doc, win) { const ffRe = /font-family\s*:\s*([^;}]+)/gi; let fm; while ((fm = ffRe.exec(html)) !== null) { - for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) { - if (f && !GENERIC_FONTS.has(f)) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + const primary = primaryFontFace(fm[1]); + if (primary) { + fonts.add(primary); + if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); } } diff --git a/.trae/skills/impeccable/scripts/detector/shared/constants.mjs b/.trae/skills/impeccable/scripts/detector/shared/constants.mjs index b9152939a..ea9bb361a 100644 --- a/.trae/skills/impeccable/scripts/detector/shared/constants.mjs +++ b/.trae/skills/impeccable/scripts/detector/shared/constants.mjs @@ -56,13 +56,27 @@ function isBrandFontOnOwnDomain(font) { return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix)); } -const GENERIC_FONTS = new Set([ +// Overused-font primary selection skips only CSS generics so a system stack +// keeps the system face as primary; GENERIC_FONTS still includes platform +// faces for design-system/serif resolution. +const CSS_GENERIC_FONTS = new Set([ 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', - 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', - '-apple-system', 'blinkmacsystemfont', 'segoe ui', 'inherit', 'initial', 'unset', 'revert', ]); +const GENERIC_FONTS = new Set([ + ...CSS_GENERIC_FONTS, + 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', + '-apple-system', 'blinkmacsystemfont', 'segoe ui', +]); + +function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) { + return String(fontFamily || '') + .split(',') + .map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()) + .find(f => f && !skip.has(f)) || null; +} + // WCAG large text thresholds are defined in points: 18pt normal text and // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch. const WCAG_LARGE_TEXT_PX = 18 * (96 / 72); @@ -104,6 +118,7 @@ export { BRAND_FONT_DOMAINS, isBrandFontOnOwnDomain, GENERIC_FONTS, + primaryFontFace, WCAG_LARGE_TEXT_PX, WCAG_LARGE_BOLD_TEXT_PX, EM_DASH_FLOOR, diff --git a/.vibe/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.vibe/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 62f5b87f1..3f5abd0a8 100644 --- a/.vibe/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.vibe/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -70,13 +70,27 @@ function isBrandFontOnOwnDomain(font) { return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix)); } -const GENERIC_FONTS = new Set([ +// Overused-font primary selection skips only CSS generics so a system stack +// keeps the system face as primary; GENERIC_FONTS still includes platform +// faces for design-system/serif resolution. +const CSS_GENERIC_FONTS = new Set([ 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', - 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', - '-apple-system', 'blinkmacsystemfont', 'segoe ui', 'inherit', 'initial', 'unset', 'revert', ]); +const GENERIC_FONTS = new Set([ + ...CSS_GENERIC_FONTS, + 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', + '-apple-system', 'blinkmacsystemfont', 'segoe ui', +]); + +function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) { + return String(fontFamily || '') + .split(',') + .map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()) + .find(f => f && !skip.has(f)) || null; +} + // WCAG large text thresholds are defined in points: 18pt normal text and // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch. const WCAG_LARGE_TEXT_PX = 18 * (96 / 72); @@ -1591,7 +1605,7 @@ function checkIconTile(opts) { function resolveSerif(fontFamily) { if (!fontFamily) return { primary: null, isSerif: false }; const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null; + const primary = primaryFontFace(fontFamily, GENERIC_FONTS); if (!primary) return { primary: null, isSerif: false }; if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true }; if (tokens.includes('serif')) return { primary, isSerif: true }; @@ -5190,8 +5204,7 @@ function checkTypography() { const style = getComputedStyle(el); const ff = style.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (!primary) continue; fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1); totalTextElements++; @@ -5436,8 +5449,7 @@ function checkPageTypography(doc, win) { if (rule.type !== 1) continue; const ff = rule.style?.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (primary) { fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); @@ -5456,11 +5468,10 @@ function checkPageTypography(doc, win) { const ffRe = /font-family\s*:\s*([^;}]+)/gi; let fm; while ((fm = ffRe.exec(html)) !== null) { - for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) { - if (f && !GENERIC_FONTS.has(f)) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + const primary = primaryFontFace(fm[1]); + if (primary) { + fonts.add(primary); + if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); } } diff --git a/.vibe/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs b/.vibe/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs index b4efa84ac..51c34224b 100644 --- a/.vibe/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +++ b/.vibe/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs @@ -1,7 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; -import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs'; +import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs'; import { checkSourceDesignSystem, collectStaticDesignSystemFindings, @@ -51,9 +51,7 @@ function checkStaticPageTypography(document, window) { for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) { const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0); if (!hasText) continue; - const ff = window.getComputedStyle(el).fontFamily || ''; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(window.getComputedStyle(el).fontFamily); if (!primary) continue; fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); diff --git a/.vibe/skills/impeccable/scripts/detector/rules/checks.mjs b/.vibe/skills/impeccable/scripts/detector/rules/checks.mjs index f15957b92..8bfb2b1df 100644 --- a/.vibe/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/.vibe/skills/impeccable/scripts/detector/rules/checks.mjs @@ -9,6 +9,7 @@ import { WCAG_LARGE_BOLD_TEXT_PX, WCAG_LARGE_TEXT_PX, isBrandFontOnOwnDomain, + primaryFontFace, } from '../shared/constants.mjs'; import { CSS_NAMED_COLORS, @@ -331,7 +332,7 @@ function checkIconTile(opts) { function resolveSerif(fontFamily) { if (!fontFamily) return { primary: null, isSerif: false }; const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null; + const primary = primaryFontFace(fontFamily, GENERIC_FONTS); if (!primary) return { primary: null, isSerif: false }; if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true }; if (tokens.includes('serif')) return { primary, isSerif: true }; @@ -3930,8 +3931,7 @@ function checkTypography() { const style = getComputedStyle(el); const ff = style.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (!primary) continue; fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1); totalTextElements++; @@ -4176,8 +4176,7 @@ function checkPageTypography(doc, win) { if (rule.type !== 1) continue; const ff = rule.style?.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (primary) { fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); @@ -4196,11 +4195,10 @@ function checkPageTypography(doc, win) { const ffRe = /font-family\s*:\s*([^;}]+)/gi; let fm; while ((fm = ffRe.exec(html)) !== null) { - for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) { - if (f && !GENERIC_FONTS.has(f)) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + const primary = primaryFontFace(fm[1]); + if (primary) { + fonts.add(primary); + if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); } } diff --git a/.vibe/skills/impeccable/scripts/detector/shared/constants.mjs b/.vibe/skills/impeccable/scripts/detector/shared/constants.mjs index b9152939a..ea9bb361a 100644 --- a/.vibe/skills/impeccable/scripts/detector/shared/constants.mjs +++ b/.vibe/skills/impeccable/scripts/detector/shared/constants.mjs @@ -56,13 +56,27 @@ function isBrandFontOnOwnDomain(font) { return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix)); } -const GENERIC_FONTS = new Set([ +// Overused-font primary selection skips only CSS generics so a system stack +// keeps the system face as primary; GENERIC_FONTS still includes platform +// faces for design-system/serif resolution. +const CSS_GENERIC_FONTS = new Set([ 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', - 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', - '-apple-system', 'blinkmacsystemfont', 'segoe ui', 'inherit', 'initial', 'unset', 'revert', ]); +const GENERIC_FONTS = new Set([ + ...CSS_GENERIC_FONTS, + 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', + '-apple-system', 'blinkmacsystemfont', 'segoe ui', +]); + +function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) { + return String(fontFamily || '') + .split(',') + .map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()) + .find(f => f && !skip.has(f)) || null; +} + // WCAG large text thresholds are defined in points: 18pt normal text and // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch. const WCAG_LARGE_TEXT_PX = 18 * (96 / 72); @@ -104,6 +118,7 @@ export { BRAND_FONT_DOMAINS, isBrandFontOnOwnDomain, GENERIC_FONTS, + primaryFontFace, WCAG_LARGE_TEXT_PX, WCAG_LARGE_BOLD_TEXT_PX, EM_DASH_FLOOR, diff --git a/plugin/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/plugin/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 62f5b87f1..3f5abd0a8 100644 --- a/plugin/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/plugin/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -70,13 +70,27 @@ function isBrandFontOnOwnDomain(font) { return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix)); } -const GENERIC_FONTS = new Set([ +// Overused-font primary selection skips only CSS generics so a system stack +// keeps the system face as primary; GENERIC_FONTS still includes platform +// faces for design-system/serif resolution. +const CSS_GENERIC_FONTS = new Set([ 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', - 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', - '-apple-system', 'blinkmacsystemfont', 'segoe ui', 'inherit', 'initial', 'unset', 'revert', ]); +const GENERIC_FONTS = new Set([ + ...CSS_GENERIC_FONTS, + 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', + '-apple-system', 'blinkmacsystemfont', 'segoe ui', +]); + +function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) { + return String(fontFamily || '') + .split(',') + .map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()) + .find(f => f && !skip.has(f)) || null; +} + // WCAG large text thresholds are defined in points: 18pt normal text and // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch. const WCAG_LARGE_TEXT_PX = 18 * (96 / 72); @@ -1591,7 +1605,7 @@ function checkIconTile(opts) { function resolveSerif(fontFamily) { if (!fontFamily) return { primary: null, isSerif: false }; const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null; + const primary = primaryFontFace(fontFamily, GENERIC_FONTS); if (!primary) return { primary: null, isSerif: false }; if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true }; if (tokens.includes('serif')) return { primary, isSerif: true }; @@ -5190,8 +5204,7 @@ function checkTypography() { const style = getComputedStyle(el); const ff = style.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (!primary) continue; fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1); totalTextElements++; @@ -5436,8 +5449,7 @@ function checkPageTypography(doc, win) { if (rule.type !== 1) continue; const ff = rule.style?.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (primary) { fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); @@ -5456,11 +5468,10 @@ function checkPageTypography(doc, win) { const ffRe = /font-family\s*:\s*([^;}]+)/gi; let fm; while ((fm = ffRe.exec(html)) !== null) { - for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) { - if (f && !GENERIC_FONTS.has(f)) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + const primary = primaryFontFace(fm[1]); + if (primary) { + fonts.add(primary); + if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); } } diff --git a/plugin/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs b/plugin/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs index b4efa84ac..51c34224b 100644 --- a/plugin/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +++ b/plugin/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs @@ -1,7 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; -import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs'; +import { OVERUSED_FONTS, primaryFontFace } from '../../shared/constants.mjs'; import { checkSourceDesignSystem, collectStaticDesignSystemFindings, @@ -51,9 +51,7 @@ function checkStaticPageTypography(document, window) { for (const el of document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, dd, blockquote, figcaption, a, button, label, span, div')) { const hasText = el.childNodes.some(n => n.nodeType === 3 && n.textContent.trim().length > 0); if (!hasText) continue; - const ff = window.getComputedStyle(el).fontFamily || ''; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(window.getComputedStyle(el).fontFamily); if (!primary) continue; fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); diff --git a/plugin/skills/impeccable/scripts/detector/rules/checks.mjs b/plugin/skills/impeccable/scripts/detector/rules/checks.mjs index f15957b92..8bfb2b1df 100644 --- a/plugin/skills/impeccable/scripts/detector/rules/checks.mjs +++ b/plugin/skills/impeccable/scripts/detector/rules/checks.mjs @@ -9,6 +9,7 @@ import { WCAG_LARGE_BOLD_TEXT_PX, WCAG_LARGE_TEXT_PX, isBrandFontOnOwnDomain, + primaryFontFace, } from '../shared/constants.mjs'; import { CSS_NAMED_COLORS, @@ -331,7 +332,7 @@ function checkIconTile(opts) { function resolveSerif(fontFamily) { if (!fontFamily) return { primary: null, isSerif: false }; const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null; + const primary = primaryFontFace(fontFamily, GENERIC_FONTS); if (!primary) return { primary: null, isSerif: false }; if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true }; if (tokens.includes('serif')) return { primary, isSerif: true }; @@ -3930,8 +3931,7 @@ function checkTypography() { const style = getComputedStyle(el); const ff = style.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (!primary) continue; fontUsage.set(primary, (fontUsage.get(primary) || 0) + 1); totalTextElements++; @@ -4176,8 +4176,7 @@ function checkPageTypography(doc, win) { if (rule.type !== 1) continue; const ff = rule.style?.fontFamily; if (!ff) continue; - const stack = ff.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()); - const primary = stack.find(f => f && !GENERIC_FONTS.has(f)); + const primary = primaryFontFace(ff); if (primary) { fonts.add(primary); if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); @@ -4196,11 +4195,10 @@ function checkPageTypography(doc, win) { const ffRe = /font-family\s*:\s*([^;}]+)/gi; let fm; while ((fm = ffRe.exec(html)) !== null) { - for (const f of fm[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) { - if (f && !GENERIC_FONTS.has(f)) { - fonts.add(f); - if (OVERUSED_FONTS.has(f)) overusedFound.add(f); - } + const primary = primaryFontFace(fm[1]); + if (primary) { + fonts.add(primary); + if (OVERUSED_FONTS.has(primary)) overusedFound.add(primary); } } diff --git a/plugin/skills/impeccable/scripts/detector/shared/constants.mjs b/plugin/skills/impeccable/scripts/detector/shared/constants.mjs index b9152939a..ea9bb361a 100644 --- a/plugin/skills/impeccable/scripts/detector/shared/constants.mjs +++ b/plugin/skills/impeccable/scripts/detector/shared/constants.mjs @@ -56,13 +56,27 @@ function isBrandFontOnOwnDomain(font) { return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix)); } -const GENERIC_FONTS = new Set([ +// Overused-font primary selection skips only CSS generics so a system stack +// keeps the system face as primary; GENERIC_FONTS still includes platform +// faces for design-system/serif resolution. +const CSS_GENERIC_FONTS = new Set([ 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', - 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', - '-apple-system', 'blinkmacsystemfont', 'segoe ui', 'inherit', 'initial', 'unset', 'revert', ]); +const GENERIC_FONTS = new Set([ + ...CSS_GENERIC_FONTS, + 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', + '-apple-system', 'blinkmacsystemfont', 'segoe ui', +]); + +function primaryFontFace(fontFamily, skip = CSS_GENERIC_FONTS) { + return String(fontFamily || '') + .split(',') + .map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase()) + .find(f => f && !skip.has(f)) || null; +} + // WCAG large text thresholds are defined in points: 18pt normal text and // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch. const WCAG_LARGE_TEXT_PX = 18 * (96 / 72); @@ -104,6 +118,7 @@ export { BRAND_FONT_DOMAINS, isBrandFontOnOwnDomain, GENERIC_FONTS, + primaryFontFace, WCAG_LARGE_TEXT_PX, WCAG_LARGE_BOLD_TEXT_PX, EM_DASH_FLOOR,