# T5 Optik mit eigener Handschrift Implementation Plan
**Status (2026-10-16):** Noch nicht begonnen. Plan-Review erledigt (Abschnitt „Review 2026-10-16“ direkt unten), Bewegungs-Sprache steht in Spec §17 und ist als Task A11 eingeplant. Offen beim Inhaber: Spec §16 Fragen 1–3 (vorläufig A/A/A) und die neue Frage 4 (Bewegungs-Sprache, Empfehlung A). Issue: [#26](https://git.encry.de/Nick.Rosenloecher/ApiCore/issues/26).
## Review 2026-10-16
Geprüft gegen den Code nach Beute L1–L5, T4a/T4b, T6 und T7 L1/L2/L3/L5. Die Korrekturen gelten vor dem jeweiligen Task-Text. Wo sie ihm widersprechen, gilt die Korrektur.
**Allgemein**
- G1 (Voraussetzungen): T4a **und T4b** sind gemergt (TASKS.md). T5b läuft damit ohne Kalender-Rückfall.
- `PHASE_SOURCE_LABELS` liegt in `src/lib/phenology/labels.ts`; die lokale Ersatzdefinition entfällt.
- `SeasonChip` liegt in `src/components/autopilot/season-chip.tsx`, `fetchAutopilotToday` in `src/lib/autopilot/api.ts`, `INTENT_ICONS` in `src/components/autopilot/next-steps-card.tsx`.
- G2 (Global Constraints, Zeile „Bewegung“): ersetzt durch Spec §17. Ab sofort gilt: `motion-full:` statt `motion-safe:` für jede Verschiebung, Tokens aus `src/lib/motion/tokens.ts`, nichts Datenhaftes bewegt sich. Rad, Lebenslinie und Waben bleiben ohne Animation.
- G3 (Bestand `motion`): Installiert ist `motion` **13.4.6**, nicht 12.x. `maath` ist da, `framer-motion` nur transitiv. Kein Quelltext importiert `motion/react` (Beute §9.8 sah es für die DOM-Teile vor, gebaut wurde CSS). Task A11 bringt die ersten DOM-Nutzer. Ohne A11 wäre die Abhängigkeit ungenutzt.
- G4 (Aufwand): T5a wächst von ≈ 4,75 auf ≈ 6,4 PT. Gründe: A7 +0,4 (31 statt 17 Checkbox-Stellen), neue Tasks A11 (0,75) und A12 (0,5). T5b bleibt bei ≈ 5,25 PT.
- G5 (Wellen T5a): A11 läuft in Welle 9 nach A8. Sie teilt `globals.css` mit A1/A4/A5, `today-panel.tsx` mit A7 und `ui/*` mit A5. A12 läuft in Welle 9 parallel; sie fasst nur `components/hive/**` an, `globals.css` nicht, weil die Holo-Sonne-Werte schon in A1 kommen. A10 rückt in Welle 10 und wartet auf A8, A9, A11 und A12.
**Je Task**
- A1:
- `--holo-*` (in `:root`/`.dark` seit Beute L3) bekommt Werte in beiden Sonne-Sätzen: Bühne `oklch(0 0 0)`, Kanten und Etikett-Text `oklch(1 0 0)`, Etikett-Fläche `oklch(0.12 0 0)`.
- Neu ist der Rahmen `.holo-stage { --ring: var(--holo-edge-strong) }` (Spec §17.8).
- `contrast.spec.ts` bekommt zwei Zeilen: `holo-label-fg`/`holo-label-bg` ≥ 7 und `holo-edge-strong`/`holo-bg` ≥ 3, in allen vier Sätzen.
- Grund: `--primary-ink` auf `--holo-bg` ≈ 2,8:1.
- A2:
- `motion-preference.ts` hat seit der Beute `useEffectiveMotionLevel` und `MOTION_EVENT`. Nutzer sind `hive-canvas.tsx`, `hive-visual-slot.tsx` und `today/use-task-actions.ts`. Die API bleibt; `useAppearance().setMotion` löst `MOTION_EVENT` aus.
- Neu ist das Ereignis `apicore:appearance` bei jedem Wechsel von Schema oder Kontrast; A12 hört darauf.
- Test dazu: `it('a motion change from the appearance section reaches useEffectiveMotionLevel in the same tab', ...)`.
- Die Stufe heißt in der Oberfläche „Dezent“.
- A3:
- Verlauf-Verbraucher sind **33 Dateien**, nicht 31. `text-white` steht zusätzlich in `components/hive/editor/editor-stage-overlay.tsx` (auf der Bühne → `text-holo-label-fg`).
- `AREAS` fehlen Bereiche aus T6/T7/Beute, die in `nav-config.ts`/`quick-actions.ts` stehen. Neue Schlüssel:
- `compliance` (`ShieldCheck`, betrieb)
- `medicines` (`Pill`, erfassung)
- `voice` (`Mic`, erfassung)
- `hive-systems` (`Box`, neutral)
- `system` (`Activity`, neutral)
- `areas.spec` prüft danach wieder jeden Nav- und Quick-Action-Eintrag.
- Neue Icon-Abweichungen, die korrigiert werden müssen:
- Arzneimittel-Packungen nutzen `Package` (= Abfüllungen) in `medicines/[id]/page.tsx` und `medicines/lots/[id]/page.tsx`.
- Beutensysteme nutzen `Boxes` (= Rohstoffe) in `settings/hive-systems/page.tsx`, die Navigation dagegen `Box`.
- `settings/voice/page.tsx` nutzt `ShieldCheck` (= Prüfbereitschaft).
- Das Dashboard trägt seit T6 `ComplianceLine` und `StandNote`; beide bleiben unverändert.
- A4:
- Die Codemod-Trefferzahlen vor dem Lauf neu zählen (`grep`), die Zahlen im Task sind vom 14.10.
- `dark:` steht weiter in genau 8 `ui/*`-Dateien.
- Die Beute-Bühne (`bg-holo-*`) bleibt unberührt: Sie ist in hell und dunkel absichtlich dunkel.
- A5:
- `sm:h-`/`sm:min-h-` stehen in **11 Dateien**, nicht in 4.
- Der Fokus-Codemod trifft 5 Stellen in `components/hive/**`. Dort entfällt der eigene Ring, und der Ring kommt aus `.holo-stage` (A1).
- `.honey-check` hat `outline: none` plus eigenen Schatten-Ring in `globals.css`. Er nimmt die globale Regel, die Ausnahme entfällt.
- A6:
- `NumberStepper` hat seit Beute L2 `step` mit Dezimalschritten; die Umstellungen nutzen nur ganze Schritte.
- Neue Radio-Eigenbauten → `ChoiceGroup`: `compliance/notices/create-notice-dialog.tsx` (`type="radio"`) und `components/phenology/phenology-report-sheet.tsx` (`type="radio"`).
- Neues `aria-pressed` in `components/hive/hive-card.tsx` („Aufbau als Liste“) → `Button pressed`.
- Ergänze die Dateien in der Files-Liste und im Testbefehl (`"src/app/(app)/compliance" src/components/phenology src/components/hive`).
- A7: Heute sind es **31 Stellen in 27 Dateien** (13 `ToggleChip`, 13 native, 4 Base UI, 1 `today-panel`).
- `ToggleChip`: 13 Stellen in 11 Dateien, neu dazugekommen:
- `compliance/notices/create-notice-dialog.tsx`
- `compliance/restrictions/create-restriction-dialog.tsx`
- `compliance/settings/page.tsx` ×3
- `medicines/medicine-form-dialog.tsx`
- `profile/telegram-link-card.tsx`
- `settings/automation/automation-rules-tab.tsx`
- `settings/automation/rule-forms/rule-form-parts.tsx`
- Natives `type="checkbox"`, neu: `settings/hive-systems/system-dialog.tsx`, `settings/voice/page.tsx`, `components/colonies/colony-takeover-section.tsx`.
- `components/form/switch-row.tsx` (`role="switch"`, Nutzer `profile/notification-settings-card.tsx`, `telegram-link-card.tsx`) geht in `CheckboxField` auf. Dafür bekommt `CheckboxField` das Prop `role?: "checkbox" | "switch"`, Umsetzung mit Base UI `Switch`. Danach wird `SwitchRow` gelöscht. Die Wächter-Regel erlaubt `role="switch"` nur in `ui/checkbox.tsx`/`ui/switch.tsx`.
- `today-panel.tsx` (`task`) kommt zuletzt, weil A11 dieselbe Datei danach anfasst.
- Aufwand 0,9 PT.
- A8:
- `ActiveHistoryTabs` hat jetzt **10 Nutzer**, neu ist `medicines/page.tsx`; sie kommt in die Files-Liste.
- `TabSwitcher` hat unverändert 2 Nutzer.
- A9:
- `SHELL_CACHE` ist weiter `apicore-shell-v4` → v5 gilt.
- `SW_ROUTES_VERSION` haben Beute L5 und T7 erhöht. „Bleibt unverändert“ heißt: A9 erhöht sie nicht noch einmal.
- `/inspections/new`, `/varroa/new`, `/voice/new` stehen in `SHELL_ROUTES`. Ein vierter Shortcut „Sprachnotiz“ (`/voice/new`) ist möglich, aber nicht Spec; nur nach Freigabe.
- A10:
- `src/app/(app)/dashboard/page.spec.tsx` gibt es schon → Modify, nicht Create.
- Die Screenshot-Matrix bekommt die Seiten aus T6, T7 und Beute:
- Volk mit Beute-Karte (Bühne, Fokus, Sheet)
- `/colonies/[id]/hive` (Umbauen)
- `/voice` und `/voice/new` (Aufnahme-Punkt)
- `/settings/voice`
- `/offline`, `/offline/apiary` und `/offline/colony`
- das Warteschlangen-Sheet (`components/offline/pending-queue-sheet.tsx`) und das Offline-Band
- `/compliance`, `/medicines`
- Die Matrix läuft je in Voll, Dezent und Aus, zusätzlich mit `prefers-reduced-motion: reduce` auf Start, Volk und `/voice/new`.
- A11 **neu**: Bewegungs-Sprache durchsetzen (Task-Text unten nach A10).
- A12 **neu**: Beute-Bühne an Sonne und Darstellungswechsel anschließen (Task-Text unten nach A11).
- B1: Die Offline-Standseite `src/app/(app)/offline/apiary/page.tsx` zeigt die Wabe aus dem Paket mit demselben `ApiaryComb` (Modify ergänzen, Test `it('the offline apiary page shows the comb from the pack', ...)`). `apiary-colonies-card.tsx` nutzt `inspectionDueOn` schon. Die Tests für den Zweig ohne T4 bleiben für alte Pakete.
- B3: Die Keys stimmen (`varroa-measurements`, `swarm-control-checks`, `next-steps`, `history`). `["colonies", id, "hive"]` gibt es neu. Beute-Änderungen als Ereignisse sind laut Beute §9.10 nicht Teil von T5; die Lebenslinie lässt sie aus.
- B4: Die Volksseite hat seit Beute L2 ein neues Raster:
- oben `NextStepsCard` über die volle Breite
- Zeile 1: Stammdaten und Beute, gleich hoch, ohne `items-start`
- Zeile 2: Aktionen | Karten
- „Desktop rechte Spalte oben“ ist jetzt die Beute. Die Lebenslinie kommt in Zeile 2 als erstes Kind der 2-Spalten-Fläche, über dem `xl:grid-cols-2`-Raster.
- Reihenfolge im DOM: Stammdaten, Beute, Aktionen, Lebenslinie, Karten.
- Test `it('the colony page puts the lifeline under „Als Nächstes“', ...)` → `it('the lifeline opens row 2 above the record cards and leaves row 1 to Stammdaten and Beute', ...)`.
- B5: Unverändert gültig. `AutopilotToday.seasons` hat weiter weder `segmentSince`, `reportedOn` und `note` noch `seasonCalendar`. Die Dateien sind `src/autopilot/next-steps.service.ts`, `src/autopilot/dto/next-steps-response.dto.ts` und `test/next-steps.e2e-spec.ts`.
- B6: `season-defaults.spec.ts` liest nicht `default-rules.ts`, sondern `ApiCore-Backend/src/automation/rule-config/phenology.config.ts` (`calendarFallback`, Schwarmzeit) und `season-autopilot.config.ts` (`winterVarroaWindow`).
- B7: Das Dashboard hat heute noch Honigbanner und `StatTile`; A3 und B7 sind also weiter nötig. `ComplianceLine` und `StandNote` bleiben über „Bienenjahr“ an ihrem Platz.
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** ApiCore ist draußen am Stand lesbar und bedienbar (Sonne, Handschuhe, eine Hand) und erkennbar eine Imker-App: vier geprüfte Farbsätze (hell, dunkel, Sonne hell, Sonne dunkel), ein durchgesetzter Baukasten und die Wabenzelle als einziges Motiv (Waben-Standkarte, Mini-Waben-Marker, Lebenslinie, Bienenjahr-Rad, Markenzeichen).
**Architecture:** Farben stehen nur als Tokens in `globals.css`; ein Wächter-Test misst jedes Paar aus Spec 4.4 in allen vier Sätzen. Ein Inline-Skript im `
` setzt Farbschema, Kontrast und Bewegung vor dem ersten Bild (`next-themes` entfällt). Die Umstellung läuft in der Reihenfolge der Spec (Tokens additiv, Darstellung, Bereichs-Icons, Farben, Fokus, Bausteine), sodass jede Seite nach jedem Task lesbar bleibt; ein Baukasten-Wächter hält das Ergebnis. Grafiken sind handgeschriebenes SVG über reine Modellfunktionen (`cell-state`, `combLayout`, `buildLifeline`, `buildYearWheel`), die Ansichten sind dünn und liefern zu jeder Grafik eine Tabelle.
**Tech Stack:** Next.js 16 App Router, React 19, Tailwind 4 (`@theme inline`, `@custom-variant`), shadcn/Base UI, TanStack Query 5, react-leaflet/Leaflet 1.9, Vitest 4 + RTL, `sharp` (neue devDependency, nur für das Icon-Skript). Backend-Zusatz in NestJS 11/Jest 30 (Task B5). Keine neue Laufzeit-Bibliothek.
**Spec:** `docs/superpowers/specs/2026-10-13-t5-optik-design.md` (bindend). Bausteine aus `docs/superpowers/plans/2026-10-13-t3-erfassung-am-stand.md` und `docs/superpowers/plans/2026-10-13-t4-logik-ketten.md`.
**Lieferungen:** T5a „Sonnentauglich und Baukasten“ (Tasks A1–A12, ≈ 6,4 PT nach Review 2026-10-16, hängt nur an T3) und T5b „Handschrift“ (Tasks B1–B8, ≈ 5,25 PT, nach T4a; ohne T4 greift der Kalender-Rückfall).
**Vorläufige Inhaber-Entscheidungen (Empfehlungen, vor der Ausführung noch überstimmbar):** Hauptknopf = heller Honig mit dunkler Tinte (Offene Frage 1, A, 9,4:1). Zellen der Waben-Standkarte nach Volkscode (Frage 2, A). Standard-Farbschema auf neuem Gerät = Hell (Frage 3, A). Kippt eine Antwort, ändert sich nur das Token bzw. eine Konstante (`DEFAULT_THEME` in Task A2, `--primary*` in Task A4, `sortCells` in Task B1); `contrast.spec.ts` hält jede Variante.
---
## Global Constraints
**Voraussetzungen (nicht neu bauen, genau so nutzen)**
- T3 ist gemergt: Tasks 7 (`nav-config.ts`, `quick-actions.ts`, Sheets), 8 (`NumberStepper`, `RatingScale` mit `RATING_TONE_CLASS`, `rating-contrast.spec.ts`), 11 (Varroa-Trend als handgeschriebenes SVG), 14 (`colonyStatusLight`, `StatusLightBadge`, `ApiaryColoniesCard`, `useColonyOverview`/`colonyOverviewKey`), 15 (Offline-Paket, `withPackFallback`, Service Worker mit `SHELL_ROUTES`).
- T5b braucht T4a: Tasks 1 (`rule-config`), 15 (`ColonyOverviewRow.inspectionDueOn`, `NextStepsService.today`, `AutopilotToday`), 17, 18 (`SeasonChip`, `SEGMENT_LABELS`, Heute-Karten), 19 (`rating`/`band` je Messung). T4b (Task 31 `PHASE_SOURCE_LABELS`) ist optional; fehlt es, definiert T5b die drei Quellen-Texte lokal ('gemeldet', 'geschätzt', 'Kalender').
- Alle Pfade relativ zu `ApiCore-Frontend/`, sofern nicht anders angegeben; Backend-Pfade relativ zu `ApiCore-Backend/`.
- Vor jedem Next.js-Code die passende Anleitung unter `node_modules/next/dist/docs/` lesen (`AGENTS.md`); für T5 vor allem `01-app/02-guides/preventing-flash-before-hydration.md`, `progressive-web-apps.md`, `01-app/03-api-reference/03-file-conventions/01-metadata/manifest.md`, `app-icons.md`.
- Grafiken: den `dataviz`-Skill laden (wie T3/T4). Gestaltung: `impeccable` führt; keine neue Laufzeit-Bibliothek, `next-themes` fliegt raus.
**Verhalten und Barrierefreiheit**
- WCAG 2.2 AA. Text ≥ 4,5:1, Bedienrahmen, Fokus und Grafik ≥ 3:1; Fließtext und Hauptknopf Ziel ≥ 7:1.
- Touch-Ziele ≥ 44 px, Sheet-Zeilen 56 px, benachbarte Ziele ≥ 4 px Abstand. Kompaktmaß (32/36 px) nur mit `pointer-fine:`, nie mit `sm:`.
- Grundschrift 16 px (`text-base`) für Fließtext, Labels, Auswahl-Knöpfe, Listentitel, Knopftext, Eingabe; Nebentext 14 px (`text-sm`); `text-xs` = 13 px nur für Achsen, Legenden, Fußzeilen; in Sonne 16/14 px. Überschriften Space Grotesk, nie in Knöpfen oder Labels.
- Ein Fokusstil: 2 px durchgehender Ring in `--ring`, 2 px Abstand (Sonne 3 px), als Basisregel auf `:where(:focus-visible)`. Einzige Ausnahme: `honeycomb-cell.tsx` (zweiter Sechseckpfad, Spec 6.6).
- Zustand nie nur über Farbe: Icon plus Wort, Rand-Stufen über Strichstärke und Strichelung. Jede Grafik hat ``, `` und eine aufklappbare Tabelle aus demselben Modell.
- Bewegung (geändert 2026-10-16): Spec §17 ist bindend. Stufen über `useEffectiveMotionLevel()` und die CSS-Variante `motion-full:` (nie `motion-safe:` für Verschiebung). Tokens kommen aus `src/lib/motion/tokens.ts` und `--motion-*`. Bewegt werden nur `transform` und `opacity`, Datenwerte, Fehlertexte und Eingaben nie. `motion` nur über `LazyMotion`/`m.*` im `(app)`-Layout. Rad, Lebenslinie und Waben erscheinen weiter sofort. Durchgesetzt wird das in Task A11, der Wächter prüft es.
- Deutsche Oberfläche, kurze Sätze, ein Hauptknopf (Honig) je Ansicht, ADHS-freundlich: wenig kognitive Last. Code-Bezeichner englisch.
- Verboten (Wächter prüft): Weiß auf Honig, `text-primary` als Schriftfarbe (nur `text-primary-ink`), `ring-ring/50`, `outline-none`, Farbverlauf als Hintergrund, `text-white`, `dark:`-Klassen, Text < 13 px, Zustand nur über Farbe, eigene `role="radio|checkbox|tab"`-Bauten außerhalb des Baukastens. Ein Bereichston ist nie eine Zustandsfarbe.
- Wiederverwenden statt kopieren: `StatusLightBadge`, `colonyStatusLight`, `ChoiceGroup`, `NumberStepper`, `Field`, `QueryState`, `PageHeader`/`DetailHeader`/`EntityCard`, `useColonyOverview`, Query-Keys der bestehenden Karten (keine Doppel-Requests).
**Tokens (exakte Werte, OKLCH ist die Quelle, alle im sRGB-Gamut)**
| Token | Hell | Dunkel |
|---|---|---|
| `--background` | `oklch(0.968 0.014 85)` | `oklch(0.19 0.016 55)` |
| `--foreground` | `oklch(0.21 0.03 55)` | `oklch(0.95 0.018 80)` |
| `--card`, `--popover`, `--sidebar` | `oklch(0.995 0.004 90)` | `oklch(0.235 0.018 55)` |
| `--muted` | `oklch(0.925 0.018 82)` | `oklch(0.28 0.018 55)` |
| `--muted-foreground` | `oklch(0.44 0.03 60)` | `oklch(0.76 0.025 75)` |
| `--primary` | `oklch(0.80 0.145 78)` | `oklch(0.80 0.14 78)` |
| `--primary-foreground` | `oklch(0.21 0.035 55)` | `oklch(0.19 0.03 55)` |
| `--primary-ink` neu | `oklch(0.47 0.115 55)` | `oklch(0.82 0.13 80)` |
| `--ring` | = `--primary-ink` | = `--primary-ink` |
| `--secondary` / `-foreground` | `oklch(0.44 0.07 145)` / `oklch(0.98 0.015 100)` | `oklch(0.66 0.08 145)` / `oklch(0.16 0.02 145)` |
| `--accent` / `-foreground` | `oklch(0.935 0.03 78)` / `oklch(0.25 0.045 50)` | `oklch(0.31 0.03 60)` / `oklch(0.95 0.018 80)` |
| `--destructive` | `oklch(0.50 0.17 28)` | `oklch(0.70 0.15 28)` |
| `--destructive-foreground` neu | `oklch(0.985 0.005 30)` | `oklch(0.17 0.03 30)` |
| `--border` (nur Trennlinien) | `oklch(0.86 0.022 78)` | `oklch(0.34 0.02 60)` |
| `--input` (Rahmen jedes Bedienelements) | `oklch(0.60 0.035 65)` | `oklch(0.56 0.025 70)` |
| `--warning` / `-foreground` | `oklch(0.79 0.15 80)` / `oklch(0.23 0.04 60)` | `oklch(0.80 0.14 80)` / `oklch(0.20 0.03 60)` |
| `--warning-ink` neu | `oklch(0.50 0.11 65)` | `oklch(0.82 0.13 82)` |
| `--success` / `-foreground` | `oklch(0.46 0.08 145)` / `oklch(0.98 0.015 100)` | `oklch(0.70 0.09 145)` / `oklch(0.16 0.02 145)` |
| `--condition-excellent` / `-foreground` / `-ink` | `0.62 0.13 150` / `0.15 0.04 150` / `0.40 0.09 150` | `0.68 0.12 150` / `0.17 0.04 150` / `0.80 0.11 150` |
| `--condition-good` | `0.645 0.13 125` / `0.17 0.04 125` / `0.41 0.09 125` | `0.72 0.12 125` / `0.18 0.04 125` / `0.83 0.11 125` |
| `--condition-average` | `0.665 0.125 88` / `0.19 0.04 90` / `0.44 0.09 85` | `0.77 0.13 90` / `0.20 0.04 90` / `0.86 0.11 90` |
| `--condition-weak` | `0.665 0.14 58` / `0.18 0.04 55` / `0.46 0.12 52` | `0.74 0.13 60` / `0.19 0.04 55` / `0.82 0.11 60` |
| `--condition-critical` | `0.62 0.17 30` / `0.15 0.04 30` / `0.47 0.16 28` | `0.68 0.16 28` / `0.16 0.04 30` / `0.78 0.11 28` |
| `--chart-1` … `--chart-5` | `0.56 0.13 55`, `0.46 0.08 145`, `0.60 0.12 85`, `0.50 0.17 28`, `0.52 0.08 250` | `0.78 0.13 70`, `0.70 0.09 145`, `0.82 0.12 90`, `0.70 0.15 28`, `0.72 0.08 250` |
| `--area-betrieb` / `-erfassung` / `-arbeit` / `-ernte` / `-neutral` neu | `0.46 0.08 150`, `0.50 0.12 60`, `0.48 0.08 250`, `0.52 0.11 75`, `0.45 0.02 60` | `0.74 0.09 150`, `0.80 0.12 70`, `0.76 0.07 250`, `0.80 0.10 80`, `0.76 0.02 70` |
| `--queen-white/-yellow/-red/-green/-blue` neu | `0.97 0.005 90`, `0.86 0.17 95`, `0.58 0.20 27`, `0.58 0.15 145`, `0.52 0.15 255` | gleich, außer rot `0.62 0.20 27`, grün `0.66 0.15 145`, blau `0.62 0.15 255` |
| `--radius`, `--shadow-soft`, `--shadow-elevated` | unverändert | unverändert |
(Condition-Zeilen: Fläche / Text auf Fläche / Tinte, jeweils `oklch(L C H)`.) Entfällt: `--gradient-honey/-sage/-sky`. Sätze in dieser Reihenfolge in `globals.css`: `:root`, `.dark`, `:root[data-contrast="sun"]`, `.dark[data-contrast="sun"]`; `@custom-variant dark (&:where(.dark, .dark *));` dazu `sun`, `pointer-fine`, `pointer-coarse`. `@media print` setzt den hellen Satz; `@media (forced-colors: active)` nutzt `CanvasText`-Striche.
**Sonne (überschreibt nur diese Tokens; Rest erbt vom Grundsatz)**
| Token | Sonne hell | Sonne dunkel |
|---|---|---|
| `--background`, `--card`, `--popover`, `--sidebar` | `oklch(1 0 0)` | `oklch(0.10 0.005 55)` |
| `--foreground`, `--primary-foreground`, `--ring` | `oklch(0.13 0.02 55)` | `--foreground`/`--ring` `oklch(0.99 0.005 85)`, `--primary-foreground` wie dunkel |
| `--muted` | `oklch(0.95 0.01 80)` | `oklch(0.20 0.01 55)` |
| `--muted-foreground` | `oklch(0.32 0.03 60)` | `oklch(0.88 0.02 80)` |
| `--border` | `oklch(0.40 0.03 60)` | `oklch(0.70 0.02 70)` |
| `--input` | `oklch(0.20 0.03 55)` | `oklch(0.92 0.01 80)` |
| `--primary-ink` | `oklch(0.38 0.10 52)` | wie dunkel |
Regeln unter `[data-contrast="sun"]`: `--text-sm: 1rem`, `--text-xs: 0.875rem`, `--focus-width: 3px`, `--control-border-width: 2px`, `--shadow-*: none`, Kopfzeile undurchsichtig (`backdrop-filter: none`), Tönungen `bg-*/15` werden über die Hilfsklasse `.tint` zu `bg-card` mit 2-px-Rahmen in der Tinte, Wabenränder 3/5/7 px statt 2/4/6 px, keine Honig-Tönung unter der Stärkekurve.
**Gemessene Kontraste (Soll, `contrast.spec.ts` prüft jede Zeile mit ihrer Grenze)**
| Paar | hell | dunkel | Sonne hell | Sonne dunkel | Grenze |
|---|---|---|---|---|---|
| Text auf Hauptknopf (`--primary-foreground` / `--primary`) | 9,38 | 9,77 | 10,61 | 9,77 | 7 |
| Grundtext / Seite | 16,23 | 15,98 | 20,15 | 20,01 | 7 |
| Grundtext / Karte | 17,56 | 14,45 | 20,15 | 20,01 | 7 |
| Nebentext / Seite | 7,13 | 8,60 | 12,77 | 14,33 | 4,5 |
| Nebentext / `--muted` | 6,27 | 6,81 | 11,03 | 12,62 | 4,5 |
| Link (`--primary-ink`) / Seite | 6,48 | 10,48 | 10,37 | 11,66 | 4,5 |
| Eingaberahmen (`--input`) / Karte | 3,92 | 3,58 | 18,19 | 16,25 | 3 |
| Eingaberahmen / Seite | 3,62 | 3,96 | 18,19 | 16,25 | 3 |
| Fokusring / Karte (mit Abstand) | 7,01 | 9,47 | 20,15 | 20,01 | 3 |
| Fokusring / Seite | 6,48 | 10,48 | 20,15 | 20,01 | 3 |
| Warn-Icon/-Text (`--warning-ink`) / Karte | 6,09 | 9,49 | 6,18 | 11,68 | 4,5 |
| Warn-Text / Warn-Tönung | 5,53 | 6,92 | 5,60 | 9,37 | 4,5 |
| Fehlertext / Karte | 6,45 | 5,87 | 6,54 | 7,23 | 4,5 |
| Fehlertext / Fehler-Tönung | 5,50 | 5,06 | 5,57 | 6,58 | 4,5 |
| Salbei-Knopf | 7,11 | 6,43 | 7,11 | 6,43 | 4,5 |
| Ampel-Abzeichen (Tinte / Tönung), schlechteste Stufe | 6,06 | 6,45 | 6,14 | 8,49 | 4,5 |
| Text auf Ampelfläche, schlechteste Stufe | 5,02 | 6,32 | 5,02 | 6,32 | 4,5 |
| Ampelfläche / Karte, schlechteste | 3,03 | 5,41 | 3,07 | 6,66 | 3 |
| `chart-1…5` / Karte, schlechteste | 3,93 | 5,87 | 3,99 | 7,23 | 3 |
| Bereichston / Karte, schlechtester | 5,54 | 7,52 | 5,54 | 7,52 | 3 |
| Königinnenfarbe mit Tinten-Ring / Karte (Ring) | 17,56 | 14,45 | 20,15 | 20,01 | 3 |
Die Honigfläche hat gegen die Karte nur 1,87:1; das ist zulässig, weil ein Knopf über seinen Text erkannt wird und gewählte Zustände Rahmen plus Haken tragen.
**Arbeitsregeln für Ausführende**
- Agents committen nie. Der Orchestrator prüft und committet pro Task einmal; Pushes gebündelt (CI nicht zuspammen).
- Standard-Verifikation **V** (Befehle in `ApiCore-Frontend/`): `npm run lint && npm run typecheck && npx vitest run && npm run build`. In parallelen Wellen baut nur der Orchestrator (`.next` ist nicht parallelfähig); Agents lassen `npm run build` dort aus und melden es.
- Standard-Browserprüfung **VB** (nur UI-Tasks): gegen `npm run build && npm run start` mit Demo-Daten, per Chrome DevTools (`emulate`, `resize_page`, `take_screenshot`): Desktop 1366 px und 390 px; Tastatur (Tab-Reihenfolge, sichtbarer Fokus, Pfeiltasten wo zutreffend); `prefers-reduced-motion: reduce`; Modi hell, dunkel, Sonne hell, Sonne dunkel (ab Task A2 schaltbar, dunkel ab A4 ohne Flag); die betroffenen Seiten sind je Task genannt. Befunde als Liste im Bericht, Bilder nicht im Repo.
- `src/styles/baukasten-guard.spec.ts` (angelegt in Task A5) wächst task-weise: jeder Task trägt die Regel ein, die er erfüllt, samt Ausnahmen mit Begründung.
---
## Review Focus
1. **Flackern und blockierter Speicher:** Der erste Frame darf nie im falschen Modus erscheinen; ohne `localStorage` (Privatmodus) muss die App hell starten statt zu brechen.
- Task A2 `the inline script sets class, data-contrast and data-motion before first paint`
- Task A2 `blocked storage falls back to the default`
2. **Unlesbarer Dunkelmodus und Ampel nur über Farbe:** Abzeichen, Warnungen und Zellen müssen in allen vier Sätzen ≥ 4,5:1 (Text) bzw. ≥ 3:1 (Fläche) haben und Icon plus Wort tragen.
- Task A4 `contrast table 4.4 holds in all four sets`
- Task A4 `every condition tone has surface, text-on-surface and ink in all four sets`
- Task B1 `a cell always shows icon and code, never colour alone`
3. **Fehlende Rechte, Offline und alte Backends:** Ohne `inspections:read`, mit 403 je Spur, ohne T4-Felder oder ohne `seasonCalendar` darf nichts brechen und nichts Falsches behaupten.
- Task B1 `without inspections:read the rim is neutral and says nothing about inspections`
- Task B3 `a 403 on a track drops the track without an error`
- Task B6 `without seasonCalendar the wheel uses the season defaults`
4. **Extreme Daten:** 0, 1 und über 100 Völker; zwanzig und mehr Stände; sehr lange Codes; Lücke von über 45 Tagen; Messwerte vor dem Einheitenwechsel; Schaltjahr und Jahreswechsel im Rad.
- Task B1 `an empty apiary shows the empty hint and no comb`
- Task B1 `100 cells render with one shared symbol`
- Task B3 `values before STRENGTH_IN_FRAMES_SINCE are not in the curve but in the table`
- Task B6 `winter rest over the year change is two arcs`
5. **Migration reißt Seiten ein:** Entfernte Props (`gradient`), gelöschte Bausteine (`ToggleChip`, `TabSwitcher`) und verlorene Fokus-/Größenklassen dürfen keine Seite unlesbar oder unbedienbar zurücklassen.
- Task A3 `no file passes a gradient prop any more`
- Task A5 `no component removes the outline without the global rule`
- Task A10 `baukasten guard passes with the documented exceptions only`
- Task A9 `every manifest icon exists with the declared size`
6. **Tastatur und Forced Colors in eigenen Bedienelementen** (Wabenzelle, Tabs, Auswahlgruppe, Marker): Fokus sichtbar, Pfeiltasten, Namen mit sichtbarem Text am Anfang.
- Task B1 `focus draws the ring on the outer hexagon path`
- Task A8 `arrow keys switch the tab and the panel is labelled by it`
---
## Abhängigkeiten und Parallelisierung
**T5a** (nur T3; Tasks A1–A5 laufen in einem kurzen Fenster ohne andere offene Frontend-Tasks, weil sie viele Dateien mechanisch berühren):
| Welle | Tasks (parallel, verschiedene Dateien) | wartet auf |
|---|---|---|
| 1 | A1 | T3 |
| 2 | A2 | A1 |
| 3 | A3 | A1, A2 (gleiche Layout-Dateien) |
| 4 | A4 | A3 (Verlauf-Verbraucher müssen vorher weg sein) |
| 5 | A5, A9 | A5 → A4; A9 → A3 (`BrandMark`-Geometrie) |
| 6 | A6 | A5 |
| 7 | A7 | A6 (`winter-check-dialog`, `label-designer`, `swarm-control-edit-dialog` gemeinsam) |
| 8 | A8 | A7 (`tasks/page.tsx`, `settings/automation/*` gemeinsam) |
| 9 | A11, A12 | A11 → A8 (`today-panel.tsx`, `ui/*`, `globals.css`); A12 → A1, A2 |
| 10 | A10 | A8, A9, A11, A12 |
**T5b** (nach T4a-Frontend; ohne T4 mit Kalender-Rückfall lauffähig, aber dieselben Dateien wie T4 Tasks 17/18/20, darum danach):
| Welle | Tasks (parallel, verschiedene Dateien) | wartet auf |
|---|---|---|
| 1 | B1, B3, B5 | T5a (A10), T4a |
| 2 | B2, B4, B6 | B2 → B1 (`summarizeColonies`); B4 → B3; B6 → B5 (Vertrag) |
| 3 | B7 | B6 |
| 4 | B8 | B2, B4, B7 |
Geteilte Dateien, deshalb je Welle nur ein Task:
- `src/app/globals.css`: A1 (W1), A2 nur falls nötig nicht, A4 (W4), A5 (W5); T5b ändert sie nicht (Waben-CSS liegt in `src/styles/comb.css`, Leaflet-CSS in `src/styles/leaflet-theme.css`, jeweils aus der Komponente importiert).
- `src/app/layout.tsx`, `src/app/providers.tsx`, `src/app/(app)/layout.tsx`, `src/app/(app)/nav-config.ts`, `src/app/(app)/quick-actions.ts`, `src/app/(app)/sidebar.tsx`, `src/app/(app)/bottom-nav.tsx`: A2 (W2), A3 (W3), A9 nur `layout.tsx` Metadaten nicht (liegt in `manifest.ts`), A10 nur Dashboard.
- `src/app/(app)/dashboard/page.tsx`: A3 (W3, Banner raus), A10 (W9, Hinweis-Karte), B7 (W3 T5b). T4 Task 18 (Welle 6 von T4) ändert dieselbe Datei: wer später dran ist, rebased; kein Parallellauf.
- `src/components/ui/*`: A4 (W4, `dark:` raus), A5 (W5), A7 (checkbox), A8 (tabs).
- `src/components/form/choice-group.tsx`, `number-stepper.tsx`, `rating-scale.tsx`: nur A6 (W6) bzw. A4 (Farbklassen, W4).
- `src/app/(app)/winter/winter-check-dialog.tsx`, `settings/label-templates/designer/label-designer.tsx`, `colonies/[id]/swarm-control-edit-dialog.tsx`: A6 (W6), A7 (W7).
- `src/app/(app)/tasks/page.tsx`, `settings/automation/*`: A7 (W7), A8 (W8).
- `src/styles/baukasten-guard.spec.ts`: A5, A6, A7, A8 (je eigene Welle), A10.
- `package.json`/`package-lock.json`: A2 (W2, `next-themes` raus), A9 (W5, `sharp` rein).
- `public/sw.js`, `src/proxy.ts`: nur A9.
- `src/lib/honeycomb/*`: B1 (`cell-state`, `layout`), B2 (`mini-comb-svg`, je eigene Datei).
- `src/app/(app)/apiaries/page.tsx`, `apiaries/[id]/*`: B1 (W1); `apiaries/map/page.tsx`, `src/components/apiary-map.tsx`: B2.
- `src/app/(app)/colonies/[id]/page.tsx`: B4 (W2). `src/lib/autopilot/types.ts`: B6 (W2).
- `ARCHITECTURE.md`, `TASKS.md`: nur der Orchestrator (Texte liefert B8).
---
## Tasks
## Lieferung T5a – Sonnentauglich und Baukasten
### Task A1: Tokens additiv und Kontrast-Wächter (Spec 10.8 Schritt 1)
**Files**
- Create: `src/styles/css-tokens.ts` (+ `css-tokens.spec.ts`), `src/styles/contrast.spec.ts`
- Modify: `src/app/globals.css`
**Interfaces**
- Produces (nur für Tests):
```ts
export type TokenSet = "light" | "dark" | "sun-light" | "sun-dark";
export function tokenSet(css: string, set: TokenSet): Record; // Sonne-Sätze liegen auf ihrem Grundsatz
export function oklchOf(value: string): [number, number, number];
export function contrastOf(a: [number, number, number], b: [number, number, number]): number;
export function blend(fg: [number, number, number], alpha: number, bg: [number, number, number]): [number, number, number];
export function inGamut(color: [number, number, number]): boolean;
```
- `contrast.spec.ts` exportiert `VALUES_SWITCHED` (Konstante, hier `false`); Task A4 setzt sie auf `true`.
Neue Tokens (`-ink`, `area-*`, `queen-*`, `--destructive-foreground`, `--primary-ink`, `--warning-ink`, `--condition-*-ink`) und die beiden Sonne-Sätze tragen hier schon ihre Endwerte; bestehende Werte bleiben unverändert, damit nichts bricht.
- [ ] **Schritt 1: Failing Tests**
```ts
// css-tokens.spec.ts
it('reads oklch values with and without alpha', ...);
it('contrast of black on white is 21', ...);
it('blends a 15 % tint over its background before measuring', ...);
it('sun-light layers over light and sun-dark over dark', ...);
// contrast.spec.ts
it('every token in @theme inline exists in light and dark', ...);
it('all tokens are inside the sRGB gamut', ...); // alle vier Sätze, auch Bestand
it.each(AREA_TONES)('area tone %s reaches 3:1 on the card in every set', ...);
it('queen marking colours with the foreground ring reach 3:1 on the card', ...);
it('the focus ring token equals --primary-ink in light and dark', ...);
it('sun sets define every token listed in the sun table', ...);
describe.skipIf(!VALUES_SWITCHED)('table 4.4', () => {
it.each(ROWS)('$id reaches its limit and the measured ratio in $set', ...); // ROWS = Zeilen aus „Gemessene Kontraste“, |gemessen − Soll| ≤ 0,05
});
```
- [ ] **Schritt 2:** `npx vitest run src/styles` → FAIL (Module und Tokens fehlen).
- [ ] **Schritt 3: Implementieren.** `css-tokens.ts` übernimmt die OKLCH-Rechnung aus `src/lib/forms/rating-contrast.spec.ts` (Ottosson), ergänzt um Alpha-Mischung und Gamut-Prüfung; `globals.css`: neue Tokens in `:root` und `.dark` mit den Tabellenwerten, `@theme inline` mit `--color-primary-ink`, `-warning-ink`, `-destructive-foreground`, `-condition-*-ink`, `-area-*`, `-queen-*`, die zwei Sonne-Sätze samt Regeln aus der Sonne-Tabelle, `@custom-variant`s (`dark` mit `:where`, `sun`, `pointer-fine`, `pointer-coarse`), `.tint`, `@media print`, `@media (forced-colors: active)`. `--gradient-*` und alle alten Werte bleiben bis Task A4. `rating-contrast.spec.ts` bleibt bis A4 bestehen.
- [ ] **Schritt 4:** `npx vitest run src/styles src/lib/forms` → PASS.
- [ ] **Schritt 5: Verifikation** – V mit `src/styles src/lib/forms src/components`; VB nur ein Blick auf Start in hell (muss unverändert aussehen). Kein Commit.
---
### Task A2: Darstellung umschalten, ohne zu flackern (Spec 5, Schritt 2)
**Files**
- Create: `src/lib/appearance/appearance.ts` (+ Spec), `src/components/appearance-script.tsx` (+ Spec), `src/app/(app)/profile/appearance-section.tsx` (+ Spec)
- Modify: `src/app/layout.tsx`, `src/app/providers.tsx` (+ `providers.spec.tsx`), `src/components/ui/sonner.tsx`, `src/lib/motion/motion-preference.ts`, `src/app/(app)/profile/page.tsx` (+ Spec), `src/app/(app)/layout.tsx` (+ `layout.spec.tsx`), `package.json`, `package-lock.json`
- Delete: `src/app/(app)/profile/motion-section.tsx`
**Interfaces**
- Produces:
```ts
export const APPEARANCE_KEYS: { theme: "apicore.theme"; contrast: "apicore.contrast"; motion: "apicore.motion" };
export type ThemeChoice = "light" | "dark" | "system";
export type ContrastChoice = "normal" | "sun";
export const DEFAULT_THEME: ThemeChoice; // "light" (Offene Frage 3, A)
export function resolveAppearance(stored: { theme?: string | null; contrast?: string | null; motion?: string | null },
env: { prefersDark: boolean; prefersMore: boolean; darkEnabled: boolean }): { dark: boolean; sun: boolean; motion: MotionLevel };
export function useAppearance(): { theme: ThemeChoice; contrast: ContrastChoice; motion: MotionLevel; resolvedTheme: "light" | "dark";
sunActive: boolean; setTheme(v: ThemeChoice): void; setContrast(v: ContrastChoice): void; setMotion(v: MotionLevel): void };
export function appearanceScript(darkEnabled: boolean): string; // IIFE-Quelltext aus APPEARANCE_KEYS und resolveAppearance
export function AppearanceScript(): JSX.Element; //