Multi-step dialog
canonOne window that walks through steps and reshapes itself between them (width and height, on the canonical spring) instead of closing and opening another window. First case: New deal — search for the person or company first (Attio "Choose record" anatomy, with a persistent footer), then the form-less detail.
27 September 2026: the Attio rework merged (feat/acrm-322-vyber-zaznamu, main 40402b85) — DealSubjectStep gained a persistent footer (↑↓ Navigate + MorphText primary action "Select record"/"Create person"), Records/Actions sections, record type in the meta slot, and paletteWidth(phone). The window reshape moved from the 0.29s cubic-bezier curve to the canonical spring (packages/ui/src/lib/spring.ts, merged as !3957) — see the `spring` and `morph-text` subcomponents. 25 September 2026: the AD had rejected the search step as shipped (palette anatomy without header/footer) — it did not follow the brief (Attio "Choose record"); rework then went to proto/obchod-vyber-attio, now merged as above. AD approved 18 September 2026 on the prototype :3777/preview/obchod-vicekrokovy.html (ACRM-322, ACRM-323). Code: acrm !3777 (<Dialog morphKey>, dialog-morph.css, story + baseline) and !3779 (new-deal flow: DealSubjectStep, Založit osobu → NewContactDialog). Merged 18 September 2026 (!3775, !3777, !3779) — canon. 14 Oct 2026 (owner: the big palette is intentional — „v malém inline dialogu se blbě hledá“): the palette shell (PickPalette, extracted from DealSubjectStep) is the RULES §21 pattern for assigning a record — chat Přiřadit ke kontaktu, Komunikace Přiřadit k projektu, Přidat produkt, Vztahy › Přidat (acrm !5039, 0c0f21ee1). Dev check 14 Oct: 1440 = 800 px, focus in search; 390 = full height 0–844 for the §21 places; Nový obchod on phone 0–844 too (re-measured after deploy). Registry repaired 17 Oct 2026 (review F2): removed classes with no CSS definition and no TSX use on main: .gsp-cancel. Registry repaired 17 Oct 2026 (review F5): DealDialog, DealSubjectStep, NewContactDialog live in apps/web (see files), not exported by @acrm/ui; dropped from react[].
Used in 104 files · 53 areas · 120× — components/CallbackTakeoverModal, components/FilePreview, components/GlobalSearch, components/IdentityImageDialog, components/accounting, components/acl …
Import
import { Dialog } from '@acrm/ui'
Props
Dialog packages/ui/src/components/Dialog.tsx
| Name | Type | Default | Description |
|---|---|---|---|
title * | string | — | — |
icon | string | — | — |
leading | ReactNode | — | Vlastní prvek NA MÍSTĚ IKONY — úplně vlevo, před názvem. Pro voliče toho, kam záznam patří (typ úkolu; vzor Linear „New issue"): určuje význam ostatních voleb, takže stojí nad nimi, ne mezi nimi. Když je zadaný, icon se nekreslí — jsou to dvě věci na jednom místě. |
size | 'm' | 'l' | — | Kanonická třída: s 400 confirm/picker · m 750 formulář · l flexi dokument/čtení/builder. s tu schválně CHYBÍ: zákaz „confirm stavy nedostane nikdy" (viz hlavička souboru) musí říct typ, ne komentář. Jinak by stačilo jedno size="s" windowState={…} a await confirm(…) by viselo na promise, kterou nikdo nemá jak vyřešit. |
width | string | number | — | @deprecated Magické číslo — použij size. Ponecháno jen pro přechod (vlna U1–U4). Přijímá i CSS řetězec ("min(800px, 94vw)") — paleta ⌘K (GlobalSearch.tsx) potřebuje přesně tuhle míru a nesedí na žádnou příčku žebříku size ani na flexi formuli (.acrm-dialog.flex bere šířku z okna se stropem 1200px, ne min(800px, 94vw)). |
bare | boolean | — | Plášť BEZ vlastní hlavy/těla/patičky — children jde přímo do .acrm-dialog (žádné .acrm-dialog-head/.acrm-dialog-body/.acrm-dialog-foot, žádný padding navíc). Sdílené zůstává všechno OSTATNÍ: scrim, lapač fokusu, zámek scrollu, inert na pozadí, escStack (Esc zavírá jen nejvýš kreslené okno), klik na scrim mimo panel. Vznikl pro paletu ⌘K: GlobalSearch.tsx nemá klasickou hlavu s titulkem — CELÝ obsah (hledací pole, stavový pruh, scrollovací tělo, patička) je jeden vlastní grid, který si kreslí appka sama (ds/global-search.css). Vnucená .acrm-dialog-head by přidala titulkový pruh navíc a .acrm-dialog-body by obsah odsadila podruhé — obojí je vzhled, který paleta nemá mít. icon/leading/subject/headerActions/footer/bodyClass/ tall se v tomhle režimu nevykreslují (nemají kam). |
scrimZIndex | number | — | Z-index scrimu nad kanonických 80 (.acrm-dialog-scrim v ds.css). Paleta ⌘K běží na 90, aby zůstala nad ostatními popovery/dialogy (dokumentováno v ds.css u z-index vrstev overlayů), ale POD toastem/P0 takeoverem (95). Bez propu by šel jen !important v app CSS — hůř dohledatelné než hodnota u volajícího. |
scrim | boolean | true | false = okno se kreslí BEZ ztmavovacího podkladu: .acrm-dialog-scrim se vůbec nevyrenderuje a panel stojí v DOM sám. Pro okno, které JE celá obrazovka a nemá tedy co ztmavovat — paleta ⌘K na telefonu (ds/global-search.css, @media (max-width: 720px)). Všechno ostatní zůstává: aria-modal, lapač fokusu, zámek scrollu, inert na pozadí, zásobník Escape i návrat fokusu na spouštěč. Právě proto tenhle prop existuje — bez něj si paleta na mobilu kreslila .acrm-dialog ručně a nic z toho neměla. ⚠️ Bez scrimu nenese z-index nikdo — panel si ho musí vzít ve své app CSS vrstvě (paleta z-index: 90), jinak se sice vykreslí, ale klepnutí chytá stránka pod ním. Zavření klikem na podklad odpadá (není kam kliknout); Escape a křížek zůstávají. |
bodyClass | string | — | Třída navíc na .acrm-dialog-body. Dokumentová třída dialogu (poznámka) má jiný žlab i rytmus než formulářová — bez tohohle by si musela postavit vlastní plášť, což §Plášť kanonu zakazuje („žádný dialog nestaví vlastní plášť"). |
className | string | — | Třída navíc na kořenový .acrm-dialog. Role dialogu, který potřebuje srovnat plášť na svou kartu (dokud sdílený shell v ds.css nese globální drift), ji použije jako scope pro přesné míry — bez dopadu na ostatní dialogy. Neplést s bodyClass (ten cílí jen na tělo). |
onClose * | () => void | — | — |
footer | ReactNode | — | — |
tall | boolean | — | Vyšší dialog (88vh) — pro pevné míry, které chtějí víc výšky. Flexi (size="l") ho nepotřebuje: výšku bere z obsahu, pracovní okna roztáhne className="work" (F51). |
subject | ReactNode | — | PŘEDMĚT dialogu hned za názvem — čeho se ta akce týká („Sdílení · Kontakt · Jan Novák"). ⛔ Nepleť s headerActions: ty stojí u PRAVÉHO kraje mezi ovládáním okna, protože ovládají okno. Předmět naopak patří k názvu — dialog beze jména záznamu nutí uživatele hádat, co vlastně mění, a když je předmět vpravo, čte se jako další tlačítko. Dřív si tenhle tvar každý dialog vyráběl sám přes order: v app CSS; jako slot ho má stejně i dialog vedle (Sdílení × Převod vlastnictví). |
headerActions | ReactNode | — | Extra ovládání v hlavičce vlevo od zavíracího křížku (např. Stáhnout u náhledu). |
center | boolean | — | Centrovat místo docku u horní hrany — confirm „na moment vlastní obrazovku". |
morphKey | string | — | KROK VÍCEKROKOVÉHO DIALOGU (ACRM-322). Změna klíče = jiný krok v TÉMŽ okně: panel se z míry předchozího kroku plynule přetvaruje do míry nového (šířka i výška) a obsah těla se prolne. Okno se neodmountuje — scrim, fokus ani pozice v zásobníku Esc se nemění. Bez klíče (dnešní dialogy) se nic neanimuje. Tvar "<tok>:<krok>" (např. deal:subject): okno, které odejde, předá míru JEN oknu TÉHOŽ toku, které ho v tomtéž commitu nahradí (hledání → Nová osoba). Klíč bez : se animuje jen v rámci svého okna a mezi okny nic nepředává. |
children * | ReactNode | — | — |
windowState | 'expanded' | 'compact' | 'min' | — | — |
onWindowState | (s: DialogWindowState) => void | — | — |
CSS classes
Files
packages/ui/src/components/Dialog.tsxapps/web/src/ds/dialog-morph.cssapps/web/src/components/deal/DealDialog.tsxapps/web/src/components/deal/DealSubjectStep.tsxapps/web/src/ds/deal-dialog.cssapps/web/src/components/person/NewContactDialog.tsxapps/web/src/ds/dialog-morph.cssapps/web/src/ds/global-search.cssapps/web/src/ds/deal-dialog.css