Record select
canonSearch-driven choice from records: framed field, or panel anchored to a rail row or a chip. Sections separated by a named hairline, one value = accent check.
Absorbed Rail picker on 12 September 2026 (AD, RP-1; acrm !3425–!3443): with anchorRef the panel hangs under a rail row or a chip instead of a framed trigger, so the rail has no picker of its own and RailPickerMenu is deleted. Also new: sections whose name sits IN the hairline (only when there are two or more; a single name would be the forbidden heading above a menu), selected pinned on top, accent check for one value (F261), diacritics-folded search over sub and keywords, quick-create footer, minWidth, loading/empty/busy, UserAvatar for a colleague. Known limit: the section name is var(--label) in --fg-muted = 2.85:1 light / 3.63:1 dark, below AA in both themes — the lighter tone is the AD's explicit choice of 12 September 2026 (ACRM-227, acrm !3460), made with the numbers in front of him; the group is announced from role=group + aria-label, not from this text. The hand-made panel shell is blocked by the validator rule recordselect-shell-handmade. Slot wrapOption(option, row) (acrm !3913, AD 26 Sep 2026): the host may WRAP an option row in an element that clicks nothing and does not change its size (hover preview EntityPreviewChip); selection, keys and look stay with RecordSelect. Used by RecordLinksChip — the one links chip of shape-A dialogs (task, deal, meeting, note), selected records show the record preview on hover.
Used in 46 files · 31 areas · 52× — components/RecordLinksChip, components/ReservationControl, components/TagsEditor, components/accounting, components/acl, components/clen-tymu …
@acrm/ui <RecordSelect>.acrm-recordselect .acrm-recordselect-trigger .acrm-recordselect-pop .acrm-recordselect-search +11SourceStoryLegacy card Inputs.htmlDefault
export const Default = () => {
const [v, setV] = useState<string | string[]>('u1');
return (
<RecordSelect
options={PEOPLE}
value={v}
onChange={setV}
placeholder="Vyberte osobu…"
ariaLabel="Řešitel"
/>
);
};Variants
export const Variants = () => {
const [multi, setMulti] = useState<string | string[]>(['u1', 'u2']);
const [one, setOne] = useState<string | string[]>('u1');
return (
<div style={grid}>
<div style={row}>
<Labeled label="single (value: string)">
<RecordSelect options={PEOPLE} value={one} onChange={setOne} ariaLabel="Řešitel" />
</Labeled>
<Labeled label="multi (value: string[]) — trigger joins names, +N over two">
<RecordSelect
options={PEOPLE}
value={multi}
onChange={setMulti}
multi
ariaLabel="Účastníci"
/>
</Labeled>
<Labeled label="clearable — × resets to empty">
<RecordSelect
options={PEOPLE}
value="u3"
onChange={() => {}}
clearable
ariaLabel="Vlastník"
/>
</Labeled>
</div>
<div style={row}>
{/* port: apps/web/src/components/record/OwnerPickerEditor.tsx:52 (rail field „Vlastník") */}
<Labeled label="single — accent check on the right (F261), no checkbox · static replica">
<RecordPanel searchPlaceholder="Hledat kolegu…">
<RecordRow
name="Jana Nováková"
sub="[email protected]"
entityType="user"
mode="single"
on
/>
<RecordSep />
<RecordRow name="Petr Svoboda" sub="[email protected]" entityType="user" mode="single" />
<RecordRow name="Karel Dvořák" sub="[email protected]" entityType="user" mode="single" />
</RecordPanel>
</Labeled>
{/* port: apps/web/src/components/TagsEditor.tsx:124 (rail field „Štítky") */}
<Labeled label="multi — DS checkbox on the right (D6) · static replica">
<RecordPanel multi searchPlaceholder="Hledat záznamy…">
<RecordRow name="Jana Nováková" sub="[email protected]" on />
<RecordSep />
<RecordRow name="Petr Svoboda" sub="[email protected]" />
</RecordPanel>
</Labeled>
</div>
<div style={row}>
<Labeled label="marks: person (circle) / company (square) — static replica">
<RecordPanel searchPlaceholder="Hledat firmu…">
{COMPANIES.map((c) => (
<RecordRow
key={c.value}
name={c.label}
sub={c.sub}
shape="square"
on={c.value === 'c1'}
/>
))}
</RecordPanel>
</Labeled>
{/* port: apps/web/src/components/RecordLinksChip.tsx:136 (chip „Provázat") */}
<Labeled label="marks: entityType — the EntityRef mark (user face, deal/project tile)">
<RecordPanel searchPlaceholder="Hledat záznam…">
<RecordRow name="Eva Procházková" sub="[email protected]" entityType="user" />
<RecordRow name="Kancelář Vinohrady" entityType="deal" />
<RecordRow name="Rekonstrukce skladu" entityType="project" />
</RecordPanel>
</Labeled>
<Labeled label="shape: 'none' — plain code list, no mark (tags)">
<RecordPanel multi searchPlaceholder="Hledat nebo založit…">
<RecordRow name="Doporučení" avatar={false} on />
<RecordRow name="Veletrh" avatar={false} />
</RecordPanel>
</Labeled>
</div>
</div>
);
};Sizes
export const Sizes = () => (
<div style={grid}>
<Labeled label="default — 280px">
<RecordSelect options={PEOPLE} value="u1" onChange={() => {}} ariaLabel="Řešitel" />
</Labeled>
<Labeled label="fullWidth — fills the container (here 480px)">
<div style={{ width: 480 }}>
<RecordSelect
options={PEOPLE}
value="u1"
onChange={() => {}}
fullWidth
ariaLabel="Řešitel"
/>
</div>
</Labeled>
{/* port: apps/web/src/components/RecordLinksChip.tsx:136 — minWidth={280} over a narrow chip */}
<Labeled label="minWidth — panel width follows the anchor, never under 280px (chip) · replica">
<RecordPanel width={280} searchPlaceholder="Hledat záznam…">
<RecordRow name="Kancelář Vinohrady" entityType="deal" on />
<RecordRow name="Rekonstrukce skladu" entityType="project" />
</RecordPanel>
</Labeled>
</div>
);States
export const States = () => (
<div style={grid}>
<div style={row}>
<Labeled label="placeholder (empty)">
<RecordSelect
options={PEOPLE}
value={null}
onChange={() => {}}
placeholder="Nastavit hodnotu…"
ariaLabel="Řešitel"
/>
</Labeled>
<Labeled label="filled">
<RecordSelect options={PEOPLE} value="u2" onChange={() => {}} ariaLabel="Řešitel" />
</Labeled>
<Labeled label="disabled">
<RecordSelect
options={PEOPLE}
value="u2"
onChange={() => {}}
disabled
ariaLabel="Řešitel"
/>
</Labeled>
</div>
<div style={row}>
<Labeled label="open — accent trigger, search row, list (static replica)">
<div style={{ display: 'grid', gap: 'var(--sp-1)', width: 280 }}>
<RecordTrigger filled open>
Petr Svoboda
</RecordTrigger>
<RecordPanel>
{PEOPLE.slice(0, 4).map((p) => (
<RecordRow key={p.value} name={p.label} sub={p.sub} on={p.value === 'u2'} />
))}
</RecordPanel>
</div>
</Labeled>
<Labeled label="open, filtered, no match — „Žádné výsledky“ (static replica)">
<div style={{ display: 'grid', gap: 'var(--sp-1)', width: 280 }}>
<RecordTrigger open>Nastavit hodnotu…</RecordTrigger>
<RecordPanel query="xyz">
<div className="acrm-recordselect-empty">Žádné výsledky</div>
</RecordPanel>
</div>
</Labeled>
</div>
<div style={row}>
{/* port: apps/web/src/components/RecordLinksChip.tsx:136 — sections by record type */}
<Labeled label="sections — the name sits IN the divider line; pinned selection on top">
<RecordPanel multi searchPlaceholder="Hledat záznam…">
<RecordRow name="Kancelář Vinohrady" entityType="deal" on />
<RecordSection name="Obchody">
<RecordRow name="Sklad Modřany" entityType="deal" />
<RecordRow name="Pobočka Brno" entityType="deal" />
</RecordSection>
<RecordSection name="Projekty">
<RecordRow name="Rekonstrukce skladu" entityType="project" />
</RecordSection>
</RecordPanel>
</Labeled>
{/* port: apps/web/src/components/TagsEditor.tsx:127 — onCreate footer + loading + busy */}
<Labeled label="onCreate footer — „Založit „…““ (Enter does the same)">
<RecordPanel
multi
query="Veletrh"
searchPlaceholder="Hledat nebo založit…"
footer={<RecordCreate text="Veletrh" />}
>
<RecordRow name="Veletrh Brno" avatar={false} />
</RecordPanel>
</Labeled>
</div>
<div style={row}>
{/* port: delegace F2 (Komu) — muted = kolega na dovolené, subTone warn = bez schránky */}
<Labeled label="muted row + warn sub (RecordOption.muted / subTone 'warn')">
<RecordPanel searchPlaceholder="Hledat kolegu…">
<RecordRow name="Jana Nováková" sub="[email protected]" mode="single" />
<RecordRow name="Bohumil Král" sub="dovolená do 6. 10." mode="single" muted />
<RecordRow name="Eva Malá" sub="Nemá připojenou schránku" mode="single" subWarn />
</RecordPanel>
</Labeled>
<Labeled label="loading — „Načítám…“ instead of the list">
<RecordPanel searchPlaceholder="Hledat kolegu…">
<div className="acrm-recordselect-empty">Načítám…</div>
</RecordPanel>
</Labeled>
<Labeled label="emptyText — empty code list (no query), wording from the caller">
<RecordPanel multi searchPlaceholder="Hledat nebo založit…">
<div className="acrm-recordselect-empty">Zatím žádné štítky</div>
</RecordPanel>
</Labeled>
<Labeled label="busy — a write is queued (aria-busy), options stay clickable">
<RecordPanel multi busy searchPlaceholder="Hledat nebo založit…">
<RecordRow name="Doporučení" avatar={false} on />
<RecordRow name="Veletrh" avatar={false} />
</RecordPanel>
</Labeled>
</div>
{/* port: apps/web/src/lib/use-record-search.ts — hledání proti serveru; stavy dává volající */}
<div style={row}>
<Labeled label="server search — short query: the server is not asked yet">
<RecordPanel query="g" searchPlaceholder="Hledat…">
<div className="acrm-recordselect-empty">Napište alespoň 2 znaky</div>
</RecordPanel>
</Labeled>
<Labeled label="server search — first request running">
<RecordPanel query="gene" searchPlaceholder="Hledat…">
<div className="acrm-recordselect-empty">Načítám…</div>
</RecordPanel>
</Labeled>
<Labeled label="server search — results (the server already filtered, filter=false)">
<RecordPanel query="gene" searchPlaceholder="Hledat…">
<RecordRow name="Generali Česká pojišťovna" sub="45272956" shape="square" mode="single" />
<RecordRow name="Generali Investments" sub="43873766" shape="square" mode="single" />
</RecordPanel>
</Labeled>
<Labeled label="server search — request failed: error + „Zkusit znovu“">
<RecordPanel query="gene" searchPlaceholder="Hledat…">
<div className="acrm-recordselect-empty acrm-recordselect-error" role="alert">
<span>Hledání se nepovedlo</span>
<Button variant="ghost" compact>
Zkusit znovu
</Button>
</div>
</RecordPanel>
</Labeled>
</div>
<Note>
Opening focuses the search input; typing filters label, `sub` and `keywords` diacritics-blind
(`foldText`). Esc is consumed by the panel (page shortcuts and the dialog underneath stay
quiet) and returns focus to the trigger, click outside closes. Single mode closes on pick and
marks the row with the accent check (F261), multi keeps the panel open and uses the DS
checkbox. With `anchorRef` the framed trigger is not drawn at all — the row or chip of the
caller is the anchor and closing is reported through `onClose`.
</Note>
</div>
);Composition
export const Composition = () => {
const [owner, setOwner] = useState<string | string[]>('u1');
const [company, setCompany] = useState<string | string[]>('');
return (
<div style={grid}>
<Labeled label="deal form — owner (clearable) and company (square avatars) next to a text input">
<div style={{ display: 'grid', gap: 'var(--sp-4)', width: 480 }}>
<Field label="Název obchodu">
<Input defaultValue="Kancelář Vinohrady" />
</Field>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--sp-4)' }}>
<Field label="Vlastník">
<RecordSelect
options={PEOPLE}
value={owner}
onChange={setOwner}
clearable
fullWidth
/>
</Field>
<Field label="Firma">
<RecordSelect
options={COMPANIES}
value={company}
onChange={setCompany}
placeholder="Vyberte firmu…"
searchPlaceholder="Hledat firmu…"
fullWidth
/>
</Field>
</div>
</div>
</Labeled>
{/* port: apps/web/src/components/record/OwnerPickerEditor.tsx:54 (anchor = the rail row value) */}
<Labeled label="rail row — the value stays as the anchor (.rr-ev.rpk-anchor), panel below it">
<div style={{ display: 'grid', gap: 'var(--sp-1)', width: 280 }}>
<div className="rr-row">
<span className="k">Vlastník</span>
<span className="rr-ev rpk-anchor">
<span className="rr-ev-tx">Jana Nováková</span>
</span>
</div>
<RecordPanel searchPlaceholder="Hledat kolegu…">
<RecordRow
name="Jana Nováková"
sub="[email protected]"
entityType="user"
mode="single"
on
/>
<RecordSep />
<RecordRow name="Petr Svoboda" sub="[email protected]" entityType="user" mode="single" />
</RecordPanel>
</div>
</Labeled>
</div>
);
};