/** * Render primitives for the preview portal: HTML escaping, slugs, the aspect * attributes for media cards, the project aspect and the date format. A LEAF * (imports only the portal types) so every render module can share them. * Extracted verbatim from `render.ts` (roadmap Phase 3d). */ import type { PreviewPortalAsset, PreviewPortalProject } from './types.js'; /** * Per-ASSET aspect attributes: class for the orientation bucket (drives the * hero/card max-height CSS) plus an exact inline aspect-ratio when dimensions * were probed. A 9:16 deliverable must render in a portrait card even inside a * 16:9 project — never letterboxed in a landscape box. */ export function mediaAspectAttrs(asset: PreviewPortalAsset): string { const cls = asset.orientation ? ` class="orient-${asset.orientation}"` : ''; const style = asset.width && asset.height ? ` style="aspect-ratio:${asset.width}/${asset.height}"` : ''; return `${cls}${style}`; } export function slugify(value: string): string { return value .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, '') || 'unknown'; } /** Project aspect ratio (from the brief's execution profile) used to size the * media cards + hero. Falls back to landscape when unknown. */ export function aspectForProject(project: PreviewPortalProject): string { const ratio = project.brief?.metadata?.executionProfile?.aspectRatio; return ratio === '9:16' || ratio === '1:1' || ratio === '16:9' ? ratio : '16:9'; } /** Compact, locale-independent card date: "2026-06-01 18:01 UTC" from an ISO * string (deterministic — no Date/locale parsing). Empty string when absent. */ export function fmtDate(iso?: string): string { if (!iso) return ''; const m = /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2})/.exec(iso); return m ? `${m[1]} ${m[2]} UTC` : iso; } export function esc(value: string): string { return value .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"'); } export function escAttr(value: string): string { return esc(value); }