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 Docs from './components/Docs.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. v0.8.0 (§6.1 / item #6): authenticated users with // `permission_state='pending'` also pass through as `viewer` with // their state attached — every write-gated affordance reads the // state and treats pending the same as anonymous, while reads // remain open. The /beta-pending page is the home root for a // pending user. const viewer = me?.authenticated ? me.user : null const isAdmin = viewer && (viewer.role === 'owner' || viewer.role === 'admin') const isPending = viewer && viewer.permission_state === 'pending' 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 Docs {viewer && ( Settings )} {isAdmin && ( Admin )} {viewer && ( )} {viewer ? ( <> {viewer.display_name} {viewer.role} Sign out ) : ( Sign in Beta )}
{isPending && }
} /> } /> } /> } /> } /> {/* §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 DocsWithSidebar({ viewer }) { return (
) } function NotificationSettingsWithSidebar({ viewer }) { return (
) } function AdminWithSidebar({ viewer }) { return (
) } function PendingAccessBanner() { // v0.8.0 — thin banner shown on every page (other than /beta-pending // itself, which carries the same message in larger form) when the // signed-in user's `permission_state='pending'`. Sign-out works // normally via the header affordance. return (
Your beta access request is in review.{' '} Learn more →
) } function Welcome({ viewer }) { // v0.8.0 — a pending user landing on "/" gets the same page they'd // see at /beta-pending, inline. This is the post-OTC home root for // a user awaiting admin grant. if (viewer && viewer.permission_state === 'pending') { return } 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.

) }