// Towbook-skinned Android phone frame. // Wraps content in a phone-shaped chrome with status bar, optional app bar, content area, and gesture nav. // Differs from the generic starter: uses Towbook brand blue app bar by default, Inter type, status pill mapping. const { useState } = React; // Status bar — battery / wifi / signal / clock. Color flips with theme. function TBStatusBar({ tone = 'light', time = '4:54' }) { // tone: 'light' (dark icons on light bg), 'dark' (light icons on dark bg), 'primary' (light icons on blue) const fg = tone === 'light' ? '#1e1f24' : '#ffffff'; return (
{time}
{/* signal */} {/* wifi */} {/* battery */}
); } // Top app bar — Towbook brand uses solid primary. Variant 'surface' for low-emphasis screens. function TBAppBar({ title, variant = 'primary', // 'primary' | 'surface' leading, // {icon, onClick} — defaults to menu / back trailing = [], // [{icon, onClick, badge?}] onMenu, onBack, centered = false, size = 'small', // 'small' | 'medium' | 'large' }) { const isPrimary = variant === 'primary'; const bg = isPrimary ? 'var(--tb-primary)' : 'var(--tb-surface)'; const fg = isPrimary ? 'var(--tb-on-primary)' : 'var(--tb-on-surface)'; const border = isPrimary ? 'transparent' : '1px solid var(--tb-divider)'; const Icon = leading?.icon || (onBack ? I.ArrowLeft : I.Menu); const onLeading = leading?.onClick || onBack || onMenu; const titleSize = size === 'large' ? 28 : size === 'medium' ? 22 : 18; const titleWeight = size === 'small' ? 600 : 600; const barHeight = size === 'large' ? 124 : size === 'medium' ? 88 : 56; return (
{onLeading !== undefined && ( )} {size === 'small' && (
{title}
)} {size !== 'small' &&
}
{trailing.map((t, i) => ( {t.badge ? {t.badge} : null} ))}
{(size === 'medium' || size === 'large') && (
{title}
)}
); } function IconButton({ children, onClick, color = 'currentColor', size = 40 }) { const [pressed, setPressed] = useState(false); return ( ); } // Bottom gesture nav (the pill) function TBNavBar({ tone = 'light', bg }) { const pillColor = tone === 'light' ? '#1e1f24' : '#ffffff'; return (
); } // The phone frame itself. function TBPhone({ width = 360, // device viewport width (Pixel-class ~ 360-412dp) height = 760, // viewport height scale = 1, appBar, // element OR null statusBarTone, // 'light' | 'dark' | 'primary' (auto if appBar variant=primary) navBarTone, // 'light' | 'dark' surface, // body background override children, label, // small label rendered underneath bezel = true, }) { // theme detection: read data-theme on the document root for tone defaults const dark = typeof document !== 'undefined' && document.documentElement.getAttribute('data-theme') === 'dark'; const sbTone = statusBarTone || (appBar?.props?.variant === 'primary' ? 'dark' : (dark ? 'dark' : 'light')); const nbTone = navBarTone || (dark ? 'dark' : 'light'); const sbBg = appBar?.props?.variant === 'primary' ? 'var(--tb-primary)' : (surface || 'var(--tb-surface)'); const bodyBg = surface || (dark ? 'var(--tb-surface-1)' : 'var(--tb-surface-1)'); const w = width, h = height; const containerW = w * scale + (bezel ? 16 : 0); const containerH = h * scale + (bezel ? 16 : 0) + (label ? 28 : 0); return (
{/* status bar */}
{/* app bar (optional) */} {appBar} {/* content body */}
{children}
{/* gesture nav */}
{label && (
{label}
)}
); } Object.assign(window, { TBPhone, TBAppBar, TBStatusBar, TBNavBar, IconButton });