MethodFrontend
partial some parts checked against the code, others not yet · what the statuses mean
Frontend architecture
In simple terms
The frontend is the web page you interact with. It’s built with React (a library for building UIs out of reusable “components”) and TypeScript (JavaScript with type-checking). When you click or type, small pieces of the page react, sometimes ask the backend for numbers, and redraw. There’s no page reload — it’s a single-page app.
Why it’s built this way
The UI needs to feel like a live instrument: edit a size, watch the numbers tween and the flow diagram re-light. React’s component model + local state makes that natural, and TypeScript keeps the many data shapes (results, financials, sweeps) honest as the app grows.
Where it lives
- Entry:
frontend/src/main.tsx→frontend/src/App.tsx. - Layouts:
frontend/src/layouts/· Pages:frontend/src/pages/· Rooms:frontend/src/rooms/· Shared UI:frontend/src/components/. - Data seam:
frontend/src/data/(client.ts,api.ts,types.ts). - Styling:
frontend/src/index.css(tokens) +frontend/src/App.css.
How it fits together
main.tsxmounts<App/>inside a<BrowserRouter>and<StrictMode>.App.tsxis the route table.AppLayoutwraps everything with global chrome (the one sidebar — projects, the open project’s stages, rooms and tools, the account menu — and a slim top bar: breadcrumb + an actions slot). Under/projects/:projectId,ProjectLayoutadds the project context and the stage strip; the book version bar lives in the Assumptions tab. Each stage frame (PrefeasShell,FeasibilityShell) fills the top bar’s slot once, for every tab, with the stage’s Export ▾ and Snapshot (shell/StageActions.tsx).- A room (e.g. the Prefeasibility cockpit) owns a screen. It reads/writes data
through
client.tsand renders shared components.
Routing
Verified in App.tsx — react-router-dom with BrowserRouter:
/→ redirect to/projects(the Portfolio is the home)./projects— portfolio landing;/projects/:projectId/<room>— the rooms (prefeasibility,feasibility,assumptions,snapshots,derivatives, the feasibility sub-rooms, and themethodsexplainers)./settings/dab— the default-assumption-book editor./settings/workspace— the workspace’s name, members, one-time links and owner./docsand/docs/:slug— this documentation (lazy-loaded / code-split).- Outside the app (no session needed):
/sign-in,/setup(the first run) and/join?token=…(a one-time invitation link).
What a role may do (since 2026-11-04): auth/access.ts copies the server’s action →
least-role table (a backend test fails when the two differ); useCan(action) reads
it. A pure write control is not drawn for a role that may not use it — through the
hook, or by carrying data-write="<action>", which one CSS rule hides from a role
useRoleOnDocument marked on <html>. Inline editors (book cells, sizing) turn
read-only, and each page shows a viewer ONE muted note. All of it is cosmetic: the
server refuses the request anyway.
See 03-frontend/routing.md (planned) for the full table and the stage/room model.
The data seam (the one boundary to respect)
UI component → client.ts → api.ts → FastAPI.
client.tsexposes intention-revealing functions (solveProject,rightSize,getFeasibilityStatus, …). Components call these; they neverfetchdirectly.api.tsdoes the typedfetchagainstVITE_API_BASE(defaulthttp://localhost:8000under the dev server;/apion the page’s own address when the backend served the built app —serve.py --share, since 2026-11-04) and declares the TypeScript response shapes.- Those shapes mirror the backend. The rule: never invent a field the backend
doesn’t send — change the
core/producer, theapi.tstype, and every consumer together.
State management
There is no global state library (no Redux/Zustand). Verified pattern:
- Local state: each room holds its own
useState/useEffect. This is where most state lives (sizing inputs, the current solve bundle, chart hover, etc.). - Shared per-project state: a
ProjectProviderReact context carries the active project + the last-visited stage across rooms. - Server state: fetched imperatively on mount / on edit, with no cache layer. Expensive async work (feasibility solve, right-size landscape, storage sweep) is a backend thread-job the room polls until done.
- Theme: a
useThemehook toggles light/dark by stampingdata-themeon the root; all colors are CSS variables so both themes work.
Forms, validation, and locale
- Forms are plain controlled inputs (e.g.
NewProjectForm, the design panel’s fields indesign/fields.tsx, the book editors). Numeric inputs use text inputs +parseDecimal(informat.ts) so both.and,decimals work — the dev machine’s OS locale is French, so this matters. - Client-side validation is light; the backend is the strict validator (Pydantic
models + explicit range checks return
422with a specific message, which the UI surfaces).
Loading, error, and empty states
Rooms consistently show: a solving indicator (SolverStatus shimmer / poll
progress), an error panel when a request fails (often with a hint to start the
backend), and a graceful empty state before the first solve. The recurring hero
motion is recalculation — numbers tween, bars/flows spring — not entrance
animation.
Design system
- Apple-HIG-inspired, dual light + dark via CSS variables in
index.css. Accent = electric blue (--primary); green (--go) = “feasible/go”; amber (--warn) = bottleneck/constraint (always paired with an icon/label, never hue alone). - Type: system UI stack for text, JetBrains Mono for all numbers (tabular).
- Charts are in-house SVG/CSS on the tokens (no chart library) —
FlowDiagram,CostBuildup,EnergyBalance,CapexBreakdown,LcoBreakdown, the feasibility dispatch/duration/heatmap charts. Every chart carries axis labels + units, a legend, and a hover read-out (a ratified legibility standard). Panel+RoomHeaderunify every room’s structure; the Methods rooms and this/docsexplorer reuse the same tokens.
Build & tooling
- Vite dev server (
npm run dev, port 5173) and build (npm run build=tsc -b && vite build). Lint:npm run lint(ESLint). - Unit tests run on Node’s own test runner (
npm test,frontend/tests/): a test loads a pure module — a sentence or a verdict in a non-component file — through Vite and checks it, with no browser and no extra dependency. The other quality gates aretsc+eslint+ a successfulbuild, plus headless browser drives for behaviour. (Introducing Vitest is a tracked option — internal known-gaps list,docs/08-maintenance/known-gaps.md.)
How to modify this safely
- Changing a response shape? Update
core/+api.ts+ allclient.ts/room consumers in one change; runtscto catch every break. - New room? Add the route in
App.tsx, the room folder underrooms/, and reuseRoomHeader/Panel. Register it in the stage/room model (stages.ts/rooms.ts). - Respect the recalculation-as-hero motion and the chart legibility standard.
Related topics
From the app’s documentation, docs/03-frontend/frontend-architecture.md · Hyprisma 1.0.5