| 1 | // ─── Section 1: Constants ─────────────────────────────────────────────────── |
| 2 | |
| 3 | const SAFE_TAGS = new Set([ |
| 4 | 'blockquote', 'nav', 'a', 'input', 'textarea', 'select', |
| 5 | 'pre', 'code', 'span', 'th', 'td', 'tr', 'li', 'label', |
| 6 | 'button', 'hr', 'html', 'head', 'body', 'script', 'style', |
| 7 | 'link', 'meta', 'title', 'br', 'img', 'svg', 'path', 'circle', |
| 8 | 'rect', 'line', 'polyline', 'polygon', 'g', 'defs', 'use', |
| 9 | ]); |
| 10 | |
| 11 | // Per-check safe-tags override for the border (side-tab / border-accent) |
| 12 | // rule. We intentionally re-allow <label> here because card-shaped clickable |
| 13 | // labels (e.g. .checklist-item wrapping a checkbox + content) are one of the |
| 14 | // canonical side-tab anti-pattern shapes and must be detected. The rule's |
| 15 | // other preconditions (non-neutral color, width >= 2px on a single side, |
| 16 | // radius > 0 or width >= 3, element size >= 20x20 in the browser path) |
| 17 | // already filter out plain inline form labels so this does not introduce |
| 18 | // false positives. See modern-color-borders.html for the test matrix. |
| 19 | const BORDER_SAFE_TAGS = new Set( |
| 20 | [...SAFE_TAGS].filter(t => t !== 'label') |
| 21 | ); |
| 22 | |
| 23 | const OVERUSED_FONTS = new Set([ |
| 24 | // Older monoculture (still ubiquitous): |
| 25 | 'inter', 'roboto', 'open sans', 'lato', 'montserrat', 'arial', 'helvetica', |
| 26 | // Newer monoculture (the Anthropic-skill / Vercel / GitHub default wave): |
| 27 | 'fraunces', 'instrument sans', 'instrument serif', |
| 28 | 'geist', 'geist sans', 'geist mono', |
| 29 | 'mona sans', |
| 30 | 'plus jakarta sans', 'space grotesk', 'recoleta', |
| 31 | ]); |
| 32 | |
| 33 | // Brand-associated fonts: don't flag these as "overused" on the brand's own domains. |
| 34 | // Keys are font names, values are arrays of hostname suffixes where the font is allowed. |
| 35 | const GOOGLE_DOMAINS = [ |
| 36 | 'google.com', 'youtube.com', 'android.com', 'chromium.org', |
| 37 | 'chrome.com', 'web.dev', 'gstatic.com', 'firebase.google.com', |
| 38 | ]; |
| 39 | const VERCEL_DOMAINS = ['vercel.com', 'nextjs.org', 'v0.app']; |
| 40 | const GITHUB_DOMAINS = ['github.com', 'githubnext.com']; |
| 41 | const BRAND_FONT_DOMAINS = { |
| 42 | 'roboto': GOOGLE_DOMAINS, |
| 43 | 'google sans': GOOGLE_DOMAINS, |
| 44 | 'product sans': GOOGLE_DOMAINS, |
| 45 | 'geist': VERCEL_DOMAINS, |
| 46 | 'geist sans': VERCEL_DOMAINS, |
| 47 | 'geist mono': VERCEL_DOMAINS, |
| 48 | 'mona sans': GITHUB_DOMAINS, |
| 49 | }; |
| 50 | |
| 51 | function isBrandFontOnOwnDomain(font) { |
| 52 | if (typeof location === 'undefined') return false; |
| 53 | const allowed = BRAND_FONT_DOMAINS[font]; |
| 54 | if (!allowed) return false; |
| 55 | const host = location.hostname.toLowerCase(); |
| 56 | return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix)); |
| 57 | } |
| 58 | |
| 59 | const GENERIC_FONTS = new Set([ |
| 60 | 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', |
| 61 | 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', |
| 62 | '-apple-system', 'blinkmacsystemfont', 'segoe ui', |
| 63 | 'inherit', 'initial', 'unset', 'revert', |
| 64 | ]); |
| 65 | |
| 66 | // WCAG large text thresholds are defined in points: 18pt normal text and |
| 67 | // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch. |
| 68 | const WCAG_LARGE_TEXT_PX = 18 * (96 / 72); |
| 69 | const WCAG_LARGE_BOLD_TEXT_PX = 14 * (96 / 72); |
| 70 | |
| 71 | // Serif faces that show up in italic-display heroes. The rule also fires when |
| 72 | // the primary face is unknown but the stack ends in the generic `serif` token, |
| 73 | // which catches custom/private faces with a serif fallback. |
| 74 | const KNOWN_SERIF_FONTS = new Set([ |
| 75 | 'fraunces', 'recoleta', 'newsreader', 'playfair display', 'playfair', |
| 76 | 'cormorant', 'cormorant garamond', 'garamond', 'eb garamond', |
| 77 | 'tiempos', 'tiempos headline', 'tiempos text', |
| 78 | 'lora', 'vollkorn', 'spectral', |
| 79 | 'source serif pro', 'source serif 4', 'source serif', |
| 80 | 'ibm plex serif', 'merriweather', |
| 81 | 'libre caslon', 'libre baskerville', 'baskerville', |
| 82 | 'georgia', 'times new roman', 'times', |
| 83 | 'dm serif display', 'dm serif text', |
| 84 | 'instrument serif', 'gt sectra', 'ogg', 'canela', |
| 85 | 'freight display', 'freight text', |
| 86 | ]); |
| 87 | |
| 88 | export { |
| 89 | SAFE_TAGS, |
| 90 | BORDER_SAFE_TAGS, |
| 91 | OVERUSED_FONTS, |
| 92 | GOOGLE_DOMAINS, |
| 93 | VERCEL_DOMAINS, |
| 94 | GITHUB_DOMAINS, |
| 95 | BRAND_FONT_DOMAINS, |
| 96 | isBrandFontOnOwnDomain, |
| 97 | GENERIC_FONTS, |
| 98 | WCAG_LARGE_TEXT_PX, |
| 99 | WCAG_LARGE_BOLD_TEXT_PX, |
| 100 | KNOWN_SERIF_FONTS, |
| 101 | }; |
| 102 |