Review — Form dialogs, and form-less as a rule (9 Sep 2026)

Subject: every dialog in apps/web that has a body of properties, judged against the form-less pattern (ACRM-132). Commissioned 9 Sep 2026: "promote form-less to a rule and review the other form dialogs with the aim of unifying them."

Measured against origin/main at 06fb6b236 (mirror /root/acrm-main). Every count in this document comes from a script over that tree, not from reading a sample; the scripts are named where the numbers appear, and both live in tools/ so the counts can be re-measured as the waves land. The tree moved three merges during the review — noted in §5.

0. Summary

The pattern is in production and the canon says nothing about it. Two dialogs run shape B on the shared engine ds/dialog-formless.css, three run shape A on three private copies of the same idea, and 43 dialogs are forms. Until today the rule existed only as prose in candidates/formless-dialog/README.md, which is not a place anyone looks before building a dialog — and it shows: ProjectMeetingPlanDialog.tsx was added as a form on 7 Sep and deleted on 9 Sep in favour of the canonical planner (c212e241b). Two days of work spent because the choice was not written down anywhere a reader would find it.

Of the 43 forms, 12 are forms because they should be — accounting documents, payroll relations, VAT periods, a 37-field segment editor. That is the useful half of this review: the rule has to say no as clearly as it says yes, or the next wave converts a 13-field accounting dialog into a label-less row list and makes it worse.

Three defects in the pattern itself came out of the measurement, all real and all in the shipped code: the participant's RSVP state has no accessible name, two of its four tones fail the 3:1 non-text minimum, and the availability day is mouse-only.

1. The rule (this is what goes into the canon)

A dialog is form-less when it creates or edits one record and either

  • its substance is text with up to about six properties around it — shape A; or
  • it answers a handful of questions that must be visible together — shape B.

A dialog stays a form when it is data entry with many typed values and the label carries the meaning. The test is not the number of fields, it is whether you can replace the label with a placeholder without losing the field's identity. Zadat místo nebo odkaz works. Základ DPH 21 % does not.

Recorded as docs/dialog/formless-dialog.usage.md (When to use / When not to use) with the anatomy measured from the engine, plus .a11y.md, plus a subcomponent in site/registry.json.

2. Inventory (tools/ds-dialog-inventory.mjs)

188 files mention <Dialog, <Confirm or .acrm-dialog; 50 have a body that can be classified. Classification is by which vocabulary builds the body — .fl-*, .tk-/.dl-/.nd-*, or DrawerField/<Field/.acrm-field/.dlg-field.

shapefileswhere
form-less B (.fl-*)2meetings/NewMeetingForm.tsx (32 hits), calendar/EditMeetingForm.tsx (31)
form-less A3tasks/TaskDialog.tsx (.tk-*, 22), deal/DealDialog.tsx (.dl-*, 14), notes/NoteDialog.tsx (.nd-*, 2)
form43see §3

Caveat worth stating, because it changes how §3 reads: the field count is per file, and a page file counts <Field used outside its dialog too. DovolenePage.tsx shows 8 and its request dialog actually asks three questions (Typ · Termín · Důvod vzniku) — the other five labels belong to the page's stat tiles. Every candidate in §3 was therefore re-checked by pulling its field labels (tools/ds-dialog-fields.mjs), and the labels are quoted below. No candidate is placed by its count alone.

3. Classification of the 43 forms

3.1 Convert — one record, few questions, labels verified

dialogquestions foundshape
meetings/InstantMeetingDialog · Nahrát teďOdkaz na schůzku · Název (nepovinné) · Připojit k osobě nebo firměB
timeline/ContactLogDialog · Zaznamenat kontaktSměr · Kdy · Poznámka · Návazný krokA (the note is the core)
projects/ProjectEntryDialog · Zaznamenat komunikaciKanál · Kdy · Předmět · Záznam · NahrávkaA
projects/NewProjectDialogNázev projektu · Šablona · Lead · SubjektyB
deal/BulkCreateDealDialog · Vytvořit obchodPipeline · FázeB
pages/ProduktyPage · Nový produktTyp produktu · Jednotka · Název · SkupinaB
pages/DovolenePage · Žádost o dovolenouTyp · Termín · Důvod vznikuB
pages/DialerPage · Nedovolal jsem seVýsledek pokusu · Poznámka (volitelná)A
acl/TransferDialog · Změnit vlastníkaowner picker + three scope switchesB

3.2 Convert first — already label-less, just not on the engine

These three render no label= at all, so they are form-less in everything but the vocabulary they use. Cheapest possible conversions and the best proof the engine is right.

  • relations/RelationDialog · Nový vztah
  • segment/AddToListDialog · Přidat do seznamu
  • company/PersonRoleDialog · Role — {{person}}

3.3 Stays a form — the label carries the meaning

Not debt. Converting these would remove information.

segment/ManageSegmentDialog (37 fields) · accounting/PostDocumentDialog (13) · orders/NewOrderForm (11) · accounting/AccountingEntityDialog (10) · clen-tymu/PayrollSection — Založit pracovní vztah (9) · accounting/InvoicePaymentDialog (8) · mail/ConnectMailboxPopup (8) · accounting/ChartAccountDialogs (6) · products/PricingSections — tariffs (5) · accounting/VatSubmissionsSection (5) · accounting/AccountingPeriodDialogs · accounting/InvoiceCancelDialog.

3.4 Neither — not a create/edit of one record

The rule does not reach these and should not be stretched to: mail/ComposeForm (its own family), MergeDialog (own canon record), mail/AutomatedMailPreviewDialog and mail/EmailReader (previews), deal/use-stage-move (a confirm), MeetingDetailPage (a rich-text editor), meetings/PlaudPickerDialog (a picker), assistant/AssistantPrompts. The remaining page files (CompanyRecordPage, OrderDetailPage, ProduktDetailPage, settings/VztahyTab, PayrollRunsPage, deal/DealRail, record/ProcessList, products/MeetingProductsSection, settings/PersonalTemplatesGovernanceSection) appear in the inventory only because of the per-file counting described in §2.

4. Findings in the design system

A1 — The RSVP dot has no accessible name (significant)

EntityPicker.tsx:191 renders the state as a bare <span class="eref-dot"> carrying title and aria-label. A span with no role maps to role="generic", which does not support naming, so a conformant screen reader drops the label. The state then reaches assistive technology only when the neighbouring .eref-meta text happens to be rendered; otherwise colour is the sole carrier. Fix: role="img" on the dot, or visually-hidden text.

A2 — Two of the four RSVP tones fail 3:1 (tools/ds-contrast.mjs)

On --surface-100, light / dark: green 3.10 / 5.50 · rose 3.81 / 4.47 · grey 5.74 / 2.97 · amber 2.14 / 7.96. Tentative in the light theme and waiting in the dark theme are below the non-text minimum for a mark that carries meaning — and per A1 the mark is currently the only carrier.

A3 — The availability day cannot be operated from the keyboard

MeetingAvailability.tsx:233: .fla-track picks a time from onClick and is marked role="presentation", so there is no keyboard path and the axis is hidden from assistive technology entirely. Mitigated — the same time can be typed into the when-row — so it is a limit, not a blocker. Same class of defect as the rail's mouse-only collapse (Records & data, A1) and the table's mouse-only sort (A2): the third instance, which makes it a systemic gap rather than three oversights.

A3b — One icon carries two different questions (tools/ds-formless-rows.mjs)

In shape B the icon is the row's only label, and the card's own rule says it must be unambiguous on its own. Reading both dialogs out of the live render shows it is not, twice:

  • link — the Provázání row (deal, project) and the video-call link row. Resolved 9 Sep 2026: the link stopped being a question of its own and became a second value of the type row (.fl-vals), because it follows from the type — an online meeting has a link. link now names only Provázání, and the edit dialog asks 7 questions instead of 8.
  • users — the Účastníci row and the meeting type Schůzka osobně. Resolved 9 Sep 2026: the in-person type took coffee, so users names only the attendees. The glyph was added through ds-icons.allowlist.txt and generated from @tabler/icons — the generator touched that one icon and left the other 348 alone. The change went in at the source (MEETING_KINDS), so the row, its menu and the listings all follow, and the two hand-written building overrides in the two dialogs are gone.

Both are now closed, and neither was an engine defect — each was a glyph choice that contradicted the rule the canon record introduces. One consequence is worth recording: a calendar event carries type: 'meeting' for an in-person and an online meeting (the backend separates only calls, calendar.service.ts:355), so the listing cannot tell them apart from the type alone. It now asks eventIcon(e), which reads the meet flag — in-person coffee, online video, matching what the dialog shows for the same type. Laid out visually at /preview/formless-otazky.html.

Decided 9 Sep 2026: the Typy událostí filter keeps users for its Schůzky row. The product owner's reasoning is the general form of this whole finding — "the filter cuts a different axis": the dialog asks what a single meeting is (in person / a call / online), while the filter switches whole categories of event on and off (meetings / calls / tasks). A coffee cup on the filter row would claim it filters in-person meetings only. So the rule is not "one glyph, one place in the app" — it is one glyph, one meaning within one axis; the same glyph may legitimately appear on two axes that never sit side by side.

A4 — Shape A is three engines

.tk-* (task-dialog.css), .dl-* (deal-dialog.css), .nd-* + .dlg-title (note-dialog.css, dialog-doc.css) draw the same anatomy three times, and MetaChip is declared twice in TSX. This is wave 0 and it is the precondition for calling shape A canon at all — the canon record deliberately documents shape B in full and shape A as a named shape awaiting the merge.

A5 — --fg-muted at 2.85:1 is load-bearing here

The pattern's only labelling is an icon plus a placeholder, both --fg-muted, which is 2.85:1 in the light theme. The token is a system-wide limit rather than this pattern's choice, but nowhere else does it carry a field's identity. Worth a decision at the token level, not per dialog.

5. The tree moved during the review

Between the first measurement (fbead4d8e) and this one (06fb6b236) three merges landed, one of which (c212e241b) deleted a dialog this review would have listed as a conversion candidate and replaced it with the canonical planner. The form-less engine and both shape-B dialogs are byte-identical across that range, so §2's shape counts and the anatomy in the canon record are unaffected.

6. Waves

wavecontentwhy in this order
0 · engineshape A onto .fl-*, one shared MetaChip; task, deal and note keep their appearance (visual baseline guards it)until this lands, "form-less" names two engines and the rule cannot be enforced by a validator
1 · already label-less§3.2 — RelationDialog, AddToListDialog, PersonRoleDialogno design decisions left in them; they prove the engine on three screens for the price of markup
2 · meetings and records§3.1 rows for InstantMeetingDialog, ContactLogDialog, ProjectEntryDialog, DialerPagethe same domain as the pattern's origin; the reader already knows the shape from the meeting dialog
3 · the rest of §3.1NewProjectDialog, BulkCreateDealDialog, Nový produkt, Žádost o dovolenou, TransferDialogeach needs one product decision (which questions survive), so they should not ride in a mechanical wave
defectsA1 and A3 with wave 0; A2 and A5 are token decisions for the ADA1 is a one-line fix and it is shipped in production today

7. Decisions for the AD

  • [ ] the rule in §1 as canon, in the wording of docs/dialog/formless-dialog.usage.md
  • [ ] §3.3 confirmed as permanently forms (so the next wave does not come for them)
  • [ ] wave 0 (one engine for shape A) as the next piece of work
  • [ ] A2 and A5: retune --ic-amber, --ic-grey and --fg-muted, or accept them as known limits
  • [x] A3b: closed 9 Sep — both collisions fixed; the Schůzky filter keeps users (a different axis)

Source: docs/REVIEW-form-dialogy.md · GitLab