|
| 1 | +# CLAUDE.md |
| 2 | + |
| 3 | +Guidance for Claude Code when working in this repository. Keep this file honest — if the architecture changes, update it here. |
| 4 | + |
| 5 | +## Commands |
| 6 | + |
| 7 | +```bash |
| 8 | +npm run dev # Start Vite dev server |
| 9 | +npm run build # Build for production (outputs to dist/) |
| 10 | +npm run lint # ESLint + Prettier check |
| 11 | +npm run lint:fix # Auto-fix lint issues |
| 12 | +npm test # Jest (runs with --passWithNoTests) |
| 13 | +``` |
| 14 | + |
| 15 | +Formatting: 2-space indent, single quotes, no semicolons, trailing commas. Enforced by Prettier. Lint is set to `--max-warnings 0`, so warnings fail CI. |
| 16 | + |
| 17 | +Path alias: `@eplant/*` maps to `Eplant/*` (see `tsconfig.json` and `vite-tsconfig-paths`). |
| 18 | + |
| 19 | +## What ePlant Is |
| 20 | + |
| 21 | +A gene-centric visualization tool for plant genomes (currently _Arabidopsis thaliana_ only). The user picks a gene of interest, then explores it through multiple "views" — each a distinct visualization (gene info, publications, eFP expression maps, chromosome viewer, interaction network, etc.). External data comes from the BAR API at `bar.utoronto.ca`. |
| 22 | + |
| 23 | +## Architecture |
| 24 | + |
| 25 | +### Routing Is the Backbone |
| 26 | + |
| 27 | +Routes are defined in [`Eplant/main.tsx`](Eplant/main.tsx) using React Router v6's `createBrowserRouter`. The root route renders [`Eplant/Eplant.tsx`](Eplant/Eplant.tsx), and each view is a child route matching a path like `/{view-id}/:geneid?`. The child route renders inside `<Outlet />` in [`Eplant/UI/Layout/ViewContainer/index.tsx`](Eplant/UI/Layout/ViewContainer/index.tsx). |
| 28 | + |
| 29 | +The component tree: |
| 30 | +``` |
| 31 | +main.tsx (Providers: Jotai, Config, QueryClient, Router) |
| 32 | + └─ Eplant.tsx (ThemeProvider, Sidebar, URLStateProvider) |
| 33 | + └─ ViewContainer (TopBar, ErrorBoundary, Outlet) |
| 34 | + └─ {view component} — e.g. PlantEFPView, GeneInfoView |
| 35 | +``` |
| 36 | + |
| 37 | +### Views Are Plain React Components |
| 38 | + |
| 39 | +A view has two parts that are registered separately: |
| 40 | + |
| 41 | +1. **The component** — a React function component under `Eplant/views/{Name}/{Name}.tsx`. It reads the current gene via `useOutletContext<ViewContext>()`, fetches its own data with `useQuery`, manages URL-synced state with `useURLState`, and renders. Registered as a route in `main.tsx`. |
| 42 | + |
| 43 | +2. **The metadata** — a `ViewMetadata` object (see [`Eplant/View/index.ts`](Eplant/View/index.ts)) exported as default from `Eplant/views/{Name}/index.tsx`. Holds `id`, `name`, `icon`, `description`, `thumbnail`, `citation`, and `actions`. Registered in [`Eplant/config.tsx`](Eplant/config.tsx) under `userViewMetadata` (gene-specific) or `genericViewMetadata` (standalone like GetStarted). |
| 44 | + |
| 45 | +**These two registrations must stay in sync** — the `id` in the metadata must match the path in `main.tsx`. |
| 46 | + |
| 47 | +### Data Fetching |
| 48 | + |
| 49 | +Each view owns its data fetching via `useQuery` from `@tanstack/react-query`. The `QueryClient` is constructed in `main.tsx` and provided app-wide. Pattern used in existing views: |
| 50 | + |
| 51 | +```tsx |
| 52 | +const { data, isLoading, isError, error } = useQuery<TData, ViewDataError>({ |
| 53 | + queryKey: [`view-id-${geneticElement?.id}`], |
| 54 | + queryFn: async () => loaderFn(geneticElement, setLoadAmount), |
| 55 | + retry: false, |
| 56 | +}) |
| 57 | +``` |
| 58 | + |
| 59 | +Loader functions live in the view's own directory (e.g. `views/GeneInfoView/loader.ts`). There is **no shared API client yet** — loaders call `fetch` or `axios` directly against hardcoded `bar.utoronto.ca` URLs. When new backend APIs arrive, wrap them in a proper client layer before adopting. |
| 60 | + |
| 61 | +### State Management |
| 62 | + |
| 63 | +Four layers, each with a specific job. Respect the boundaries. |
| 64 | + |
| 65 | +| Layer | Purpose | Where | |
| 66 | +|---|---|---| |
| 67 | +| **TanStack Query** | Server data, caching, loading states | Per-view `useQuery` | |
| 68 | +| **URL search params** | View-specific state that should be shareable/bookmarkable | `useURLState<T>()` from [`state/URLStateProvider.tsx`](Eplant/state/URLStateProvider.tsx), validated by Zod schemas | |
| 69 | +| **Jotai atoms** | Global UI state (dark mode, sidebar, active gene/view ids, gene collections) | [`state/index.tsx`](Eplant/state/index.tsx) | |
| 70 | +| **IndexedDB** | Persisting Jotai atoms across sessions | `atomWithOptionalStorage` wrapper over [`util/Storage/index.tsx`](Eplant/util/Storage/index.tsx) | |
| 71 | + |
| 72 | +**URL state pattern:** define a Zod schema with `.default(...)` on each field, call `initializeState(schema)` in a `useEffect` on mount, read `state` and call `setState(...)` to update. The provider serializes to query params with a 50ms debounce. See any eFP view for an example. |
| 73 | + |
| 74 | +### Active Gene / Active View |
| 75 | + |
| 76 | +Stored in Jotai atoms (`activeGeneIdAtom`, `activeViewIdAtom`) and also reflected in the URL. `ViewContainer` has two `useEffect`s that sync them — one reads URL → atoms on mount, one writes atoms → URL on change. This is fragile and worth keeping in mind when editing routing logic. |
| 77 | + |
| 78 | +## Adding a New View |
| 79 | + |
| 80 | +1. Create `Eplant/views/{Name}/{Name}.tsx` — the component. Read gene via `useOutletContext<ViewContext>()`, fetch with `useQuery`, handle loading/error states via `<LoadingPage />`. |
| 81 | +2. Create `Eplant/views/{Name}/index.tsx` — export `ViewMetadata` as default. Include `id`, `name`, `icon`, `citation`. |
| 82 | +3. If the view has URL-persisted state, define a Zod schema alongside it. |
| 83 | +4. Register the route in [`Eplant/main.tsx`](Eplant/main.tsx) with path `{view-id}/:geneid?`. |
| 84 | +5. Register the metadata in [`Eplant/config.tsx`](Eplant/config.tsx) under `userViewMetadata`. |
| 85 | + |
| 86 | +Copy [`views/PlantEFP/`](Eplant/views/PlantEFP/) or [`views/GeneInfoView/`](Eplant/views/GeneInfoView/) as templates — they follow the current pattern cleanly. |
| 87 | + |
| 88 | +## Known Legacy Code — Don't Copy These Patterns |
| 89 | + |
| 90 | +These exist but predate the routing refactor. Treat them as refactor targets, not references. |
| 91 | + |
| 92 | +- **`EFP` class** in [`Eplant/views/eFP/index.tsx`](Eplant/views/eFP/index.tsx) — defines a `component` method that calls React hooks. Violates rules-of-hooks assumptions. Mixes data fetching (`getInitialData`) with rendering. Should be split into a hook + function component. |
| 93 | +- **`Species` static registry** in [`Eplant/GeneticElement.ts`](Eplant/GeneticElement.ts) — species self-register into a static array at module load. Hidden global mutation, hard to test. Eventually replace with explicit registration via Config context. |
| 94 | +- **Top-level `await`** in [`Eplant/state/index.tsx`](Eplant/state/index.tsx) (`citationsAtom`) — blocks module load on a network request. Move to a React Query call. |
| 95 | +- **`atomWithOptionalStorage`** is marked with a `TODO` for removal. |
| 96 | +- **`dangerouslySetInnerHTML`** in eFP views renders SVGs from the BAR server. If the source ever becomes untrusted, route through `dompurify` (already a dep). |
| 97 | +- **`flexlayout-react`** is still imported in `state/index.tsx` but the dockable-panel layout it provided was replaced by React Router. Remove when convenient. |
| 98 | +- **eFP colour calculation** has historically been a source of confusion. Downstream renderers (`useStyles`, `EFPTooltip`) must use `group.control` (per-group) not `data.control` (cross-group average) for correct relative-mode colours and log2 fold change values. `data.control` is a valid fallback only. |
| 99 | + |
| 100 | +## Stack |
| 101 | + |
| 102 | +React 18 · TypeScript 5 · Vite 4 · MUI v5 · Jotai · TanStack Query v5 · React Router v6 · Zod · Cytoscape (+ cose-bilkent, automove, popper) · D3 · idb (IndexedDB) · axios · lodash · flexlayout-react (legacy) · Jest + React Testing Library + MSW · ESLint + Prettier |
| 103 | + |
| 104 | +## Environment Variables |
| 105 | + |
| 106 | +Set via `.env` (Vite convention: must be prefixed `VITE_`): |
| 107 | + |
| 108 | +- `VITE_MAPS_API_KEY` — Google Maps API key for the WorldEFP view |
| 109 | +- `VITE_MAP_ID` — Google Maps style ID for the WorldEFP view |
| 110 | +- `BASE_URL` — build-time base path (set to `/ePlant` in CI for GitHub Pages) |
| 111 | + |
| 112 | +## CI / Deploy |
| 113 | + |
| 114 | +Three workflows in `.github/workflows/`: |
| 115 | +- `build.yml` — runs on branches other than main/staging; Node 22 |
| 116 | +- `linting.yml` — Prettier + ESLint on all pushes/PRs; Node 20 |
| 117 | +- `deploy.yml` — builds and publishes to GitHub Pages from `staging-debug`; Node 22 |
| 118 | +- `node.js.yml` — legacy, runs on main with Node 16 (EOL — scheduled for removal/update) |
| 119 | + |
| 120 | +## When In Doubt |
| 121 | + |
| 122 | +- Check [`RoutingChanges.md`](RoutingChanges.md) for the rationale behind the current view architecture. |
| 123 | +- Prefer editing an existing working view as a reference over inferring patterns from this file. |
| 124 | +- If you find this file contradicts the code, trust the code and flag the discrepancy. |
0 commit comments