git:20261014.e378299 to git:20261014.97fa750
1 added, 4 removed. Audit A to A.
---
name: cratis-components-stepper-command-dialog
description: Build a multi-step wizard dialog for a single Cratis Arc command with StepperCommandDialog and StepperPanel from Cratis Components — named steps, per-step validation, linear navigation, vertical or horizontal orientation, and pre-populated edit wizards. Use when one command gathers information across several stages or a form has too many fields to show at once. Do not use for an ordinary single-page command dialog or for a page shell.
license: MIT
---
# Cratis Components stepper command dialogs
`StepperCommandDialog` splits **one** command's form across named steps. The
user moves with Previous and Next; Submit appears on the last step once every
field across every step is valid.
## Verified product sources
| Package | Verified from |
| --- | --- |
| `@cratis/components` | its package manifest, `Source/CommandDialog/*` (`StepperCommandDialog`, `CommandStepper`, `StepperPanel`), `Documentation/StepperCommandDialog/*`, `Documentation/CommandStepper/*` |
| `@cratis/arc.react` | peer range declared by `@cratis/components` |
## Choose it over `CommandDialog` when
- the form has too many fields to show at once;
- the fields group into logical stages ("Contact → Details → Budget");
- guided linear input with per-step feedback is what the task deserves.
A four-field form is usually still an ordinary `CommandDialog`.
## Step 1 — One command for the whole wizard
Every step contributes properties to the same command instance. Define the
command on the backend and run a Debug build so the TypeScript proxy exists
before importing it.
## Step 2 — Build the dialog
```tsx
import { StepperCommandDialog, StepperPanel } from '@cratis/components/CommandDialog';
import { InputTextField, NumberField, TextAreaField } from '@cratis/components/CommandForm';
import { DialogResult, useDialogContext } from '@cratis/arc.react/dialogs';
import { CreateProject } from '../api/Projects/CreateProject';
export const CreateProjectDialog = () => {
const { closeDialog } = useDialogContext();
return (
<StepperCommandDialog<CreateProject>
command={CreateProject}
title='Create new project'
okLabel='Create'
onSuccess={() => closeDialog(DialogResult.Ok)}
onCancel={() => closeDialog(DialogResult.Cancelled)}>
<StepperPanel header='Contact info'>
<InputTextField<CreateProject> value={c => c.email} title='Contact email' type='email' />
</StepperPanel>
<StepperPanel header='Project details'>
<InputTextField<CreateProject> value={c => c.name} title='Project name' />
<TextAreaField<CreateProject> value={c => c.description} title='Description' rows={4} />
</StepperPanel>
<StepperPanel header='Budget'>
<NumberField<CreateProject> value={c => c.budget} title='Budget' />
</StepperPanel>
</StepperCommandDialog>
);
};
```
Rules:
- Give every `StepperPanel` a `header` — the house convention for a named step.
Its type is optional `ReactNode`, not a required string.
- All `CommandForm` fields inside any panel bind to the **same** command
instance through their `value` accessor.
- Each command property belongs on exactly one step.
- Next is disabled while the current step has validation errors.
- Submit appears on the last step only when every field across every step is
valid.
- `StepperCommandDialog` reads the dialog context when it is hosted by
`useDialog`, and works standalone when it is not.
## Step 3 — Open it from the page
```tsx
import { useDialog } from '@cratis/arc.react/dialogs';
const [CreateProjectWrapper, showCreateProject] = useDialog(CreateProjectDialog);
<MenuItem label='New project' icon={FaPlus} command={() => showCreateProject()} /> // icon is a component type, e.g. from 'react-icons/fa6'
<CreateProjectWrapper />
```
## Validation timing
Pass `validateOnInit` to run validation on mount so a wizard opened with
pre-populated invalid values shows its errors immediately instead of after the
first keystroke. `validateOn` (`'blur' | 'change' | 'both'`) chooses when
subsequent validation runs.
## Pre-populating an edit wizard
`initialValues` is the synchronous baseline and also the change-tracking
baseline — use it for values that must be present but are not user-entered.
`currentValues` is the reactive overlay for values that arrive late, such as
from a query.
```tsx
<StepperCommandDialog
command={UpdateProject}
title='Update project'
initialValues={{ projectId: project.id }}
currentValues={{ name: project.name, description: project.description }}>
```
Never seed a value required for validity in `onBeforeExecute` — validation runs
against the pre-transform values, so the Submit button would stay permanently
disabled.
## Orientation and labels
```tsx
<StepperCommandDialog
command={RegisterEmployee}
title='Register employee'
orientation='vertical'
okLabel='Register'
nextLabel='Continue'
previousLabel='Back'>
```
## Props
Dialog-level props:
| Prop | Type | Default |
| --- | --- | --- |
| `command` | the generated command class | **required** |
| `title` | `string` | **required** |
| `children` | `StepperPanel` elements | — |
| `visible` | `boolean` | `true` |
| `width` | `string` | `'600px'` |
| `okLabel` | `string` | `'Submit'` |
| `nextLabel` | `string` | `'Next'` |
| `previousLabel` | `string` | `'Previous'` |
| `showCancel` | `boolean` | `false` |
| `cancelLabel` | `string` | `'Cancel'` |
| `isValid` | `boolean` | extra validity gate on top of form validity |
| `onConfirm` / `onCancel` / `onClose` | close gates | — |
| `onBeforeExecute` | `(values) => values` | transformer; must return the values |
| `confirmBeforeExecute` | `(values) => boolean \| Promise<boolean>` | after validation and transformation; only `true` executes |
| `style` / `contentStyle` | `CSSProperties` | — |
| `resizable` | `boolean` | `false` — accepted but has no effect |
Stepper-level props (inherited from the stepper customization surface):
| Prop | Type | Default |
| --- | --- | --- |
| `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` |
| `headerPosition` | `'top' \| 'bottom'` | `'top'` |
| `linear` | `boolean` | `true` |
| `start` / `end` | `React.ReactNode` | extra content beside the step headers |
| `onChangeStep` | `(event: { index: number }) => void` | once per accepted move: header, Previous, Next |
Command-form props (`initialValues`, `currentValues`, `validateOn`,
`validateOnInit`, `onSuccess`, `onValidationFailure`, `onFailed`,
`autoServerValidate`, …) all apply as well. Horizontal steppers (Requires Components 4.26.3) show names below
numbers and wrap. Panels are named by their headers through
`aria-labelledby`; custom header ids are preserved.
While `confirmBeforeExecute` is pending, fields are disabled; changed command
values discard approval and skip execution. Confirm the new values again.
### Two part targets
The inherited `pt` (`StepperParts`: `root`, `list`, `step`, `header`, `number`,
`title`, `separator`, `panels`, `panel`) targets the **inner stepper**. Use
`dialogPt` (`DialogParts`) and `dialogClassName` to reach the **outer dialog**.
Getting these the wrong way round is the usual cause of a `pt` that appears to do
nothing. (`ptOptions`/`unstyled` are compatibility no-ops;
`dialogPtOptions`/`dialogUnstyled` are also deprecated no-ops.)
## Dismissal while the command runs
The dialog withdraws its close control, Escape, and backdrop dismissal while the
command is executing, so a half-submitted wizard cannot be abandoned mid-flight.
Do not add your own Cancel button to the footer — set `showCancel` if the wizard
needs one.
## Related components
`CommandStepper` (same subpath) is the stepper without the dialog chrome, for
embedding a wizard directly in a page. It takes the same `StepperPanel`
children. It reports `onException`, `onUnauthorized`, and `onFailed` (including
validation failures), and guards against double submission.
## Common mistakes
| Mistake | Fix |
| --- | --- |
| A hand-rolled Cancel button in the footer | Set `showCancel`, or rely on the header close control |
| One step per field | Group related fields; aim for two to five per step |
| The same property bound on two steps | Each property appears on exactly one step |
| A `StepperPanel` without `header` | `header` is the navigation label |
| Several panels wrapped in one fragment | A fragment counts as **one** step — give each step its own `StepperPanel` child |
| A raw control (`TextInput`, a native `<input>`) for a command value | Use a `CommandForm` field, or the value is never bound and validation never re-runs |
| Seeding a required value in `onBeforeExecute` | Use `initialValues` |
| `pt` applied expecting it to reach the dialog | `pt` targets the stepper; use `dialogPt` |
## Route near misses
- An accepted `.play` model under the model root covers the behavior, or the
- repository is opted in (the root holds a committed `.play` file (`git ls-tree -r --name-only HEAD` lists a `.play` file there, narrowed to `-- <root>` when a root is configured), or the project set
- `mcpServers.screenplay.root` in `.cratis/ai.json`; an empty directory, install
- output, an uncommitted `.play` draft or a `.play` file outside the root does not count; master definition:
- `cratis-screenplay-modeling-lifecycle`): change the model first with
+ repository is opted in (run `cratis-screenplay-modeling-lifecycle`'s `scripts/model-first-status.mjs`): change the model first with
`cratis-screenplay-event-modeling`. If the Screenplay skills are not installed,
say so and do not author `.play` from memory.
Edit code here only for infrastructure, clients, adapters, Screenplay code
attachments, or gap-fill scope (`cratis-screenplay-render-and-gap-fill`);
never edit Stage-managed output.
## Verify
- Imports come from `@cratis/components/CommandDialog` and
`@cratis/components/CommandForm`, not the root barrel.
- Every `StepperPanel` has a `header` and is a direct child.
- Every command property appears on exactly one step.
- Required values come from `initialValues`.
- `onBeforeExecute`, if present, returns the values.
- Pass-through props target the intended element.
- Lint and the TypeScript build pass.