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:
Claude Agent
2026-06-22 19:39:12 +00:00
parent ae7960294f
commit b48501d8e4
11 changed files with 515 additions and 16 deletions

View File

@@ -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.

View File

@@ -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>

View File

@@ -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

View File

@@ -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>

View File

@@ -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.

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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;">&#9728;</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 = '&#9790;';
btn.setAttribute('aria-label', 'Comuta tema (intunecat)');
btn.title = 'Comuta tema (intunecat)';
} else {
btn.innerHTML = '&#9728;';
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>