feat(web): light/dark mode cu comutator persistat + anti-FOUC (PRD 5.3)
Tema light ca bloc [data-theme="light"] peste variabilele :root (dark nemodificat la octet). Comutator soare/luna in header pe toate paginile, default OS-aware (prefers-color-scheme, fallback dark), persistenta in localStorage doar la comutare explicita, script anti-FOUC in <head> pre-paint. Suprafetele de stare hardcodate convertite la color-mix in base.html + 7 fragmente _*.html (light lizibil, contrast WCAG AA). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because one or more lines are too long
256
docs/prd/prd-5.3-light-dark-mode.md
Normal file
256
docs/prd/prd-5.3-light-dark-mode.md
Normal file
@@ -0,0 +1,256 @@
|
||||
# PRD 5.3 — Light/Dark mode (comutator tema persistat)
|
||||
|
||||
**Stare**: inchis
|
||||
|
||||
> Proces complet: `docs/ROADMAP.md` §5. Contract RAR (sursa de adevar): `docs/api-rar-contract.md`.
|
||||
> Starea trece: `draft → aprobat → in-executie → verify-pass → inchis` (actualizata de lead).
|
||||
|
||||
## 1. Obiectiv
|
||||
|
||||
Dashboard-ul web e azi **doar dark** (paleta fixa in `:root`). Adaugam o tema **light** si un
|
||||
**comutator in header** care persista alegerea utilizatorului. Service-urile care vin din Visual
|
||||
FoxPro / soft propriu lucreaza des in birouri luminoase si pe monitoare unde dark-mode obositor sau
|
||||
greu de citit la videoproiector — un toggle light/dark e o cerinta de ergonomie de baza (Etapa 5).
|
||||
|
||||
CSS-ul **e deja pe variabile** (`--bg`, `--card`, `--ink`, `--muted`, `--line`, `--ok`, `--warn`,
|
||||
`--err`, `--accent` in `base.html`). Tema light = un bloc `[data-theme="light"]` care **suprascrie
|
||||
aceleasi variabile** cu o paleta deschisa. Efort mic, zero logica de domeniu, zero backend.
|
||||
|
||||
**Invariant de design (motivul cheie):** comutarea nu trebuie sa **palpaie** (FOUC — flash of
|
||||
unstyled / wrong-theme content). Tema se aplica **inainte de primul paint** printr-un script inline
|
||||
mic in `<head>`, care citeste preferinta din `localStorage` si seteaza `data-theme` pe `<html>`
|
||||
sincron, inainte ca `<body>` sa randeze. Fara asta, fiecare incarcare de pagina ar clipi dark→light.
|
||||
|
||||
## 2. Non-Goals (anti scope-creep)
|
||||
|
||||
- **NU backend / cookie / ruta noua.** Persistenta = `localStorage` pur client-side (roadmap zice
|
||||
"cookie/localStorage" — alegem localStorage: zero suprafata server + anti-FOUC prin scriptul din
|
||||
`<head>`). Nu se atinge `routes.py`, `auth.py`, sesiunea, baza de date.
|
||||
- **NU redesign de paleta dark.** Tema dark ramane **identica la octet** cu cea de azi (default
|
||||
pastrat); adaugam doar varianta light + un toggle. Nicio culoare dark existenta nu se schimba.
|
||||
- **NU teme multiple / personalizate / culoare de accent reglabila.** Doar doua: `dark` (default)
|
||||
si `light`.
|
||||
- **NU atinge worker, masina de stari, idempotenta, mapping, schema, validation.py, API.** Strict
|
||||
`app/web/templates/base.html` (+ eventual un test de template).
|
||||
- **NU restilizeaza fragmentele HTMX.** Toate fragmentele (`_*.html`) mostenesc variabilele din
|
||||
`base.html` — comuta automat cu tema. (Conditie: zero culori hardcodate care sa nu adapteze —
|
||||
vezi US-001 pentru suprafetele care azi au fundal hardcodat.)
|
||||
|
||||
## 3. Stories atomice
|
||||
|
||||
> Ambele stories ating **acelasi fisier** (`base.html`) → **secventiale**, un singur worker (sau
|
||||
> lead direct, livrabila mica — ROADMAP §5.5). NU se paralelizeaza (regula fisier-comun §5.5).
|
||||
|
||||
### US-001: Tema light (paleta + suprafete theme-aware)
|
||||
**Ca** utilizator al dashboard-ului **vreau** o paleta light corecta si lizibila **pentru ca** sa pot
|
||||
folosi gateway-ul confortabil in birou luminos / la videoproiector, fara contrast slab sau zone care
|
||||
raman intunecate.
|
||||
|
||||
- **Depinde de**: —
|
||||
- **Fisiere**: `app/web/templates/base.html` (bloc CSS `[data-theme="light"]` + conversia
|
||||
suprafetelor cu fundal hardcodat la variabile), `tests/test_tema.py` (~2 fisiere)
|
||||
- **Test intai (RED)**: `tests/test_tema.py` —
|
||||
- `test_paleta_light_definita` — HTML-ul `GET /login` (sau dashboard) contine un selector
|
||||
`[data-theme="light"]` care redefineste `--bg`, `--card`, `--ink`, `--muted`, `--line`.
|
||||
- `test_dark_ramane_default` — `:root` contine inca paleta dark exacta (`--bg:#0f1115`,
|
||||
`--card:#181b22`, `--ink:#e6e9ef`) → default neschimbat.
|
||||
- `test_suprafete_fara_fundal_hardcodat` — fundalurile de stare (banner eroare/warn, flash) NU mai
|
||||
folosesc literal hex dark fix (`#241a1a`, `#201c0f`, `#16241c`) ci variabile/`color-mix` ce
|
||||
adapteaza la tema (asertie pe absenta literalilor in `<style>`).
|
||||
- **Acceptance criteria**:
|
||||
- [ ] Exista un bloc `[data-theme="light"]` in `<style>` care suprascrie cel putin
|
||||
`--bg`, `--card`, `--ink`, `--muted`, `--line` cu o paleta deschisa (fundal deschis, text
|
||||
inchis). Contrastul text/fundal ≥ WCAG AA (4.5:1 pentru `--ink` pe `--bg` si pe `--card`).
|
||||
- [ ] Paleta **dark** din `:root` ramane neschimbata la octet (default) — comportament identic cu azi
|
||||
cand nu exista preferinta salvata si OS-ul nu cere light.
|
||||
- [ ] Suprafetele cu fundal azi hardcodat dark (`.banner`, `.banner.warn`, `.flash`, eventual
|
||||
`.drop-zone.drag-over`) sunt facute theme-aware (variabile sau `color-mix` peste paleta), astfel
|
||||
incat in light arata corect (fundal deschis colorat, nu pata intunecata).
|
||||
- [ ] Culorile semantice (`--ok`/`--warn`/`--err`/`--accent`) raman lizibile pe fundal light
|
||||
(ajustate daca e nevoie pentru contrast ≥ AA pe text mic).
|
||||
- [ ] `python3 -m pytest -q` verde.
|
||||
- **Verificare E2E**: browser pe `/` cu `document.documentElement.dataset.theme="light"` setat manual →
|
||||
fundal deschis, text inchis lizibil, banner/flash/pill citibile, tabele cu linii vizibile, niciun
|
||||
text "invizibil" (acelasi ton ca fundalul).
|
||||
|
||||
### US-003: Suprafete theme-aware si in fragmentele HTMX (fix VERIFY r1)
|
||||
**Ca** utilizator pe light mode **vreau** ca bannerele de eroare/warn/flash din fragmentele HTMX sa
|
||||
fie lizibile **pentru ca** azi raman pete intunecate cu text invizibil (defect prins la VERIFY r1).
|
||||
|
||||
- **Depinde de**: US-001 (extinde aceeasi conversie theme-aware dincolo de `base.html`)
|
||||
- **Motiv**: US-001 a convertit la `color-mix` DOAR `base.html`; aceleasi fundaluri hardcodate dark
|
||||
(`#241a1a` err, `#201c0f` warn) traiesc ca **inline-style** in 7 fragmente `_*.html` (10 aparitii)
|
||||
— randate in dashboard, deci vizibile in light ca text invizibil. Testul US-001 scana doar `<style>`
|
||||
din base.html → trecea vacuu.
|
||||
- **Fisiere**: `_status.html`, `_banner.html`, `_upload.html`, `_preview_import.html`,
|
||||
`_preview_rand.html`, `_trimitere_detaliu.html`, `_mapcoloane.html` (toate `app/web/templates/`),
|
||||
`tests/test_tema.py` (extins)
|
||||
- **Test intai (RED)**: `tests/test_tema.py` —
|
||||
- `test_fragmente_fara_fundal_hardcodat` — scaneaza TOATE fisierele `app/web/templates/_*.html`
|
||||
(continutul brut) si asigura ca niciunul nu contine literalii `#241a1a`, `#201c0f`, `#16241c`.
|
||||
- **Acceptance criteria**:
|
||||
- [ ] Niciun fisier din `app/web/templates/` (inclusiv fragmentele) nu mai contine literalii hex
|
||||
dark-fix `#241a1a`/`#201c0f`/`#16241c`; fundalurile folosesc `color-mix` peste paleta
|
||||
(`var(--err)`/`var(--warn)`/`var(--ok)` 12% peste `var(--card)`), exact ca in `base.html`.
|
||||
- [ ] In light mode bannerele/flash-urile de stare au fundal deschis colorat cu text lizibil
|
||||
(verificat E2E pe dashboard: banner "Cont in asteptare de activare" nu mai e cutie neagra).
|
||||
- [ ] In dark mode aspectul ramane practic identic cu azi (color-mix peste `--card` dark dă aproape
|
||||
aceeasi nuanta).
|
||||
- [ ] Testul de protectie scaneaza fragmentele, nu doar `base.html` (lacuna r1 inchisa).
|
||||
- [ ] `python3 -m pytest -q` verde.
|
||||
- **Verificare E2E**: dashboard pe light mode → banner pending-account + orice flash de eroare/warn
|
||||
lizibile (fundal deschis); comuta la dark → aspect neschimbat.
|
||||
|
||||
### US-002: Comutator tema in header + persistenta + anti-FOUC
|
||||
**Ca** utilizator **vreau** un buton in header care comuta light/dark si imi tine minte alegerea
|
||||
**pentru ca** sa nu re-comut la fiecare incarcare si sa nu vad un flash de tema gresita.
|
||||
|
||||
- **Depinde de**: US-001 (vizual; tehnic ating acelasi fisier → oricum secvential)
|
||||
- **Fisiere**: `app/web/templates/base.html` (script inline anti-FOUC in `<head>` + buton toggle in
|
||||
`<header>` + handler de comutare/persistenta), `tests/test_tema.py` (extins, acelasi fisier ca US-001)
|
||||
- **Test intai (RED)**: `tests/test_tema.py` —
|
||||
- `test_script_antifouc_in_head_inainte_de_style` — `<head>` contine un `<script>` care citeste
|
||||
`localStorage` cheia `theme` si seteaza `document.documentElement` `data-theme` **inainte** de
|
||||
tag-ul `<style>` (pozitie in HTML: index script < index `<style>`).
|
||||
- `test_buton_toggle_in_header_cu_eticheta` — `<header>` contine un control de comutare cu
|
||||
`aria-label`/`title` descriptiv (ex. "Comuta tema") si un `id`/atribut stabil pentru handler.
|
||||
- `test_toggle_pe_login_si_dashboard` — butonul apare si pe `/login` (neautentificat) si pe dashboard
|
||||
(ambele extind `base.html`).
|
||||
- **Acceptance criteria**:
|
||||
- [ ] Script inline in `<head>`, plasat **inaintea** `<style>`, citeste preferinta:
|
||||
`localStorage.theme` daca exista, altfel `prefers-color-scheme` (fallback final: `dark`), si
|
||||
seteaza `data-theme` pe `<html>` sincron → **fara FOUC** la incarcare/reload.
|
||||
- [ ] Buton de comutare in `<header>` (langa `env`/`version`), cu `aria-label` descriptiv, atins
|
||||
usor (≥ 36px zona de atins), care comuta `data-theme` light↔dark **fara reload**.
|
||||
- [ ] La comutare se scrie `localStorage.theme` → alegerea persista peste reload si peste navigari
|
||||
(deep-link `?tab=`), inclusiv pe paginile neautentificate (login/signup).
|
||||
- [ ] Butonul reflecta starea curenta (eticheta/iconita arata ce face: "→ light" cand e dark si
|
||||
invers), accesibil la tastatura (e un `<button>`).
|
||||
- [ ] Functioneaza pe toate cele 4 pagini top-level (login, signup, dashboard, admin) — toate extind
|
||||
`base.html`, deci o singura implementare le acopera.
|
||||
- [ ] `python3 -m pytest -q` verde.
|
||||
- **Verificare E2E** (Playwright MCP / `/browse`): pe `/login` apoi pe dashboard — (a) click toggle →
|
||||
paleta comuta instant (fundal/text), (b) reload pagina → tema aleasa **persista** (citeste din
|
||||
localStorage, fara flash de tema veche), (c) comuta inapoi → persista invers, (d) zero erori in
|
||||
consola, (e) fara FOUC vizibil la reload (tema corecta din primul frame).
|
||||
|
||||
## 4. Riscuri
|
||||
|
||||
- **FOUC** (risc principal) → mitigat prin scriptul inline din `<head>` ASAMBLAT INAINTE de `<style>`,
|
||||
care seteaza `data-theme` sincron, pre-paint. Verificat E2E (reload nu clipeste).
|
||||
- **Suprafete hardcodate raman dark in light** (banner/flash cu hex fix) → mitigat de US-001 (asertie
|
||||
de test pe absenta literalilor + theme-aware via variabile/`color-mix`).
|
||||
- **Contrast slab in light** (text gri pe alb, accent palid) → mitigat: AC explicit ≥ WCAG AA pe
|
||||
`--ink`/`--muted` peste `--bg`/`--card`; verificare E2E vizuala (text lizibil).
|
||||
- **Regresie pe dark** (refactor accidental al paletei existente) → mitigat: `test_dark_ramane_default`
|
||||
lock-uieste hex-urile dark exacte in `:root`; Non-Goal explicit "dark identic la octet".
|
||||
- **localStorage indisponibil** (mod privat strict / dezactivat) → script defensiv: `try/catch` in
|
||||
jurul citirii/scrierii; cade pe default (dark/OS) fara sa arunce. (AC implicit: zero erori consola.)
|
||||
- **Acelasi fisier in 2 stories** (`base.html`) → NU se paralelizeaza; un singur worker secvential
|
||||
(US-001 apoi US-002). Notat in §6.
|
||||
|
||||
## 5. Intrebari deschise
|
||||
|
||||
> Rezolvate cu utilizatorul la poarta de aprobare PRD (2026-06-22).
|
||||
|
||||
- **Default pentru utilizator nou (fara preferinta salvata)** — REZOLVAT: **OS-aware cu fallback dark**
|
||||
(onoreaza `prefers-color-scheme`; cand OS-ul nu cere light → `dark`, look-ul actual). [user 2026-06-22]
|
||||
- **Persistenta** — REZOLVAT: **`localStorage`** (client-only, anti-FOUC prin script in `<head>`, zero
|
||||
backend; nu atinge `routes.py`/sesiune). [user 2026-06-22]
|
||||
- **Aspect comutator** — REZOLVAT: **iconita soare/luna + `aria-label`** descriptiv, compact in header.
|
||||
[user 2026-06-22]
|
||||
|
||||
## 6. Valuri de executie (graful de dependente)
|
||||
|
||||
```
|
||||
Val 1: [US-001] → [US-002] ← SECVENTIAL (acelasi fisier base.html). Un singur worker (sau lead direct).
|
||||
```
|
||||
|
||||
Livrabila mica, un singur fisier de productie atins (`base.html`): poate rula fara `TeamCreate`
|
||||
(un worker Sonnet TDD, ambele stories secvential). VERIFY in context curat + writeback raman
|
||||
obligatorii (ROADMAP §5.5).
|
||||
|
||||
## 7. Review-uri de plan (aplicate inainte de cod — ROADMAP §5.3)
|
||||
|
||||
> Se completeaza la PLAN inainte de aprobare. CEO + Eng obligatorii; Design — DA (atinge UI).
|
||||
|
||||
**CEO (valoare/scope) — PASS.** Cerinta directa de ergonomie din Etapa 5 (decizie utilizator
|
||||
2026-06-22), efort mic peste o fundatie deja pregatita (CSS pe variabile). Calea cea mai scurta:
|
||||
reuse `:root` + override `[data-theme]`, zero backend. Inversiune ("ce-l face inutil?"): FOUC la
|
||||
incarcare (face produsul sa para buggy) — neutralizat prin scriptul anti-FOUC din `<head>`; si
|
||||
suprafetele hardcodate care raman dark in light (arata stricat) — neutralizate prin theme-aware in
|
||||
US-001. Scope minim corect (doua teme, fara personalizare). Niciun scope creep.
|
||||
|
||||
**Eng (fezabilitate/teste) — PASS.** Fezabilitate triviala, un singur fisier de productie, zero
|
||||
atingere de backend/schema/worker. Testele acopera contractul de template (paleta light prezenta,
|
||||
dark neschimbat, suprafete theme-aware, script anti-FOUC pozitionat corect, toggle prezent +
|
||||
accesibil + pe paginile neautentificate); comportamentul vizual + persistenta + anti-FOUC raman pe
|
||||
E2E browser (corect — nu se pot prinde la TestClient). Risc unic real = FOUC, prins doar in browser
|
||||
→ E2E explicit cu reload. `localStorage` defensiv (try/catch) acoperit ca AC zero-erori.
|
||||
|
||||
**Design — PASS (cu note).** Atinge UI direct. Paleta light trebuie sa respecte contrast WCAG AA
|
||||
(AC explicit). Comutatorul: iconita soare/luna + `aria-label`, plasat in header langa env/version,
|
||||
zona de atins ≥ 36px (consistent cu `.cardlink` existent). Suprafetele semantice (ok/warn/err) sa
|
||||
ramana distincte si lizibile pe light, nu doar inversate. Tranzitia de comutare poate fi instant
|
||||
(fara animatie) ca sa nu para lenta; daca se adauga `transition` pe culori, scurta (≤ 150ms, ca
|
||||
restul UI-ului). De confirmat vizual la VERIFY E2E.
|
||||
|
||||
---
|
||||
|
||||
## Raport VERIFY
|
||||
|
||||
> Completat de subagentul verificator (context curat) in faza VERIFY — vezi ROADMAP §5.6.
|
||||
|
||||
### Runda 1 (2026-06-22) — FAIL
|
||||
|
||||
Verificator independent (context curat, rol qa-only). **VERDICT: FAIL** (un blocker, US-001).
|
||||
|
||||
- **Suita — PASS.** `python3 -m pytest -q` → 583 passed; `tests/test_tema.py` → 6 passed.
|
||||
- **US-002 (toggle + persistenta + anti-FOUC) — PASS integral (E2E browser).** Toggle pe
|
||||
login/signup/dashboard; comutare instant (bg `#f6f7f9`↔`#0f1115`, aria-label + iconita comuta);
|
||||
persistenta `localStorage.theme` doar la click; reload pastreaza tema fara FOUC; OS-aware fara
|
||||
scriere (load simplu → `localStorage.theme`=null); zero erori consola (doar 404 favicon preexistent).
|
||||
- **US-001 (paleta light + suprafete theme-aware) — FAIL.** Paleta light + dark-neschimbat + `base.html`
|
||||
color-mix = PASS, DAR suprafetele de stare raman pete intunecate cu **text invizibil** in light:
|
||||
literalii dark `#241a1a`/`#201c0f` NU au fost convertiti in **fragmentele HTMX** (10 aparitii, 7
|
||||
fisiere: `_status.html`, `_banner.html`, `_upload.html`, `_preview_import.html`, `_preview_rand.html`,
|
||||
`_trimitere_detaliu.html`, `_mapcoloane.html`) — inline-style, randate in dashboard. Dovada: banner
|
||||
"Cont in asteptare de activare" = cutie `rgb(32,28,15)` pe fundal `#f6f7f9`. Testul `test_suprafete_
|
||||
fara_fundal_hardcodat` scana DOAR `<style>` din base.html → verde inselator (vacuu).
|
||||
- **Regresie de aur — PASS.** Pur frontend; worker/coada/API/schema neatinse (worker a procesat o
|
||||
orfana `sent idPrezentare=68801` la pornire → pipeline send functional, neafectat de 5.3).
|
||||
|
||||
**Remediu (US-003, adaugat in §3):** muta literalii la `color-mix` in cele 7 fragmente + extinde testul
|
||||
sa scaneze fragmentele. Re-VERIFY cu subagent NOU dupa fix.
|
||||
|
||||
### Runda 2 (2026-06-22) — PASS
|
||||
|
||||
Verificator independent NOU (context curat, rol qa-only), dupa fix US-003. **VERDICT GLOBAL: PASS.**
|
||||
|
||||
- **Suita — PASS.** `python3 -m pytest -q` → 584 passed; `tests/test_tema.py` → 7 passed (incl. noul
|
||||
`test_fragmente_fara_fundal_hardcodat`).
|
||||
- **Anti-regresie protectie — PASS.** `grep -rn -E '#201c0f|#241a1a|#16241c' app/web/templates/` →
|
||||
GOL. Testul scaneaza fisierele `_*.html` de pe disc (`Path.glob`), nu doar base.html → lacuna r1 inchisa.
|
||||
- **US-001/US-003 (light lizibil) — PASS (E2E, dovada cheie).** Cont inactive fortat → banner
|
||||
"Cont in asteptare de activare" din `_status.html` in light: `background` ≈ `rgb(246,234,225)` (crema
|
||||
deschis), text `rgb(26,29,36)` (contrast ~13:1 ≫ AA), border amber. Cutia neagra `rgb(32,28,15)` din
|
||||
r1 a DISPARUT. Screenshot confirma caseta peach cu text negru lizibil. Scan al tuturor suprafetelor
|
||||
vizibile in light: zero zone genuin intunecate. Dark mode: aspect practic neschimbat.
|
||||
- **US-002 — PASS (re-confirmat).** Toggle instant; `localStorage.theme` doar la click; aria-label se
|
||||
inverseaza; persista peste reload in ambele sensuri; anti-FOUC (script in `<head>` inainte de `<style>`);
|
||||
toggle pe `/login` + dashboard; zero erori JS de tema in consola.
|
||||
- **Regresie de aur — PASS.** Diff strict frontend (`base.html` + 7 fragmente + `tests/test_tema.py`);
|
||||
worker/coada/API/schema/mapping NEATINSE. Flux LIVE RAR catre RAR test NEPROBAT (creds key efemera;
|
||||
send neatins de 5.3) — NEPROBAT, nu FAIL.
|
||||
- **Nota cleanup:** verificatorul a lasat un cont de test inactive (id=5, verifyr2@test.com) in DB-ul
|
||||
de test efemer folosit pentru a forta bannerul — inofensiv (date de test, mediu test).
|
||||
|
||||
### CLOSE — `/code-review` high (2026-06-22)
|
||||
|
||||
1 finding real reparat: in paleta light `--ok:#16a34a` (green-600) folosit ca text (`.s-sent`/`.s-ok`,
|
||||
bife verzi din bara de stare) pe `--card:#ffffff` dadea contrast ~3.3:1 — sub WCAG AA 4.5:1 pentru text
|
||||
mic, incalcand AC-ul US-001. Reparat → `--ok:#15803d` (green-700, ~5.0:1, trece AA); paleta dark
|
||||
neatinsa. Restul semanticelor light trec deja AA (--err 6.2:1, --warn 5.0:1, --accent 5.1:1). Schimbare
|
||||
de valoare CSS (fara comportament) → fara re-VERIFY. Refutate: suport `color-mix` (universal pe browsere
|
||||
moderne, audienta B2B); duplicarea inline a `color-mix` in fragmente (precede acest diff, candidat de
|
||||
cleanup viitor — macro/clase `.flash.err`/`.flash.warn`). 584 teste pass.
|
||||
Reference in New Issue
Block a user