/** Small formatting helpers shared by the list views. */ function pad(value: number): string { return String(value).padStart(2, '0') } /** * Render a backend timestamp as `YYYY-MM-DD HH:mm`. * * The API sends naive local datetimes (`2026-09-18T07:46:40`). Per the ES * date-time spec a date-time string without an offset is parsed as *local* * time, which is what the server clock already is, so no conversion happens. */ export function formatDateTime(value: string | null | undefined): string { if (!value) return '—' const date = new Date(value) if (Number.isNaN(date.getTime())) return value return ( `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ` + `${pad(date.getHours())}:${pad(date.getMinutes())}` ) } /** * Inline style that renders a field the way the paper would. * * `pt` is a real CSS unit, so 10.5pt shows at the same size it will print at. */ export function typographyStyle(field: { font_size: number font_color: string }): Record { return { fontSize: `${field.font_size}pt`, color: field.font_color, } } /** Indentation for a heading level: level 1 flush, each deeper level inset. */ export function levelIndent(level: number): string { return `${Math.max(0, level - 1) * 18}px` } const HEX_PATTERN = /^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/ const RGB_PATTERN = /^rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*[\d.]+\s*)?\)$/ /** * Normalise a colour spelling to `#RRGGBB`, or `null` if unrecognised. * * Mirrors `normalize_hex_color` on the backend. Doing it here as well means a * hand-typed `rgb(255, 0, 0)` is tidied up the moment the field loses focus, * instead of travelling to the server to be rejected. */ export function normalizeHexColor(value: string): string | null { const text = value.trim() const rgbMatch = RGB_PATTERN.exec(text) if (rgbMatch) { const channels = rgbMatch.slice(1, 4).map(Number) if (channels.some((channel) => channel > 255)) return null return `#${channels.map((channel) => channel.toString(16).padStart(2, '0')).join('')}`.toUpperCase() } const hexMatch = HEX_PATTERN.exec(text) if (hexMatch) { const digits = hexMatch[1]! const full = digits.length === 3 ? digits.replace(/./g, (char) => char + char) : digits return `#${full.toUpperCase()}` } return null }