Entity picker
betaRecord reference field: removable record chips plus Add, built on the canonical RecordSelect (server search, recent, create from e-mail).
`apps/web/src/lib/anchored-panel.ts` is gone from origin/main — anchoring moved into the shared Popover (acrm 26de4b54b, AddRefMenu/RecipientInput/AssignPicker/deal-parts); the reference was dropped from this entry. Optional prop `recent` (27 Sep 2026, `RelationDialog`'s entity picker is the first consumer): on an empty/below-minimum query it offers recently opened records of the allowed `types` instead of the "type at least N characters" hint, sourced from `useRecentRecords` (`GET /search/recent` → `record_views`, written server-side when a record's detail loads — real opens, not search clicks). Rows reuse the exact `.acrm-recordselect-opt` markup and `add()` path as a search result; already-selected and locked records are filtered out client-side. Without the prop the picker behaves exactly as before. Debt: `EntityPicker` lives in `apps/web/src/components/EntityPicker.tsx`, not `packages/ui`, so it has no Ladle story — behaviour is covered by `relation-dialog.test` instead. Move it to `packages/ui` to get stories (AD 27 Sep 2026). 3 Oct 2026 (AD): deprecated → beta. The duplicate shell is gone (AddRefMenu removed, 4a9a934e0; ProductPicker on RecordSelect since 1 Oct); it is a composition over RecordSelect, not a second picker. Migrating its consumers would rebuild the same composition each time. Canon after: move into @acrm/ui with stories (Default · States · Composition) + baselines, usage/a11y rewritten. 3 Oct 2026: shell is DS.RecordRefField in @acrm/ui (acrm !4502); EntityPicker / ProductPicker are data wrappers around it, DOM 1:1. Focus returns to Přidat on close. Registry repaired 17 Oct 2026 (review F1): AddRefMenu.tsx is gone from main (file reference dropped). Registry repaired 17 Oct 2026 (review F2): removed classes with no CSS definition and no TSX use on main: .acrm-popover.eref-add-pop.
Used in 13 files · 11 areas · 14× — components/ProductPicker, components/calendar, components/deal, components/dialer, components/mail, components/meetings …
apps/web <EntityPicker> <RecordRefField>.eref-field .eref-add .add-ref .acrm-recordselect-search +4SourceLegacy card Inputs.htmlDefault
export const Default = () => <Live label="Účastníci" initial={['c1', 'o1']} />;States
export const States = () => (
<div style={grid}>
<Labeled label="empty — only “Přidat”, no visible input (references float)">
<div style={field}>
<Live label="Účastníci" />
</div>
</Labeled>
<Labeled label="filled — tokens wrap, “Přidat” stays last">
<div style={field}>
<Live label="Účastníci" initial={['c1', 'c2', 'o1', 'd1']} />
</div>
</Labeled>
{/* port: apps/web/src/components/person/NewContactDialog.tsx (Firma, max={1}) */}
<Labeled label="max reached (max={1}) — “Přidat” is gone, × gives the slot back">
<div style={field}>
<Live label="Firma" initial={['o1']} max={1} />
</div>
</Labeled>
{/* port: EntityPicker statusOf — meeting invitations (dot after the name) */}
<Labeled label="token state — dot after the name, title + aria-label carry the meaning">
<Static>
<span className="eref-wstate">
<EntityRef type="person" name="Jana Nováková" removable onRemove={() => {}} />
<span className="eref-dot green" title="Přijato" aria-label="Přijato" />
</span>
<span className="eref-wstate">
<EntityRef type="person" name="Petr Svoboda" removable onRemove={() => {}} />
<span className="eref-dot amber" title="Bez odpovědi" aria-label="Bez odpovědi" />
<span className="eref-meta">Neodesláno</span>
</span>
</Static>
</Labeled>
<Labeled label="open — below the minimum query (state, not an explanation)">
<RecordPanel width={320} searchPlaceholder="Hledat osobu nebo firmu…">
<div className="acrm-recordselect-empty">Napište alespoň 3 znaky</div>
</RecordPanel>
</Labeled>
<Labeled label="open — results (adding mode: nothing checked, picked records are tokens)">
<RecordPanel width={320} query="nov" searchPlaceholder="Hledat osobu nebo firmu…">
<RecordRow
name="Jana Nováková"
sub="[email protected] · Kontakt"
entityType="contact"
mode="single"
/>
<RecordRow
name="Novák a syn"
sub="11223344 · Firma"
entityType="organization"
mode="single"
/>
</RecordPanel>
</Labeled>
{/* port: EntityPicker emails — New meeting, typed address first */}
<Labeled label="open — invite by e-mail: the typed address is the first row">
<RecordPanel width={320} query="[email protected]" searchPlaceholder="Hledat osobu nebo firmu…">
<button
type="button"
className="acrm-recordselect-opt acrm-recordselect-create"
role="option"
aria-selected={false}
>
<Icon name="mail" />
<span className="acrm-recordselect-tx">
<span className="nm">Nový kontakt: [email protected]</span>
</span>
</button>
</RecordPanel>
</Labeled>
{/* port: apps/web/src/components/ProductPicker.tsx (free text, P7) */}
<Labeled label="open — products: free text in the footer when nothing matches by name (no empty sentence — the answer is the action)">
<RecordPanel
width={340}
query="Poradenství"
searchPlaceholder="Hledat produkt…"
footer={
<button
type="button"
className="acrm-recordselect-opt acrm-recordselect-create"
role="option"
aria-selected={false}
>
<Icon name="writing" />
<span className="acrm-recordselect-tx">
<span className="nm">Použít text „Poradenství“</span>
</span>
</button>
}
>
{null}
</RecordPanel>
</Labeled>
</div>
);Composition
export const Composition = () => (
<div style={grid}>
<Note>
Entity picker = this field + records from /mentions/search (recent, e-mail invites). Product
picker = this field + the product catalog (top by usage, free text). One value in a form row
is a plain Record select, not this field.
</Note>
{/* port: apps/web/src/components/calendar/EditMeetingForm.tsx (Obchody — DEAL_TYPES) */}
<Labeled label="form row — deals of a meeting">
<div style={field}>
<Live label="Obchody" initial={['d1']} />
</div>
</Labeled>
{/* port: apps/web/src/components/deal/DealRail.tsx (hideLabel — the rail draws its key) */}
<Labeled label="rail editor — hideLabel, the rail row draws the key on the left">
<div className="rr-row" style={{ width: 340 }}>
<span className="k">Kontakty</span>
<span className="v">
<Live label="Kontakty" initial={['c1']} hideLabel />
</span>
</div>
</Labeled>
</div>
);