import { createRoot } from "react-dom/client"; import { Suspense, use, useEffect, useLayoutEffect, useRef, useState, type ComponentType } from "react"; import { createBrowserRouter, Link, NavLink, Outlet, RouterProvider, useLocation, useNavigationType, type RouteObject } from "react-router-dom"; import type { AuthUser } from "@stc/contracts"; import { AppShell, installRouteScroll, LoadingLine, routeScroll } from "@stc/ui"; import { webFeatures } from "./features"; import { visibleNav } from "./nav"; function Home() { return

No features installed yet

; } /** The signed-in user (GET /api/me), refreshed on navigation so sign-in and sign-out update the nav. */ function useSessionUser(): AuthUser | null { const { pathname } = useLocation(); const [user, setUser] = useState(null); useEffect(() => { let live = true; fetch("/api/me", { credentials: "same-origin" }) .then((r) => (r.ok ? r.json() : { user: null })) .then((b: { user: AuthUser | null }) => { if (live) setUser(b.user ?? null); }) .catch(() => { if (live) setUser(null); }); return () => { live = false; }; }, [pathname]); return user; } /** Scroll per history entry: top on a new navigation, the saved position on back/forward. */ function useRouteScroll() { const loc = useLocation(); const type = useNavigationType(); useEffect(() => installRouteScroll(), []); useLayoutEffect(() => { routeScroll().routeChanged(loc.key, type === "POP", loc.hash); }, [loc.key]); } /** * After a navigation, the new page's heading (its first h1) is announced in a polite live region once it renders, * and takes focus (tabindex -1), unless the navigation came from the dock or the course outline: those controls * stay put, so focus stays on the one that was used (Enter on Next twice goes two lessons on). The first page load * keeps the browser's own focus. */ function useRouteFocus(): string { const loc = useLocation(); const [said, setSaid] = useState(""); const last = useRef(null); const first = useRef(true); useEffect(() => { const main = document.querySelector(".stc-main"); if (!main) return; const initial = first.current; first.current = false; const take = () => { const h = main.querySelector("h1"); if (!h || h === last.current) return false; last.current = h; if (initial) return true; setSaid(h.textContent ?? ""); if (document.activeElement?.closest(".stc-dock, .lr-side")) return true; if (!h.hasAttribute("tabindex")) h.setAttribute("tabindex", "-1"); h.focus({ preventScroll: true }); return true; }; if (take()) return; const mo = new MutationObserver(() => { if (take()) mo.disconnect(); }); mo.observe(main, { childList: true, subtree: true }); const stop = setTimeout(() => mo.disconnect(), 10_000); return () => { mo.disconnect(); clearTimeout(stop); }; }, [loc.key]); return said; } function Layout() { useRouteScroll(); const said = useRouteFocus(); // Links the user cannot use are hidden; the pages and APIs still enforce access themselves. const nav = visibleNav(webFeatures, useSessionUser()); const header = ( <> STCourses {nav.length ? ( ) : null} ); return ( <> }>
{said}
); } /** * A route component loaded on first use, like React.lazy, but preloadable: once one route of a feature has * rendered, the feature's other routes load straight after. Moving between them (course page to lesson) then never * suspends, so the course sidebar is not dropped for a frame on the first visit. */ function lazyRoute(load: () => Promise<{ default: ComponentType }>, siblings: (() => unknown)[]) { let Loaded: ComponentType | null = null; let pending: Promise | null = null; const preload = () => (pending ??= load().then((m) => { Loaded = m.default; })); function Route() { if (!Loaded) use(preload()); useEffect(() => { siblings.forEach((p) => void p()); }, []); const C = Loaded!; return ; } return { Route, preload }; } const featureRoutes: RouteObject[] = webFeatures.flatMap((f) => { const siblings: (() => unknown)[] = []; return f.routes.map((r) => { const { Route, preload } = lazyRoute(r.element, siblings); siblings.push(preload); return { path: r.path, element: }; }); }); const router = createBrowserRouter([ { element: , children: [...featureRoutes, ...(featureRoutes.some((r) => r.path === "/") ? [] : [{ path: "*", element: }])] }, ]); // Navigation swaps at once (no View Transitions, no holding the old page, ruling R19); the new content region // plays its CSS entrance (@stc/ui tokens.css) and a loading line shows if code or data takes over 150 ms. createRoot(document.getElementById("root")!).render();