Files
rar-autopass/docs/prd/prd-5.3-light-dark-mode.md
Claude Agent b48501d8e4 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>
2026-06-22 19:39:12 +00:00

18 KiB

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: backgroundrgb(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.