# Working rules for `frontend-v3`

Last updated: 8 October 2026. These are the technical rules the code follows. They came from the Vue 3 rewrite (14 rules, proposed 30 Sep 2026, German original in `docs/archive/rewrite-repo/CLAUDE.md`) and are now the rules of `frontend-v3`. They apply to **new or touched code**, not to a big-bang rewrite. Three of them are checked by tests; the budgets in those tests may only get smaller.

General rules for working in this repository (staging, never running root `npm` scripts, backend changes only after agreement) are in `CLAUDE.md`. The plan and the parity checklist are in `V3-PARITY-PLAN.md`.

## The rules

1. **Views do not call `api` or `http` directly.** Data access goes through a composable or a store; the view receives ready data and triggers actions. *Test: `architecture.spec.ts`: no new view may import the transport; the list of old views can only get shorter.*
2. **Anything derivable from plain data lives in its own module** without Vue, DOM or `fetch`: data in, result out, testable without a component (`utils/`). A `computed` that only calculates from props or response data belongs there.
3. **Small components.** One component, one job, roughly under 300 lines. `PollDetailView.vue` is being cut down step by step (structural work track in the parity plan).
4. **IDs are strings** (Hashids). Do not parse them, compare them numerically or sort by them.
5. **Never mix the two answer scales:** `preferences.value` per option (`-2` maybe, `-1` none, `0` no, `1` yes, `2` if needed, `3` waiting list) and `participants.choice` per participant (guest lists only).
6. **No environment-specific values in code** (hosts, addresses). They come from an environment variable.
7. **Every visible text goes through `t()`** (`src/i18n/`). *Not yet true: many screens still have German texts in the template; closing that is parity row 1.10 (language).*
8. **The backend decides** counts, permissions, capacity and waiting lists. The view shows the result and does not recompute it (course planner: never decide "full, so waiting list" in the browser). When in doubt the API contract wins; where it does not reach, ask instead of guessing.
9. **Rules have tests** (Vitest, `npm test`). A bug starts with a failing test.
10. **Every deliberate difference from the classic UI** is written, with the reason, in the header of the module ("Differs from MyPolls.vue: ...").
11. **No new library without a note** in the commit message. Every one is reviewed when the code moves. Known: `xlsx` 0.18.5 has open security reports.
12. **Colors and type only through tokens.** Chain: variable `--k-*` (`src/index.css`, light and dark) to Tailwind name (`tailwind.config.js`) to component or view.
    - Use the Tailwind names in templates (`bg-primary`, `text-foreground`, `text-muted-foreground`, `border-border`, `bg-surface`, `text-destructive`, `bg-destructive-soft`, opacity with `bg-primary/20`), and `var(--k-...)` in CSS.
    - **No** fixed values (`#F2760A`, `rgb()`, `bg-[#...]`) and **no** palette classes (`red-500`, `gray-200`) in `.vue` files.
    - **If a tone is missing**, add a token to `src/index.css` first (light, dark and in `.k-light-overlay`), then name it in `tailwind.config.js`. Not as a one-off value in a component.
    - Color alone carries no meaning: states (error, success, waiting list) also have text or a symbol. Keyboard focus stays visible (`ring` or `--k-focus-ring`); an `outline-none` needs a replacement.
    - **Type by roles, not sizes:** `text-display`, `text-h1`, `text-h2`, `text-h3`, `text-body`, `text-body-sm`, `text-label`, `text-caption` (size, line height and weight together, in `rem`). No role is smaller than 12 px. One family: `--k-font-sans`; no own `font-family` and no fonts from foreign servers (GDPR). No new arbitrary values (`text-[13px]`, `leading-[...]`, `tracking-[...]`, `font-[...]`, `font-size:` in `.vue` files). `text-xs` and `text-sm` stay allowed in old places; new places use the roles. If several places need the same new size, add a role first.
    - *Test: `designTokens.spec.ts`: a budget per file for color literals and type size literals; it may only go down.*
13. **Check for reuse before building; discuss a refactoring before doing it.** Applies to everybody, human or Claude Code.
    - Before any new building block (component, composable, rule module, dialog, side panel) look for an existing one in `src/components/`, `src/composables/`, the rule modules and the views. Use or extend it; do not build a second one beside it.
    - If a third copy of a pattern (shell, header, switch, dialog, save call) would appear, or you notice something worth merging: do not rebuild silently and do not copy silently. Write it up in the pull request or in the structural track of the parity plan (where found, benefit, size) and agree before building.
    - **Exception:** a small extraction without a change of behaviour inside your own change may go in the same pull request, with one sentence in its description.
    - Every pull request has one sentence "Reuse checked: ..." (what was found and used, or why nothing fitted).
14. **Form fields and buttons come from `src/components/ui/`.** `TextField`, `SelectField`, `TextAreaField` (label, error line, hint and ARIA built in) and `AppButton`; no raw `<input>`, `<select>`, `<textarea>` and no hand-styled buttons in views.
    - A field without a visible label (a time in a row) gets `hide-label`, not no label. Fields in rows: `size="sm"`; time and number: `align="center"`.
    - Other attributes (`maxlength`, `placeholder`, `@blur`, `@input`) go to the input element, `class` to the frame (width). The look is in `.k-field-input` (`src/index.css`), not in the view.
    - Exceptions (not text fields): checkbox, radio, range, color, file. *Test: `formFieldsGuard.spec.ts`: its budget is empty, no new raw field is allowed.*

## How we test a change

- Rules get unit tests; behaviour of a screen gets a page-level test that mounts the real view with the network replaced (`src/__tests__/pollDetailKit.ts` is the toolkit). Before moving code out of a big view, write the characterisation test first, then move.
- A test that waits (`until(...)`) must be followed by an explicit `expect`; `until` does not fail on a timeout.
- Check a new test by breaking the code on purpose and seeing it fail.
- Before a commit: `cd frontend-v3`, then `npx vitest run` and `npm run build` (type check included). Never the root `npm run build`.
- A change that can be seen is also checked in a browser against the classic UI on the same poll (`?classic=1`), including the network panel and the phone width.
