import { useEffect, useState } from 'react' import { Routes, Route, Link, useNavigate } from 'react-router-dom' import { getMe, subscribeToNotifications } from './api' import Catalog from './components/Catalog.jsx' import Inbox from './components/Inbox.jsx' import RFCView from './components/RFCView.jsx' import PRView from './components/PRView.jsx' import ProposalView from './components/ProposalView.jsx' import ProposeModal from './components/ProposeModal.jsx' import Landing from './components/Landing.jsx' import Login from './components/Login.jsx' import BetaPending from './components/BetaPending.jsx' import Philosophy from './components/Philosophy.jsx' import NotificationSettings from './components/NotificationSettings.jsx' import Admin from './components/Admin.jsx' import ToastHost, { showToast } from './components/ToastHost.jsx' import CookieConsentBanner from './components/CookieConsentBanner.jsx' import Privacy from './pages/Privacy.jsx' import Cookies from './pages/Cookies.jsx' import './App.css' export default function App() { const [me, setMe] = useState(null) const [loading, setLoading] = useState(true) const [proposeOpen, setProposeOpen] = useState(false) const [catalogVersion, setCatalogVersion] = useState(0) const [inboxOpen, setInboxOpen] = useState(false) const [unreadCount, setUnreadCount] = useState(0) const [inboxTick, setInboxTick] = useState(0) // §14.5: a tick that, when bumped, asks to // re-open even if the user has already made a choice. The settings // "Privacy & cookies" tab dispatches a `rfc-app:cookie-consent-reopen` // event that bumps this. const [consentReopenTick, setConsentReopenTick] = useState(0) const navigate = useNavigate() useEffect(() => { const handler = () => setConsentReopenTick(t => t + 1) window.addEventListener('rfc-app:cookie-consent-reopen', handler) return () => window.removeEventListener('rfc-app:cookie-consent-reopen', handler) }, []) useEffect(() => { getMe() .then(setMe) .catch(() => setMe({ authenticated: false })) .finally(() => setLoading(false)) }, []) // §15.3 — subscribe to the live SSE stream for authenticated viewers // so the badge counter and the toast surface stay in lockstep with // the inbox. Tabs that miss an event because they were closed pick // it up on next sign-in via the snapshot frame. useEffect(() => { if (!me?.authenticated) return undefined const close = subscribeToNotifications({ onSnapshot: payload => setUnreadCount(payload.unread_count || 0), onNotification: payload => { setUnreadCount(c => c + 1) setInboxTick(t => t + 1) // §15.3: personal-direct events get a toast even when the user // isn't on the relevant view — they're the named subject. // Churn never toasts; structural toasts only when it lands on // a slug the user is currently viewing (URL match). const isPersonal = payload.category === 'personal-direct' const onCurrentSlug = payload.rfc_slug && window.location.pathname.includes(`/rfc/${payload.rfc_slug}`) if (isPersonal || onCurrentSlug) { showToast({ summary: payload.summary, category: payload.category, link: payload.rfc_slug ? `/rfc/${payload.rfc_slug}` : null, }) } }, onRead: () => { setUnreadCount(c => Math.max(0, c - 1)) setInboxTick(t => t + 1) }, }) return close }, [me?.authenticated]) if (loading) { return
Loading…
} // The deployment is in private beta: anonymous visitors get the full // app in read-only mode (viewer = null is passed through to every // component), and write affordances are hidden at the component // level. /beta-pending is the post-OAuth-rejection page reachable by // anyone. The original §14.1 Landing surface is retained for the // `/welcome` URL only, in case a deployment wants to link to it. const viewer = me?.authenticated ? me.user : null const isAdmin = viewer && (viewer.role === 'owner' || viewer.role === 'admin') return (
{import.meta.env.VITE_APP_NAME}
{/* §14.3: the persistent About link. One word, no badge, no state — visible from every screen so a viewer mid-PR who wonders why a conversation is public can reach the answer in two clicks. Anonymous viewers see it too. */} About {viewer && ( Settings )} {isAdmin && ( Admin )} {viewer && ( )} {viewer ? ( <> {viewer.display_name} {viewer.role} Sign out ) : ( Sign in Beta )}
} /> } /> } /> } /> {/* §14.5 / §14.6: cookie-consent companions to /philosophy. Available to anonymous and authenticated viewers alike. */} } /> } /> {viewer && ( } /> )} {isAdmin && ( } /> )} setProposeOpen(true)} version={catalogVersion} />
} /> } /> } /> setCatalogVersion(v => v + 1)} />} />
} />
{proposeOpen && viewer && ( setProposeOpen(false)} onSubmitted={({ pr_number }) => { setProposeOpen(false) setCatalogVersion(v => v + 1) navigate(`/proposals/${pr_number}`) }} /> )} {inboxOpen && viewer && ( setInboxOpen(false)} lastChangeTick={inboxTick} /> )}
) } function PolicyShell({ children }) { // §14.5 / §14.6 policy pages reuse the chrome-pane shape so they // render full-width without the catalog rail. The components inside // carry their own back affordance per Philosophy.jsx's pattern. return
{children}
} function PhilosophyWithSidebar({ viewer }) { // The chrome surfaces (§14.2 philosophy, §15 settings, §6/§17 admin) // all use the full app body — no catalog left pane, no propose modal. // The header carries the navigation back; the body is a single // reading surface. return (
) } function NotificationSettingsWithSidebar({ viewer }) { return (
) } function AdminWithSidebar({ viewer }) { return (
) } function Welcome({ viewer }) { if (!viewer) { return (

Welcome.

The catalog on the left lists every super-draft and active RFC in the framework. Open one to read the canonical body and the public conversation behind each definition.

Discussion and contribution are in private Beta — read freely, and sign in if your email has been invited.

Wondering why a conversation is public, why graduation costs what it does, or why the model is in the chat? Read the philosophy.

) } return (

Welcome, {viewer.display_name}.

The catalog on the left lists every super-draft and active RFC in the framework. Open one to read the canonical body, or use{' '} Propose New RFC at the bottom of the catalog to open an idea PR against the meta repository.

Wondering why a conversation is public, why graduation costs what it does, or why the model is in the chat? Read the philosophy.

) }