/** * Extract computed design styles from key DOM elements. * * Targets ~50 elements (headings, body text, buttons, cards, nav) and extracts * only design-relevant CSS properties. Output is a compact, pre-clustered * design system summary — not raw computed styles per element. * * All page.evaluate() calls use string expressions to avoid * tsx/esbuild __name injection (see esbuild issue #1031). */ import type { Page } from "puppeteer-core"; import type { DesignStyles } from "./types.js"; const EXTRACT_DESIGN_STYLES_SCRIPT = `(() => { var isVisible = (el) => { var s = getComputedStyle(el); return s.display !== "none" && s.visibility !== "hidden" && s.opacity !== "0" && el.getBoundingClientRect().height > 0; }; function rgbToHex(color) { if (!color) return ""; if (color.startsWith('#')) return color.toUpperCase(); // capture optional alpha (group 4), allowing both comma and modern slash (rgb r g b / a) syntax. var m = color.match(/rgba?\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)(?:\\s*[,/]\\s*([\\d.]+))?/); if (!m) return color; // fully-transparent fill (rgba(...,0)) → sentinel, NOT #000000 — otherwise a transparent // chip/tab/stat ground reads as solid black on a light-ground site. if (m[4] !== undefined && parseFloat(m[4]) === 0) return "transparent"; return '#' + ((1<<24) + (parseInt(m[1])<<16) + (parseInt(m[2])<<8) + parseInt(m[3])).toString(16).slice(1).toUpperCase(); } function cleanFont(f) { return f.split(",")[0].replace(/['"]/g, "").trim(); } // keep only gradient background-images (drop url() sprites + "none"); gradients are a core // brand signal (Stripe/ElevenLabs/Snowflake mesh washes) that a flat background-color misses. function gradientOf(v) { return v && v.indexOf("gradient") >= 0 ? v.trim() : ""; } function getStyles(el) { var s = getComputedStyle(el); var bf = s.backdropFilter || s.webkitBackdropFilter || ""; return { fontFamily: cleanFont(s.fontFamily), fontSize: s.fontSize, fontWeight: s.fontWeight, lineHeight: s.lineHeight, letterSpacing: s.letterSpacing, color: rgbToHex(s.color), background: rgbToHex(s.backgroundColor), backgroundImage: gradientOf(s.backgroundImage), backdropFilter: bf === "none" ? "" : bf, padding: s.padding, borderRadius: s.borderRadius, border: s.border, boxShadow: s.boxShadow === "none" ? "none" : s.boxShadow, height: s.height }; } // ── 1. Typography hierarchy ── // Sample each text role and deduplicate by fontSize var typographyMap = {}; var roleSelectors = [ { role: "display", sel: "h1", max: 3 }, { role: "heading-2", sel: "h2", max: 5 }, { role: "heading-3", sel: "h3", max: 5 }, { role: "heading-4", sel: "h4", max: 3 }, { role: "body", sel: "p", max: 10 }, { role: "body-small", sel: "figcaption, .caption, [class*='caption'], [class*='subtitle'], small", max: 5 }, { role: "label", sel: "label, [class*='label'], [class*='tag'], [class*='badge']", max: 5 }, { role: "link", sel: "a:not([class*='btn']):not([class*='button']):not([role='button'])", max: 5 }, { role: "code", sel: "code, pre, [class*='mono']", max: 3 } ]; for (var ri = 0; ri < roleSelectors.length; ri++) { var spec = roleSelectors[ri]; var els = Array.from(document.querySelectorAll(spec.sel)).slice(0, spec.max); for (var ei = 0; ei < els.length; ei++) { if (!isVisible(els[ei])) continue; var s = getStyles(els[ei]); var key = s.fontSize + "|" + s.fontWeight + "|" + s.fontFamily; if (!typographyMap[key]) { var text = (els[ei].textContent || "").trim().replace(/\\s+/g, " ").slice(0, 60); typographyMap[key] = { role: spec.role, fontFamily: s.fontFamily, fontSize: s.fontSize, fontWeight: s.fontWeight, lineHeight: s.lineHeight, letterSpacing: s.letterSpacing, color: s.color, sampleText: text }; } } } // Sort by font size descending var typography = Object.values(typographyMap); typography.sort(function(a, b) { return parseFloat(b.fontSize) - parseFloat(a.fontSize); }); // Deduplicate roles — keep only the first (largest) for each role prefix var seenRoles = {}; var uniqueTypo = []; for (var ti = 0; ti < typography.length; ti++) { var baseRole = typography[ti].role.replace(/-\\d+$/, ""); if (!seenRoles[baseRole]) { seenRoles[baseRole] = true; typography[ti].role = baseRole; uniqueTypo.push(typography[ti]); } else if (baseRole === "heading") { // Keep multiple heading levels uniqueTypo.push(typography[ti]); } } // ── 2. Buttons ── // a page's primary CTA is very often a FILLED pill in the nav/header ("Sign up", "Start for free"). // Old code dropped everything under