// app/chrome.jsx — device frames (laptop / iPad / iPhone), device switcher,
// responsive navigation (top header + mobile bottom nav), and the Shell wrapper.
const { useState: useStateC, useEffect: useEffectC, useRef: useRefC, useCallback: useCallbackC } = React;
// ─────────────────────────────────────────────────────────────
// Fit-to-viewport scaling
// ─────────────────────────────────────────────────────────────
function useFitScale(outerW, outerH, { padX = 40, padY = 96, max = 1.4 } = {}) {
const [scale, setScale] = useStateC(1);
useEffectC(() => {
const compute = () => {
const availW = window.innerWidth - padX;
const availH = window.innerHeight - padY;
const k = Math.min(availW / outerW, availH / outerH, max);
setScale(Math.max(0.12, k));
};
compute();
window.addEventListener('resize', compute);
return () => window.removeEventListener('resize', compute);
}, [outerW, outerH, padX, padY, max]);
return scale;
}
// ─────────────────────────────────────────────────────────────
// The device-switcher bar (lives outside the scaled frame)
// ─────────────────────────────────────────────────────────────
const DEVICE_ICONS = {
laptop: (s) => (
),
ipad: (s) => (
),
iphone: (s) => (
),
};
function DeviceSwitcher() {
const { device, pickDevice, exitDevice, resetAll } = useApp();
return (
luis
cooks.
{['laptop', 'ipad', 'iphone'].map((d) => {
const active = device === d;
return (
);
})}
);
}
// ─────────────────────────────────────────────────────────────
// Device frames — each renders the app (children) inside a fixed screen
// ─────────────────────────────────────────────────────────────
function ScreenSlot({ w, h, radius, children }) {
return (
{children}
);
}
function LaptopFrame({ children }) {
const { w, h } = DEVICE_SCREEN.laptop;
const lidPadX = 16, lidPadTop = 16, chin = 30;
const lidW = w + lidPadX * 2;
const lidH = lidPadTop + h + chin;
const baseExtra = 130;
const baseW = lidW + baseExtra;
const baseH = 22;
const outerW = baseW;
const outerH = lidH + baseH;
const scale = useFitScale(outerW, outerH);
return (
{/* Lid */}
{/* camera */}
{children}
{/* chin logo */}
luis
cooks.
{/* Base / hinge */}
);
}
function IpadFrame({ children }) {
const { w, h } = DEVICE_SCREEN.ipad;
const bezel = 26;
const outerW = w + bezel * 2;
const outerH = h + bezel * 2;
const scale = useFitScale(outerW, outerH);
return (
{/* camera on long top edge */}
{children}
);
}
function IphoneFrame({ children }) {
const { w, h } = DEVICE_SCREEN.iphone;
const bezel = 13;
const outerW = w + bezel * 2;
const outerH = h + bezel * 2;
const scale = useFitScale(outerW, outerH, { max: 1.35 });
return (
{children}
{/* Dynamic island */}
);
}
function DeviceFrame({ children }) {
const { device } = useApp();
if (device === 'laptop') return {children};
if (device === 'ipad') return {children};
return {children};
}
// ─────────────────────────────────────────────────────────────
// Navigation
// ─────────────────────────────────────────────────────────────
const NAV_TABS = [
{ id: 'recipes', label: 'Recipes', icon: (p) => },
{ id: 'cook', label: 'Cook', icon: (p) => },
{ id: 'plan', label: 'Plan', icon: (p) => },
{ id: 'groceries', label: 'Groceries', icon: (p) => },
];
function AvatarMenu() {
const { logout, go, state } = useApp();
const [open, setOpen] = useStateC(false);
return (
{open && (
<>
setOpen(false)} style={{ position: 'fixed', inset: 0, zIndex: 30 }} />
Luis
{state.favourites.length} favourites
{[
{ l: 'Settings', fn: () => { go('settings'); setOpen(false); } },
{ l: 'From other kitchens', fn: () => { go('shared'); setOpen(false); } },
{ l: 'Sign out', fn: () => { logout(); setOpen(false); } },
].map((m) => (
))}
>
)}
);
}
function AppHeader({ active, compact }) {
const { setTab, go } = useApp();
const pad = compact ? '16px 32px' : '20px 48px';
return (
);
}
function MobileHeader({ title }) {
const { setTab } = useApp();
return (
);
}
function MobileBottomNav({ active }) {
const { setTab } = useApp();
return (
);
}
// ─────────────────────────────────────────────────────────────
// Shell — wraps a screen with chrome appropriate to the layout.
// chrome: 'full' (header + nav), 'none' (bare, e.g. cook mode / login)
// ─────────────────────────────────────────────────────────────
function Shell({ tab, chrome = 'full', children }) {
const { layout, isLive } = useApp();
// Prototype: 38px clears the fake iPhone dynamic island. Live: respect the real
// device's safe-area inset (notch/status bar), falling back to a small inset.
const frameStyle = { height: '100%', display: 'flex', flexDirection: 'column',
overflow: 'hidden', position: 'relative',
'--lc-mobile-top': isLive ? 'max(14px, env(safe-area-inset-top))' : '38px',
'--lc-mobile-bottom': isLive ? 'max(0px, env(safe-area-inset-bottom))' : '0px' };
if (chrome === 'none') {
return
{children}
;
}
if (layout === 'mobile') {
return (
);
}
const compact = layout === 'tablet';
return (
);
}
Object.assign(window, {
DeviceSwitcher, DeviceFrame, LaptopFrame, IpadFrame, IphoneFrame,
AppHeader, MobileHeader, MobileBottomNav, Shell, NAV_TABS, DEVICE_ICONS,
});