Form-less dialog
The dialog that creates or edits one record without form fields. There are no labels and no input boxes: an icon carries the meaning of each row and the placeholder says what belongs there, so the record's structure is readable at a glance instead of being read off a column of labels. The reference behaviour is Google Calendar's Create event.
The shell is the canonical Dialog at size="m"; this pattern replaces only the body. The engine is apps/web/src/ds/dialog-formless.css (.fl-*), with the phone layer in apps/web/src/ds/responsive.css.
Two shapes, decided by what the record is:
- Shape A — content. The substance of the record is text: a task, a note, a deal, a logged call. A borderless writing surface, properties as chips in the footer, an "add" row underneath.
- Shape B — questions. The record answers what · when · with whom · how · where, and those answers must be visible together: a meeting, a person, a company, a relation. A writing line for what, then one icon + input row per question.
Both shapes now share the engine. The task dialog moved onto .fl-* on 11 Sep 2026 (approved by the AD) and brought the pieces shape A needs: a chip row in the body, a dual chip, and a slot at the left of the header. The deal and the note dialogs followed on 13 Sep 2026 with the same anatomy (FL-5, acrm !3466–!3476) — properties as chips in the body, the classification in the header, the footer for actions only; the per-dialog copies .dl-* and .nd-author are gone. Shape A has one look, not three. The deal keeps the canonical 750px; the note keeps its larger work window (1128 × 828) and the air around the text — the AD asked for the anatomy, not the size (12 Sep 2026), so the reader can concentrate on writing. See docs/REVIEW-form-dialogy.md.
The work report (/pracovni-reporty, acrm !3885, approved 25 Sep 2026) is the third document in shape A and is built from the note's parts, not a copy of them: the same work window (size="l" + work, document body bodyClass="doc"), the person whose report it is as the primary entity in the header (LeadEntity, type user) with the date beside it, the heading in the note's title setting (.dlg-title, 28px) and the text rendered by NoteMarkdown, switching to NoteEditor on click. The report has no name of its own, so the heading is a heading, not a field. The footer carries the one action (Schválit a odeslat); the close button closes.
The two shapes are not two designs. Shape A writes the record as text plus properties; shape B asks questions. A dialog may use rows from B (the note row) inside A — the task dialog does.
When to use
This is the rule for choosing between a form-less dialog and a form. It is decided by the record, not by taste.
- The dialog creates or edits one record, and its properties are either a text core with up to about six properties (shape A) or a handful of questions the reader needs to see at once (shape B).
- The name of each property is obvious from its icon and its placeholder — when, who, where, reminder. If you can say it in a placeholder, you do not need a label.
- The reader is deciding, not transcribing: they are arranging a meeting or writing down what happened, and the dialog should read like a sentence about the record.
When not to use
- Data entry with many typed values, where the label carries the meaning — an accounting document (13 fields), a payroll relation, a VAT period, a mailbox connection,
Spravovat segment(37 fields). A label is doing real work there; removing it would hide the field's identity, not simplify it. These stay forms and that is correct. - A question with two answers — that is the confirm variant of
dialog. - A create form inside a listing screen used to be
drawer-form; that is deprecated since 14 Sep 2026 (AD, OV-2) and its forms move to this pattern — the first case is „Nový kontakt". - A preview or a reader (an attachment, an e-mail preview) — the body is content, not properties.
- A picker anchored to a control — that is
popover.
Anatomy
Measured from dialog-formless.css on origin/main.
- Shell:
.acrm-dialog.formless— the canonical panel, 750px (size="m"). Body paddingvar(--sp-5) var(--sp-5) var(--sp-4),gap: var(--sp-4). - Height wrapper
.fl-body: the component measures the content and writesheightin pixels, so the window springs to its new size (0.42s) instead of jumping when a row expands. The classis-animatedis added after the first frame, so opening is not animated. - Column
.fl-stack:grid-template-columns: minmax(0, 1fr),align-content: start,gap: var(--sp-4). Theminmax(0, 1fr)is load-bearing — without it the widest row stretches the grid past the dialog (measured: 401px of content on a 360px screen). - What
.fl-title: borderless,500 22px/1.25 var(--font-display), tight tracking. Larger than every other row, because it names the record. It is atextarea rows=1withfield-sizing: contentandoverflow-wrap: anywhere, so a long name wraps onto the next line and the field grows instead of scrolling sideways (ACRM-226). Enter never starts a new line: it moves on (task → description, deal and meeting → note, note → body). - Rows
.fl-rows/.fl-row: one grid column, no gap between rows; each row isdisplay: flex,align-items: flex-start,gap: var(--sp-4),min-height: 40px,padding: var(--sp-1) var(--sp-2),radius-sm. Flex-start, not center: a block may hold ten attendees and the icon stays beside the first one (same rule as multi-value rows in the rail). - Icon
.fl-ic:width: var(--ico-lg)(20px),min-height: 28px— one value line, glyph centred inside it — colour--fg-muted. - Value
.fl-in:min-height: 28px, wraps,14pxtext (one step above--p, to sit with a 20px icon). input.fl-text— borderless, inherits the row's font; looks like text until it is typed into..fitaddsfield-sizing: contentso the value keeps its own width and a trailing icon (open-map, copy) sits immediately after it rather than at the right edge..fl-in.menu— a select trigger as wide as its content (flex: none,white-space: nowrap), caret right after the value,min-height: 28px(the height of a task-dialog meta chip, not of a 30px control). Never full width..fl-in.ph— the placeholder state,--fg-muted..fl-hint— a qualifier after the value on the same line (room availability),--small;.ok/.errtint it..fl-copy— a 24px icon target inside the row, same shape as the rail's pencil.- Several values in one row
.fl-vals: a column inside the row (gap: var(--sp-1)) for a value that belongs to the one above it rather than asking its own question — the meeting type and, under it, the video-call link. The icon stays with the first value, the same rule as the attendee block and as multi-value rows in the rail. Decided 9 Sep 2026: a link speaks for itself and follows from the type, so giving it an icon of its own both invented a question and put thelinkglyph on two different rows. - When
.fl-when:500 15px/1.4— larger than the other rows, smaller than the title. A button that turns into the canonicalDateTimeRangeFieldinside the same row (.fl-when-edit). - Who —
EntityPickerwith the field shell removed. Each participant is its own row (.eref-fieldbecomes a column), so ten people read as a list and "Add" stays last. The token's grey pill and hairline appear only on hover/focus, and it is pulled left by its own inset so the content lines up with the placeholder text of the other rows. - A canonical field inside a row (DateField, Input, Select while editing, directly or one wrapper deep): the row icon box takes the field height —
--field-h, on the phone--field-h-touch(40px) for DateField — so the glyph sits on the field's axis (acrm !4455, 2 Oct 2026). The.fl-notewriting area is a borderless value line and stays on the 28px box. A value at rest is text (.fl-whenfor a date), the field appears only while editing. - Note
.fl-note: borderless textarea,field-sizing: content,min-height: 28pxat rest and66pxon focus (three lines — an invitation to write more than a sentence), transitioning in0.18s. A closed note is abutton.fl-inthat looks like row text. The note is aMentionInput(.mi-editor.mi-multiline— a form field with frame, fill, padding, focus ring and 88px), so the form-less selectors also carry.mi-editor.mi-multiline.fl-note: the note stays borderless at rest, on focus and while typing, whatever order the CSS loads in (ACRM-355, acrm !3775). - Status dot
.eref-dot:var(--sp-2)circle after a participant's name —greenaccepted,ambertentative,rosedeclined, default grey waiting. One step smaller than a pill's dot; it distinguishes a state, it does not shout. - Error
.fl-err:--small,--critical-ink, below the rows. - Footer: the canonical
.acrm-dialog-footwithgap: var(--sp-3)— an optional switch, then the actions. The primary carries⌘↵in a.tk-kb; further actions over an existing record live in a⋮kebab beside it, never loose next to Save. - Amounts and structured values in a row (acrm !4524, 3 Oct 2026):
<AmountInput variant="formless">(andMaskedInput variant="formless") is the same canonical input — mask, arrows, validation — without the.acrm-inputwrapframe, rendered as.fl-textlike any other row value; the currency is a suffix after the typed amount. Never a bareinput type="number"or a framed field inside a form-less row. Required fields carry no asterisk: the disabled primary button says what is missing. Validation errors show inline under the row (.fl-vals > .fl-err,aria-invalid,aria-describedby), save errors inline too — never a toast. - Money row (acrm !4542, 3 Oct 2026):
FlMoneyRow(components/dialog) is the one amount row —AmountInput formlesswith the currency as a choice in the amount's suffix (symbol from the canonical money format, „320 Kč ⌄“). Without an amount there is no currency choice; with a fixed currency the suffix is plain text. Never a separate currency row next to the amount — the same value twice. Used in asset, budget, phone line, vehicle and cost dialogs. - Phone and date rows (acrm !4535, 3 Oct 2026):
<PhoneInput variant="formless">is the canonical phone field (country code + mask) without its frame inside.fl-in; when the number is the main value of the record (a phone line), it is the first row and the dialog has no title row. An incomplete number is reported only after the field is left („Telefon není celý“).FlDateRow(components/dialog) is the one shared date row — at rest text (.fl-when, year by the year convention), on clickDateField; never copy it into a dialog. - Structured values use masks in every form-less dialog: IČO, DIČ, PSČ, SPZ, VIN (
field-masksin@acrm/ui), amountsAmountInput formless, year as digits with an inline range error. - Drawer forms migrate here (drawer-form wave 1, acrm !4524): Týmy, Šanony, Pokladna, Banka, Provizní sazby and cash movements are form-less dialogs; short code lists are
PickerMenu, people and budgetsChipSearchMenu row, a date at rest is.fl-whentext that turns intoDateFieldon click. Longer forms (13–20 fields) keep this shape with progressive disclosure (the NewContactDialog pattern), not a new layout. - Availability panel (meetings):
.acrm-dialog.formless.availmakes the body two columns,minmax(0, 1fr) 360px, hairline between them, no gap — the day reaches the window's edges. The panel's height is driven by the form, not by the 24-hour axis (.flaisposition: relativewith absolute content), so the window does not inflate. Arrival is choreographed: the panel unrolls to the right while the shell widens (--fla-shift), then the whole window settles back to centre (flaSettle,0.72s). - Phone (
responsive.css): the dialog becomes a bottom sheet — full width,max-height: 92dvhof the visual viewport (--vv-h), rounded top corners only,acrmSheetUp. Rows grow tomin-height: 44px(touch minimum), the row overhang is dropped (it caused horizontal scroll at 390px),.fl-body's measured height is released toautoso the body scrolls,⌘↵hints are hidden, and the footer keepsenv(safe-area-inset-bottom)clear of the home indicator. The availability panel moves below the form at42dvh.
Shape A — the property row
Measured in the browser on the approved prototype (11 Sep 2026). The chips themselves are the canonical .tk-chip; what is new is where they live and how many of them are shown.
- Chip row
.flc:display: flex,flex-wrap: wrap,gap: var(--sp-1). It sits in the body, under the text, not in the footer. The footer then carries actions only — which is why the task dialog could go back to the canonical 640px (size="m"; widened to 750px on 17 September 2026, acrm MR !3705) after years of an 800px exception whose only reason was "so the property chips fit in the footer". Inside.flca chip takespadding-left: var(--sp-2)instead of--sp-3: a stroked glyph has ~2.3px of blank inside its 24-viewBox, so a symmetric gutter reads as more space on the left. - Quick steps
.flc-navrhy: a grey group for things that are one click, not properties — marking a task done, picking one of the project's next milestones. Same tokens as the filter bar's "or" group (--line-100,--radius-md, an--sp-0bleed cancelled by a negative vertical margin so the group does not make the row taller), one step lighter than--active-surface: nothing here is switched on, the group only shows what belongs together. It hugs its content (display: inline-flex,justify-self: start) — a band across the whole dialog would claim the rows below it. No label: chips inside a tinted group read as an offer on their own. - Suggestions from the typed title (acrm !3771, ACRM-213, approved 17 Sep 2026 — Linear's "Apply all quick suggestions"): in the new task dialog the same group also offers what the title already says — "zavolat Petrovi do pátku, hoří" →
v pátek·Urgentní·Petr Kornuta. Only due date, priority and assignee (a sentence carries those reliably; project or tags would be guessing), only for a property that is still empty (the default "Ty" assignee counts as empty), and an assignee only when the name fits exactly one colleague. Each chip carries the glyph of the property it sets (calendar·flag·user), not a shared "AI" mark, and its label repeats the sentence's own word ("zítra", "v pátek") so the user sees where it came from. - Apply all — a quiet
IconButtonbolt(size="sm", 24px) before the chips: it drives them, and a control is read first in a row. Drawn only from two suggestions up; with one it would save no tap. A used suggestion disappears — the property is filled, offering it again is noise. Nothing in the title is rewritten or removed. - Computed in the browser from enumerable Czech words (
apps/web/src/lib/task-suggest.ts), no model and no server round-trip: the offer has to follow every typed word. - Dual chip
.tk-duo: two properties that belong together in one chip with a divider — project and its milestone. It is not a new family: both halves are.tk-chipand the wrapper only takes away their own outline and draws one shared border. The radius targets the wrappers (.tk-duo > *:first-child .tk-chip), because each half sits inside its own trigger wrapper and:first-childon the chip would round the inner corners too. Use it when the second value cannot exist without the first and its menu depends on it — not merely when two properties are often filled together. The second half is not drawn when there is nothing to choose from; then the dual chip is an ordinary chip. - Header — the primary record, then the classification (AD, 12 Sep 2026; ACRM-223/224; acrm !3476). The header reads left to right like a breadcrumb without slashes: the record this one belongs to, then what this one is —
Skanska a.s. Úkol ACM-42,Jan Novák Upravit poznámku,Karlín — rekonstrukce Nová schůzka. - The primary record is the canonical
EntityRef(16pxEntityMark+ name) in theleadingslot (LeadEntity,components/dialog/). It stays in ink — it is a reference to a record. - Then the classification: in the task a
.tk-chipwith the type's own glyph, elsewhere the window title. With.head-mutedon the shell the title and the ID take--fg-muted(the tone the header already uses for.acrm-dialog-subject-ct), so the record reads first and its description second. - The two are separated by rhythm, not a character:
.acrm-dialog-lead .flc { gap: var(--sp-5) }(16px, twice the header's slot gap). Where the title carries the classification, the slot addscalc(var(--sp-5) - var(--sp-3))so the gap comes out the same. The slash was proposed and rejected by the AD — the separator would have been a new element and a breadcrumb inside an identity is forbidden in the section top bar. - No word may repeat in the header: an empty window title is not drawn (
.acrm-dialog-title:empty), so the task's header does not read „Úkol Úkol ACM-42". - Which record is primary comes from the model, not from a guess: the deal's
primaryEntityType(the old „Reprezentuje" row moved here), the note's first link in the order person → company → deal, the task's and the meeting's first link. - A click opens the choice of links and „Odebrat"; removing the primary promotes the next link, and with none left the header falls back to the dialog's icon. It does not navigate (PROV-1).
- A long name is clipped with an ellipsis — only the name: the type chip is
flex: none, andmin-width: 0runs through the whole chain (slot, thePickerMenuwrapper, the row) so the title and the close control are never pushed out of the window. - Measured: header 53px in all four dialogs (the deal came down from 55), no window grows.
- Attachments
.tk-attsit above the chip row — an attachment is content, not a property. Each tile's wrapper.tk-att-wholds an 18px.tk-att-xin the top-right corner, revealed on hover and on keyboard focus. The remove button is a sibling of the tile, never its child: a clickable element inside a clickable one is a trap for mouse and keyboard alike.
Shape A — the contact log dialog
On origin/main — ContactLogDialog (apps/web/src/components/timeline/ContactLogDialog.tsx, class formless head-muted on the shell). „Zaznamenat kontakt" ("Log a contact") is the ONE dialog for writing a free-text timeline entry on a person, company, deal or project — there is no per-entity variant. Product owner, 27 Sep 2026, on being asked to add it to the project screen too: "to je ten stejný dialog… použij ten samý prvek z DS" ("it's the same dialog… use the same DS element"). Project as the main entity (acrm !4084, 28 Sep 2026, entityType="project") writes its link as the 4th POST /timeline/entry slot (projectId, added in acrm !4048) rather than through the links chip; the older, project-only ProjectEntryDialog (279 lines + its own test file) is removed, and ProjectMeetingsTab now opens this dialog like every other entity screen.
- The note is the core, same rule as the task/deal/note bodies (
REVIEW-form-dialogy.md§3.1): a borderlessMentionInput(.mi-editor.mi-multiline.fl-note), mentions turned off (mentions= {false}) because a logged entry has no relation to save a mention into — the links chip below it is the only way to connect the entry to another record..fl-note.fl-note-openkeeps the writing area at its focused, three-line height (min-height: 66px) even after focus leaves it for a chip. Before this class existed the area collapsed back to one line on every chip click and the whole dialog jumped 38px (28 Sep 2026, acrm !4072). Pitfall already hit once: do not name this state a bare.openon.fl-note—ds:sync-checkthen reads.x.openelsewhere in the DS sheet as adopted-and-missing and broke the person-record golden. The dialog-specific name.fl-note-openis the fix; see the same warning under Don't. - Chip row
.flcunder the note, canonical.tk-chip/MetaChippattern: Direction · When ·EntryLinksField(Propojit) · Recording · Follow-up. Each is aPickerMenudriving aMetaChiptrigger, same shell as every other shape-A property chip. - Recording chip (acrm !4072, 28 Sep 2026, approved "doplň možnost přidání nahrávky všude" — "add the option to attach a recording everywhere"):
MetaChipwith glyphmicrophone, driven by aPickerMenuwhose items are "Z Plaudu" (only whenplaudQ.data?.configured— no dead option when the tenant has no Plaud connection) and "Nahrát soubor" (opens a hidden<input type="file" accept="audio/*,video/*">), plus a clear row "Odebrat nahrávku" once something is picked (clearLabel/clearIcon="x"/onClear, the family's standard chip-clear anatomy). Icon and item order come from the sharedrecording-sources.ts(RECORDING_SOURCE_ICON) — the same source the note dialog's "Add audio" menu uses, so the two do not drift. - The chip's value is the SOURCE, never the file name. A picked file reads "Soubor", one Plaud pick reads "Z Plaudu", several read "N z Plaudu" — dictaphone items and uploaded files both tend to carry long names that a 28px chip cannot show without truncating, and truncating a name is never allowed in this DS. The full name is visible in the picker itself, not in the chip.
- A logged entry with a recording is submitted as a proběhlá SCHŮZKA ("meeting that already happened") — only a meeting can hold a recording, so
POST /timeline/entryfirst returns ameetingId, then the binary upload or the Plaud import runs as a second call (meetingsApi.uploadRecording/api.importPlaudToMeeting). If that second call fails, the dialog still closes as on success and shows a warning toast (contactLog.recordingAttachError) instead of reopening a half-filled form over an entry that was, in fact, already written (review !4072). - Header centring (acrm !4072, 28 Sep 2026 — product owner: "položky v záhlaví nejsou zarovnané vertikálně na střed", "the header items aren't vertically centred"):
.acrm-dialog-lead .flc { align-items: center }, plus, only in a row that holds more than the entity alone,margin-top: -1pxon the wrapper around the entity's.eref-chip. The chip's 1px dashed name underline extends its box 1px further down than a plain chip, which — before the correction — put the entity's optical centre 2.8px above the classification chip and the close×. The single- entity header next to the title alone (ACRM-319) is untouched: the correction only fires with a second flex child in the row.
Shape B — the person dialog
On origin/main since 15 September 2026 (acrm !3561; approved by the product owner the same day) — NewContactDialog (apps/web/src/components/person/NewContactDialog.tsx), with its classes in dialog-formless.css. It is the next shape-B instance after the meeting dialogs, and the first dialog to reuse the record rail's own multi-value channel editor over a draft record that does not exist on the server yet (!3548). A second MR (!3575) replaces the old drawer panel with it in the people list and the command palette.
- Name — the title textarea
.fl-titleholds the whole name; it is split into first/last name only on submit (splitQueryToName, the same helper⌘Kuses). - Rows, each with a leading icon per the family's row anatomy: position (
briefcase), e-mail (mail), phone (phone) with a "Do not call" switch next to it (TkToggle, the canonical.tk-more+.swpill — the same toggle as the task dialog's "Create more"), and company (building,EntityPickercapped at one result). - Channel rows reuse the record rail's editor, in draft mode. E-mail and phone are edited through the same
ChannelsPopoverEditor/MultiValuePopoverthe rail uses (useChannelFieldwithdraft: true, plainValues: true) — visually and behaviourally identical, but writing to an in-memory list (local-channels.ts, same operation signatures and the same primary/position/ label semantics as the server) instead of the API, because the record has no id yet. Nothing reaches the server until the dialog creates the record: the primary e-mail and phone go intocreateContact, further values are added afterwards throughaddChannel. New wrapper classes:.fl-chan(the value column,.fl-valsvariant for a channel row) and.fl-chan-v(the clickable value cell that opens the editor). - Entry ≠ detail. Inside this dialog a channel value is plain text and a tap opens the editor to edit it — never a
tel:/mailto:action — per.claude/rules/design-system.md§"Vstupy a mobil" point 5. The row that opens the editor guards its click withe.currentTarget.contains(e.target)so the portalled editor's own "Done" tap cannot bubble back up and reopen it. - Progressive disclosure behind one chip. Title prefix/suffix, birth date + gender, birth number, city, work (profession/employment type) and tag start hidden behind a single
.tk-chip.empty"More details" chip (ACRM-275) — the same canonical chip as shape A's property chips, just triggering visibility instead of a menu. - Sticky rows. A row that has ever held a value stays visible after the value is cleared — the dialog tracks which fields were "newly filled" and keeps them shown (
stickystate) so clearing a date of birth, say, does not hide the field mid-edit. - Duplicate notice
.fl-dupsits inline under the row's value (.fl-dup→ iconalert-square-rounded+.fl-dup-txtext), not as a blocking panel: e-mail and phone duplicates come fromfind-duplicateskeyed on the channel value, and a separate query matches on name + complete birth date. Actions inside the notice ("take over" the number, "Restore from trash") arebutton.dup-link— text-styled links (--accent, underline), not full buttons — and the duplicate's own name links to its record viaDuplicateName. Styling lives indialog-formless.css(ACRM-274):.fl-dupis--smalltext in--fg-secondary(dark grey), the icon.fl-dup > .tiis--criticalred at--ico-smand sits on the first line of a wrapped sentence (height: 1lh);.dup-linkis an unstyled button in--accentwith an underline,--accent-presson hover. The notice informs, it never blocks creating the record. - Custom fields of the entity belong in the create dialog too (acrm !3699, 16 September 2026, ACRM-277). Behind the same "More details" chip, after the tag row, the dialog draws whatever the tenant has configured as custom fields of a person — read from the same registry the record rail reads (
api.customFields('contact')), so the two cannot drift; a deactivated definition is not drawn. Until this landed, the rail could edit them but the create dialog could not, so a tenant's own property could be filled only after the record existed — and a reviewer looking at the prototype asked what the unknown "Poradce" field even was. - The definition's type picks the input, it is not free text: a date goes into the
dd.mm.rrrrmask, a select and a yes/no are aPickerMenu(free text would let a value outside the code list into the jsonb, where no filter would ever find it again), a number gets the numeric keyboard but nottype="number". - Items sit two to a row like the title prefix/suffix or profession + employment type; one per row would turn five short properties into five nearly empty rows. The leading mark is
clipboard-list— the same glyph the rail gives them, so one property reads the same on both surfaces. - Values are written after the record exists, through
setCustomValues(the only path that merges the jsonb — a PATCH would overwrite keys put there by import, enrichment or the agent), and only the filled ones are sent: an empty key in the data looks like an answered question. - Footer matches the family footer: "Create more" toggle (
TkToggle, reused), CancelEsc, primary "Create contact"⌘↵— no kebab, this is a create dialog with nothing yet to act on.
Shape B — the relation dialog
On origin/main since 27 September 2026 (acrm !4011) — RelationDialog (apps/web/src/components/relations/RelationDialog.tsx, rules and queries in relations/relations-data.ts). One dialog for both directions of "New relation" and "Edit relation" on a person or company card.
- Anchor is fixed, not a row. A relation is an edge between two subjects: the card's own subject sits in the header as the
LeadEntity(same slot as task/note/meeting) and is never repicked —PATCH /relations/:iddoes not acceptfrom/to— so editing shows the anchor's side only as read text, never as a field. - The other side is a generic entity, not a company-only or person-only field: one
EntityPickerwithtypes={['organization','contact']}andmax={1}(product owner, 27 Sep 2026 — "vztah můžu mít i s osobou", one picker rather than two dialogs). It is the first consumer ofEntityPicker'srecentprop — see Entity picker. - Type follows the picked entity's kind. Relation type is a
PickerMenufiltered to the chosen entity's kind (person↔person, person↔company or company↔company have their own type catalogues); before an entity is picked, the menu shows both catalogues at once, separated by asep: truerow — never a group heading, per the family's "no menu eyebrow" rule. Choosing a type before an entity, then picking an entity of the other kind, drops the type instead of saving a mismatched pair. - Position and share % rows are conditional on the type, not always drawn: they render only when the selected type's
allowsPosition/allowsSharesay so, and a field outside the allowed set is always sent asnullon submit — switching type (e.g. "Společník" → "Zaměstnanec") clears the stale value server-side instead of leaving it behind unseen. - "Valid from" is optional, typed straight into the
dd.mm.rrrrmask (MaskedDateInput, text-field family) — a button showing the placeholder until clicked. Placeholder reads "Platí od dnes" while creating with nothing chosen (empty submits today's date) and "Platí od" while editing (empty on edit keeps the stored date — clearing the field is never a silent delete of the relation's start). - Footer follows the family rule for edit dialogs: create = Cancel
Esc+ "Add relation"⌘↵; edit = "Save" only, no Cancel (over an existing relation the word would read as "cancel the relation", and the close control already closes the window). - Relationship type catalogue, person↔person (acrm !4010, migration
0416_relation_types_chot_prarodic.sql, same 27 Sep 2026 change): "Manžel/ka" renamed to "Choť" (one word for either spouse), "Partner/ka" renamed to "Partner", and a new "Prarodič / Vnouče" (Grandparent/Grandchild) type added after Parent/Child. The migration only rewrites rows still holding the old default name, so an admin rename made in Nastavení → Data → Vztahy is never overwritten. presetOtherreplaces the old person-only dialog (acrm !4020, 27 Sep 2026). "Change role" on a person row on the company card now opens this sameRelationDialoginstead of a second, parallel dialog — the earlierPersonRoleDialogis removed. The thin wrappercompany/PersonRoleRelationDialog.tsxresolves the existing relation row fromperson.affiliationId(if the person already has one) and, only when there is none yet, passespresetOther— aMentionnaming the person — so a brand-new relation opens with the other side already fixed by context instead of asking the reader to pick it again with theEntityPicker.presetOtheronly ever applies to creation; editing an existing relation always has both sides already fixed by therelationprop.
Shape B — the add-to-list dialog
On origin/main since 27 September 2026 (acrm !4019) — AddToListDialog (apps/web/src/components/segment/AddToListDialog.tsx). "Přidat do seznamu" ("Add to list") on a person or company card. It is the pattern's multi-select-from-search instance: one dialog adds or removes the record from any number of the tenant's manual lists (segment kind='manual'), not just one relation.
- Anchor is fixed, not a row — same rule as the relation dialog: the card's own record is the
LeadEntityin the header and is not repicked. - One field. The body has a single row, "Hledat nebo založit seznam" ("Search or create a list"), that filters the entity's manual lists by name (
norm()— NFD, diacritic-stripped, case-folded) as the reader types. - A list row has CheckMenu anatomy, not the old
.atl-row. Each match renders as.acrm-popover-item— the family's own menu-item shell — wrapping the list's segment tile (.sg, 18px, the same tile as the left menu's manual lists; see "Segment tile debt" insidebar-nav-item.usage.md), the name, the member count, and.acrm-checkboxon the right (aria-pressedon the row reflects membership). A click toggles membership immediately — there is no separate "apply" step, which is why the footer holds only "Hotovo" ("Done"). The rows sit in.atl-list(apps/web/src/ds/segment-screen.css); the older.atl-row/.atl-icrow this dialog used before 27 Sep 2026 is now only drawn by the Trash page and the duplicates panel. - "Create list" row is exact-match-gated. A trailing
Založit seznam „…"row is appended only when the typed text has no exact match among the entity's lists — a partial match still shows the row (so "fin" offers to create "fin" and still lists "Fintech — sledovat"), an exact match (case/diacritic-insensitive) hides it, so a list that already exists can only be toggled, never duplicated. - Enter is match-count-driven, not exact-match-only. With the field narrowed to exactly one visible row — even by a partial match like "fin" → "Fintech — sledovat" — Enter toggles that row; with zero or several rows still shown, Enter creates the typed name instead. Verified in
apps/web/src/components/segment/add-to-list-dialog.test.tsx(cases "přesná shoda …" and "Enter při jediné ČÁSTEČNÉ shodě list přepne, nezaloží duplicitu (review !4019)"); the same file also guards the family's Enter-under-a-pending-request bug (8 Sep 2026) for the create path. - Dynamic (filter) segments are excluded. An earlier shape of this dialog listed the entity's dynamic segments underneath, read-only, with a line explaining they can't be added to by hand — pure noise once the rest of the dialog went form-less, so the current version does not query or render them at all (
listSegmentsis not even called — see the test case "dynamické segmenty se už nenačítají").
Best practices
Do
- Give every row an icon that is unambiguous on its own, and a placeholder that names the property in the words the reader would use (
Zadat místo nebo odkaz, notMísto). - Show the properties that are filled in almost every time; put the rest behind
⋯. The row starts as a handful of chips, not as a list of empty boxes — in the task dialog five (state · due · priority · links · assignee), with project, milestone and the rest offered from an overflow button at the end of the row. Three properties do not need an overflow; introduce it when there are more. Two rules keep it honest: a property that already has a value is always shown, whether or not this session added it, and the overflow only offers what makes sense in context (a milestone only after a project). Picking a property from⋯opens its menu straight away — revealing an empty chip the reader then has to click is two clicks for one decision. - Let a chip carry the value, and the property's name only when empty. An empty chip says
Prioritain muted text; it never saysBez priority. Stating a negative is describing the mechanism, not answering the question. The links chip follows the same rule: it names the linked records (Jan Novák + 1), never a count of them (2 propojení), and it never repeats the primary record shown in the header — the same record is never in both places. - Name the record before it can exist. In the task dialog the title is the only requirement: without it a listing, a board card and a notification have nothing to show. A description alone does not make a task.
- Keep a glyph unambiguous within one axis, not across the whole app. The dialog asks what a single record is; a filter switches whole categories on and off. The same glyph on those two axes is not a collision — they never sit side by side (AD, 9 Sep 2026). Two rows of the same dialog wearing one glyph is.
- Keep the writing line for what at the top and larger than the rest; it is the record's name.
- Let a select trigger be as wide as its value, with the caret right behind it.
- Put a qualifier after the value on the same line; use the muted second line only when there is genuinely something to add.
- Keep one row per value in a multi-value block, with the icon beside the first.
- Put destructive and secondary actions over an existing record in the footer kebab.
- Offer
⌘↵for the primary action, and hide the hint on touch.
Don't
- Add a label to a row. If the row needs a label, it is a form field and the dialog is a form.
- Stretch a select to the full width of the dialog.
- State the obvious. There is no "Neopakuje se" row: absence of a recurrence says it.
- Draw a box around an input. The row is the box.
- Put a second emphasised button in the footer.
- Keep a
Zrušitin a dialog that edits an existing record: there it reads as cancel the meeting, not close the window, and the close control already does the latter (product owner, 8 Sep 2026 — removed from the meeting edit dialog). On a create dialog the word is not ambiguous, because there is nothing yet to cancel;NewMeetingFormstill carries it and that is not currently a defect. - Give a value its own icon when it follows from the row above it — put it in
.fl-valsinstead. - Centre the icon against a tall block of values.
- Rebuild the row engine per dialog. Three copies of shape A are exactly the debt this record exists to end.
- Name a dialog-specific state class a bare
.openon a shared.fl-*element.ds:sync-checkgreps.x.openproject-wide to catch adopted-but-missing classes elsewhere in the DS sheet, and a bare.fl-note.openreads as one of those — it broke the person-record golden once (contact log dialog, 28 Sep 2026). Give the state its own compound name instead (.fl-note-open). - Turn the links chip into navigation. In a dialog the chip edits which records this one is linked to; opening a linked record belongs to the record's detail (the links row of the task detail rail), not to the chip's panel. A link out of the panel would be a new element inside the picker, in every dialog that uses it (AD, 11 Sep 2026, PROV-1).
Content
- The title placeholder is the question the record answers (
Co se bude dít?), not a field name. - A placeholder says what to enter and what is accepted:
Pozvat účastníky — osoba, firma, kolega nebo e-mail. - The primary action is the verb alone (
Vytvořit,Uložit); the title already carries the noun. - No explanatory sentences in the body. If a row needs a sentence, the row is wrong.
Related
multi-step-dialog— New deal without context starts with a search step and reshapes into this dialog (ACRM-322); Založit osobu „…" hands over to the New person dialog (ACRM-323).
dialog— the shell this pattern fills.drawer-form— deprecated; its create/edit forms move to this pattern.dialog(confirm variant) — the two-answer question.entity-reference,select,date-time,text-field— the canonical inputs the rows host.record-rail— where the one-row-per-value rule comes from.- Review and the unification waves:
docs/REVIEW-form-dialogy.md. - Mounted by
apps/web/src/components/meetings/NewMeetingForm.tsx,apps/web/src/components/calendar/EditMeetingForm.tsx(shape B), andapps/web/src/components/tasks/TaskDialog.tsx,deal/DealDialog.tsx,notes/NoteDialog.tsx(shape A). Shared pieces:components/dialog/(LeadEntity,MetaChip,ChipSearchMenu,ChipOverflowMenu). - Person dialog:
apps/web/src/components/person/NewContactDialog.tsx— described above;masked-date-input(text-field family) for its date-of-birth row;record-railfor the reused draft channel editor. - Relation dialog:
apps/web/src/components/relations/RelationDialog.tsx(rules inrelations/relations-data.ts) — described above;entity-pickerfor the generic other-side picker (recentprop) andmasked-date-inputfor "Valid from". Also mounted, viapresetOther, byapps/web/src/components/company/PersonRoleRelationDialog.tsx("Change role" on the company card's person row) —PersonRoleDialogis removed. - Add-to-list dialog:
apps/web/src/components/segment/AddToListDialog.tsx— described above;check-menufor the row anatomy (.acrm-popover-item+.acrm-checkbox) andsidebar-nav-itemfor the reused.sgsegment tile. - Contact log dialog:
apps/web/src/components/timeline/ContactLogDialog.tsx— described above; unified across person/company/deal/project (acrm !4048, !4072, !4084). Recording sources shared withnote-dialog's "Add audio" menu:apps/web/src/components/meetings/ recording-sources.ts,apps/web/src/components/meetings/PlaudPickerDialog.tsx.
Seeing it. The Examples tab is empty on purpose: the pattern lives in apps/web, which packages/ui must not import, so Ladle has nothing to build from and the stories will have to be ported markup (the route Filter bar and Command palette took). Until they exist, the questions each dialog asks — every row with its icon name, its control and its measured height, generated from the live render — are laid out beside the real dialog at /preview/formless-otazky.html, and the dialogs themselves run as live prototypes (real components, fixtures instead of the API) at /preview/formless-schuzka-interaktivni.html and /preview/formless-schuzka-uprava.html.
Source: docs/dialog/formless-dialog.usage.md · GitLab