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>
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 =
localStoragepur client-side (roadmap zice "cookie/localStorage" — alegem localStorage: zero suprafata server + anti-FOUC prin scriptul din<head>). Nu se atingeroutes.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) silight. - 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 dinbase.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-ulGET /login(sau dashboard) contine un selector[data-theme="light"]care redefineste--bg,--card,--ink,--muted,--line.test_dark_ramane_default—:rootcontine 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-mixce 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,--linecu o paleta deschisa (fundal deschis, text inchis). Contrastul text/fundal ≥ WCAG AA (4.5:1 pentru--inkpe--bgsi pe--card). - Paleta dark din
:rootramane 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 saucolor-mixpeste 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 -qverde.
- Exista un bloc
- Verificare E2E: browser pe
/cudocument.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-mixDOARbase.html; aceleasi fundaluri hardcodate dark (#241a1aerr,#201c0fwarn) 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(toateapp/web/templates/),tests/test_tema.py(extins) - Test intai (RED):
tests/test_tema.py—test_fragmente_fara_fundal_hardcodat— scaneaza TOATE fisiereleapp/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 folosesccolor-mixpeste paleta (var(--err)/var(--warn)/var(--ok)12% pestevar(--card)), exact ca inbase.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
--carddark dă aproape aceeasi nuanta). - Testul de protectie scaneaza fragmentele, nu doar
base.html(lacuna r1 inchisa). python3 -m pytest -qverde.
- Niciun fisier din
- 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 citestelocalStoragecheiathemesi seteazadocument.documentElementdata-themeinainte de tag-ul<style>(pozitie in HTML: index script < index<style>).test_buton_toggle_in_header_cu_eticheta—<header>contine un control de comutare cuaria-label/titledescriptiv (ex. "Comuta tema") si unid/atribut stabil pentru handler.test_toggle_pe_login_si_dashboard— butonul apare si pe/login(neautentificat) si pe dashboard (ambele extindbase.html).
- Acceptance criteria:
- Script inline in
<head>, plasat inaintea<style>, citeste preferinta:localStorage.themedaca exista, altfelprefers-color-scheme(fallback final:dark), si seteazadata-themepe<html>sincron → fara FOUC la incarcare/reload. - Buton de comutare in
<header>(langaenv/version), cuaria-labeldescriptiv, atins usor (≥ 36px zona de atins), care comutadata-themelight↔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 -qverde.
- Script inline in
- Verificare E2E (Playwright MCP /
/browse): pe/loginapoi 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 seteazadata-themesincron, 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/--mutedpeste--bg/--card; verificare E2E vizuala (text lizibil). - Regresie pe dark (refactor accidental al paletei existente) → mitigat:
test_dark_ramane_defaultlock-uieste hex-urile dark exacte in:root; Non-Goal explicit "dark identic la octet". - localStorage indisponibil (mod privat strict / dezactivat) → script defensiv:
try/catchin 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 atingeroutes.py/sesiune). [user 2026-06-22] - Aspect comutator — REZOLVAT: iconita soare/luna +
aria-labeldescriptiv, 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); persistentalocalStorage.themedoar 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.htmlcolor-mix = PASS, DAR suprafetele de stare raman pete intunecate cu text invizibil in light: literalii dark#241a1a/#201c0fNU 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" = cutiergb(32,28,15)pe fundal#f6f7f9. Testultest_suprafete_ fara_fundal_hardcodatscana 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=68801la 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. noultest_fragmente_fara_fundal_hardcodat). - Anti-regresie protectie — PASS.
grep -rn -E '#201c0f|#241a1a|#16241c' app/web/templates/→ GOL. Testul scaneaza fisierele_*.htmlde 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.htmlin light:background≈rgb(246,234,225)(crema deschis), textrgb(26,29,36)(contrast ~13:1 ≫ AA), border amber. Cutia neagrargb(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.themedoar 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.