Files
sharper.ly/CLAUDE.md
T

139 lines
8.5 KiB
Markdown
Raw Normal View History

2026-09-11 02:46:56 +02:00
# 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 (en default, ar) · 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
2026-09-23 21:57:24 +02:00
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.
2026-09-11 02:46:56 +02:00
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.
2026-09-11 02:46:56 +02:00
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-` |
2026-09-11 02:46:56 +02:00
| 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.