Layout mobil · Navigare
Navigarea aplicației.
Un job, două forme. Pe desktop (≥1024px) navigarea e rail-ul .sidebar. Pe mobil (<1024px) același rail se desparte după frecvență: destinațiile de zi cu zi în .tabbar — în Dublin Acasă · De făcut · Cameră · Banii tăi · Meniu, cu camera ca acțiune ridicată, fiindcă pe telefon jobul principal e să încarci o cheltuială; tot restul în .mobile-menu, deschis de itemul Meniu. Pagina asta e intrarea în secțiunea de layout mobil — de aici pleacă header-ul, foaia și rândul de listă. Contract: components/app-nav.json.
Markup canonic · components/app-nav.json
Cum e organizată
Pe desktop rail-ul are loc să arate totul deodată: destinații, grupuri colapsabile, footer de cont, acțiunea principală. Pe telefon nu există loc pentru toate, așa că împărțirea se face după cât de des atingi un lucru: ce folosești zilnic stă în bară, mereu vizibil; ce folosești rar stă în meniu, la un tap distanță. Nu e o a doua navigare — e aceeași listă, împachetată.
Shell-ul care le ține e .app-mobile, shell-ul de produs: header voalat sus (Header), bară jos, conținutul între ele. Un flux — wizard, semnare, onboarding — n-are navigare globală pe nicio platformă; pe mobil stă în .flow-chrome (Flow chrome). Un ecran de detaliu pierde bara (.app-mobile.no-bar) și primește „înapoi" în header — navigarea globală e în urmă, nu jos.
Ce se schimbă la 1024px se schimbă bidirecțional: niciodată .sidebar sub prag, niciodată .tabbar sau .mobile-menu peste. Clasele nu s-au redenumit — doar contractul, pagina și intrarea din navigare poartă numele jobului. Pe telefon acțiunea principală e camera (încarci o cheltuială), contorul de pe .outline pe .filled.pink, rândurile din meniu sunt List row, iar tot ce urcă de jos e Sheet.
.app-mobile + .tabbar; flux (un singur „înainte" și un sfârșit) → .flow-chrome. Nu le combina pe același ecran.
Tranziția la 1024px — rail-ul se desparte după frecvență
Un job, două forme, bidirecțional. Pe desktop navigarea aplicației e rail-ul .sidebar; pe mobil (tablete incluse) se desparte în .tabbar (mereu vizibilă) și .mobile-menu (deschis de itemul Meniu din bară). Niciodată .sidebar sub 1024px, niciodată .tabbar peste — aceeași regulă ca .flow-chrome vs .app-top + .app-footer. Clasele nu s-au redenumit; doar contractul, pagina și intrarea din navigare poartă numele jobului.
.sidebarRail-ul are loc să arate totul deodată: destinații, grupuri colapsabile, footer de cont, CTA. Stă în .app-shell, în afara grilei.
Conținut pagină
.tabbar + .mobile-menuTelefonul arată cele patru-cinci lucruri pe care le atingi zilnic și împachetează restul în spatele itemului Meniu. Contorul rămâne neutru în meniu (.badge.sm.neutral — rândurile sunt .list-row pe carduri albe) și devine .filled.pink doar în bară, unde stă peste glifă. Acțiunea ridicată e camera.
Bună, Andrei
Bună, Andrei
| Semnal | Desktop · .sidebar | Mobil |
|---|---|---|
| Destinațiile de nivel 1 | linkuri cu icon în rail | .tabbar-item în .tabbar — cel mult 5 |
| Acțiunea principală | .sidebar-cta — .btn.primary full-width, cel mai jos | .tabbar-action — .btn.primary.lg.icon-only (44), ridicat 8px |
| Identitatea | .sidebar-brand + contextul din .topbar | rândul de identitate din .mobile-menu (.list-row.has-sub + .avatar.lg) |
| Grupuri secundare | .sidebar-section colapsabile, .sidebar-grp | .list-section (.t12 + .list-group) în meniu |
| Cont, ajutor | .sidebar-footer | rânduri .list-row.link în meniu |
| Ieșirea | link în footer | button.list-row simplu (ca celelalte rânduri, fără .danger) în .list-group.footer, pinat jos |
| Contor | .badge.sm.outline pe rând implicit, .badge.sm.neutral pe rândul .active — la capătul rândului; în .icon-only rămâne .outline pe ambele | .badge.sm.filled.pink pe glifă în bară · .badge.sm.neutral la capătul rândului în meniu (rând alb, ca orice .list-row) |
| Vox | .vox-fab.ai jos-dreapta | fără FAB; nu are slot fix în bara Dublin — dacă un produs îl aduce pe telefon, e un .tabbar-item cu message-circle, în cele cinci |
| Activ | .active — pilulă albă | aria-current="page" — icon --c-accent-deep + etichetă 700, fără fill |
Mobil · .tabbar + .mobile-menu, în .app-mobile
Tabbar · destinațiile de nivel 1
Bara Dublin: Acasă · De făcut · Cameră · Banii tăi · Meniu. Camera e acțiunea ridicată — pe telefon jobul principal e să încarci o cheltuială. Meniu e ultimul item, un <button> care deschide .mobile-menu. Cel mult cinci iteme; al șaselea intră în meniu. Sub 1024px nu există .vox-fab. Contorul e .badge.sm.filled.pink pe glifă, doar numere, niciodată un punct gol și niciodată 0.
Bună, Andrei
Bună, Andrei
.app-mobile, absolută jos, cu --safe-bottom ca padding. Fiecare destinație e un <a>, Meniu e un <button>, amândouă cu .tabbar-ic (glifa Lucide cu doar .ic — bara îi dă 24px) și .tabbar-label (11px, singurul 11 ne-uppercase din DS). Activul e aria-current="page" sau .active: icon --c-accent-deep și etichetă 700, fără fill. Acțiunea ridicată e .tabbar-action cu .btn.primary.lg.icon-only — 44 pe scară (D5 a rămas deschisă pe mărime; 44 e implicitul), ridicat 8px, umbra --sh-pink; în Dublin poartă glifa camera și aria-label="Încarcă o cheltuială".Reguli ferme · formele mobile
Nu .sidebar sub 1024px, nu .tabbar / .mobile-menu de la 1024px în sus. La schimbarea benzii consumatorul face swap, ca la flow chrome.
Cel mult cinci .tabbar-item; al șaselea intră în .mobile-menu. Un singur activ per bară.
Acțiunea ridicată e camera — jobul principal pe telefon e să încarci o cheltuială. Meniu e ultimul item. .vox-fab rămâne pe desktop.
.tabbar-action > .btn.primary.lg.icon-only — 44 pe scară, ridicat 8px. Nu 56: al cincilea pas de mărime e interzis de button.json.
.badge.sm.filled.pink în bară (pip peste glifă), .badge.sm.neutral în meniu (capăt de rând alb). În rail-ul full urmează rândul: .badge.sm.outline pe rând implicit, .badge.sm.neutral pe rândul .active; în .icon-only stă pe rail, deci .outline pe ambele. Doar numere — nu punct gol, nu 0.
Un .flow-chrome n-are .tabbar: fluxul are un singur „înainte" și nicio navigare globală.
.list-row în .list-group, cu regulile din list-row.json. Nicio clasă proprie de meniu.
Activul e icon --c-accent-deep + etichetă 700. Nu fill, nu gradient, nu text roz.
Desktop · .sidebar, în .app-shell
Default · 240px
Cu labels. Folosit ca chrome pe paginile DS și în Dublin full mode. Sub-elemente: .sidebar-brand (logo non-clickabil + separator), .sidebar a (icon opțional + eticheta .sidebar-label), .sidebar-section (categorie-părinte colapsabilă), .sidebar-footer (links secundare), .badge.sm.outline / .badge.sm.neutral (contor opțional pe item, varianta urmează starea rândului — vezi Contor pe item).
Conținut pagină
<nav class="sidebar"> + .sidebar-brand (logo non-clickabil) + <a> cu <svg> icon opțional și eticheta în <span class="sidebar-label"> + .sidebar-footer. Eticheta e un span, nu text gol, ca în .icon-only să poată deveni tooltip. Sticky și 100vh sunt default.
Secțiuni colapsabile · model shadcn
Opt-in. Categoria-părinte expandabilă = <button class="sidebar-trigger"> cu text .t14 + icon + .sidebar-grp-caret, înfășurată într-un .sidebar-section cu data-state="open|closed". Copiii (.sidebar-section-list a) sunt .t14 cu inset, fără icon. Lista animă pe grid-template-rows (0fr ↔ 1fr) — fără max-height hardcodat. Icon DOAR pe categoria-părinte. Eyebrow-ul .sidebar-grp rămâne disponibil ca descriptor opțional non-actionable, dar nu e folosit în chrome-ul DS.
Conținut pagină
.sidebar-grp, „Bibliotecă”) = descriptor opțional non-actionable pentru un grup de categorii. CTA primary opțional (.sidebar-cta + .btn.primary) stă cel mai jos, sub footer. Comportamentul (toggle + persistare în localStorage) e wired de components/_nav.js pentru orice .sidebar-section din pagină. Default state: secțiunea cu pagina activă open, restul closed. Icon DOAR pe categoria-părinte; copiii sunt .t14 inset fără icon. În .icon-only rămân doar iconurile părinte.
Icon-only · 64px
Aceeași componentă .sidebar cu modificator .icon-only și același markup. Eticheta (.sidebar-label) rămâne în DOM — e numele accesibil al linkului — și apare ca tooltip la dreapta railului, la hover și la focus. Treci cu mouse-ul peste iconuri. Footer-ul devine icon-buttons 40×40 (icon obligatoriu în markup). Railul icon-only nu scrollează. Folosit în Dublin compact mode.
Conținut pagină
.sidebar + modificator .icon-only. Eyebrow și caret sunt ascunse automat. Eticheta .sidebar-label e bula .tooltip (aceeași culoare, tipografie, umbră — selector grupat în bono-ds.css), așezată la dreapta railului: apare la hover cu 120ms întârziere, ca trecerea mouse-ului să nu aprindă tot railul, și imediat la :focus-visible. Fără title pe link — ar dubla bula cu tooltipul nativ. Railul are overflow: visible ca bula să iasă din el, deci nu scrollează: la 41px pe item încap ~15 destinații în 768px de înălțime; mai multe înseamnă sidebar full cu secțiuni, nu rail mai lung. Footer-ul (Setări/Ieși) și .sidebar-cta devin icon-buttons 40×40 și primesc aceeași etichetă.
Contor pe item · .badge.sm
Un item de meniu poate purta un contor (facturi neprocesate, scadențe azi). Contorul e componenta DS .badge — nu o clasă locală. .sm e obligatoriu: .sidebar e în sizes.scale[".badge.sm"].required_in (components/badge.json). Un contor nu e o stare, deci nu poartă un token semantic — rămân două variante neutre, iar alegerea o face rândul, pentru că rândul își schimbă suprafața sub contor: rândul implicit stă pe --c-bej-1 (railul), deci contorul e .outline — un badge alb; rândul .active e o pilulă albă, deci contorul e .neutral — fill --c-bej-1. Invers, fiecare dispare în propriul rând: alb pe alb, bej pe bej. Inversiunea e regula rail-ului full. În .icon-only contorul e agățat de colțul de sus-dreapta — peste colțul tăiat al pilulei și peste rail — deci suprafața de sub el e --c-bej-1 în ambele stări, și rămâne .outline pe ambele. Niciodată pink — roz e pentru beneficiul de brand („Nou", „Beta", „Pro") și pentru cele trei statusuri de atenție („De făcut", „Necitit", „Curent"), iar un număr nu e niciuna. Plasarea o dă DS-ul singur, în ambele geometrii: la capătul rândului în rail-ul full, agățat de colț în .icon-only.
Conținut pagină
<a href="#"><svg…>Facturi<span class="badge sm outline">7</span></a>, iar pe rândul activ din rail-ul full <a href="#" class="active">…<span class="badge sm neutral">7</span></a>. În .icon-only markup-ul e .outline pe ambele — singura diferență de markup între cele două geometrii, și vine din plasare: acolo contorul stă pe rail, nu în rând. Perechea stă în markup, nu în CSS: când muți .active, muți și varianta contorului (inclusiv în JS/framework, unde activul se schimbă la runtime). DS-ul nu rescrie varianta din CSS intenționat — altfel clasa din markup ar spune .outline și s-ar randa .neutral, iar lintul n-ar mai avea ce verifica. Plasarea, în schimb, o face DS-ul singur: margin-left:auto pe rail-ul full, absolut în colț pe .icon-only. Nu poziționa contorul local. Nu afișa 0 — ascunde contorul dacă n-are ce număra (badge.json → anti_patterns).
references/referinta-dublin-aligned.html își definise contoarele ca .badge-mini și .count — clase locale, cu background: var(--c-pink). Regula exista în badge.json, dar nu exista exemplul, iar agenții iau ritmul din referință: când exemplul contrazice regula, câștigă exemplul, și contoarele pink au ajuns copiate mai departe într-o pagină de produs. De asta secțiunea asta există, și de asta scripts/check-aligned-lint.sh (regula 19) pică acum clasele paralele de badge și orice contor cu fundal de brand. Pe 2026-09-21 regula a primit și perechea de variante — .outline pe rând implicit, .neutral pe rândul .active al rail-ului full — fiindcă varianta unică lăsa contorul să se topească în pilula albă a activului; regula 19 pică și perechea greșită.
Reguli & folosire · rail-ul desktop
bono-ds.css. Folosit pe paginile DS, pe această pagină (components/app-nav.html) și în references/referinta-dublin-aligned.html. Nu redefini local — folosește clasele și modificatorii.
- Default: bg
--c-bej-1, border-right--c-rule-soft, sticky 100vh. - Brand:
.sidebar-brand= logo simplu (<div>) + separator dedesubt, non-clickabil. - Eyebrow (.sidebar-grp):
.t12uppercase, fără dot. Descriptor opțional non-actionable. - Colaps (.sidebar-section):
data-state="open|closed". Părinte =.sidebar-trigger(.t14 + icon + caret). Copii =.t14inset, fără icon. Lista animă pegrid-template-rows. Persistare înlocalStorage['ds-sidebar-sections']. - Link (.sidebar a):
.t14styling (14px, 500), eticheta în.sidebar-label. Active: white highlight —--c-whitebg +--c-inktext + subtle lift shadow + colțuri rotunjite--r-lg(20px), contrast peste bg-ul tonal--c-bej-1. - Contor (
.sidebar a > .badge): componenta DS,.smobligatoriu, varianta urmează rândul în rail-ul full —.outline(alb) pe rând implicit,.neutral(bej) pe rândul.active; în.icon-only.outlinepe ambele. Niciodată pink, niciodată o clasă locală, niciodată0. Plasarea vine din DS — capăt de rând pe full, colț pe.icon-only. Vezi Contor pe item. - Padding lateral: top-level (nav direct +
.sidebar-trigger)12px; copiii inset10px, cu un increment de diferență pentru ierarhie. - Spațiere rânduri:
1pxîntre items (gappe.sidebar, margine pe copiii inset și pe link-urile de footer) — pilula albă a activului și hover-ul rândului de dedesubt nu se ating. - Footer (.sidebar-footer):
.t14weight 400 + color--c-mist. În.icon-only→ icon-buttons 40×40 (icon obligatoriu în markup). - CTA (.sidebar-cta): buton
.btn.primaryfull-width, opțional, stă cel mai jos — sub footer. În.icon-only→ icon-button 40×40 (doar iconul). - Icon opacity 0.6 default, 1 când active. SVG
16×16stroke-based currentColor. - Link-uri (nav + trigger) folosesc
--r-lg(hover și active împart aceeași formă), footer--r-xs. - Mobile (
<1024px, breakpoint-ul unic al DS-ului): sidebar ascuns (display:none) — navigarea devine.tabbar+.mobile-menu, vezi mai sus. Pagina folosește grid-template-columns 1fr. - Etichetă la hover (
.icon-only):.sidebar-labeldevine bula.tooltipla dreapta railului, la hover (120ms) și:focus-visible(imediat). Fărătitle. Se aplică pe link,.sidebar-trigger, footer și CTA. - Railul
.icon-onlynu scrollează (overflow: visible, ca bula să iasă din el) — ~15 destinații în 768px; mai multe = sidebar full cu secțiuni. - În
.app-shellrailul stă pe stratul 50 („sticky surfaces", scara dinfoundation-grid.html) — eticheta iese peste conținutul poziționat; grila.layout-12se lipește de rail (foundation-layout.html).
Explorează secțiunea
Desktop .topbar · mobil .app-mobile-top în trei configurări: root, secțiune, detaliu.
Foaia unică de jos — .attached, .floating, .form — pentru meniuri, explicații și formulare scurte.
Rândul telefonului: .link, .value, .control, static, .danger — în .list-group, sub o etichetă .t12.
Forma mobilă a tabelului: un card per rând, 3–4 câmpuri, suma la dreapta. Forma nu e decisă.
Shell-ul fluxurilor: înapoi sus, un singur CTA jos, veil + blur — pe mobil; .app-top + .app-footer pe desktop.
Breakpoint-ul unic, .app-shell + .layout-12 pe desktop, cele două shell-uri mobile.
.badge.sm.outline / .neutral în rail-ul full (după starea rândului), .outline în .icon-only, .badge.sm.filled.pink doar în bară, .neutral pe orice rând de pagină — meniul mobil inclus.
Șapte ecrane Dublin reale, în ambele shell-uri — de aici copiază constructorul.