import { BORDER_SAFE_TAGS, GENERIC_FONTS, KNOWN_SERIF_FONTS, OVERUSED_FONTS, SAFE_TAGS, WCAG_LARGE_BOLD_TEXT_PX, WCAG_LARGE_TEXT_PX, isBrandFontOnOwnDomain, } from '../shared/constants.mjs'; import { colorToHex, contrastRatio, getHue, hasChroma, isNeutralColor, parseGradientColors, parseRgb, relativeLuminance, } from '../shared/color.mjs'; import { extractGoogleFontFamilies } from '../shared/fonts.mjs'; const DETECTOR_IS_BROWSER = typeof window !== 'undefined'; // ─── Section 3: Pure Detection ────────────────────────────────────────────── function checkBorders(tag, widths, colors, radius, opts = {}) { // Badge-shaped s (own visible background) are a real stripe target // for the top/bottom variant — the inline-tag exemption exists to quiet // text-level borders, not chips. They skip the left/right arms below. const spanBadge = tag === 'span' && !!opts.badgeLike; if (BORDER_SAFE_TAGS.has(tag) && !spanBadge) return []; const findings = []; const sides = ['Top', 'Right', 'Bottom', 'Left']; for (const side of sides) { const w = widths[side]; if (w < 1 || isNeutralColor(colors[side])) continue; const otherSides = sides.filter(s => s !== side); const maxOther = Math.max(...otherSides.map(s => widths[s])); if (!(w >= 2 && (maxOther <= 1 || w >= maxOther * 2))) continue; const sn = side.toLowerCase(); const isSide = side === 'Left' || side === 'Right'; if (isSide) { if (spanBadge) continue; if (radius > 0) findings.push({ id: 'side-tab', snippet: `border-${sn}: ${w}px + border-radius: ${radius}px` }); else if (w >= 3) findings.push({ id: 'side-tab', snippet: `border-${sn}: ${w}px` }); } else { if (radius > 0 && w >= 2) findings.push({ id: 'border-accent-on-rounded', snippet: `border-${sn}: ${w}px + border-radius: ${radius}px` }); // Horizontal variant of the side-tab stripe: a thick chromatic accent // riding the top or bottom edge of a card/badge/container. Same // dominant-edge + chroma gates as left/right, 3-12px band. Selected- // tab underlines are exempt via opts.tabContext (adapters look for // tablist/nav/tab ancestors and aria-selected); links, buttons, // table cells, and
never reach here (BORDER_SAFE_TAGS). else if (!opts.tabContext && w >= 3 && w <= 12) { findings.push({ id: 'side-tab', snippet: `border-${sn}: ${w}px` }); } } } return findings; } // Returns true if the given text is composed entirely of emoji characters // (plus whitespace / variation selectors). Emojis render as multicolor glyphs // regardless of CSS `color`, so contrast checks against the element's text // color are meaningless for these nodes. const EMOJI_CHAR_RE = /[\u{1F1E6}-\u{1F1FF}\u{1F300}-\u{1F9FF}\u{1FA00}-\u{1FAFF}\u{2600}-\u{27BF}\u{2300}-\u{23FF}\u{FE0F}\u{200D}\u{1F3FB}-\u{1F3FF}]/u; const EMOJI_CHARS_GLOBAL = /[\u{1F1E6}-\u{1F1FF}\u{1F300}-\u{1F9FF}\u{1FA00}-\u{1FAFF}\u{2600}-\u{27BF}\u{2300}-\u{23FF}\u{FE0F}\u{200D}\u{1F3FB}-\u{1F3FF}]/gu; function isEmojiOnlyText(text) { if (!text) return false; if (!EMOJI_CHAR_RE.test(text)) return false; return text.replace(EMOJI_CHARS_GLOBAL, '').trim() === ''; } function checkColors(opts) { const { tag, textColor, bgColor, effectiveBg, effectiveBgStops, fontSize, fontWeight, hasDirectText, isEmojiOnly, bgClip, bgImage, classList } = opts; if (SAFE_TAGS.has(tag)) { // Exception for and