| 1 | import type { DesignContract } from '@shared/generation' |
| 2 | |
| 3 | export const DEFAULT_TITLE_FONT = 'Inter' |
| 4 | export const DEFAULT_BODY_FONT = 'Noto Sans SC' |
| 5 | |
| 6 | const DEFAULT_PALETTE = ['#ffffff', '#111827', '#2563eb', '#64748b'] |
| 7 | |
| 8 | export const createDefaultDesignContract = (): DesignContract => ({ |
| 9 | theme: 'clean modern presentation', |
| 10 | background: 'light canvas with subtle neutral depth', |
| 11 | palette: DEFAULT_PALETTE, |
| 12 | titleStyle: 'text-4xl font-semibold text-slate-950', |
| 13 | layoutMotif: 'clear editorial grids with balanced whitespace', |
| 14 | chartStyle: 'simple readable charts with restrained color', |
| 15 | shapeLanguage: '8px radius, light borders, subtle shadows', |
| 16 | titleFont: DEFAULT_TITLE_FONT, |
| 17 | bodyFont: DEFAULT_BODY_FONT |
| 18 | }) |
| 19 | |
| 20 | const parseRecord = (value: unknown): Record<string, unknown> | null => { |
| 21 | if (typeof value === 'string') { |
| 22 | if (value.trim().length === 0) return null |
| 23 | try { |
| 24 | const parsed = JSON.parse(value) as unknown |
| 25 | return parseRecord(parsed) |
| 26 | } catch { |
| 27 | return null |
| 28 | } |
| 29 | } |
| 30 | if (!value || typeof value !== 'object' || Array.isArray(value)) return null |
| 31 | return value as Record<string, unknown> |
| 32 | } |
| 33 | |
| 34 | const normalizeText = (value: unknown): string => String(value ?? '').replace(/\s+/g, ' ').trim() |
| 35 | |
| 36 | const normalizePalette = (value: unknown, fallback: string[]): string[] => { |
| 37 | if (!Array.isArray(value)) return fallback |
| 38 | const colors = value.map((item) => normalizeText(item)).filter(Boolean).slice(0, 6) |
| 39 | return colors.length >= 3 ? colors : fallback |
| 40 | } |
| 41 | |
| 42 | export const normalizeDesignContract = (value: unknown): DesignContract => { |
| 43 | const fallback = createDefaultDesignContract() |
| 44 | const record = parseRecord(value) |
| 45 | if (!record) return fallback |
| 46 | |
| 47 | return { |
| 48 | theme: normalizeText(record.theme) || fallback.theme, |
| 49 | background: normalizeText(record.background) || fallback.background, |
| 50 | palette: normalizePalette(record.palette, fallback.palette), |
| 51 | titleStyle: normalizeText(record.titleStyle) || fallback.titleStyle, |
| 52 | layoutMotif: normalizeText(record.layoutMotif) || fallback.layoutMotif, |
| 53 | chartStyle: normalizeText(record.chartStyle) || fallback.chartStyle, |
| 54 | shapeLanguage: normalizeText(record.shapeLanguage) || fallback.shapeLanguage, |
| 55 | titleFont: normalizeText(record.titleFont) || fallback.titleFont, |
| 56 | bodyFont: normalizeText(record.bodyFont) || fallback.bodyFont |
| 57 | } |
| 58 | } |
| 59 | |
| 60 | export const resolveDesignContract = ( |
| 61 | value: unknown |
| 62 | ): { contract: DesignContract; shouldPersist: boolean } => { |
| 63 | const record = parseRecord(value) |
| 64 | const contract = normalizeDesignContract(record) |
| 65 | if (!record) return { contract, shouldPersist: true } |
| 66 | return { |
| 67 | contract, |
| 68 | shouldPersist: JSON.stringify(contract) !== JSON.stringify(record) |
| 69 | } |
| 70 | } |
| 71 |