Files
sharper.ly/CLAUDE.md
T

139 lines
8.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CLAUDE.md — working brief for agent sessions
Read this before touching anything. Human setup instructions live in [README.md](README.md);
this file is the short version plus the rules that are easy to break.
## What the project is
**SHARP-ER** — *Shared Action on Rule of Law Progress – Exit Route*. EU-funded, nationwide
Libya, December 2025 – December 2027. Implemented by **Acted** (lead), **United Efforts
Consultancy (UEC)** and **Stichting Bureau Ensemble (SBE)**. Supports Libyan civil society,
local institutions and communities on justice, accountability and governance. Successor to
SHARP I and II (2019–2024).
Tone in all copy: professional, neutral, factual, inclusive, non-confrontational.
**Not** political, **not** advocacy, **not** campaign-style.
## Architecture in one screen
```
apps/web React Router 8 (framework mode, ssr: true) · React 19 · Vite 8 · Tailwind 4
SSR only reads the CMS from loaders. No client-side fetch on first paint.
/ 302 (Vary: Accept-Language) -> /en or /ar
/:lang locale layout, validates params.lang, provides { locale, dir }
/:lang/slug ten top-level sections + detail routes
Route ids: root · locale · home · about · whatWeDo · activities · events ·
applications · news · contact · resources · knowledgeHub (+ *Detail)
apps/cms Strapi 5.53 (TypeScript) · PostgreSQL 16 · i18n (ar default, en) · draft & publish
src/api/** content types (schema.json is the contract, commit it)
src/bootstrap/** runs on every boot: creates locales, grants Public permissions
(find/findOne on content; create-only on submission types)
config/ server · admin · database · middlewares (CORS) · plugins
docs/ Source material: ToR, approved EN/AR draft copy, brand guideline. READ ONLY.
```
Data flow: browser → React Router SSR loader → Strapi REST (`~/lib/strapi`, server only) →
PostgreSQL. `STRAPI_TOKEN` never reaches the browser (`~/lib/env.server.ts` is `.server`, and
the bundler enforces it). Every page falls back to bundled draft copy when the CMS is
unreachable — the site must never 500 because Strapi is down.
## Hard rules
1. **Design tokens are two-tier. Components use tier 2 only.**
Tier 1 (`app/styles/primitives.css`) holds raw values; tier 2 (`semantic.css`,
`typography.css`) holds meanings. Never a raw hex, never a tier-1 primitive, never a raw
`font-size` / `line-height` — use the type-role utilities (`display-sm`, `heading-h2`,
`body-md`, `label-md`, …).
Check: `grep -rn "#[0-9a-fA-F]\{6\}\|--color-green\|--color-neutral\|--color-red" apps/web/app/components apps/web/app/routes`
→ must return nothing.
2. **Logical properties only. Never `left` / `right`.**
`ps-` `pe-` `ms-` `me-` `start-` `end-`, `margin-inline-start`, `text-align: start`.
The site ships RTL Arabic; physical directions silently break it.
Check: `grep -rnE "\b(pl|pr|ml|mr|left|right)-[0-9]" apps/web/app` → nothing meaningful.
3. **Surfaces, not a theme.** `:root` *is* the dark surface; `data-surface="light"` flips a
subtree and repaints itself. Never add a second theme system or a `dark:` variant. **The
one exception, accepted deliberately:** the nav's theme toggle flips *which* surface `<html>`
stands on (dark default ↔ light), persisted via the `sharper-theme` cookie read in the root
loader and set by the root action — SSR-rendered so the first byte and first paint always
match. The mechanism stays surfaces-only: the toggle just re-roots them. `--surface-nav`
must keep a value in BOTH surface blocks.
4. **Figma is READ ONLY.** Only `get_design_context`, `get_screenshot`, `get_metadata`,
`get_variable_defs`. Never call anything that writes (`use_figma`, `generate_figma_design`,
`create_new_file`, `upload_assets`, shaders, plugins, …). Before the first
`get_design_context`, read `skill://figma/figma-design-to-code/SKILL.md` and pass
`skillNames: "resource:figma-design-to-code"`. File key `yvKHp3Ha2rQmv9kt9tCC7j`.
5. **Never invent statistics, dates, names or quotes.** If a number is not in `docs/` or in
the CMS, it does not go on the page. Leave the slot empty or omit the component.
The same rule governs the seed: anything invented belongs in `src/seed/demo.ts`, never
in `approved.ts`, and `pnpm seed` without `--demo` must stay publishable.
6. **`docs/` is read-only source material.** Never edit, move or "clean up" those files.
7. **Both locales, always.** New UI text goes into `app/i18n/messages/en.json` *and*
`ar.json` with identical keys (the `Messages` type enforces it). A component that hard-codes
an English string is a bug — that is why `Card.Content`'s `actionLabel` is required.
8. **EU visibility is contractual.** "Funded by the European Union" badge top-right, project
logo top-left, the three partner logos always shown together, no standalone partner
branding. Do not restyle or omit them.
9. **No long-running servers or installs without being asked.** Do not run `pnpm install`,
`pnpm dev`, `strapi develop` or `docker compose up` speculatively. If you need a new
dependency, report it instead of installing it.
10. **No `?lang=` parameter, ever.** The locale is a URL prefix on both languages.
## Where things live
| You need | It is in |
| --- | --- |
| Tokens, surfaces, type roles | `apps/web/app/styles/` (+ its own `README.md`) |
| UI components | `apps/web/app/components/ui/{Button,Input,Card,Navigation,Footer,LanguageSwitcher}` |
| Page modules | `apps/web/app/routes/*.tsx`; shared kits in `routes/_page-kit/`, `routes/_shared/`, `routes/_events-opportunities/` |
| Route config, slugs | `apps/web/app/routes.ts`, `apps/web/app/lib/route-slugs.ts` |
| i18n | `apps/web/app/i18n/` (`config.ts`, `paths.ts`, `t.ts`, `format.ts`, `messages/*.json`) |
| Strapi client, SEO, env | `apps/web/app/lib/{strapi.ts,seo.ts,env.server.ts,route-handle.ts,site-navigation.ts}` |
| HTML shell, canonical, hreflang | `apps/web/app/root.tsx` |
| Content types | `apps/cms/src/api/<name>/content-types/<name>/schema.json` |
| Boot-time locale + permission setup | `apps/cms/src/bootstrap/` |
| CMS seed content (`pnpm seed`) | `apps/cms/src/seed/` — `approved.ts` is from `docs/`; `demo.ts` is invented, opt-in behind `--demo`, every slug prefixed `demo-` |
| CORS, upload rules, i18n locales | `apps/cms/config/{middlewares.ts,plugins.ts}` |
## Conventions that bite
- **`meta` reads `loaderData`, not `data`.** In React Router 8, `matches[0].data` is
`undefined`; use `readRootSeo(matches)` from `~/routes/_shared/listing`.
- **Do not pass `alternates: true` to `buildMeta`.** `root.tsx` emits the canonical and the
`hreflang` alternates for every route; a leaf that repeats them duplicates the tags. Use
`handle.canonicalPath` to override the canonical.
- **`app/routes.ts` existence-checks section modules** and Vite does not re-evaluate it when a
file appears — restart the dev server after adding a route module.
- **Strapi 5 addresses entries by `documentId`**, not by a numeric `id`, and localisation is a
`locale` query parameter on the same entry.
- **Submission content types are write-only for the public** (`create`, never `find`). They
hold personal data. The field is `language`, not `locale` — `locale` is reserved by the i18n
plugin.
- **`.server.ts` files never reach the browser.** Anything touching `STRAPI_TOKEN`, the
database or `process.env` belongs in one.
- **Licensed fonts are not in the repository** (`.gitignore` excludes `public/fonts/*.woff2`).
Expect fallback typefaces locally; never commit a font file.
## Verifying a change
```bash
pnpm typecheck # react-router typegen + tsc (web; the CMS is checked by its build)
pnpm --filter web build # the SSR build must succeed
bash -n scripts/<name>.sh # any shell script you touched
docker compose config # any docker-compose.yml change (needs the docker CLI)
```
Then, in a browser or with `curl`, on **both** locales:
```bash
curl -sI http://localhost:5173/ # 302 + Vary: Accept-Language
curl -s http://localhost:5173/en | head -c 200
curl -s http://localhost:5173/ar | head -c 200 # lang="ar" dir="rtl" in the first bytes
```
Checklist before calling a UI change done: renders in **both** locales; Arabic mirrors
correctly; no raw hex or tier-1 token; no physical-direction utility; keyboard focus is
visible; text comes from the message catalogue or the CMS, not from a literal in the
component.