// theme.jsx — Recadly design tokens + shared chrome primitives // Semantic color tokens (light/dark), spacing, type scale, SF-style icons. const TOKENS = { light: { bg: '#FFFFFF', surface: '#F5F5F5', surface2: '#FAFAFA', // very subtle layer above bg (compose bar, etc.) heading: '#252525', body: '#333333', secondary: '#8A8A8A', accent: '#FF6F61', accentSoft:'#FF8885', accentText:'#E55B50', accentFill:'#FFF1EF', // tint for AI chips on light separator: '#E8E8E8', onAccent: '#FFFFFF', chrome: '#FFFFFF', // nav bar bg chromeBlur:'rgba(255,255,255,0.82)', }, dark: { bg: '#252525', surface: '#333333', surface2: '#2D2D2D', heading: '#FFFFFF', body: '#F5F5F5', secondary: '#9A9A9A', accent: '#FF6F61', accentSoft:'#FF8885', accentText:'#FF8885', accentFill:'#3A2A28', // deep coral-tinted surface separator: '#3D3D3D', onAccent: '#FFFFFF', chrome: '#252525', chromeBlur:'rgba(37,37,37,0.82)', }, }; const FONT = '-apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text", system-ui, sans-serif'; // useT: returns the active token set for a given theme key function useT(theme) { return TOKENS[theme === 'dark' ? 'dark' : 'light']; } // ───────────────────────────────────────────────────────────── // Icons — SF-Symbols-style line glyphs. stroke 1.8, rounded caps. // All accept { size, color } and render at currentColor by default. // ───────────────────────────────────────────────────────────── const Icon = ({ d, size = 22, color = 'currentColor', fill = 'none', strokeWidth = 1.8, ...rest }) => ( {d} ); const Icons = { chevronLeft: (p) => } />, chevronRight:(p) => } />, chevronDown: (p) => } />, ellipsis: (p) => } />, phone: (p) => } />, video: (p) => } />, send: (p) => } />, // Sparkle — the AI mark. Four-point star with two small accent sparkles. sparkle: ({ size = 22, color = 'currentColor', strokeWidth = 1.6 }) => ( ), sparkleFill: ({ size = 22, color = 'currentColor' }) => ( ), search: (p) => } />, plus: (p) => } />, check: (p) => } />, x: (p) => } />, pencil: (p) => } />, bell: (p) => } />, tag: (p) => } />, flow: (p) => } />, inbox: (p) => } />, settings: (p) => } />, instagram: ({ size = 22, color = 'currentColor', strokeWidth = 1.8 }) => ( // generic camera/square — placeholder for the "connect IG" CTA, not the // real Instagram glyph (we explicitly avoid copying that branding). ), shieldCheck: (p) => } />, bolt: (p) => } />, globe: (p) => } />, chat: (p) => } />, // Two-bubble comment glyph — distinct from "chat" so the inbox can // visually separate post/reel comments from DMs. comment: (p) => } />, heart: (p) => } />, }; // ───────────────────────────────────────────────────────────── // Avatar — letter monogram on a soft tinted disc. No fake faces. // ───────────────────────────────────────────────────────────── const AVATAR_TINTS = [ ['#F5D6CF', '#9E3A2E'], // peach ['#D8E4D2', '#3A6B3F'], // sage ['#D7DCE8', '#3B4A6B'], // slate ['#EDDCC5', '#7A4E1F'], // sand ['#E2D6E6', '#5B3A6B'], // mauve ['#CFD9D9', '#2F4F4F'], // teal-grey ['#E8D6D1', '#7A3525'], // terracotta ]; function avatarTint(seed) { let h = 0; for (let i = 0; i < seed.length; i++) h = (h * 31 + seed.charCodeAt(i)) | 0; return AVATAR_TINTS[Math.abs(h) % AVATAR_TINTS.length]; } function Avatar({ name, size = 36, ring }) { const [bg, fg] = avatarTint(name || '?'); const initials = (name || '?').split(/\s+/).slice(0,2).map(s=>s[0]).join('').toUpperCase(); return (
{initials}
); } // ───────────────────────────────────────────────────────────── // Bubble — chat message bubble. side: 'in' | 'out'. tail squares // the bottom-outer corner for a familiar messaging silhouette. // ───────────────────────────────────────────────────────────── function Bubble({ side, theme, children, tail = true, maxWidth = '76%' }) { const t = useT(theme); const out = side === 'out'; const bg = out ? t.accent : t.surface; const fg = out ? t.onAccent : t.body; const r = 20, tip = tail ? 6 : 20; const radius = out ? `${r}px ${r}px ${tip}px ${r}px` : `${r}px ${r}px ${r}px ${tip}px`; return (
{children}
); } // ───────────────────────────────────────────────────────────── // Chip — small rounded pill used for AI suggestions, filters, etc. // variant: 'suggest' (coral-tinted) | 'plain' (surface) | 'accent' (filled) // ───────────────────────────────────────────────────────────── function Chip({ theme, variant = 'plain', icon, children, style = {} }) { const t = useT(theme); const styles = { suggest: { background: t.accentFill, color: t.accentText, border: `1px solid ${theme === 'dark' ? 'rgba(255,136,133,0.18)' : 'rgba(229,91,80,0.16)'}`, }, plain: { background: t.surface, color: t.body, border: 'none' }, accent:{ background: t.accent, color: t.onAccent, border: 'none' }, }[variant]; return (
{icon} {children}
); } // ───────────────────────────────────────────────────────────── // StatusBar + HomeIndicator — minimal, theme-aware. The starter's // IOSDevice provides these too, but we want custom-styled screens // without its nav bar, so we re-render here scoped to the screen. // ───────────────────────────────────────────────────────────── function StatusBar({ theme, time = '9:41' }) { const c = theme === 'dark' ? '#fff' : '#000'; return (
{time}
); } function HomeIndicator({ theme }) { return (
); } // ───────────────────────────────────────────────────────────── // Phone — minimal phone shell. We do NOT use the starter IOSDevice // directly because most screens want full-bleed custom nav rather // than its built-in nav bar. width 402 / height 874 matches it. // ───────────────────────────────────────────────────────────── function Phone({ theme, children, statusBar = true, dynamicIsland = true }) { const t = useT(theme); return (
{dynamicIsland && (
)}
{statusBar && }
{children}
); } Object.assign(window, { TOKENS, FONT, useT, Icons, Avatar, Bubble, Chip, StatusBar, HomeIndicator, Phone });