Sidebar nav item
canonRow of the left navigation: top-level row, section divider and sub-item, plus the split parent that navigates and folds.
React implementation lives in the app (apps/web/src/components/AppShell.tsx — WorkspaceNav/NavGo), not in @acrm/ui. The dead rules .as-row .notif/.count were removed on 11 September 2026 (NAV-1, acrm !3403); counts are .acrm-pill. Debt: no standalone segment-tile component — `.sg` (the 18px coloured tile with the list's glyph) exists only as `.as-sub .sg` in `apps/web/src/ds/app-shell.css`. `AddToListDialog` (acrm !4019, 27 Sep 2026, see the "add-to-list dialog" subsection of formless-dialog.usage.md) reuses it by extending the same selector rather than duplicating the rule — `.as-sub .sg, .atl-list .sg { … }` — one definition serving both the left menu and the dialog's list rows; accepted by the AD 27 Sep 2026 as a stopgap. Target: extract a canonical `SegmentTile` so a third consumer does not repeat the same selector-extension trick. 30 Sep 2026 (acrm !4296, AD FA216/FA233): optional dot?: blue | amber | green | purple | rose | critical draws the canonical category dot .il-dot[data-tone] (6 px, gap --sp-1) before the PLAIN count, no capsule. Category meaning only (Komunikace: Důležité critical, Delegováno mnou purple — owner R15, Delegováno mně amber); never unread (unread = blue dot elsewhere). acrm !4319 (1. 10. 2026, ověřeno na devu 1440/390 světlá+tmavá): tichý počet .as-navcnt místo modré kapsle, kapsle jen kritický odznak, horní blok 6, ⋯ akce seznamů (MenuButton) a + u Seznamů. Komunikace menu (20 Oct 2026, owner "1 Schváleno", proto/komunikace-menu-zobrazit-vse@0b7180f3a, ACRM-558, 560–569): rarely used Mail views (Newsletters, Sent, Archive, Trash, Snoozed, Drafts, Scheduled) live in a collapsible section "Další" (`.as-grp.as-more`, heading styled like "Chaty", collapsed by default, hidden-unread count on the right while collapsed); the active hidden view is shown as a copy above it; rows drag (HTML5 DnD) within and between groups and move with Alt+↑/↓ (aria-live announces the new place); personal preference in localStorage. No ⋮ menu on views. Row marks: `sgDot` = colour dot in the label colour (`.as-sub .sg.dot`, labels), `sgPlain` = bare muted glyph ("+ New label/folder/deal" rows), `sgTint` = bare glyph in the channel colour (chats: Telegram --ic-blue, WhatsApp --ic-green, SMS new token --ic-mint #34c759; the Chaty section is collapsible and open by default); a folder tile takes its own icon and tone (fallback grey `folder`). Tokens: new `--ic-mint`. Module icon Komunikace = `message-chatbot`, "All chats" = `message-circle`. AD 24 Oct 2026 (a2a FA278, acrm !5560): menu axis UX-62 accepted — .as-row-chev / .as-sub-chev / .as-sub-more share one axis (the sub-menu ⋯ sits exactly on the count, count hides on hover/open); .as-shared = sharing badge with ONE meaning only: the subject is shared with other users (who + level reachable on touch via ⋮/detail, not hover-only). Amber capsule (badgeAmber) NOT approved: the only coloured capsule in the menu stays critical; other counts are quiet. Pending from the author: stories States „sdílený“ + „osa“ with light/dark baseline, 390 check.
Used in 5 files · 4 areas · 22× — components/AppShell, components/NavItemMenu, components/segment, pages/settings
.as-nav .as-row .as-row-nav .as-row-chev +7SourceStoryLegacy card nav-item.htmlLegacy card App Shell.htmlDefault
export const Default = () => {
const [active, setActive] = useState('obchody');
return (
<Side>
<Row icon="home" active={active === 'prehled'} onClick={() => setActive('prehled')}>
Přehled
</Row>
<Row icon="target" active={active === 'obchody'} onClick={() => setActive('obchody')}>
Obchody
</Row>
<Row icon="users" active={active === 'lide'} onClick={() => setActive('lide')}>
Lidé
</Row>
<Row icon="building" active={active === 'firmy'} onClick={() => setActive('firmy')}>
Firmy
</Row>
</Side>
);
};Variants
export const Variants = () => {
const [colGrp, setColGrp] = useState(false);
const [colRow, setColRow] = useState(false);
return (
<div style={{ display: 'grid', gap: 'var(--sp-5)' }}>
<Labeled text="section divider — click the label to fold the block under it (.as-grp)">
<Side>
<Group collapsed={colGrp} onToggle={() => setColGrp((v) => !v)}>
Obchod
</Group>
{colGrp ? null : (
<>
<Row icon="target">Obchody</Row>
<Row icon="clipboard-list">Úkoly</Row>
</>
)}
<Group>Nastavení</Group>
</Side>
</Labeled>
<Labeled text="split parent row — the body navigates, the chevron folds; two sibling controls, both reachable by keyboard">
<Side>
<ParentRow icon="users" collapsed={colRow} onToggle={() => setColRow((v) => !v)} active>
Lidé
</ParentRow>
{colRow ? null : (
<>
<Sub icon="star" color="var(--ic-amber)">
Moji klienti
</Sub>
<Sub icon="filter" color="var(--ic-green)">
Bonitní Pražané
</Sub>
</>
)}
</Side>
</Labeled>
<Labeled text="sub-items and their indents — a saved segment (colour tile) and the segment wizard (wand)">
<Side>
<Row icon="users">Lidé</Row>
<Sub icon="star" color="var(--ic-amber)">
Moji klienti
</Sub>
<Sub indent="lvl1" icon="filter" color="var(--ic-green)">
Praha 30–45
</Sub>
<Sub indent="lvl2" icon="filter" color="var(--ic-purple)">
S termínem
</Sub>
<button type="button" className="as-sub">
<span className="wand">
<Icon name="adjustments-horizontal" />
</span>
<span className="lbl">Nový segment</span>
</button>
</Side>
</Labeled>
</div>
);
};States
export const States = () => (
<div style={{ display: 'grid', gap: 'var(--sp-5)' }}>
<Labeled text="rest and active — the active row is the one the screen was opened from, not the raw URL (P230); it carries aria-current=„page“">
<Side>
<Row icon="home">Přehled</Row>
<Row icon="target" active>
Obchody
</Row>
<Row icon="users">Lidé</Row>
</Side>
</Labeled>
<Labeled text="with a count — top-level rows use the blue pill, sub-items the amber one (pending additions in a saved segment)">
<Side>
<Row icon="inbox" badge="12">
Pošta
</Row>
<Row icon="clipboard-list" badge="3">
Úkoly
</Row>
<Sub icon="filter" color="var(--ic-green)" badge="5">
Bonitní Pražané
</Sub>
</Side>
</Labeled>
<Labeled text="rail — icon-only sidebar (≥901px); labels, groups and sub-items are hidden, the name survives as title/aria-label">
<div style={{ width: 56 }}>
<Side rail>
<Row icon="home">Přehled</Row>
<Row icon="target" active>
Obchody
</Row>
<Row icon="users">Lidé</Row>
<Row icon="building">Firmy</Row>
</Side>
</div>
</Labeled>
<Labeled text="long navigation — the nav scrolls inside the sidebar, the footer with the rail toggle stays put">
<div style={{ height: 260, display: 'grid' }}>
<aside
className="as-side"
style={{
border: '1px solid var(--border)',
borderRadius: 'var(--radius-md)',
minHeight: 0,
}}
>
<nav className="as-nav" style={{ overflowY: 'auto' }}>
{Array.from({ length: 14 }, (_, i) => (
<Row key={i} icon="folder">{`Položka ${i + 1}`}</Row>
))}
</nav>
<div className="as-side-foot">
<button type="button" className="as-row as-rail-toggle" aria-expanded="true">
<Icon name="chevron-left" />
<span className="lbl">Sbalit navigaci</span>
</button>
</div>
</aside>
</div>
</Labeled>
</div>
);With icon & kbd
export const WithIconAndKbd = () => (
<div style={{ display: 'grid', gap: 'var(--sp-5)' }}>
<Labeled text="every row carries an icon — in the rail it is the only thing left, so a row without one is not allowed">
<Side>
<Row icon="home">Přehled</Row>
<Row icon="target">Obchody</Row>
<Row icon="calendar">Kalendář</Row>
</Side>
</Labeled>
<span style={label}>
Nav rows carry no keyboard shortcut chips — the only shortcut in the shell is ⌘K for global
search, and it lives in the top bar.
</span>
</div>
);Composition
export const Composition = () => {
const [active, setActive] = useState('obchody');
const [rail, setRail] = useState(false);
const [col, setCol] = useState(false);
return (
<div
style={{
display: 'flex',
gap: 0,
height: 380,
border: '1px solid var(--border)',
borderRadius: 'var(--radius-md)',
overflow: 'hidden',
}}
>
<aside className={'as-side' + (rail ? ' rail' : '')} style={{ minHeight: 0 }}>
<nav className="as-nav" style={{ overflowY: 'auto' }}>
<Row icon="home" active={active === 'prehled'} onClick={() => setActive('prehled')}>
Přehled
</Row>
<Group collapsed={col} onToggle={() => setCol((v) => !v)}>
Obchod
</Group>
{col ? null : (
<>
<Row
icon="target"
active={active === 'obchody'}
onClick={() => setActive('obchody')}
badge="4"
>
Obchody
</Row>
<Row
icon="clipboard-list"
active={active === 'ukoly'}
onClick={() => setActive('ukoly')}
>
Úkoly
</Row>
<Sub
icon="filter"
color="var(--ic-green)"
active={active === 'seg'}
onClick={() => setActive('seg')}
>
Bonitní Pražané
</Sub>
</>
)}
<Row
icon="settings"
active={active === 'nastaveni'}
onClick={() => setActive('nastaveni')}
>
Nastavení
</Row>
</nav>
<div className="as-side-foot">
<button
type="button"
className="as-row as-rail-toggle"
aria-expanded={!rail}
aria-label={rail ? 'Rozbalit navigaci' : 'Sbalit navigaci'}
onClick={() => setRail((v) => !v)}
>
<Icon name={rail ? 'chevron-right' : 'chevron-left'} />
<span className="lbl">Sbalit navigaci</span>
</button>
</div>
</aside>
<div
style={{ flex: 1, padding: 'var(--sp-5)', color: 'var(--fg-muted)', font: 'var(--small)' }}
>
Obsah stránky — the rail toggle at the bottom switches the whole sidebar to icons and back.
</div>
</div>
);
};