// App — routing, floating glass tab bar, pull-to-capture gesture. // On real mobile (iPhone), renders full-screen without the IOSDevice phone frame. const _isMobile = window.innerWidth <= 430 || /iPhone|iPad|Android/i.test(navigator.userAgent); function App() { const [tab, setTab] = React.useState('today'); const [subView, setSubView] = React.useState(null); const [captureOpen, setCaptureOpen] = React.useState(false); const [pullY, setPullY] = React.useState(0); const scrollerRef = React.useRef(null); // ── persistence React.useEffect(() => { try { const s = JSON.parse(localStorage.getItem('lifeos:state') || '{}'); if (s.tab) setTab(s.tab); if (s.subView !== undefined) setSubView(s.subView); } catch {} }, []); React.useEffect(() => { localStorage.setItem('lifeos:state', JSON.stringify({ tab, subView })); }, [tab, subView]); const go = (target, arg) => { if (target === 'person') { setTab('radar'); setSubView(`person:${arg}`); } else { setTab(target); setSubView(null); } if (scrollerRef.current) scrollerRef.current.scrollTop = 0; }; // ── pull-to-capture (only on Today screen, only when at scroll top) const pullState = React.useRef({ startY: 0, active: false }); const onTouchStart = (e) => { if (tab !== 'today' || captureOpen) return; if (scrollerRef.current && scrollerRef.current.scrollTop > 1) return; pullState.current = { startY: e.touches[0].clientY, active: true }; }; const onTouchMove = (e) => { if (!pullState.current.active) return; const dy = e.touches[0].clientY - pullState.current.startY; if (dy < 0) { setPullY(0); return; } setPullY(Math.min(dy / 180, 1)); }; const onTouchEnd = () => { if (!pullState.current.active) return; pullState.current.active = false; if (pullY > 0.6) { setCaptureOpen(true); } setPullY(0); }; // pointer events (mouse drag for desktop/simulator) const pState = React.useRef({ y: 0, active: false }); const onPointerDown = (e) => { if (tab !== 'today' || captureOpen) return; if (scrollerRef.current && scrollerRef.current.scrollTop > 1) return; if (e.pointerType === 'touch') return; pState.current = { y: e.clientY, active: true }; }; const onPointerMove = (e) => { if (!pState.current.active) return; const dy = e.clientY - pState.current.y; if (dy < 0) { setPullY(0); return; } setPullY(Math.min(dy / 180, 1)); }; const onPointerUp = () => { if (!pState.current.active) return; pState.current.active = false; if (pullY > 0.6) setCaptureOpen(true); setPullY(0); }; // ── render active screen let screen; if (tab === 'today') screen = setCaptureOpen(true)} />; else if (tab === 'chat') screen = ; else if (tab === 'radar' && subView && subView.startsWith('person:')) { screen = ; } else if (tab === 'radar') screen = ; else if (tab === 'body') screen = ; else if (tab === 'inbox') screen = ; else if (tab === 'people') screen = ; const pendingCount = (LifeData.approvals || []).length; // ── pull-to-capture indicator (same in both mobile/desktop modes) const pullIndicator = tab === 'today' && pullY > 0 && (
0.6 ? T.blue : 'rgba(255,255,255,0.9)', color: pullY > 0.6 ? '#fff' : T.label2, padding: '8px 16px', borderRadius: 999, fontSize: 13, fontWeight: 600, display: 'flex', alignItems: 'center', gap: 6, boxShadow: '0 4px 12px rgba(0,0,0,0.12)', backdropFilter: 'blur(20px) saturate(180%)', WebkitBackdropFilter: 'blur(20px) saturate(180%)', border: pullY > 0.6 ? 'none' : '0.5px solid rgba(0,0,0,0.06)', }}> 0.6 ? '#fff' : T.label2} /> {pullY > 0.6 ? 'Release to capture' : 'Pull to capture'}
); const scrollerDiv = (
{screen}
); const fabButton = tab === 'today' && (
); if (_isMobile) { // Full-screen mobile layout — no phone frame, respects safe area via CSS return (
{pullIndicator} {scrollerDiv} setCaptureOpen(false)} /> {fabButton}
); } // Desktop — wrapped in the iOS device frame for the phone-in-browser look return ( {pullIndicator} {scrollerDiv} setCaptureOpen(false)} /> {fabButton} ); } function TabBar({ tab, setTab, pendingCount }) { const tabs = [ { id: 'today', label: 'Today', Icon: IconHouse }, { id: 'chat', label: 'Chat', Icon: IconChat }, { id: 'radar', label: 'Radar', Icon: IconRadar }, { id: 'people', label: 'People', Icon: IconPeople }, { id: 'inbox', label: 'Inbox', Icon: IconInbox, badge: pendingCount }, ]; return (
{tabs.map(t => { const active = tab === t.id; return ( ); })}
); } // ── Entry point: wait for data, then render ────────────────────────────────── window.LifeDataReady.then(function() { if (!window.LifeData) { document.getElementById('root').innerHTML = '
' + '
⚠️
' + '
Could not connect to Life OS
' + '
Make sure the backend is running on port 8443
'; return; } const root = ReactDOM.createRoot(document.getElementById('root')); root.render(); });