Modal / Dialog

Markup canonic · components/modal.json

sursa = JSON · generat de scripts/build-catalog.py

Scrim live · deschide/închide

Demo interactivă a tratamentului canonic: background: var(--c-backdrop) (ink @ 25%) + backdrop-filter: blur(var(--scrim-blur)) (6px), cu animația reală (modal-fade 180ms pe backdrop, modal-in 220ms pe card). Închide cu X, click pe backdrop, sau Escape — cele trei căi obligatorii din „Comportament obligatoriu" (docs/components/modal.md).

apasă butonul pentru a vedea scrimul intrând — nu un mockup static

Eyebrow parametric

Eticheta .t-eyebrow de deasupra titlului e opțională — o folosești când modalul are nevoie de context sau categorie (ex. „Factură nouă"). Layoutul cardului funcționează identic cu sau fără ea; când lipsește, titlul .t28 urcă primul și primește +4px spațiu sus.

cu eyebrow · fără eyebrow — același card, aceeași structură

Variantă · acțiuni suprapuse

Dialog de dismiss cu două acțiuni suprapuse vertical, într-un .modal-actions.stacked (= .btn-group.stacked): dismiss ca .btn.outline deasupra, confirm ca .btn.primary dedesubt. Ambele size default (md), întinse de grup; spațiere moderată: gap 8px (--sp-8). Rămâne o singură acțiune primary per modal, și aceeași ordine la orice lățime.

.modal-actions.stacked · .btn.outline + .btn.primary · size default · gap var(--sp-8)

Dialog de ștergere · distructiv

Confirmarea unei acțiuni distructive, în același .modal-actions.stacked. Acțiunea distructivă e negru cu iconiță (.btn.danger), dismiss-ul deasupra ca .btn.outline — dedesubt niciodată, la nicio lățime. Pentru ștergeri cu impact mare adaugă confirmare prin nume: inputul rămâne în stare de eroare până când textul se potrivește, cu .field-help.error aliniat sub input.

.modal-actions.stacked · .btn.outline + .btn.danger (negru + iconiță) · confirmare prin nume cu stare de eroare

Structură HTML

markup minim pentru a porni un modal
<div class="modal-backdrop" onclick="if(event.target===this)closeModal()">
  <div class="modal">
    <button class="btn outline sm icon-only" aria-label="Închide">...</button>
    <div class="t-eyebrow">...</div>          <!-- opțional — fără el, titlul primește margin-top:4px -->
    <h2 class="t28">Titlu <em>modal</em></h2>
    <!-- inputs lg -->
    <button class="btn primary lg full">Acțiune</button>

    <!-- SAU varianta stacked-actions (dismiss dialog) -->
    <div class="modal-actions stacked">
      <button class="btn outline">Renunță</button>
      <button class="btn primary">Confirmă</button>
    </div>
  </div>
</div>
JS: backdrop.classList.toggle('open')

Sloturile

Părțile pe care orice modal le are oricum, formalizate ca să nu fie reinventate per ecran. Titlul și descrierea nu au tipografie proprie: .modal-title e .t28 și .modal-body e .t14 — același selector în regula treptei din scală. Slotul adaugă doar spațierea.

.modal-title     = .t28 · 22→28px / 700 · ink · 8px dedesubt
                 padding-right 40px (lasă loc pentru X)
.modal-body      = .t14 · 14px / 20px / 500 · fog · 24px dedesubt
.modal-actions   flex end · gap 10px · ordinea [secundar][primar]
                 sub 1024px: column, aceeași ordine, fiecare buton full-width
.modal-actions.stacked
                 coloană la orice lățime · gap --sp-8 · suprapuse

.modal-actions e .btn-group — același selector în CSS, nu o a doua implementare. Un modal nu are alt rând de acțiuni decât restul aplicației, deci nici alt CSS. Ordinea e [secundar][primar] pe mobil și pe desktop; .stacked schimbă doar axa, niciodată ordinea.

Alert dialog · .modal.confirm

Aceeași suprafață, conținut simplificat: un titlu și o propoziție. Fără X — o întrebare blocantă cere un răspuns explicit, nu o ieșire laterală. Exemplul de aici e distructiv, deci poartă regula de ștergere: .modal-actions.stacked, cu .btn.outline deasupra și .btn.danger cu iconiță dedesubt — aceeași ca la Dialog de ștergere. Un .modal.confirm nedistructiv (confirmare de trimitere, de exemplu) folosește același .modal-actions, fără .stacked.

.modal.confirm   max-width 400px · padding 28px 28px 24px
                 aceeași suprafață: bej-1 · --r-xl · outline alb offset 16px
                 butonul de închidere ascuns
acțiuni          dacă e DISTRUCTIV: .modal-actions.stacked — .btn.outline
                 deasupra + .btn.danger (negru + iconiță) dedesubt —
                 aceeași regulă ca „Dialog de ștergere" mai sus
                 dacă NU e distructiv: același .modal-actions fără .stacked —
                 rând aliniat dreapta pe desktop, coloană full-width sub 1024px
                 ordinea [secundar][primar] în ambele cazuri, la orice lățime
role             alertdialog (nu dialog) + aria-modal + aria-labelledby
De ce nu .modal.alert. Ăsta era numele evident, dar .alert e deja o componentă — iar .modal.alert prinde și regula ei, care e display: flex: titlul, textul și butoanele se așează pe un rând. Un modificator nu poate purta numele unei componente existente. Prins la randare, nu la citire.
Butonul distructiv e .btn.danger — fill --c-danger (ink), nu roșu. Și fără accent de brand în modalul distructiv: fără pink, fără <em> în titlu.

Modal în pași · .modal.stepped

Un flux scurt — editare → … → confirmare — într-o singură fereastră. Confirmarea nu mai e un al doilea modal deschis peste primul, ci ultimul pas al aceluiași modal. Pașii stau sub titlu (--sp-16 deasupra, --sp-28 dedesubt), iar fereastra are aceeași înălțime la toți pașii (--modal-stepped-h = cel mai înalt pas): se schimbă doar conținutul din .modal-step, butoanele stau pe loc. Exemplele sunt din Vertigo: Status ONRC în 2 pași și Sediu social în 4 pași pe .modal.lg, la 700px.

pasul 1 · Editare — pasul 2 · Confirmare · aceeași fereastră, aceeași înălțime (--modal-stepped-h: 460px)
4 pași · Sediu social · .modal.lg · pasul 2 Atribute — pasul 4 Confirmare · aceeași înălțime (--modal-stepped-h: 700px)
.modal.stepped    display flex · column · height var(--modal-stepped-h)
                  max-height calc(100dvh − 64px)
ordine            X · .modal-title · .stepper · .modal-step · .modal-actions
.stepper          SUB titlu — --sp-16 deasupra, --sp-28 dedesubt
                  mărimea de conținut: chip 28 · label 14 · gap 28
.modal-step       descrierea + câmpurile pasului · se derulează dacă depășește
.modal-actions    lipit jos · --sp-24 deasupra · rând pe desktop
                  ambele butoane md (40px) — .btn.outline + .btn.primary
                  .lg doar pe mobil
înălțime          aceeași la toți pașii = cel mai înalt pas (pus de flux)
pași              2–5 · ultimul = Confirmare (.quantum-stack + „Da, salvează")
                  4–5 pași sau câmpuri pe 2 coloane → .modal.lg
                  5 pași → stepperul trece singur pe treapta compactă (26 · 13 · gap 22)
                  > 5 pași sau > 6 câmpuri pe pas → ecran de flux, nu modal
acțiuni           primul [Anulează][Continuă] · mijloc [Înapoi][Continuă]
                  ultimul [Înapoi][Da, salvează] · Continuă disabled până e valid
focus             la schimbarea pasului → titlul (h2 tabindex=-1)
mobil             TBD — varianta nu e desenată sub 1024px
Mobil (<1024px): design TBD. Varianta e desenată doar pe desktop. Pe mobil nu sunt încă decise forma stepperului (orizontal, .stepper.mobile sau contor „2 din 4"), înălțimea fixă și dacă fereastra devine ecran de flux. Până atunci, nu o folosi pe mobil fără un design aprobat.
De ce nu un al doilea modal. Confirmarea deschisă peste modalul de editare punea două ferestre una peste alta: două scrimuri, două X-uri, două rânduri „Anulează / Continuă" vizibile în același timp. Ca ultim pas al aceleiași ferestre, confirmarea își păstrează contextul (pașii arată de unde vii) și se poate întoarce la editare fără să piardă nimic.