Components › Overlays & dialogs › Dialog › Multi-step dialog

Multi-step dialog

canon

One 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

NameTypeDefaultDescription
title *string——
iconstring——
leadingReactNode—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.
widthstring | 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)).
bareboolean—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).
scrimZIndexnumber—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.
scrimbooleantruefalse = 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í.
bodyClassstring—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ášť").
classNamestring—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——
footerReactNode——
tallboolean—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).
subjectReactNode—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í).
headerActionsReactNode—Extra ovládání v hlavičce vlevo od zavíracího křížku (např. Stáhnout u náhledu).
centerboolean—Centrovat místo docku u horní hrany — confirm „na moment vlastní obrazovku".
morphKeystring—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

.acrm-dialog.morph.acrm-dialog.deal.pick.gsp-q.gsp-body.gsp-sec.acrm-dialog-foot

Files