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

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

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

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

Î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 footerbutton.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-dreaptafă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

Î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
Acasă activ · De făcut activ — camera ridicată la mijloc, Meniu la capăt
Bara e ultimul copil al lui .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ă".

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 itemul Meniu din bară, î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 un button.list-row simplu — aceeași greutate ca rândurile de deasupra, fără .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ă.

Camera la mijloc

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.

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

.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