Frontend¶
Stack¶
- React 19 — including the new
use()hook, suspense boundaries, and improved error boundaries. - Vite 6 — dev server + prod build.
- TypeScript 5 — strict mode.
- TailwindCSS 4 —
@themedriven;alma-*andgold-*token ramps. - shadcn/ui components — installed individually rather than via
shadcn init. Pulled intocomponents/ui/. - Radix UI primitives under shadcn.
- TanStack Query 5 — server state.
@tanstack/react-table— DataTable primitive.@dnd-kit/sortable— drag-reorder for table headers.recharts— Insights charts.react-force-graph-2d/react-force-graph-3d— clustered embedding graph (paper map).lucide-react— icons.
Routing¶
Hash-routed via lib/hashRoute.ts. No React Router. The SPA's URL
shape is #/feed, #/library?tab=saved, #/discovery?lens=….
This was chosen for two reasons:
- The backend's catch-all route serves
index.htmlfor any non-API path. Hash routing keeps the server contract trivial. - Deep-linking from the CommandPalette uses the same shape as the
sidebar — both go through
navigateTo(...)fromlib/hashRoute.ts.
Pages¶
One file per top-level surface, in frontend/src/pages/:
| File | Path |
|---|---|
FeedPage.tsx |
#/feed |
DiscoveryPage.tsx |
#/discovery |
AuthorsPage.tsx |
#/authors |
LibraryPage.tsx |
#/library |
InsightsPage.tsx |
#/insights |
HealthPage.tsx |
#/health |
AlertsPage.tsx |
#/alerts |
SettingsPage.tsx |
#/settings |
Each page composes feature components from components/<feature>/
plus shared primitives from components/shared/ and
components/ui/.
Primitives¶
Canonical, app-wide primitives — reach for these before hand-rolling:
MetricTile(components/shared/MetricTile.tsx) — the one number tile. Bordered + icon-led variants; tonesneutral / success / warning / critical / info / accent. Settings'StatTileis a thin shim that delegates here.ConceptCallout(components/ui/concept-callout.tsx) — the in-page "What is this?" explainer for a complex feature. Once per surface, near the top. Never nested.JargonHint(components/shared) — per-term info popover for a single word of jargon inside a paragraph or label.Card/SubPanel/Surface(components/ui/) — the surface primitives that drive the elevation ladder (Cardlifts one level,SubPanellifts + recesses). Never hand-write a surface background.
Settings-scoped helpers the rest of the app also re-uses:
SettingsCard— titled card.SettingsSection— collapsible disclosure.AsyncButton— debounced + loading-state.ToggleRow— labelled switch row.OptionCard— selectable card.SettingsNumberField— spinner-input.KeyValueRow,PackageChip.
For paper rows: PaperCard (compact / default / detailed variants),
PaperActionBar (the rating verbs), StatusBadge (the only badge
path).
DataTable¶
components/ui/data-table/DataTable.tsx is the shared table
primitive. Built on @tanstack/react-table + @dnd-kit. Used by:
- Library Saved compact view.
- Settings → Corpus Explorer modal.
- Insights Reports tab.
- Authors followed-list table.
- Feed compact view.
Features: column visibility toggle, drag-reorder, resize, sort,
optional row selection, persistence to localStorage per
storageKey.
State¶
Server state — TanStack Query. Each page declares its queries with
keys like ['library-saved'], ['library-workflow-summary'],
['feed-inbox']. Mutations invalidate the matching keys via
invalidateQueries(qc, ...keys) from lib/queryHelpers.ts.
Local state — useState, useReducer. Forms use
react-hook-form + zod schemas (Settings, Authors resolve dialog,
Alerts).
Toasts and dialogs¶
- Toasts —
useToast()/errorToast()fromhooks/useToast. Sonner-backed. Use for success / failure feedback after a mutation. - Dialogs —
Dialogfromcomponents/ui/dialog(Radix-backed). Use for forms / large modals (Import dialog, paper detail panel). - Confirms —
AlertDialogfor destructive actions (window.confirmis forbidden by convention).
Avoid¶
window.confirm/window.alert— useAlertDialog.- Per-surface badge implementations — use
StatusBadge. - Per-surface paper row markup — use
PaperCard. - Inline
useStateforms for anything stateful — usereact-hook-form. - Redundant query helpers — use
invalidateQueries(qc, ...)fromlib/queryHelpers.
Design language¶
Distinctive over generic. ALMa is a research tool, not a SaaS landing page. Rules of thumb:
- No glassy gradients, no marketing-style hero sections.
- One neutral elevation ladder decides every surface colour
(
bg-surface-N/border-edge-N) — never a hand-pickedbg-white/bg-slate-*. Colour meaning routes through semantic tokens (accent= the single interactive identity,primary= the one heavy navy fill,success / warning / critical / info,gold= trim only);slate-*is the text ramp, never a surface. Thesurface-guardtest fails CI on raw surface/semantic classes. - Tabular numerics where it matters (counts, scores, citations).
- Tooltips and HoverCards over modals when surfacing detail.
- Empty states are explicit ("No suggestions — refresh this lens") not generic ("Nothing here yet").
Tests¶
Frontend logic and rendering are covered by Vitest — *.test.ts(x)
files that live beside the code (src/lib/*.test.ts,
src/components/**/*.test.tsx, guards like
src/test/surface-guard.test.ts). Broader behaviour is exercised by
the Python integration suite against the backend.
cd frontend
npm run test # Vitest, single run
npm run typecheck # type check (strict). NOT bare `tsc --noEmit`:
# tsconfig.json is references-only and checks nothing.
npm run build # full Vite production build
All three are fast and catch the overwhelming majority of regressions. See Testing for the full picture.