docs(quick-260917-gsh): Plan — Akzentfarbe als Hex-Code eingebbar, Bildmarke uebernimmt Akzentfarbe

This commit is contained in:
2026-09-17 12:12:18 +02:00
parent 13ce596bea
commit f6eda20fcb
@@ -0,0 +1,220 @@
---
phase: quick-260917-gsh
plan: 01
type: execute
wave: 1
depends_on: []
autonomous: true
requirements: [QUICK-260917-GSH]
files_modified:
- apps/web/src/lib/color.ts
- apps/web/src/lib/color.test.ts
- apps/web/src/components/settings/account-settings-form.tsx
- apps/web/src/components/settings/account-settings-form.test.tsx
- apps/web/src/messages/de.json
- apps/web/src/messages/en.json
- apps/web/src/components/brand/tessera-logo.tsx
- apps/web/src/components/brand/tessera-logo.test.tsx
- apps/web/src/components/brand/brand.ts
- CHANGELOG.md
estimate:
tokens: 32000
raw_tokens: 32000
tasks: 3
confidence: low
must_haves:
truths:
- "Einstellungen → Konto zeigt neben dem Farbwaehler ein Textfeld (`<input type=\"text\">`, monospace, `maxLength={7}`, `aria-label` = settings.account.accentColorHex), vorbelegt mit dem gespeicherten Wert des Nutzers; der bisherige reine Anzeige-`<span>` (Z. 222 im Bestand) existiert nicht mehr."
- "Tippt der Nutzer in das Textfeld `FFED00`, `#FFED00` oder `#fe0`, wird der Wert per `normalizeHexColor()` (apps/web/src/lib/color.ts) auf `#ffed00` bzw. `#ffee00` normalisiert; `accentColor` und damit der Farbwaehler folgen sofort. Beim Verlassen des Feldes (onBlur) steht die kanonische Form im Textfeld."
- "Aendert der Nutzer den Farbwaehler, uebernimmt das Textfeld denselben Wert (`#rrggbb`, Kleinbuchstaben). Zuruecksetzen setzt Farbwaehler UND Textfeld auf `#ffed00`."
- "Ist der Text kein gueltiger Farbwert (`#ggg`, `#12345`, leer), traegt das Textfeld `aria-invalid=\"true\"` und die Klasse `border-destructive`, unter der Zeile steht settings.account.accentColorHexInvalid, und der Knopf „Farbe speichern“ ist `disabled`. Bei gueltigem Wert: `aria-invalid=\"false\"`, `border-input`, Knopf aktiv."
- "`normalizeHexColor(input: string): string | null` ist eine reine Funktion mit vitest-Test (apps/web/src/lib/color.test.ts): gueltig `#ffed00`→`#ffed00`, `FFED00`→`#ffed00`, `#fe0`→`#ffee00`, ` #FfEd00 `→`#ffed00`; ungueltig (`null`): `#ggg`, `#12345`, ``, `#`, `#1234567`."
- "de.json und en.json enthalten unter settings.account die neuen Schluessel `accentColorHex` und `accentColorHexInvalid` (deutsch in Sie-Form, englische Entsprechung); beide Dateien bleiben gueltiges JSON."
- "In `LogoMark` (tessera-logo.tsx) traegt die gedrehte Kachel (`transform=\"rotate(12 51 21)\"`) kein `fill`-Praesentationsattribut mehr, sondern den Inline-Style `fill: var(--primary, #ffed00)` (Vorlage-String mit BRAND_YELLOW als Rueckfall). Die vier Olivkacheln und die Grundplatte sind unveraendert. Damit nimmt die Bildmarke in Kopfzeile, Seitenleiste und leerem Dashboard die per auth-store.ts gesetzte Akzentfarbe an; auf der Anmeldeseite (kein Nutzer, `--primary` = CSS-Standard Markengelb) bleibt sie gelb."
- "tessera-logo.test.tsx prueft: fuenf Kacheln, genau eine mit Inline-Style-Fuellung `var(--primary, #ffed00)` (aus BRAND_YELLOW gebildet), diese ist die einzige gedrehte, keine Kachel traegt ein `fill`-Attribut gleich BRAND_YELLOW, vier Kacheln tragen `fill` = BRAND_OLIVE. jsdom 29.1.1 behaelt `var()`-Werte in `element.style.fill` (vom Planer geprueft)."
- "brand.ts dokumentiert BRAND_YELLOW als Standard-Gelb der Signalkachel und Rueckfall ohne Akzentfarbe; der Kommentar an der Kachel in tessera-logo.tsx erklaert, warum Inline-Style statt Praesentationsattribut."
- "`pnpm --filter @tessera/web exec vitest run` und `pnpm --filter @tessera/web type-check` enden gruen. Kein Docker-Build, kein `git push`, keine Dateien ausserhalb der autorisierten Liste (files_modified + .planning/)."
- "CHANGELOG.md, `## Unveröffentlicht`: je genau ein neuer Stichpunkt unter `### Neu` (Hex-Eingabe) und `### Geändert` (Bildmarke in Akzentfarbe), Stil wie im Bestand (typografische Anfuehrungszeichen, `→`, kein Punkt am Ende, kein Fliesstext)."
- "Drei Commits: `feat(settings): …` (Task 1), `feat(brand): …` (Task 2), `docs: …` (Task 3)."
artifacts:
- "apps/web/src/lib/color.ts — `normalizeHexColor` (neu)"
- "apps/web/src/lib/color.test.ts — Unit-Test der Normalisierung (neu)"
- "apps/web/src/components/settings/account-settings-form.tsx — Hex-Textfeld, Zustand `hexInput`, Sync mit Farbwaehler, Speichern-Sperre"
- "apps/web/src/components/settings/account-settings-form.test.tsx — Komponententest Sync/Sperre/Reset (neu)"
- "apps/web/src/messages/de.json, en.json — settings.account.accentColorHex, accentColorHexInvalid"
- "apps/web/src/components/brand/tessera-logo.tsx — gedrehte Kachel mit Inline-Style `var(--primary, …)`"
- "apps/web/src/components/brand/tessera-logo.test.tsx — angepasster Kacheltest"
- "apps/web/src/components/brand/brand.ts — Kommentar zu BRAND_YELLOW"
- "CHANGELOG.md — zwei Stichpunkte"
key_links:
- "auth-store.ts `applyAccentColor` (Z. 21-41) setzt `--primary` inline auf `document.documentElement`; `LogoMark` liest denselben Token per `var(--primary, …)`. Das ist die einzige Verdrahtung zwischen Akzentfarbe und Bildmarke — kein Prop, kein Store-Zugriff im Logo."
- "`<input type=\"color\">` akzeptiert nur `#rrggbb`; deshalb wird NUR der normalisierte Wert in `accentColor` geschrieben, der Rohtext lebt getrennt in `hexInput`. Ein ungueltiger Rohtext darf nie in `accentColor` landen, sonst faellt jsdom/Browser auf `#000000` zurueck."
- "`updateAccentColorAction` (auth-actions.ts Z. 220) leitet an `PATCH /users/me/accent-color` weiter, Server-Regex `/^#[0-9a-fA-F]{6}$/`; die Client-Normalisierung liefert immer diese Form, Speichern ist bei ungueltigem Text zusaetzlich gesperrt."
- "tessera-logo.test.tsx Z. 97-109 filtert Kacheln ueber `getAttribute('fill') === BRAND_YELLOW` — nach Task 2 sind das null Treffer, der Test MUSS umgeschrieben werden (Inline-Style pruefen), sonst rot."
---
<objective>
Zwei Ergaenzungen an der persoenlichen Akzentfarbe (Einstellungen → Konto):
1. **Hex-Eingabe.** Neben dem Farbwaehler ein Textfeld fuer den Hex-Code (`#rrggbb`), vorbelegt mit dem aktuellen Wert. Eingabe wird ueber eine reine Funktion `normalizeHexColor()` normalisiert (fuehrendes `#` optional, 3-stellige Kurzform wird expandiert, Ausgabe klein). Farbwaehler und Textfeld bleiben in beide Richtungen synchron; ungueltiger Text zeigt einen Fehlerzustand und sperrt „Farbe speichern“. Der reine Anzeige-`<span>` entfaellt.
2. **Bildmarke in Akzentfarbe.** Die gedrehte Signalkachel in `LogoMark` bekommt statt der festen gelben Fuellung den Inline-Style `fill: var(--primary, #ffed00)` — dadurch folgt die Bildmarke ueberall (Kopfzeile, Seitenleiste, leeres Dashboard) der per `applyAccentColor` gesetzten Akzentfarbe; vor der Anmeldung gilt der CSS-Standard (Markengelb).
Dazu Unit-Tests (Normalisierung, Formular-Sync, Logo) und zwei CHANGELOG-Stichpunkte. Browser-Nachweis macht der Orchestrator selbst — kein Docker-Build, kein Push.
Purpose: Nutzer koennen eine exakte Firmenfarbe eintippen statt sie im Farbwaehler zu treffen; die Bildmarke wirkt dann nicht mehr wie ein Fremdkoerper in der gewaehlten Farbe.
Output: `color.ts` + Test, angepasstes Kontoformular + Test, i18n-Schluessel de/en, angepasstes Logo + Test + Kommentar, CHANGELOG, drei Commits.
</objective>
<execution_context>
@~/.claude/gsd-core/workflows/execute-plan.md
@~/.claude/gsd-core/templates/summary.md
</execution_context>
<context>
@/home/vicolab/projects/tessera-ctl/CLAUDE.md
@/home/vicolab/projects/tessera-ctl/apps/web/src/components/settings/account-settings-form.tsx
@/home/vicolab/projects/tessera-ctl/apps/web/src/components/brand/tessera-logo.tsx
@/home/vicolab/projects/tessera-ctl/apps/web/src/components/brand/tessera-logo.test.tsx
@/home/vicolab/projects/tessera-ctl/apps/web/src/components/brand/brand.ts
@/home/vicolab/projects/tessera-ctl/apps/web/src/lib/stores/auth-store.ts
@/home/vicolab/projects/tessera-ctl/apps/web/src/components/settings/desktop-app-settings.test.tsx
</context>
<tasks>
<task type="tracer" tdd="true">
<name>Task 1: Hex-Eingabe — Normalisierung, Formular-Sync, i18n, Tests</name>
<files>apps/web/src/lib/color.ts, apps/web/src/lib/color.test.ts, apps/web/src/components/settings/account-settings-form.tsx, apps/web/src/components/settings/account-settings-form.test.tsx, apps/web/src/messages/de.json, apps/web/src/messages/en.json</files>
<read_first>
- apps/web/src/components/settings/account-settings-form.tsx (Z. 39-54 Zustand, Z. 115-140 Speichern/Zuruecksetzen, Z. 209-250 Markup)
- apps/web/src/components/settings/desktop-app-settings.test.tsx Z. 1-40 (Muster: de.json-gestuetzter next-intl-Mock, `vi.hoisted` + `vi.mock` fuer ein lib-Modul)
- apps/web/src/lib/app-version.test.ts Z. 1-10 (Kopfkommentar-Stil fuer lib-Tests)
- apps/web/src/messages/de.json Z. 146-151 und en.json Z. 146-151 (settings.account.accentColor*)
</read_first>
<behavior>
color.test.ts (`describe('normalizeHexColor')`):
- `'#ffed00'` → `'#ffed00'`
- `'FFED00'` → `'#ffed00'` (ohne `#`, Grossbuchstaben)
- `'#fe0'` → `'#ffee00'` (Kurzform expandiert)
- `' #FfEd00 '` → `'#ffed00'` (Leerraum getrimmt)
- `'#ggg'` → `null`; `'#12345'` → `null`; `''` → `null`; `'#'` → `null`; `'#1234567'` → `null`
account-settings-form.test.tsx (`describe('AccountSettingsForm — Akzentfarbe Hex-Eingabe')`):
- Nach dem Laden (fetchCurrentUser liefert accentColor `#123456`): Textfeld `getByRole('textbox', { name: 'Hex-Code' })` hat Wert `#123456`, `input[type="color"]` hat Wert `#123456`.
- `fireEvent.change(textfeld, 'FFED00')` → Farbwaehler-Wert `#ffed00`, Knopf „Farbe speichern“ nicht disabled, Textfeld `aria-invalid="false"`.
- `fireEvent.change(textfeld, '#ggg')` → Textfeld `aria-invalid="true"`, Klasse enthaelt `border-destructive`, Fehlertext (de.json settings.account.accentColorHexInvalid) sichtbar, Knopf „Farbe speichern“ disabled, Farbwaehler behaelt letzten gueltigen Wert.
- `fireEvent.change(farbwaehler, '#00ff00')` → Textfeld-Wert `#00ff00`.
- `fireEvent.blur(textfeld)` nach Eingabe `#fe0` → Textfeld-Wert `#ffee00`.
- Klick „Zurücksetzen“ → Textfeld und Farbwaehler `#ffed00`, `updateAccentColorAction` mit `null` aufgerufen.
- Klick „Farbe speichern“ bei gueltigem `#ffed00` → `updateAccentColorAction` mit `'#ffed00'` aufgerufen, Erfolgsmeldung (settings.account.accentColorSuccess) erscheint (`findByText`).
</behavior>
<action>
RED zuerst: beide Testdateien anlegen und rot sehen (`color.test.ts` scheitert mit fehlendem Modul, Formulartest mit fehlendem Textfeld), dann GREEN.
**1. `apps/web/src/lib/color.ts` (neu).** Exportiere `normalizeHexColor(input: string): string | null`: trimmen, ein optionales fuehrendes `#` abschneiden, dann pruefen — genau 3 oder genau 6 Hex-Zeichen (`/^[0-9a-f]{3}$|^[0-9a-f]{6}$/i`); bei 3 Zeichen jedes Zeichen verdoppeln; Ergebnis kleingeschrieben mit `#` davor zurueckgeben; alles andere `null`. Keine Abhaengigkeiten, kein `'use client'`. Deutscher Kopfkommentar (Zweck: Hex-Eingabe der Akzentfarbe, quick-260917-gsh; Server-Regex in `PATCH /users/me/accent-color` verlangt `#rrggbb`).
**2. `apps/web/src/lib/color.test.ts` (neu).** Faelle aus `<behavior>`; Kopfkommentar im Stil von app-version.test.ts.
**3. i18n.** In `apps/web/src/messages/de.json` und `en.json` unter `settings.account` direkt nach `accentColorError` (Z. 151) zwei Schluessel einfuegen: `accentColorHex` = „Hex-Code“ / „Hex code“; `accentColorHexInvalid` = „Ungültiger Farbwert. Bitte geben Sie sechs Hexadezimalzeichen ein, z. B. #ffed00.“ / „Invalid color value. Please enter six hexadecimal characters, e.g. #ffed00.“ Kommasetzung im JSON beachten.
**4. `account-settings-form.tsx`.**
- Import `normalizeHexColor` aus `@/lib/color`.
- Neuer Zustand `const [hexInput, setHexInput] = useState<string>(DEFAULT_ACCENT);` neben `accentColor`; abgeleitet `const isHexValid = normalizeHexColor(hexInput) !== null;`. `accentColor` bleibt die einzige Quelle fuer Farbwaehler und Speichern und enthaelt IMMER einen gueltigen `#rrggbb`-Wert (ein ungueltiger Rohtext darf dort nie landen — `<input type="color">` faellt sonst auf `#000000`).
- `useEffect` (Z. 45-54): nach `setAccentColor(c)` zusaetzlich `setHexInput(c)` mit demselben Wert.
- Farbwaehler `onChange`: `setAccentColor(v)` UND `setHexInput(v)`.
- Textfeld `onChange`: `setHexInput(raw)`; `const n = normalizeHexColor(raw); if (n) setAccentColor(n);`. Textfeld `onBlur`: falls gueltig, `setHexInput(normalisiert)` (kanonische Form — Ermessensentscheidung des Planers, damit `FFED00` nicht dauerhaft in Grossbuchstaben stehen bleibt).
- `handleResetAccentColor`: zusaetzlich `setHexInput(DEFAULT_ACCENT)`.
- `handleSaveAccentColor`: am Anfang `if (!isHexValid) return;` als Sicherheitsnetz; sendet weiterhin `accentColor`.
- Markup (Z. 215-231): den reinen Anzeige-`<span>` mit `font-mono`, der nur den Wert als Text wiederholt (Z. 222), entfernen und an seiner Stelle das Textfeld setzen: `<input id="accentColorHex" type="text" inputMode="text" autoComplete="off" spellCheck={false} maxLength={7} placeholder={DEFAULT_ACCENT} value={hexInput} aria-label={t('account.accentColorHex')} aria-invalid={!isHexValid} …>`; Klassen `h-10 w-28 rounded-md border bg-background px-3 py-2 font-mono text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring` plus bedingt `border-destructive` (ungueltig) bzw. `border-input` (gueltig). Direkt unter der Zeile (vor den Erfolgs-/Fehlermeldungen) bei `!isHexValid` ein `<p className="text-xs text-destructive mb-3">{t('account.accentColorHexInvalid')}</p>`.
- Knopf „Farbe speichern“: `disabled={isAccentPending || !isHexValid}`.
**5. `account-settings-form.test.tsx` (neu).** next-intl-Mock de.json-gestuetzt wie desktop-app-settings.test.tsx (die Namespaces `settings` und `auth` muessen beide aufloesen — der Mock ist namespace-generisch). `@/lib/auth-actions` per `vi.hoisted` + `vi.mock` komplett ersetzen: `fetchCurrentUser` → `mockResolvedValue({ id: 'u1', username: 'max', displayName: 'Max', role: 'USER', tenantId: 't1', isLocalUser: true, hasAvatar: false, accentColor: '#123456' })`; `updateAccentColorAction` → `mockResolvedValue({ success: true })`; `changePasswordAction`, `uploadAvatarAction`, `deleteAvatarAction` als `vi.fn()`. `useAuthStore` NICHT mocken (echter zustand-Store, `user` ist null, `setUser` wird nicht erreicht). Farbwaehler ueber `container.querySelector('input[type="color"]')` greifen (kein ARIA-Rollenname). Nach `render` mit `await screen.findByDisplayValue('#123456')` auf das Laden warten; nach Klicks auf Speichern/Zuruecksetzen mit `waitFor`/`findByText` warten (useTransition ist asynchron). `afterEach`: `cleanup()` + `vi.clearAllMocks()`.
Nicht anfassen: Passwort- und Avatar-Abschnitte, auth-actions.ts, auth-store.ts, API.
</action>
<verify>
<automated>cd /home/vicolab/projects/tessera-ctl && pnpm --filter @tessera/web exec vitest run src/lib/color.test.ts src/components/settings/account-settings-form.test.tsx && pnpm --filter @tessera/web type-check && node -e "for (const l of ['de','en']) { const m = JSON.parse(require('fs').readFileSync('apps/web/src/messages/'+l+'.json','utf8')); for (const k of ['accentColorHex','accentColorHexInvalid']) { const v = m.settings.account[k]; if (typeof v !== 'string' || !v) { console.error('fehlt: '+l+' settings.account.'+k); process.exit(1); } } } console.log('i18n ok')" && ! grep -q '">{accentColor}</span>' apps/web/src/components/settings/account-settings-form.tsx && grep -c 'normalizeHexColor' apps/web/src/components/settings/account-settings-form.tsx | grep -qv '^0$' && echo TASK1-OK</automated>
</verify>
<done>Beide neuen Testdateien gruen (Normalisierung 9 Faelle, Formular 7 Faelle), Typpruefung gruen, beide Sprachdateien enthalten die zwei neuen Schluessel als nichtleere Strings, der Anzeige-`<span>` ist aus dem Formular verschwunden, das Formular importiert und nutzt `normalizeHexColor`. Commit `feat(settings): Akzentfarbe zusätzlich als Hex-Code eingebbar` (nur die sechs Dateien dieses Tasks).</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: Bildmarke — gedrehte Kachel in Akzentfarbe, Test und Kommentare</name>
<files>apps/web/src/components/brand/tessera-logo.tsx, apps/web/src/components/brand/tessera-logo.test.tsx, apps/web/src/components/brand/brand.ts</files>
<behavior>
tessera-logo.test.tsx — der Test „renders exactly five tiles, exactly one in the brand yellow and exactly one rotated“ (Z. 97-109) wird ersetzt durch „renders exactly five tiles; only the rotated one is filled from the accent token with the brand yellow as fallback“:
- `mark.querySelectorAll('g rect')` hat Laenge 5.
- Genau eine Kachel hat `(tile as SVGRectElement).style.fill === \`var(--primary, ${BRAND_YELLOW})\`` (Erwartung aus der Konstante gebildet); diese Kachel hat das Attribut `transform`; keine andere Kachel hat `transform`.
- Keine Kachel hat `getAttribute('fill') === BRAND_YELLOW`.
- Genau vier Kacheln haben `getAttribute('fill') === BRAND_OLIVE` (Import aus `./brand` ergaenzen) und keinen `style`-Attributwert.
Alle uebrigen Tests der Datei bleiben unveraendert und gruen.
</behavior>
<action>
RED zuerst: Test wie in `<behavior>` umschreiben, rot sehen (Inline-Style fehlt), dann GREEN.
**`tessera-logo.tsx`, `LogoMark`, gedrehte Kachel (Z. 72-80):** Das `fill`-Praesentationsattribut dieser einen `<rect>` (aktuell an `BRAND_YELLOW` gebunden) entfernen und stattdessen `style={{ fill: \`var(--primary, ${BRAND_YELLOW})\` }}` setzen. Grund als deutscher Kommentar direkt ueber der `<rect>`: `var()` ist in SVG-Praesentationsattributen nicht zuverlaessig, im Inline-Style schon; `--primary` wird von `applyAccentColor` in `auth-store.ts` gesetzt, ohne Nutzer (Anmeldeseite) gilt der CSS-Standard aus `globals.css` (Markengelb), der Rueckfall in `var()` greift nur, wenn der Token gar nicht definiert ist. Der Import von `BRAND_YELLOW` bleibt (fuer den Rueckfall). Die vier Olivkacheln, die Grundplatte, `plateOutline`, Props und die `horizontal`-Variante nicht anfassen. Im JSDoc von `TesseraLogo` (Z. 88-93) einen Satz ergaenzen: die gedrehte Signalkachel folgt der persoenlichen Akzentfarbe (`--primary`).
**`brand.ts`:** Kommentar ueber `BRAND_YELLOW` (Z. 11) aendern zu: Standard-Gelb der gedrehten Signalkachel und Rueckfall, wenn keine Akzentfarbe (`--primary`) gesetzt ist; die Kachel selbst wird in `tessera-logo.tsx` per `var(--primary, BRAND_YELLOW)` gefuellt. Werte der drei Konstanten unveraendert (login/page.tsx und account-settings-form.tsx importieren sie weiterhin).
**`tessera-logo.test.tsx`:** Test Z. 97-109 gemaess `<behavior>` ersetzen, `BRAND_OLIVE` importieren. Hinweis: jsdom 29.1.1 (aktuelle Aufloesung unter apps/web) haelt `var(--primary, #ffed00)` in `element.style.fill` und im `style`-Attribut — vom Planer per JSDOM-Probe bestaetigt; `getAttribute('style')` waere die Alternative, `style.fill` ist die klarere Zusicherung.
</action>
<verify>
<automated>cd /home/vicolab/projects/tessera-ctl && pnpm --filter @tessera/web exec vitest run src/components/brand/tessera-logo.test.tsx && ! grep -q 'fill={BRAND_YELLOW}' apps/web/src/components/brand/tessera-logo.tsx && test "$(grep -cF 'var(--primary, ${BRAND_YELLOW})' apps/web/src/components/brand/tessera-logo.tsx)" = "1" && test "$(grep -c 'fill={BRAND_OLIVE}' apps/web/src/components/brand/tessera-logo.tsx)" = "4" && grep -q 'primary' apps/web/src/components/brand/brand.ts && pnpm --filter @tessera/web type-check && echo TASK2-OK</automated>
</verify>
<done>tessera-logo.test.tsx gruen (alle Tests inkl. des umgeschriebenen Kacheltests), in tessera-logo.tsx gibt es kein gelbes `fill`-Praesentationsattribut mehr, genau eine Vorlage-Fuellung `var(--primary, …)` und weiterhin vier Olivkacheln, brand.ts erwaehnt den `--primary`-Rueckfall, Typpruefung gruen. Commit `feat(brand): gedrehte Kachel der Bildmarke übernimmt die Akzentfarbe` (nur die drei Dateien dieses Tasks).</done>
</task>
<task type="auto">
<name>Task 3: CHANGELOG — zwei Stichpunkte, Gesamtlauf</name>
<files>CHANGELOG.md</files>
<action>
In `CHANGELOG.md` unter `## Unveröffentlicht`:
- `### Neu`, direkt nach dem Stichpunkt `- Favoriten-Widget: optionaler Titel (ohne Titel keine Kopfzeile)` (Z. 12): `- Einstellungen → Konto: Akzentfarbe zusätzlich als Hex-Code eingebbar (z. B. #ffed00)`
- `### Geändert`, direkt nach dem Stichpunkt `- Kalender-Widget: Plakette am Tag in der Farbe des Kalenders; …` (Z. 17): `- Tessera-Bildmarke: die gedrehte Kachel übernimmt die persönliche Akzentfarbe`
Stil wie im Bestand: echte Umlaute, `→`, kein Punkt am Ende, kein Fliesstext, keine weiteren Aenderungen an der Datei. Danach den vollstaendigen Web-Testlauf und die Typpruefung als Abschlussgate ausfuehren. Das `<verify>` VOR dem Commit laufen lassen — der Diff-Zaehler misst den Arbeitsbaum gegen HEAD (Tasks 1 und 2 sind zu diesem Zeitpunkt bereits committet, CHANGELOG.md noch nicht).
</action>
<verify>
<automated>cd /home/vicolab/projects/tessera-ctl && test "$(sed -n '/^## Unveröffentlicht/,/^## 1\.1\.0/p' CHANGELOG.md | grep -c 'Akzentfarbe zusätzlich als Hex-Code')" = "1" && test "$(sed -n '/^## Unveröffentlicht/,/^## 1\.1\.0/p' CHANGELOG.md | grep -c 'gedrehte Kachel übernimmt die persönliche Akzentfarbe')" = "1" && NUMSTAT=$(git diff --numstat HEAD -- CHANGELOG.md) && test "$(printf '%s' "$NUMSTAT" | awk '{print $1"/"$2}')" = "2/0" && pnpm --filter @tessera/web exec vitest run && pnpm --filter @tessera/web type-check && echo TASK3-OK</automated>
</verify>
<done>Beide Stichpunkte stehen je genau einmal im Abschnitt „Unveröffentlicht“ (Neu bzw. Geändert), CHANGELOG-Diff = genau zwei eingefuegte Zeilen, kompletter Web-Testlauf (bisher 55 Dateien / 365 Tests plus die drei neuen bzw. geaenderten Dateien) und Typpruefung gruen. Commit `docs: CHANGELOG — Hex-Eingabe der Akzentfarbe, Bildmarke in Akzentfarbe` (nur CHANGELOG.md).</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| Browser → API (`PATCH /users/me/accent-color`) | Vom Nutzer getippter Hex-Text verlaesst den Client; Server prueft bereits `/^#[0-9a-fA-F]{6}$/` |
| Nutzertext → `<input type="color">` / `--primary` | Freitext darf nicht ungeprueft in den Farbwaehler-Wert oder in `style.setProperty('--primary', …)` gelangen |
## STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|-----------|----------|-----------|----------|-------------|-----------------|
| T-gsh-01 | Tampering | `normalizeHexColor` / Speichern-Pfad | low | mitigate | Nur der normalisierte `#rrggbb`-Wert erreicht `accentColor` und die Server-Action; Speichern bei ungueltigem Text gesperrt; Server-Regex bleibt die letzte Instanz (unveraendert) |
| T-gsh-02 | Tampering | `applyAccentColor` (`--primary` inline) | low | accept | Wert stammt aus der API-Antwort desselben Nutzers, ist serverseitig auf `#rrggbb` beschraenkt; kein neuer Pfad in diesem Task |
| T-gsh-03 | Information Disclosure | Fehlertext `accentColorHexInvalid` | low | accept | Statischer i18n-Text, gibt keine Eingabe wieder |
| T-gsh-SC | Tampering | npm/pnpm installs | low | accept | Keine Paketinstallationen in diesem Plan (nur bestehende Abhaengigkeiten: vitest, testing-library, jsdom) |
</threat_model>
<verification>
- `pnpm --filter @tessera/web exec vitest run` gruen (inkl. `src/lib/color.test.ts`, `src/components/settings/account-settings-form.test.tsx`, `src/components/brand/tessera-logo.test.tsx`).
- `pnpm --filter @tessera/web type-check` gruen.
- `git status --porcelain` nach den drei Commits: nur `.planning/`-Dateien (SUMMARY) offen; keine Datei ausserhalb von `files_modified` veraendert.
- Browser-Nachweis (Orchestrator, nicht Teil dieses Plans): Einstellungen → Konto, `FFED00` bzw. `#fe0` tippen → Farbwaehler folgt; `#ggg` → roter Rand, Speichern gesperrt; Farbe speichern → Kachel der Bildmarke in Kopfzeile/Seitenleiste wechselt sofort in die gewaehlte Farbe; Zuruecksetzen → gelb; Anmeldeseite nach Abmelden → Kachel gelb.
</verification>
<success_criteria>
- Hex-Textfeld vorhanden, synchron mit dem Farbwaehler in beide Richtungen, Fehlerzustand + Speichersperre bei ungueltigem Wert, Reset setzt beides zurueck.
- `normalizeHexColor` als reine Funktion mit den neun Testfaellen aus Task 1.
- Bildmarke: gedrehte Kachel per Inline-Style an `--primary` gebunden, Rueckfall Markengelb; Logo-Tests angepasst und gruen; Kommentare in tessera-logo.tsx und brand.ts aktualisiert.
- Zwei CHANGELOG-Stichpunkte, i18n de/en vollstaendig, drei Commits, alle Gates gruen.
</success_criteria>
<output>
Create `.planning/quick/260917-gsh-akzentfarbe-in-einstellungen-konto-zusae/260917-gsh-SUMMARY.md` when done
</output>