export interface CssTokenAudit { declared: string[]; used: string[]; missing: string[]; unused: string[]; } /** Audit framework design-token declarations and var() references. */ export function auditWrnTokens(css: string): CssTokenAudit { const declared = new Set(); const used = new Set(); for (const match of css.matchAll(/(--wrn-[A-Za-z0-9_-]+)\s*:/g)) declared.add(match[1]!); for (const match of css.matchAll(/var\(\s*(--wrn-[A-Za-z0-9_-]+)/g)) used.add(match[1]!); return { declared: [...declared].sort(), used: [...used].sort(), missing: [...used].filter((token) => !declared.has(token)).sort(), unused: [...declared].filter((token) => !used.has(token)).sort(), }; } export interface StyleSource { path: string; reason?: string; } /** Normalize/dedupe Tailwind scan sources without allowing line injection. */ export function normalizeStyleSources(values: readonly (string | StyleSource)[]): StyleSource[] { const result = new Map(); for (const value of values) { const source = typeof value === "string" ? { path: value } : value; const path = source.path.trim(); if (!path || /[\r\n]/.test(path)) continue; result.set(path, { path, reason: source.reason }); } return [...result.values()]; } export function tailwindSourceDirectives(values: readonly (string | StyleSource)[]): string { return normalizeStyleSources(values) .map(({ path }) => `@source ${JSON.stringify(path)};`) .join("\n"); } export interface ContrastResult { ratio: number; level: "fail" | "aa-large" | "aa" | "aaa"; } function hexRgb(value: string): [number, number, number] | null { const hex = value.trim().replace(/^#/, ""); const normalized = hex.length === 3 ? [...hex].map((part) => part + part).join("") : hex; if (!/^[0-9a-f]{6}$/i.test(normalized)) return null; return [0, 2, 4].map((offset) => Number.parseInt(normalized.slice(offset, offset + 2), 16)) as [ number, number, number, ]; } function luminance(value: string): number | null { const rgb = hexRgb(value); if (!rgb) return null; const channels = rgb.map((channel) => { const scaled = channel / 255; return scaled <= 0.03928 ? scaled / 12.92 : ((scaled + 0.055) / 1.055) ** 2.4; }); return 0.2126 * channels[0]! + 0.7152 * channels[1]! + 0.0722 * channels[2]!; } export function contrast(foreground: string, background: string): ContrastResult | null { const left = luminance(foreground); const right = luminance(background); if (left === null || right === null) return null; const ratio = (Math.max(left, right) + 0.05) / (Math.min(left, right) + 0.05); return { ratio: Math.round(ratio * 100) / 100, level: ratio >= 7 ? "aaa" : ratio >= 4.5 ? "aa" : ratio >= 3 ? "aa-large" : "fail", }; } export interface CssPerformanceAuditIssue { code: string; severity: "error" | "warning" | "info"; message: string; line?: number; } function cssLineAt(source: string, offset: number): number { return source.slice(0, offset).split(/\r?\n/).length; } /** Detect CSS patterns that commonly increase style, paint, or compositing cost. */ export function auditCssPerformance(source: string): CssPerformanceAuditIssue[] { const issues: CssPerformanceAuditIssue[] = []; const checks: Array<{ pattern: RegExp; code: string; severity: CssPerformanceAuditIssue["severity"]; message: string; }> = [ { pattern: /transition\s*:\s*all\b/gi, code: "WRN-CSS-TRANSITION-ALL", severity: "warning", message: "Avoid transition: all; list only properties that should animate.", }, { pattern: /backdrop-filter\s*:\s*[^;]*(?:blur\((?:[3-9]\d|\d{3,})px\))/gi, code: "WRN-CSS-BACKDROP-BLUR", severity: "warning", message: "Large backdrop blur areas can be expensive to composite.", }, { pattern: /box-shadow\s*:[^;]*(?:,\s*[^;]+){4,}/gi, code: "WRN-CSS-MANY-SHADOWS", severity: "warning", message: "Many layered shadows can increase paint cost.", }, { pattern: /(?:^|[},])\s*\*\s*(?:[,{])/gm, code: "WRN-CSS-UNIVERSAL-SELECTOR", severity: "info", message: "Review universal selectors used in large DOM subtrees.", }, ]; for (const check of checks) { for (const match of source.matchAll(check.pattern)) { issues.push({ code: check.code, severity: check.severity, message: check.message, line: cssLineAt(source, match.index ?? 0), }); } } const keyframes = new Map(); for (const match of source.matchAll(/@keyframes\s+([A-Za-z_][\w-]*)/g)) { const name = match[1]!; keyframes.set(name, (keyframes.get(name) ?? 0) + 1); } for (const [name, count] of keyframes) { if (count > 1) { issues.push({ code: "WRN-CSS-DUPLICATE-KEYFRAMES", severity: "warning", message: `@keyframes ${name} is declared ${count} times.`, }); } } return issues; }