// 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 (
setPressed(true)}
onMouseUp={() => setPressed(false)}
onMouseLeave={() => setPressed(false)}
style={{
width: size, height: size,
margin: 4,
border: 0, background: pressed ? 'rgba(255,255,255,0.18)' : 'transparent',
borderRadius: '50%',
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
color, cursor: 'pointer',
position: 'relative',
transition: 'background 120ms var(--ease-in)',
}}>
{children}
);
}
// 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 });