Select

Alegere dintr-o listă închisă. Forma implicită e componenta React cu meniu popover — react/select.tsx, Radix îmbrăcat în clasele DS: .select-trigger poartă valoarea, .popover ține lista, iar rândul ales primește bifa de brand .popover-check. Aceeași formă se scrie de mână în HTML cu .select-anchor + .select-trigger + .popover. <select class="select"> nativ rămâne o variantă secundară — validă și nedepreciată, dar pe locul doi. Aceleași dimensiuni, stări și contexte ca .input pe toate.

React · react/select.tsx — forma implicită

Înlocuiește components/ui/select.tsx din shadcn fără să atingi call-site-urile: același API, alt strat de stil.

<Select defaultValue="q" onValueChange={setPeriod}>
  <SelectTrigger className="block"><SelectValue /></SelectTrigger>
  <SelectContent>
    <SelectItem value="m">Luna curentă</SelectItem>
    <SelectItem value="q">Trimestrul curent</SelectItem>
  </SelectContent>
</Select>

Import din @/components/ui/select. .block ține lățimea câmpului — nu există .select-anchor, fiindcă Radix portalează lista. Mai există SelectGroup, SelectLabel și SelectSeparator, toate cu semnătura din shadcn.

Clasele DS, nu utilitare. Componenta poartă clasele din bono-ds.css și nu re-declară stil în TypeScript. Un rând scris în utilitare ar fi a doua definiție a aceluiași lucru, iar prima schimbare de token ar ajunge la HTML și nu la React — exact deriva pentru care există scripts/check-stamp-drift.py. scripts/build-catalog.py pică pe utilitare în react/. Detalii în react/README.md.

Dependență: npm i @radix-ui/react-select.

Ce vine din Radix: rolul de listbox, navigarea cu săgeți, type-ahead-ul, Esc, focus trap-ul, aria-expanded pe trigger (de care atârnă rotirea chevronului din CSS) și poziționarea. Ce vine din DS: suprafața, rândurile, starea de ales. .popover.popper e modificatorul care predă coordonatele — Radix își pune poziția pe wrapper-ul lui, iar ancorarea proprie a DS-ului ar deplasa meniul dacă ar rămâne activă.

Markup canonic · components/select.json

sursa = JSON · generat de scripts/build-catalog.py — forma implicită. Click pe trigger: lista e a DS-ului.

Varianta secundară · <select class="select">

Nativul nu e depreciat — e al doilea. Rămâne răspunsul corect când lista e text simplu și unul dintre astea contează: nu există React pe suprafață (pagini statice, e-mail, referințele -aligned), vrei roata nativă de pe mobil, sau ai nevoie de zero JS. Antetul calendarului îl folosește exact din motivul ăsta.

<select> nativ · lista e desenată de SISTEMUL DE OPERARE, nu de DS
Ce vezi când îl deschizi e lista sistemului de operare — font, spațiere, bifă și colțuri sunt ale lui, nu ale design system-ului, și arată diferit pe macOS, Windows și Android. Ăsta e prețul formei native, și motivul pentru care nu mai e implicită: .select stilizează doar cutia închisă. Dacă lista trebuie să arate a Bono — sau să poarte grupuri, rânduri secundare ori starea de ales cu bifă — e forma cu popover.

A · Select alb pe card bej-1 (context Dublin)

Stările, pe forma implicită — .select-anchor + .select-trigger, adică exact ce randează react/select.tsx. <select> nativ are aceleași stări și aceleași metrici; e varianta secundară, demonstrată separat în blocul ei.

Default + label + help
Iris poate reîncadra automat.
Focus state · border fog + halo 3px
Error state
Câmp obligatoriu.
Disabled
Large · 44h / fs 16 / pad-x 20
Tonal pe card alb

Focus state

:focus-visible · border fog + halo 3px
Deschis = aria-expanded="true", care arată la fel ca focus: border --c-fog + halo neutru. Un al doilea tratament n-ar spune nimic în plus.

Forma cu popover · .select-anchor + .select-trigger

Aceeași alegere dintr-o listă închisă, alt mecanism de deschidere: un <button> care poartă valoarea și deschide .popover. Nu e .btn — un buton propune o acțiune și își declară intenția în variantă; ăsta poartă o valoare și trebuie citit ca un câmp, deci împrumută înălțimea, chenarul, radiusul și halo-ul neutru ale lui .select.

FoloseștiCândDe ce
.select — implicitOpțiunile sunt text simpluLista nativă dă gratis tastatura, roata de pe mobil, cititoarele de ecran și un singur tab stop. Nu se plătește nimic pentru ea.
.select-triggerLista nu poate fi text: o grilă de calendar, un drapel, un rând secundar (cod fiscal, email), grupuri cu etichetă<option> e redat de OS și nu primește markup. Când lista are structură, suprafața trebuie să fie a DS-ului.
.typeaheadUtilizatorul filtrează scriindCâmpul rămâne .input; vezi docs/components/typeahead.md.
închis · deschis (aria-expanded="true") — deschis arată ca focus, aceeași bordură fog și același halo

Mărimi și variante

.sm 30px · implicit 40px · .lg 44px — aceeași scară ca .select, plus o treaptă de 30px pentru antete
.bare — fără chenar, hover pe fill bej-1. Într-un antet chenarul ar desena o a doua cutie
Perioadă
.select-anchor.block — trigger-ul ține lățimea, ca .select într-un .field

Stări

.placeholder — nimic ales încă, text pe --c-mist
.error — bordură + fundal de eroare; pe focus-in eroarea se ridică
Câmp obligatoriu.
:disabled — bej-1, text mist, săgeata pe opacity: 0 (n-a mai rămas niciun meniu de promis)
.static — valoarea e fixă, dar nimic nu e blocat. Fără chevron: n-are ce deschide

Cei trei consumatori

Primitiva există fiindcă aceeași formă era desenată de trei ori: un .input readonly într-un div cu position:relative scris inline (câmpul de dată), o bară de filtre care urmează, și .cur-select-trigger (moneda, v6.1 — de azi alias).

1 · Câmp de dată — popoverul ține .calendar, adică exact lista care nu poate fi <option>

Grila întreagă → components/calendar.html. Înăuntrul popoverului, luna și anul rămân <select> native — un popover în popover n-are poveste de închidere.

2 · Bară de filtre — .sm, aliniate, valoarea nealeasă pe .placeholder
3 · Monedă — .select-trigger.lg în .amount-row la erou, .sm.bare în addon-ul unui .input-group la mărime normală (Money input). Rândul de sumă îi dă greutatea 600; restul, lista cu bifă inclusiv, vine din primitivă
4.250

.cur-select și .cur-select-trigger rămân valide ca alias-uri — aceleași reguli, nu un al doilea corp de declarații. Se scot cel mai devreme la N+2.

Reguli — select

Săgeată custom

SVG inline în background-image. appearance: none ascunde nativul. Padding-right 36px (md) / 40px (lg).

States identice cu input

Focus, error, disabled, tonal, lg — aceleași clase, aceleași reguli vizuale.

Folosește cu .field

Label .field-label + select .select + help .field-help — structură identică cu input.

Nu stiliza <option>

Elementele <option> sunt redate de OS — nu le stiliza. Textul lor moștenește fontul paginii.

Nativul e implicit

.select-trigger cere un motiv: lista nu poate fi text simplu. Lista nativă dă gratis tastatura, roata de pe mobil și un singur tab stop.

Trigger-ul NU e .btn

Un buton propune o acțiune; ăsta poartă o valoare. Împrumută scara lui .select, nu a lui .btn.

Deschis = aria-expanded

Starea se citește din atributul care e oricum obligatoriu. .open pe ancoră e alias vechi — două locuri unde scrie „e deschis" înseamnă unul care poate minți.

Fără popover în popover

Înăuntrul unui .popover deschis, alegerea rămâne nativă. Click-ul din afară care închide meniul interior îl închide și pe cel exterior.

Esc întoarce focusul

Obligatoriu aria-expanded, aria-haspopup și focus înapoi în trigger pe Esc — cel mai frecvent defect al componentei.

Ancora e o clasă

.select-anchor, nu style="position:relative". O clasă se auditează, un stil inline nu.