/** * Palette contract for card frames. Starter and custom frames share the * same slots; CardFrame resolves a palette from these keys. */ export const PALETTE_SLOTS = [ { key: 'outer', label: 'Card body' }, { key: 'border', label: 'Frame border' }, { key: 'titleBar', label: 'Title bar' }, { key: 'typeBar', label: 'Type bar' }, { key: 'textBox', label: 'Text box' }, { key: 'artBacking', label: 'Art backing' }, { key: 'text', label: 'Body text' }, { key: 'titleText', label: 'Title text' }, { key: 'accent', label: 'Accent' }, ]; const HEX_RE = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/; /** * Validate/normalize a palette object. Returns { palette } with all nine * slots as lowercase hex, or { error } when a slot is missing/invalid. */ export function validatePalette(input) { if (!input || typeof input !== 'object' || Array.isArray(input)) { return { error: 'Palette must be an object' }; } const palette = {}; for (const { key } of PALETTE_SLOTS) { const value = input[key]; if (typeof value !== 'string' || !HEX_RE.test(value.trim())) { return { error: `Invalid or missing color for "${key}"` }; } palette[key] = value.trim().toLowerCase(); } return { palette }; } /** * Validate/normalize frame layout zones (art + text windows as 0-1 * fractions of the card). Returns { layout } or { error }. */ export function validateLayout(input) { if (!input || typeof input !== 'object' || Array.isArray(input)) { return { error: 'Layout must be an object' }; } const layout = {}; for (const zone of ['art', 'text']) { const z = input[zone]; if (!z || typeof z !== 'object') { return { error: `Layout zone "${zone}" is required` }; } const { x, y, w, h } = z; for (const [key, value] of Object.entries({ x, y, w, h })) { if (typeof value !== 'number' || !Number.isFinite(value)) { return { error: `Layout zone "${zone}" has invalid ${key}` }; } } if (w <= 0 || h <= 0) { return { error: `Layout zone "${zone}" must have positive size` }; } if (x < 0 || y < 0 || x + w > 1.001 || y + h > 1.001) { return { error: `Layout zone "${zone}" extends past the card` }; } layout[zone] = { x: Math.max(0, x), y: Math.max(0, y), w: Math.min(w, 1 - Math.max(0, x)), h: Math.min(h, 1 - Math.max(0, y)), }; } return { layout }; }