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, cel mult cinci, Vox inclus; tot restul în .mobile-menu, în spatele hamburgerului. 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

sursa = JSON · generat de scripts/build-catalog.py · meniul e [hidden] în markup; pagina îl arată deschis

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. Vox trece de pe FAB pe un item din bară, contorul de pe .outline pe .filled.pink, rândurile din meniu sunt List row, iar tot ce urcă de jos e Sheet.

Regulă
Alegi shell-ul după ce e ecranul, nu după ce ai la îndemână: produs (acasă, liste, setări, detalii) → .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 (în spatele hamburgerului). 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.

≥ 1024px · desktop .sidebar

Rail-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ă

< 1024px · mobil .tabbar + .mobile-menu

Telefonul arată cele patru-cinci lucruri pe care le atingi zilnic și împachetează restul în spatele hamburgerului. 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ă. Vox devine un item.

Bună, Andrei

Încasat · septembrie
42.180Lei
↑ 12% vs august
Scadent · 25 sept
2.184Lei
TVA · pregătit de Alina

Bună, Andrei

Încasat · septembrie
42.180Lei
↑ 12% vs august
Scadent · 25 sept
2.184Lei
TVA · pregătit de Alina
ADDublin Software SRLCUI RO42118903 · Andrei Dima
rădăcina cu bara · meniul deschis
SemnalDesktop · .sidebarMobil
Destinațiile de nivel 1linkuri 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 .topbarrâ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-footerrânduri .list-row.link în meniu
Ieșirealink în footer.list-row.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-dreaptaun .tabbar-item cu message-circle — fără FAB
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

Cel mult cinci iteme; al șaselea intră în meniu. Vox e un item (glifa message-circle) — 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

Încasat · septembrie
42.180Lei
↑ 12% vs august
Scadent · 25 sept
2.184Lei
TVA · pregătit de Alina

Bună, Andrei

Încasat · septembrie
42.180Lei
↑ 12% vs august
Scadent · 25 sept
2.184Lei
TVA · pregătit de Alina
5 iteme, Vox inclus · 4 iteme + acțiunea ridicată
Bara e ultimul copil al lui .app-mobile, absolută jos, cu --safe-bottom ca padding. Fiecare item e un <a> 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.

Mobile menu · restul rail-ului

Tot ce nu încape în bară: grupurile secundare, contul, ajutorul, ieșirea. Nu e o a doua navigare — e aceeași listă din rail, împachetată. Rândurile sunt .list-row (contract list-row.json), nu clase proprii de meniu — și se supun regulilor lor, contorul inclus: .badge.sm.neutral la capătul rândului, ca pe orice rând de pagină. Rândul e alb, deci fill-ul --c-bej-1 se citește pe el (ΔL* 6.82; 3.43 când rândul e hover-at pe --c-bej-05) — aceeași logică cu rândul .active din rail: contorul ia varianta care contrastează cu suprafața de sub el. Până pe 2026-09-21 era .filled.pink, o excepție de la regula lui .list-row; pink a rămas doar în bară, unde contorul nu e la capăt de rând, ci un pip peste glifă.

Bună, Andrei

Încasat · septembrie
42.180Lei
↑ 12% vs august
Scadent · 25 sept
2.184Lei
TVA · pregătit de Alina
ADDublin Software SRLCUI RO42118903 · Andrei Dima
.mobile-menu deschis · [hidden] când e închis
Overlay pe toată rama (position:absolute; inset:0, bej-0), deschis de hamburgerul din header-ul root, închis de același X (.btn.secondary.icon-only). Conținutul e numai componente existente: rândul de identitate (.list-row.has-sub + .avatar.lg.tinted), grupuri .list-section cu .list-row.link, iar ieșirea e .list-row.danger într-un .list-group.footer pinat jos cu margin-top:auto. Clasele n-au stare: gazda pune și scoate hidden.

Reguli ferme · formele mobile

Bidirecțional

Nu .sidebar sub 1024px, nu .tabbar / .mobile-menu de la 1024px în sus. La schimbarea benzii consumatorul face swap, ca la flow chrome.

Max 5

Cel mult cinci .tabbar-item; al șaselea intră în .mobile-menu. Un singur activ per bară.

Vox e un item

Pe mobil Vox stă în bară, cu message-circle. .vox-fab rămâne pe desktop.

Acțiunea ridicată

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

Contorul

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

Nu în flux

Un .flow-chrome n-are .tabbar: fluxul are un singur „înainte" și nicio navigare globală.

Rândurile din meniu

.list-row în .list-group, cu regulile din list-row.json. Nicio clasă proprie de meniu.

Fără fill pe activ

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

.sidebar · 240px

Conținut pagină

Markup canonică: <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.

.sidebar-section · click pe titlu

Conținut pagină

Eyebrow (.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.

.sidebar.icon-only · 64px
Aceeași clasă .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 — brandul e rezervat beneficiului („Nou", „Beta", „Pro"), iar un număr nu e un beneficiu. 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.

.sidebar a > .badge.sm — full: .neutral pe rândul activ, .outline pe restul · icon-only: .outline pe ambele
Markup: <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).
Ce a mers prost o dată. 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

Un singur sidebar. Definit canonical în 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): .t12 uppercase, fără dot. Descriptor opțional non-actionable.
  • Colaps (.sidebar-section): data-state="open|closed". Părinte = .sidebar-trigger (.t14 + icon + caret). Copii = .t14 inset, fără icon. Lista animă pe grid-template-rows. Persistare în localStorage['ds-sidebar-sections'].
  • Link (.sidebar a): .t14 styling (14px, 500), eticheta în .sidebar-label. Active: white highlight — --c-white bg + --c-ink text + subtle lift shadow + colțuri rotunjite --r-lg (20px), contrast peste bg-ul tonal --c-bej-1.
  • Contor (.sidebar a > .badge): componenta DS, .sm obligatoriu, varianta urmează rândul în rail-ul full — .outline (alb) pe rând implicit, .neutral (bej) pe rândul .active; în .icon-only .outline pe 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 inset 10px, cu un increment de diferență pentru ierarhie.
  • Spațiere rânduri: 1px între items (gap pe .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): .t14 weight 400 + color --c-mist. În .icon-only → icon-buttons 40×40 (icon obligatoriu în markup).
  • CTA (.sidebar-cta): buton .btn.primary full-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×16 stroke-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-label devine bula .tooltip la dreapta railului, la hover (120ms) și :focus-visible (imediat). Fără title. Se aplică pe link, .sidebar-trigger, footer și CTA.
  • Railul .icon-only nu scrollează (overflow: visible, ca bula să iasă din el) — ~15 destinații în 768px; mai multe = sidebar full cu secțiuni.
  • În .app-shell railul stă pe stratul 50 („sticky surfaces", scara din foundation-grid.html) — eticheta iese peste conținutul poziționat; grila .layout-12 se lipește de rail (foundation-layout.html).

Explorează secțiunea