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:
@@ -1,5 +1,5 @@
|
||||
{% if not account_active %}
|
||||
<div class="card banner" style="border-color:var(--warn); background:#201c0f;"
|
||||
<div class="card banner" style="border-color:var(--warn); background:color-mix(in srgb, var(--warn) 12%, var(--card));"
|
||||
hx-get="/_fragments/banner" hx-trigger="every 15s" hx-swap="outerHTML">
|
||||
<strong>Cont in asteptare de activare.</strong>
|
||||
Configureaza creds RAR si pregateste importul ACUM; trimiterea catre RAR porneste automat dupa activare de catre admin.
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</h2>
|
||||
|
||||
{% if message %}
|
||||
<div class="flash" style="{% if error %}border-color:var(--err); background:#241a1a;{% endif %} margin-bottom:12px;"
|
||||
<div class="flash" style="{% if error %}border-color:var(--err); background:color-mix(in srgb, var(--err) 12%, var(--card));{% endif %} margin-bottom:12px;"
|
||||
{% if error %}role="alert"{% endif %}>
|
||||
{{ message }}
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</div>
|
||||
|
||||
{% if message %}
|
||||
<div class="flash" style="{% if error %}border-color:var(--err); background:#241a1a;{% endif %} margin-bottom:12px;"
|
||||
<div class="flash" style="{% if error %}border-color:var(--err); background:color-mix(in srgb, var(--err) 12%, var(--card));{% endif %} margin-bottom:12px;"
|
||||
{% if error %}role="alert"{% endif %}>
|
||||
{{ message }}
|
||||
</div>
|
||||
@@ -56,7 +56,7 @@
|
||||
|
||||
<!-- Panou inline: operatii fara cod RAR, mapabile in flux (fara re-upload) -->
|
||||
{% if unmapped_ops %}
|
||||
<div class="card" style="border-color:var(--err); background:#241a1a; margin-bottom:14px;">
|
||||
<div class="card" style="border-color:var(--err); background:color-mix(in srgb, var(--err) 12%, var(--card)); margin-bottom:14px;">
|
||||
<h3 style="font-size:14px; margin:0 0 6px;">Operatii de mapat la cod RAR</h3>
|
||||
<p class="muted" style="margin:0 0 12px; font-size:13px;">
|
||||
Aceste operatii din fisier nu au inca un cod RAR. Alege codul (sugestia e
|
||||
|
||||
@@ -31,12 +31,12 @@
|
||||
</div>
|
||||
|
||||
{% if message %}
|
||||
<div class="flash" style="border-color:var(--err); background:#241a1a; margin-bottom:10px;"
|
||||
<div class="flash" style="border-color:var(--err); background:color-mix(in srgb, var(--err) 12%, var(--card)); margin-bottom:10px;"
|
||||
role="alert">{{ message }}</div>
|
||||
{% endif %}
|
||||
<div class="rand-eroare-banner" role="alert"
|
||||
style="display:none; margin-bottom:10px; padding:8px 12px; border:1px solid var(--err);
|
||||
background:#241a1a; border-radius:6px; font-size:13px;">
|
||||
background:color-mix(in srgb, var(--err) 12%, var(--card)); border-radius:6px; font-size:13px;">
|
||||
Salvarea nu a reusit (retea / sesiune). Valorile introduse sunt pastrate — reincearca.
|
||||
</div>
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<!-- Cont in asteptare de activare (regasit din vechiul _banner; mereu vizibil) -->
|
||||
{% if not account_active %}
|
||||
<div style="margin-bottom:12px; padding:8px 10px; border-left:3px solid var(--warn);
|
||||
background:#201c0f; border-radius:6px; font-size:13px;">
|
||||
background:color-mix(in srgb, var(--warn) 12%, var(--card)); border-radius:6px; font-size:13px;">
|
||||
<strong>Cont in asteptare de activare.</strong>
|
||||
Configureaza credentialele RAR si pregateste importul acum; trimiterea catre RAR
|
||||
porneste automat dupa activare de catre administrator.
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
<h3 style="font-size:14px; margin:0 0 8px;">Corecteaza si re-trimite</h3>
|
||||
|
||||
{% if corectie_msg %}
|
||||
<div class="flash" style="{% if corectie_error %}border-color:var(--err); background:#241a1a;{% endif %} margin-bottom:10px;"
|
||||
<div class="flash" style="{% if corectie_error %}border-color:var(--err); background:color-mix(in srgb, var(--err) 12%, var(--card));{% endif %} margin-bottom:10px;"
|
||||
{% if corectie_error %}role="alert"{% endif %}>{{ corectie_msg }}</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
@@ -9,12 +9,12 @@
|
||||
{% endif %}
|
||||
|
||||
{% if error %}
|
||||
<div class="flash" style="border-color:var(--err); background:#241a1a; margin-bottom:12px;"
|
||||
<div class="flash" style="border-color:var(--err); background:color-mix(in srgb, var(--err) 12%, var(--card)); margin-bottom:12px;"
|
||||
role="alert">{{ error }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if sheets %}
|
||||
<div class="flash" style="border-color:var(--warn); background:#201c0f; margin-bottom:12px;">
|
||||
<div class="flash" style="border-color:var(--warn); background:color-mix(in srgb, var(--warn) 12%, var(--card)); margin-bottom:12px;">
|
||||
Fisierul are mai multe foi de lucru. Alege foaia de mai jos si incarca din nou.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -13,9 +13,26 @@
|
||||
// useTemplateFragments parseaza tot intr-un <template>, pastrand rand + OOB impreuna.
|
||||
htmx.config.useTemplateFragments = true;
|
||||
</script>
|
||||
<script>
|
||||
// Anti-FOUC (US-001 PRD 5.3): citeste preferinta tema din localStorage inainte de
|
||||
// primul paint; seteaza data-theme pe <html> sincron, fara blink dark->light.
|
||||
(function() {
|
||||
try {
|
||||
var t = localStorage.getItem('theme');
|
||||
if (!t) {
|
||||
t = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
||||
}
|
||||
document.documentElement.setAttribute('data-theme', t);
|
||||
} catch(e) {
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<style>
|
||||
:root { --bg:#0f1115; --card:#181b22; --ink:#e6e9ef; --muted:#8b93a7; --line:#262b36;
|
||||
--ok:#3ecf8e; --warn:#e6b34a; --err:#e5605e; --accent:#5b8def; }
|
||||
[data-theme="light"] { --bg:#f6f7f9; --card:#ffffff; --ink:#1a1d24; --muted:#5c6473; --line:#e2e5ea;
|
||||
--ok:#15803d; --warn:#b45309; --err:#dc2626; --accent:#2563eb; }
|
||||
* { box-sizing:border-box; }
|
||||
body { margin:0; font:15px/1.5 ui-sans-serif,system-ui,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
|
||||
background:var(--bg); color:var(--ink); -webkit-font-smoothing:antialiased; }
|
||||
@@ -24,7 +41,7 @@
|
||||
header .env { font-size:12px; color:var(--muted); border:1px solid var(--line); padding:2px 8px; border-radius:99px; }
|
||||
main { padding:24px; max-width:1100px; margin:0 auto; }
|
||||
.card { background:var(--card); border:1px solid var(--line); border-radius:10px; padding:16px 20px; margin-bottom:16px; }
|
||||
.banner { border-left:3px solid var(--err); background:#241a1a; }
|
||||
.banner { border-left:3px solid var(--err); background:color-mix(in srgb, var(--err) 12%, var(--card)); }
|
||||
.banner.hidden { display:none; }
|
||||
/* Tabelele de date au multe coloane; pe ecrane inguste scroll IN card, nu
|
||||
impinge layout-ul paginii (altfel toata pagina scrolleaza orizontal). */
|
||||
@@ -46,7 +63,7 @@
|
||||
text-align:center; transition:border-color .15s,background .15s; }
|
||||
.drop-zone.drag-over { border-color:var(--accent); background:rgba(91,141,239,.05); }
|
||||
/* Banner varianta warn (nu eroare) */
|
||||
.banner.warn { border-left-color:var(--warn); background:#201c0f; }
|
||||
.banner.warn { border-left-color:var(--warn); background:color-mix(in srgb, var(--warn) 12%, var(--card)); }
|
||||
/* Bara confirmare sticky */
|
||||
.sticky-bar { position:sticky; bottom:0; background:var(--card); border-top:1px solid var(--line);
|
||||
padding:12px 16px; display:flex; align-items:flex-start; gap:16px;
|
||||
@@ -59,7 +76,7 @@
|
||||
.cardlink { font-size:13px; padding:7px 10px; border-radius:6px; display:inline-flex;
|
||||
align-items:center; min-height:36px; white-space:nowrap; }
|
||||
.cardlink:hover { background:var(--line); }
|
||||
.flash { background:#16241c; border-left:3px solid var(--ok); padding:8px 12px; border-radius:6px;
|
||||
.flash { background:color-mix(in srgb, var(--ok) 12%, var(--card)); border-left:3px solid var(--ok); padding:8px 12px; border-radius:6px;
|
||||
margin:0 0 12px; font-size:13px; }
|
||||
.maprow { display:flex; gap:16px; align-items:center; padding:12px 0; border-bottom:1px solid var(--line);
|
||||
flex-wrap:wrap; }
|
||||
@@ -92,8 +109,48 @@
|
||||
<header>
|
||||
<h1>Gateway RAR AUTOPASS</h1>
|
||||
<span class="env">{{ rar_env }}</span>
|
||||
<span class="muted" style="margin-left:auto; font-size:13px;">v{{ version }}</span>
|
||||
<div style="margin-left:auto; display:flex; align-items:center; gap:8px;">
|
||||
<button id="tema-toggle"
|
||||
aria-label="Comuta tema (luminos/intunecat)"
|
||||
title="Comuta tema"
|
||||
style="background:transparent; border:1px solid var(--line); color:var(--ink); cursor:pointer; border-radius:6px; min-height:36px; min-width:36px; font-size:16px; padding:4px 8px; line-height:1; display:inline-flex; align-items:center; justify-content:center;">☀</button>
|
||||
<span class="muted" style="font-size:13px;">v{{ version }}</span>
|
||||
</div>
|
||||
</header>
|
||||
<main>{% block content %}{% endblock %}</main>
|
||||
<script>
|
||||
// Handler comutator tema (US-002 PRD 5.3): click toggle light<->dark, persista in localStorage.
|
||||
// Separare init (doar sincronizare iconita) de persistenta (doar la click explicit).
|
||||
// Motivatie: scrierea in localStorage la init ar ingloba imediat preferinta OS-aware ca alegere
|
||||
// explicita, impiedicand urmarirea ulterioara a modificarilor de tema ale sistemului de operare.
|
||||
(function() {
|
||||
var btn = document.getElementById('tema-toggle');
|
||||
if (!btn) return;
|
||||
// Sincronizeaza iconita si aria-label dupa tema curenta -- FARA efecte secundare in localStorage.
|
||||
function _syncIcon(t) {
|
||||
if (t === 'light') {
|
||||
btn.innerHTML = '☾';
|
||||
btn.setAttribute('aria-label', 'Comuta tema (intunecat)');
|
||||
btn.title = 'Comuta tema (intunecat)';
|
||||
} else {
|
||||
btn.innerHTML = '☀';
|
||||
btn.setAttribute('aria-label', 'Comuta tema (luminos)');
|
||||
btn.title = 'Comuta tema (luminos)';
|
||||
}
|
||||
}
|
||||
// Aplica o tema noua, seteaza data-theme si persista in localStorage -- apelat DOAR la click.
|
||||
function _setTheme(t) {
|
||||
document.documentElement.setAttribute('data-theme', t);
|
||||
try { localStorage.setItem('theme', t); } catch(e) {}
|
||||
_syncIcon(t);
|
||||
}
|
||||
// La init: sincronizeaza doar iconita din data-theme curent (setat deja de scriptul anti-FOUC).
|
||||
_syncIcon(document.documentElement.getAttribute('data-theme') || 'dark');
|
||||
btn.addEventListener('click', function() {
|
||||
var cur = document.documentElement.getAttribute('data-theme') || 'dark';
|
||||
_setTheme(cur === 'dark' ? 'light' : 'dark');
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
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.
|
||||
186
tests/test_tema.py
Normal file
186
tests/test_tema.py
Normal file
@@ -0,0 +1,186 @@
|
||||
"""Teste US-001 + US-002 (PRD 5.3): Light/Dark mode comutator tema.
|
||||
|
||||
TDD: testele se scriu INAINTE de implementare (RED), dupa implementare trec (GREEN).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def client(monkeypatch):
|
||||
tmp = tempfile.mkdtemp()
|
||||
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "tema.db"))
|
||||
from app.config import get_settings
|
||||
get_settings.cache_clear()
|
||||
from app.main import app
|
||||
with TestClient(app) as c:
|
||||
yield c
|
||||
get_settings.cache_clear()
|
||||
|
||||
|
||||
def _create_user(email: str = "tema@test.com", password: str = "parolasecreta"):
|
||||
from app.accounts import create_account
|
||||
from app.users import create_user
|
||||
from app.db import get_connection
|
||||
conn = get_connection()
|
||||
try:
|
||||
acct_id = create_account(conn, "Service Tema", active=True)
|
||||
create_user(conn, acct_id, email, password)
|
||||
return acct_id
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def _login(client, email: str, password: str = "parolasecreta") -> None:
|
||||
resp = client.get("/login")
|
||||
m = re.search(r'name="csrf_token"\s+value="([^"]+)"', resp.text) or \
|
||||
re.search(r'value="([^"]+)"\s+name="csrf_token"', resp.text)
|
||||
assert m, "csrf_token negasit in /login"
|
||||
resp = client.post(
|
||||
"/login",
|
||||
data={"email": email, "parola": password, "csrf_token": m.group(1)},
|
||||
follow_redirects=False,
|
||||
)
|
||||
assert resp.status_code == 303, f"Login esuat: {resp.status_code}"
|
||||
|
||||
|
||||
# ── US-001: Tema light ─────────────────────────────────────────────────────────
|
||||
|
||||
def test_paleta_light_definita(client):
|
||||
"""HTML de la GET /login contine un selector [data-theme="light"] care redefineste
|
||||
cel putin --bg, --card, --ink, --muted, --line."""
|
||||
resp = client.get("/login")
|
||||
assert resp.status_code == 200
|
||||
html = resp.text
|
||||
|
||||
assert '[data-theme="light"]' in html, 'Lipseste blocul [data-theme="light"] in HTML'
|
||||
|
||||
light_block = re.search(r'\[data-theme=["\']light["\']\]\s*\{([^}]+)\}', html, re.DOTALL)
|
||||
assert light_block, 'Nu am gasit blocul CSS [data-theme="light"] { ... }'
|
||||
block = light_block.group(1)
|
||||
for var in ("--bg", "--card", "--ink", "--muted", "--line"):
|
||||
assert var in block, f"Variabila {var} lipseste din blocul [data-theme=\"light\"]"
|
||||
|
||||
|
||||
def test_dark_ramane_default(client):
|
||||
""":root contine inca paleta dark exacta: --bg:#0f1115, --card:#181b22, --ink:#e6e9ef."""
|
||||
resp = client.get("/login")
|
||||
assert resp.status_code == 200
|
||||
html = resp.text
|
||||
|
||||
assert "--bg:#0f1115" in html, "Paleta dark --bg:#0f1115 a fost modificata sau stearsa"
|
||||
assert "--card:#181b22" in html, "Paleta dark --card:#181b22 a fost modificata sau stearsa"
|
||||
assert "--ink:#e6e9ef" in html, "Paleta dark --ink:#e6e9ef a fost modificata sau stearsa"
|
||||
|
||||
|
||||
def test_suprafete_fara_fundal_hardcodat(client):
|
||||
"""<style> NU mai contine literalii hex dark-fix #241a1a, #201c0f, #16241c
|
||||
(banner eroare / banner warn / flash facute theme-aware)."""
|
||||
resp = client.get("/login")
|
||||
assert resp.status_code == 200
|
||||
|
||||
style_match = re.search(r'<style>(.*?)</style>', resp.text, re.DOTALL)
|
||||
assert style_match, "<style> negasit in HTML"
|
||||
style = style_match.group(1)
|
||||
|
||||
assert "#241a1a" not in style, "Fundalul hardcodat #241a1a (banner eroare) inca in <style>"
|
||||
assert "#201c0f" not in style, "Fundalul hardcodat #201c0f (banner warn) inca in <style>"
|
||||
assert "#16241c" not in style, "Fundalul hardcodat #16241c (flash) inca in <style>"
|
||||
|
||||
|
||||
# ── US-002: Comutator tema + anti-FOUC ────────────────────────────────────────
|
||||
|
||||
def test_script_antifouc_in_head_inainte_de_style(client):
|
||||
"""<head> contine un <script> care citeste localStorage (cheia 'theme') si seteaza
|
||||
data-theme pe document.documentElement, pozitionat INAINTE de <style>."""
|
||||
resp = client.get("/login")
|
||||
assert resp.status_code == 200
|
||||
html = resp.text
|
||||
|
||||
head_match = re.search(r'<head>(.*?)</head>', html, re.DOTALL | re.IGNORECASE)
|
||||
assert head_match, "<head> negasit in HTML"
|
||||
head = head_match.group(1)
|
||||
|
||||
style_pos = head.find('<style>')
|
||||
assert style_pos >= 0, "<style> negasit in <head>"
|
||||
|
||||
head_before_style = head[:style_pos]
|
||||
assert 'localStorage' in head_before_style, \
|
||||
"Scriptul anti-FOUC (cu localStorage) trebuie sa fie in <head> INAINTE de <style>"
|
||||
assert 'theme' in head_before_style, \
|
||||
"Scriptul anti-FOUC trebuie sa citeasca cheia 'theme' din localStorage"
|
||||
assert ('data-theme' in head_before_style or 'dataset.theme' in head_before_style), \
|
||||
"Scriptul anti-FOUC trebuie sa seteze data-theme pe documentElement"
|
||||
|
||||
|
||||
def test_buton_toggle_in_header_cu_eticheta(client):
|
||||
"""<header> contine un <button> de comutare cu aria-label descriptiv (contine 'tema')."""
|
||||
resp = client.get("/login")
|
||||
assert resp.status_code == 200
|
||||
html = resp.text
|
||||
|
||||
header_match = re.search(r'<header>(.*?)</header>', html, re.DOTALL | re.IGNORECASE)
|
||||
assert header_match, "<header> negasit in HTML"
|
||||
header = header_match.group(1)
|
||||
|
||||
labels = re.findall(r'<button[^>]*aria-label=["\']([^"\']+)["\']', header, re.IGNORECASE)
|
||||
assert labels, "<button> cu aria-label negasit in <header>"
|
||||
assert any('tema' in lbl.lower() for lbl in labels), \
|
||||
f"Niciun <button> in <header> cu aria-label care contine 'tema'. Gasit: {labels}"
|
||||
|
||||
|
||||
def test_toggle_pe_login_si_dashboard(client):
|
||||
"""Butonul toggle apare atat pe /login (neautentificat) cat si pe dashboard (autentificat)."""
|
||||
# Pe /login
|
||||
resp = client.get("/login")
|
||||
assert resp.status_code == 200
|
||||
header_match = re.search(r'<header>(.*?)</header>', resp.text, re.DOTALL | re.IGNORECASE)
|
||||
assert header_match, "<header> negasit pe /login"
|
||||
assert re.search(
|
||||
r'<button[^>]*aria-label=["\'][^"\']*tema[^"\']*["\']',
|
||||
header_match.group(1),
|
||||
re.IGNORECASE,
|
||||
), "Butonul toggle lipseste pe /login"
|
||||
|
||||
# Pe dashboard (autentificat)
|
||||
_create_user("tema_dash@test.com", "parolasecreta")
|
||||
_login(client, "tema_dash@test.com", "parolasecreta")
|
||||
resp = client.get("/")
|
||||
assert resp.status_code == 200
|
||||
header_match = re.search(r'<header>(.*?)</header>', resp.text, re.DOTALL | re.IGNORECASE)
|
||||
assert header_match, "<header> negasit pe dashboard"
|
||||
assert re.search(
|
||||
r'<button[^>]*aria-label=["\'][^"\']*tema[^"\']*["\']',
|
||||
header_match.group(1),
|
||||
re.IGNORECASE,
|
||||
), "Butonul toggle lipseste pe dashboard"
|
||||
|
||||
|
||||
# ── US-003: Fragmente HTMX fara fundal hardcodat ──────────────────────────────
|
||||
|
||||
def test_fragmente_fara_fundal_hardcodat():
|
||||
"""Niciun fisier _*.html din app/web/templates/ nu contine literalii hex dark-fix
|
||||
#241a1a, #201c0f, #16241c (suprafete banner eroare / warn / flash)."""
|
||||
templates_dir = Path(__file__).parent.parent / "app" / "web" / "templates"
|
||||
fragmente = sorted(templates_dir.glob("_*.html"))
|
||||
assert fragmente, f"Nu am gasit fragmente _*.html in {templates_dir}"
|
||||
|
||||
vinovate = []
|
||||
for f in fragmente:
|
||||
continut = f.read_text(encoding="utf-8")
|
||||
for literal in ("#241a1a", "#201c0f", "#16241c"):
|
||||
if literal in continut:
|
||||
vinovate.append(f"{f.name}: {literal}")
|
||||
|
||||
assert not vinovate, (
|
||||
"Fragmente cu fundal hardcodat dark (nu adapteaza la tema light):\n"
|
||||
+ "\n".join(vinovate)
|
||||
)
|
||||
Reference in New Issue
Block a user