import type { GenerationOptions, ThemeProposal } from './types.js'; import { enforceContrast } from './contrast.js'; /** * Deterministic local "brand vibe → palette" generator. Maps prompt * keywords to a hand-curated palette family, then derives the rest of * the proposal from there. * * Wave 13.E.1. The default. Hosts can pass `opts.generate` to swap in * a real LLM (or a fancier embedding-based generator) — the contrast * enforcement (Wave 13.E.2) runs after either path. */ const PALETTES: Array<{ test: RegExp; name: string; accent: string; surface: string; surfaceCard: string; surfaceMuted: string; textPrimary: string; textSecondary: string; }> = [ { test: /\b(midnight|noir|deep|dark|nocturne|raven|space)\b/i, name: 'midnight', accent: '#7c8cff', surface: '#0b0f1a', surfaceCard: '#131a2b', surfaceMuted: '#1c2440', textPrimary: '#e7ecff', textSecondary: '#9aa6cf', }, { test: /\b(citrus|sunshine|sunrise|amber|warm|orange|honey|gold)\b/i, name: 'citrus', accent: '#ff8a00', surface: '#fff7eb', surfaceCard: '#ffffff', surfaceMuted: '#ffe0b8', textPrimary: '#3b2a05', textSecondary: '#7a5a18', }, { test: /\b(forest|leaf|moss|botanical|sage|jungle|emerald|verdant)\b/i, name: 'forest', accent: '#10b981', surface: '#f0faf5', surfaceCard: '#ffffff', surfaceMuted: '#cce8d8', textPrimary: '#0b3a23', textSecondary: '#365c45', }, { test: /\b(ocean|sea|wave|aqua|coastal|maritime|blue|sky)\b/i, name: 'ocean', accent: '#0ea5e9', surface: '#f0f8ff', surfaceCard: '#ffffff', surfaceMuted: '#cee5fb', textPrimary: '#0a2b45', textSecondary: '#3a5d7d', }, { test: /\b(rose|blossom|peach|pink|romantic|gentle)\b/i, name: 'rose', accent: '#ec4899', surface: '#fff5fa', surfaceCard: '#ffffff', surfaceMuted: '#fbd5e6', textPrimary: '#46123a', textSecondary: '#7b3f6b', }, { test: /\b(graphite|monochrome|brutalist|stark|grayscale|minimal)\b/i, name: 'graphite', accent: '#374151', surface: '#f5f6f8', surfaceCard: '#ffffff', surfaceMuted: '#dcdfe5', textPrimary: '#0d1219', textSecondary: '#4b5563', }, { test: /\b(violet|purple|cosmos|cosmic|mystic|nebula|lavender)\b/i, name: 'violet', accent: '#8b5cf6', surface: '#faf7ff', surfaceCard: '#ffffff', surfaceMuted: '#e5d8ff', textPrimary: '#1f0d49', textSecondary: '#5a4690', }, ]; const DEFAULT_PALETTE: ThemeProposal = { name: 'default', accent: '#6366f1', surface: '#ffffff', surfaceCard: '#ffffff', surfaceMuted: '#f5f5f8', textPrimary: '#0f172a', textSecondary: '#475569', success: '#10b981', warning: '#f59e0b', danger: '#ef4444', info: '#0ea5e9', }; /** * Generate (and contrast-check) a theme from a brand prompt. * * Defaults to a local deterministic mapper; pass `opts.generate` to * swap in a real LLM. AA contrast is enforced by default; pass * `enforce: 'aaa'` to lock the higher tier, or `'off'` to disable. */ export async function generateThemeFromPrompt( prompt: string, opts: GenerationOptions = {}, ): Promise { const raw = await (opts.generate ? opts.generate(prompt) : localGenerate(prompt)); const enforce = opts.enforce ?? 'aa'; return enforce === 'off' ? raw : enforceContrast(raw, enforce); } /** Synchronous local generator — exposed for tests + previews. */ export function localGenerate(prompt: string): ThemeProposal { for (const p of PALETTES) { if (p.test.test(prompt)) { return { ...DEFAULT_PALETTE, name: p.name, accent: p.accent, surface: p.surface, surfaceCard: p.surfaceCard, surfaceMuted: p.surfaceMuted, textPrimary: p.textPrimary, textSecondary: p.textSecondary, }; } } return { ...DEFAULT_PALETTE, name: 'default' }; }