Record tabs
canonRecord tab row: tab strip plus „+ N další“ overflow and a personal layout; an empty tab drops out, a loading one does not.
React implementation lives in the app (record/RecordTabsRow.tsx + record-tabs.ts); it uses TabStrip internally and adds the overflow trigger and the layout menu. 14 Oct 2026 (owner "24g scvaleno", proto/zalozky-jeden-ovladac@489bb3bd6, acrm !5043): ONE icon button „Záložky" with a count badge of hidden tabs replaces „+ N další" and ⚙; its menu lists all tabs — name = open, switch = show/hide, grip/Alt+↑↓ = order; phone = bottom sheet with „Hotovo". Badge --accent-press + white, 5.73:1. Dev check 14 Oct 1440 + 390 (company Druid): badge 8, 15 switches, sheet ok.
Used in 4 files · 4 areas · 4× — components/company, pages/CompanyRecordPage, pages/ObchodDetailPage, pages/PersonRecordPage
Default
export const Default = () => {
const [tab, setTab] = useState('aktivita');
return (
<Row
tabs={PERSON}
counts={{ ukoly: 3, posta: 12, poznamky: 2 }}
value={tab}
onChange={setTab}
/>
);
};Variants
export const Variants = () => {
const [a, setA] = useState('prehled');
const [b, setB] = useState('prehled');
return (
<div style={{ display: 'grid', gap: 'var(--sp-6)' }}>
<Labeled text="person — the fullest set; what does not fit moves under „+ N další“, which stays right after the last tab (P240)">
<Row tabs={PERSON} counts={{ ukoly: 3, posta: 12 }} value={a} onChange={setA} />
</Labeled>
<Labeled text="company — a different catalogue, same row; the tab set follows the record type, not the screen">
<Row
tabs={COMPANY}
counts={{ lide: 12, obchody: 4 }}
shownCount={4}
value={b}
onChange={setB}
/>
</Labeled>
<Labeled text="everything fits — no overflow trigger at all, only the gear">
<Row
tabs={COMPANY.slice(0, 3)}
counts={{ lide: 12 }}
shownCount={3}
value="prehled"
onChange={() => {}}
/>
</Labeled>
</div>
);
};States
export const States = () => {
const [tab, setTab] = useState('dokumenty');
return (
<div style={{ display: 'grid', gap: 'var(--sp-6)' }}>
<Labeled text="counts loaded — the number says how much is inside; a tab with 0 is dropped from the row unless the user pinned it">
<Row
tabs={PERSON.slice(0, 5)}
counts={{ ukoly: 3, posta: 12, poznamky: 0 }}
shownCount={5}
value="aktivita"
onChange={() => {}}
/>
</Labeled>
<Labeled text="counts still loading — undefined, not 0, so nothing is hidden while the data is on its way and the row does not jump">
<Row
tabs={PERSON.slice(0, 5)}
counts={{}}
shownCount={5}
value="aktivita"
onChange={() => {}}
/>
</Labeled>
<Labeled text="the active tab is in the overflow — it is pulled back into the visible part, so the underline is never off-screen">
<Row tabs={PERSON} counts={{ ukoly: 3 }} shownCount={3} value={tab} onChange={setTab} />
</Labeled>
</div>
);
};Composition
export const Composition = () => {
const [tab, setTab] = useState('aktivita');
return (
<div
style={{
border: '1px solid var(--border)',
borderRadius: 'var(--radius-lg)',
overflow: 'hidden',
background: 'var(--bg-canvas)',
}}
>
<div className="tbar">
<div className="tbar-id">
<span className="tbar-h">Josef Novák</span>
</div>
<span className="vspace" />
<button type="button" className="tchip">
<Icon name="pencil" />
<span className="k">Upravit</span>
</button>
</div>
<div style={{ padding: '0 var(--sp-5)' }}>
<Row tabs={PERSON} counts={{ ukoly: 3, posta: 12 }} value={tab} onChange={setTab} />
</div>
<div style={{ padding: 'var(--sp-5)', color: 'var(--fg-muted)', font: 'var(--small)' }}>
Obsah záložky „{PERSON.find((t) => t.key === tab)?.label}“ — the row sits under the record's
top bar; the gear on the right changes the tab set for good, „+ N další“ only navigates.
</div>
</div>
);
};