// app.jsx — shell, routing, tweaks, auth const { useState: uSA, useEffect: uEA } = React; const NAV = [ { key: 'home', label: 'Home', icon: 'home' }, { key: 'budget', label: 'Budget', icon: 'budget' }, { key: 'claims', label: 'Claims', icon: 'receipt' }, { key: 'invoices', label: 'Invoice vault', icon: 'doc', feature: 'document_vault' }, { key: 'workers', label: 'Support workers', icon: 'people', feature: 'support_workers' }, { key: 'providers', label: 'Providers', icon: 'card' }, { key: 'review', label: 'Plan review', icon: 'sparkles', feature: 'plan_review_prep' }, ]; // curated accent palettes [primary, ink, soft] const ACCENTS = { Rose: ['#c2557a', '#a23d62', '#f8e7ee'], Teal: ['#3f9488', '#2f7569', '#e1efec'], Indigo: ['#6a6bd0', '#4f50b0', '#e8e8f7'], Plum: ['#9a5ba6', '#7c4488', '#f1e6f3'], }; const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "accent": "Rose", "headingFont": "Warm serif", "density": "regular", "plainEnglish": true }/*EDITMODE-END*/; function NavItem({ item, active, onClick, badge, locked }) { const [h, setH] = uSA(false); const I = Icons[item.icon]; return ( ); } // ── loading / error placeholders (no doc yet, no PIN/screen chrome) ── function CenteredCard({ children }) { return (
{children}
); } function Brand() { return (
); } function LoadingScreen() { return

Loading your records…

; } function ErrorScreen({ S }) { return (

Something went wrong

We couldn't load your records. Try again, or sign in again.

{S.lastError &&
{S.lastError}
}
); } // ── sign in / sign up ── // Defaults to 'signup' — a real subscriber's FIRST visit is always account // creation, never sign-in (there's no account yet to sign in to). Once // someone has actually signed in successfully once, remember that and // default to 'signin' for them next time. Getting this default backwards is // exactly what confused the very first real subscriber to try it. function AuthScreen({ S }) { const [mode, setMode] = uSA(() => { try { return localStorage.getItem('haven_has_signed_in') ? 'signin' : 'signup'; } catch (e) { return 'signup'; } }); const [email, setEmail] = uSA(() => { try { return localStorage.getItem('haven_last_email') || ''; } catch (e) { return ''; } }); const [password, setPassword] = uSA(''); const [busy, setBusy] = uSA(false); const [err, setErr] = uSA(''); const submit = async () => { if (!email.trim() || !password) return; setBusy(true); setErr(''); try { if (mode === 'signin') await S.signIn(email.trim(), password); else await S.signUp(email.trim(), password); try { localStorage.setItem('haven_has_signed_in', '1'); } catch (e) {} } catch (e) { setErr(e.message || 'Something went wrong.'); } finally { setBusy(false); } }; return (

{mode === 'signin' ? 'Welcome back' : 'Activate your Haven subscription'}

{mode === 'signin' ? 'Sign in to open your records.' : 'Enter the email your Haven subscription is under, and choose a password for your account.'}

setEmail(e.target.value)} placeholder="Email" style={{ padding: '13px 16px', fontSize: 15, fontFamily: 'var(--sans)', border: '1.5px solid var(--line-2)', borderRadius: 13, outline: 'none', background: 'var(--surface)', color: 'var(--ink)' }} /> setPassword(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && submit()} placeholder="Password" style={{ padding: '13px 16px', fontSize: 15, fontFamily: 'var(--sans)', border: '1.5px solid var(--line-2)', borderRadius: 13, outline: 'none', background: 'var(--surface)', color: 'var(--ink)' }} />
{err &&
{err}
}
); } // ── first-time onboarding: create the participant profile ── function OnboardingScreen({ S }) { const [name, setName] = uSA(''); const [ndis, setNdis] = uSA(''); const [parent, setParent] = uSA('Mum'); const [busy, setBusy] = uSA(false); const [err, setErr] = uSA(''); const submit = async () => { if (!name.trim()) return; setBusy(true); setErr(''); try { await S.createParticipant({ name: name.trim(), ndis: ndis.trim(), parent: parent.trim() || 'Mum' }); } catch (e) { setErr(e.message || 'Something went wrong.'); setBusy(false); } }; return (

Let's set up your records

Just the basics for now — you can add everything else as you go.

setName(e.target.value)} placeholder="Child's name" autoFocus style={{ padding: '13px 16px', fontSize: 15, fontFamily: 'var(--sans)', border: '1.5px solid var(--line-2)', borderRadius: 13, outline: 'none', background: 'var(--surface)', color: 'var(--ink)' }} /> setNdis(e.target.value)} placeholder="NDIS number (optional)" style={{ padding: '13px 16px', fontSize: 15, fontFamily: 'var(--sans)', border: '1.5px solid var(--line-2)', borderRadius: 13, outline: 'none', background: 'var(--surface)', color: 'var(--ink)' }} /> setParent(e.target.value)} placeholder="What should we call you? (e.g. Mum, Dad)" style={{ padding: '13px 16px', fontSize: 15, fontFamily: 'var(--sans)', border: '1.5px solid var(--line-2)', borderRadius: 13, outline: 'none', background: 'var(--surface)', color: 'var(--ink)' }} />
{err &&
{err}
}
); } // ── account badge (sidebar) ── function AccountBadge({ S }) { const email = S.session && S.session.user && S.session.user.email; return (
{email}
); } function App() { const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); const S = useHaven(); const [route, setRoute] = uSA(() => { const h = (location.hash || '').replace('#', ''); return NAV.some((n) => n.key === h) || h === 'settings' ? h : 'home'; }); const [arg, setArg] = uSA(null); const go = (r, a = null) => { setRoute(r); setArg(a); location.hash = r; const m = document.getElementById('main'); if (m) m.scrollTop = 0; }; const clearArg = () => setArg(null); // apply accent + heading font + plain-english + density uEA(() => { const a = ACCENTS[t.accent] || ACCENTS.Rose; const r = document.documentElement; r.style.setProperty('--primary', a[0]); r.style.setProperty('--primary-ink', a[1]); r.style.setProperty('--primary-soft', a[2]); r.style.setProperty('--capacity', a[0]); r.style.setProperty('--capacity-soft', a[2]); r.style.setProperty('--serif', t.headingFont === 'Clean sans' ? "'Figtree', sans-serif" : "'Newsreader', Georgia, serif"); document.body.setAttribute('data-density', t.density); setPlain(t.plainEnglish); }, [t.accent, t.headingFont, t.density, t.plainEnglish]); if (S.status === 'loading') return ; if (S.status === 'need-auth') return ; if (S.status === 'need-onboarding') return ; if (S.status === 'error' || !S.doc) return ; // badge counts (live, from the store) — S.doc is guaranteed non-null past this point const claimTodo = S.doc.claims.filter((c) => c.status === 'draft' || c.status === 'rejected').length; const invWaiting = S.doc.invoices.filter((i) => !i.claimed).length; const sheetsTodo = S.doc.timesheets.filter((s) => s.status === 'pending').length; const badges = { claims: claimTodo, invoices: invWaiting, workers: sheetsTodo }; const Screen = { home: Dashboard, budget: Budget, claims: Claims, invoices: Invoices, workers: Workers, providers: Providers, review: Review, settings: Settings }[route] || Dashboard; return (
{/* ── Sidebar ── */} {/* ── Main ── */}
{/* ── Tweaks ── */} { const name = Object.keys(ACCENTS).find((k) => ACCENTS[k][0] === v[0]) || 'Rose'; setTweak('accent', name); }} /> setTweak('headingFont', v)} /> setTweak('density', v)} /> setTweak('plainEnglish', v)} />
); } ReactDOM.createRoot(document.getElementById('root')).render();