@commercetools/nimbus-mcp 2.11.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/data/docs/route-manifest.json +1421 -605
  2. package/data/docs/routes/components-data-display-card.json +71 -5
  3. package/data/docs/routes/components-data-display-data-table.json +66 -34
  4. package/data/docs/routes/components-data-display-draggable-list.json +62 -7
  5. package/data/docs/routes/components-feedback-toast.json +1 -1
  6. package/data/docs/routes/components-inputs-checkbox.json +2 -2
  7. package/data/docs/routes/components-layout-defaultpage.json +4 -4
  8. package/data/docs/routes/components-layout-modalpage.json +4 -4
  9. package/data/docs/routes/components-layout-scrollarea.json +3 -3
  10. package/data/docs/routes/components-layout-splitter.json +654 -0
  11. package/data/docs/routes/components-media-avatar.json +24 -2
  12. package/data/docs/routes/components-utilities-region.json +265 -0
  13. package/data/docs/routes/home-getting-started-bundler-plugins.json +248 -0
  14. package/data/docs/routes/hooks-usedraganddrop.json +310 -0
  15. package/data/docs/routes/patterns-actions-form-action-bar.json +412 -0
  16. package/data/docs/routes/patterns-actions.json +78 -0
  17. package/data/docs/routes/patterns-dialogs-confirmation-dialog.json +391 -0
  18. package/data/docs/routes/patterns-dialogs-form-dialog.json +358 -0
  19. package/data/docs/routes/patterns-dialogs-info-dialog.json +315 -0
  20. package/data/docs/routes/patterns-dialogs.json +78 -0
  21. package/data/docs/routes/patterns-pages-public-page-layout.json +371 -0
  22. package/data/docs/routes/patterns-pages.json +78 -0
  23. package/data/docs/search-index.json +1 -1
  24. package/data/docs/types/AccordionContent.json +32 -32
  25. package/data/docs/types/AccordionHeader.json +102 -102
  26. package/data/docs/types/AccordionItem.json +28 -28
  27. package/data/docs/types/AccordionRoot.json +15 -15
  28. package/data/docs/types/AlertDescription.json +8 -8
  29. package/data/docs/types/AlertDismissButton.json +89 -89
  30. package/data/docs/types/AlertTitle.json +8 -8
  31. package/data/docs/types/Avatar.json +8 -8
  32. package/data/docs/types/Badge.json +2 -2
  33. package/data/docs/types/Body.json +6 -6
  34. package/data/docs/types/Box.json +6 -6
  35. package/data/docs/types/Button.json +97 -97
  36. package/data/docs/types/Calendar.json +111 -65
  37. package/data/docs/types/Caption.json +6 -6
  38. package/data/docs/types/Card.json +1 -1
  39. package/data/docs/types/{CardContent.json → CardBody.json} +2 -2
  40. package/data/docs/types/CardFooter.json +27 -0
  41. package/data/docs/types/CardRoot.json +18 -48
  42. package/data/docs/types/Cell.json +20 -20
  43. package/data/docs/types/Checkbox.json +99 -99
  44. package/data/docs/types/Code.json +10 -10
  45. package/data/docs/types/CollapsibleMotionContent.json +2 -2
  46. package/data/docs/types/CollapsibleMotionRoot.json +2 -2
  47. package/data/docs/types/CollapsibleMotionTrigger.json +4 -4
  48. package/data/docs/types/Column.json +8 -8
  49. package/data/docs/types/ColumnGroup.json +8 -8
  50. package/data/docs/types/ColumnHeader.json +18 -18
  51. package/data/docs/types/ComboBoxListBox.json +80 -80
  52. package/data/docs/types/ComboBoxOption.json +77 -77
  53. package/data/docs/types/ComboBoxPopover.json +77 -77
  54. package/data/docs/types/ComboBoxRoot.json +8 -8
  55. package/data/docs/types/ComboBoxSection.json +29 -29
  56. package/data/docs/types/ComboBoxTrigger.json +6 -6
  57. package/data/docs/types/ConfirmationDialog.json +224 -0
  58. package/data/docs/types/Content.json +2 -2
  59. package/data/docs/types/DataTable.json +17 -2
  60. package/data/docs/types/DataTableBody.json +24 -24
  61. package/data/docs/types/DataTableHeader.json +31 -31
  62. package/data/docs/types/DataTableRoot.json +17 -2
  63. package/data/docs/types/DataTableTable.json +35 -20
  64. package/data/docs/types/DateInput.json +84 -84
  65. package/data/docs/types/DatePicker.json +65 -65
  66. package/data/docs/types/DateRangePicker.json +99 -99
  67. package/data/docs/types/DateRangePickerField.json +99 -99
  68. package/data/docs/types/DefaultPageBackLink.json +16 -16
  69. package/data/docs/types/DefaultPageRoot.json +2 -2
  70. package/data/docs/types/DialogCloseTrigger.json +87 -87
  71. package/data/docs/types/DialogTrigger.json +2 -2
  72. package/data/docs/types/DragAndDropItemData.json +9 -0
  73. package/data/docs/types/DragAndDropProps.json +9 -0
  74. package/data/docs/types/DraggableListField.json +159 -70
  75. package/data/docs/types/DraggableListItem.json +63 -63
  76. package/data/docs/types/DraggableListRoot.json +159 -70
  77. package/data/docs/types/DrawerCloseTrigger.json +87 -87
  78. package/data/docs/types/DrawerTrigger.json +2 -2
  79. package/data/docs/types/FieldErrors.json +2 -2
  80. package/data/docs/types/Flex.json +22 -22
  81. package/data/docs/types/Footer.json +6 -6
  82. package/data/docs/types/FormActionBar.json +200 -0
  83. package/data/docs/types/FormDialog.json +198 -0
  84. package/data/docs/types/FormFieldRoot.json +2 -2
  85. package/data/docs/types/Grid.json +24 -24
  86. package/data/docs/types/Group.json +12 -12
  87. package/data/docs/types/Header.json +6 -6
  88. package/data/docs/types/Heading.json +8 -8
  89. package/data/docs/types/Icon.json +4 -4
  90. package/data/docs/types/IconButton.json +97 -97
  91. package/data/docs/types/IconToggleButton.json +84 -84
  92. package/data/docs/types/Image.json +30 -30
  93. package/data/docs/types/Indicator.json +6 -6
  94. package/data/docs/types/InfoDialog.json +104 -0
  95. package/data/docs/types/InlineSvg.json +2 -2
  96. package/data/docs/types/Item.json +6 -6
  97. package/data/docs/types/Kbd.json +8 -8
  98. package/data/docs/types/Link.json +31 -31
  99. package/data/docs/types/ListIndicator.json +6 -6
  100. package/data/docs/types/ListItem.json +6 -6
  101. package/data/docs/types/ListRoot.json +10 -10
  102. package/data/docs/types/LoadingSpinner.json +2 -2
  103. package/data/docs/types/MakeElementFocusable.json +19 -19
  104. package/data/docs/types/MenuItem.json +75 -75
  105. package/data/docs/types/MenuRoot.json +63 -63
  106. package/data/docs/types/MenuSection.json +35 -54
  107. package/data/docs/types/MenuSubmenuTrigger.json +5 -5
  108. package/data/docs/types/MenuTrigger.json +102 -102
  109. package/data/docs/types/MultilineTextInput.json +134 -134
  110. package/data/docs/types/MultilineTextInputField.json +131 -131
  111. package/data/docs/types/NumberInput.json +100 -100
  112. package/data/docs/types/NumberInputField.json +95 -95
  113. package/data/docs/types/PageContentColumn.json +6 -6
  114. package/data/docs/types/PageContentRoot.json +6 -6
  115. package/data/docs/types/PasswordInput.json +129 -129
  116. package/data/docs/types/PasswordInputField.json +129 -129
  117. package/data/docs/types/ProgressBar.json +14 -14
  118. package/data/docs/types/PublicPageLayout.json +99 -0
  119. package/data/docs/types/RadioInputOption.json +64 -64
  120. package/data/docs/types/RadioInputRoot.json +55 -55
  121. package/data/docs/types/RangeCalendar.json +90 -71
  122. package/data/docs/types/Region.json +114 -0
  123. package/data/docs/types/RegionProvider.json +25 -0
  124. package/data/docs/types/RegionTarget.json +112 -0
  125. package/data/docs/types/RichTextInput.json +2 -2
  126. package/data/docs/types/Root.json +10 -10
  127. package/data/docs/types/Row.json +6 -6
  128. package/data/docs/types/SPLITTER_SIZE_TOKENS.json +9 -0
  129. package/data/docs/types/ScrollArea.json +6 -6
  130. package/data/docs/types/SearchInput.json +136 -136
  131. package/data/docs/types/SearchInputField.json +131 -131
  132. package/data/docs/types/SelectOption.json +66 -66
  133. package/data/docs/types/SelectOptionGroup.json +22 -22
  134. package/data/docs/types/SelectOptions.json +74 -74
  135. package/data/docs/types/SelectRoot.json +102 -102
  136. package/data/docs/types/Separator.json +4 -4
  137. package/data/docs/types/SimpleGrid.json +28 -28
  138. package/data/docs/types/SplitButton.json +12 -12
  139. package/data/docs/types/Splitter.json +12 -0
  140. package/data/docs/types/SplitterAside.json +27 -0
  141. package/data/docs/types/SplitterHandle.json +27 -0
  142. package/data/docs/types/SplitterMain.json +27 -0
  143. package/data/docs/types/SplitterRoot.json +271 -0
  144. package/data/docs/types/SplitterSizeToken.json +9 -0
  145. package/data/docs/types/Stack.json +2 -2
  146. package/data/docs/types/StepsNextTrigger.json +2 -2
  147. package/data/docs/types/StepsPrevTrigger.json +2 -2
  148. package/data/docs/types/StepsRoot.json +2 -2
  149. package/data/docs/types/StepsTrigger.json +2 -2
  150. package/data/docs/types/Switch.json +38 -38
  151. package/data/docs/types/TabNavItem.json +18 -18
  152. package/data/docs/types/TabNavRoot.json +2 -2
  153. package/data/docs/types/TableBody.json +6 -6
  154. package/data/docs/types/TableCaption.json +6 -6
  155. package/data/docs/types/TableCell.json +20 -20
  156. package/data/docs/types/TableColumn.json +8 -8
  157. package/data/docs/types/TableColumnGroup.json +8 -8
  158. package/data/docs/types/TableColumnHeader.json +18 -18
  159. package/data/docs/types/TableFooter.json +6 -6
  160. package/data/docs/types/TableHeader.json +6 -6
  161. package/data/docs/types/TableRoot.json +32 -32
  162. package/data/docs/types/TableRow.json +6 -6
  163. package/data/docs/types/TableScrollArea.json +6 -6
  164. package/data/docs/types/TabsTab.json +2 -2
  165. package/data/docs/types/TagGroupRoot.json +27 -27
  166. package/data/docs/types/TagGroupTag.json +68 -68
  167. package/data/docs/types/TagGroupTagList.json +18 -18
  168. package/data/docs/types/Text.json +8 -8
  169. package/data/docs/types/TextInput.json +132 -132
  170. package/data/docs/types/TextInputField.json +129 -129
  171. package/data/docs/types/TimeInput.json +78 -78
  172. package/data/docs/types/ToggleButton.json +86 -86
  173. package/data/docs/types/ToggleButtonGroupButton.json +33 -33
  174. package/data/docs/types/ToggleButtonGroupRoot.json +20 -20
  175. package/data/docs/types/Toolbar.json +12 -12
  176. package/data/docs/types/TooltipContent.json +31 -31
  177. package/data/docs/types/TooltipRoot.json +18 -18
  178. package/data/docs/types/Trigger.json +4 -4
  179. package/data/docs/types/UseDragAndDropOptions.json +9 -0
  180. package/data/docs/types/VisuallyHidden.json +7 -7
  181. package/data/docs/types/createArrayHandlers.json +12 -0
  182. package/data/docs/types/createItemsFromCsvDrop.json +833 -0
  183. package/data/docs/types/createItemsFromDirectoryDrop.json +833 -0
  184. package/data/docs/types/createItemsFromFileDrop.json +833 -0
  185. package/data/docs/types/createItemsFromImageDrop.json +833 -0
  186. package/data/docs/types/createItemsFromJsonDrop.json +833 -0
  187. package/data/docs/types/createItemsFromTextDrop.json +12 -0
  188. package/data/docs/types/createListDataHandlers.json +102 -0
  189. package/data/docs/types/manifest.json +32 -2
  190. package/data/docs/types/toast.json +0 -15
  191. package/data/docs/types/useDragAndDrop.json +174 -0
  192. package/data/docs/types/useRegion.json +1052 -0
  193. package/data/docs/types/useResponsiveSplitterSizes.json +143 -0
  194. package/package.json +7 -7
@@ -0,0 +1,358 @@
1
+ {
2
+ "meta": {
3
+ "id": "Patterns-FormDialog",
4
+ "title": "Form dialog",
5
+ "exportName": "FormDialog",
6
+ "description": "A pre-composed save/cancel dialog with a flat, opinionated API for hosting a form in a modal.",
7
+ "order": 999,
8
+ "repoPath": "packages/nimbus/src/patterns/dialogs/form-dialog/form-dialog.mdx",
9
+ "menu": [
10
+ "Patterns",
11
+ "Dialogs",
12
+ "Form dialog"
13
+ ],
14
+ "route": "patterns/dialogs/form-dialog",
15
+ "tags": [
16
+ "component",
17
+ "pattern",
18
+ "dialog",
19
+ "FormDialog"
20
+ ],
21
+ "toc": [
22
+ {
23
+ "value": "Overview",
24
+ "href": "#overview",
25
+ "depth": 2,
26
+ "numbering": [
27
+ 1,
28
+ 1
29
+ ],
30
+ "parent": "root"
31
+ },
32
+ {
33
+ "value": "When to use",
34
+ "href": "#when-to-use",
35
+ "depth": 3,
36
+ "numbering": [
37
+ 1,
38
+ 1,
39
+ 1
40
+ ],
41
+ "parent": "root"
42
+ },
43
+ {
44
+ "value": "When not to use",
45
+ "href": "#when-not-to-use",
46
+ "depth": 3,
47
+ "numbering": [
48
+ 1,
49
+ 1,
50
+ 2
51
+ ],
52
+ "parent": "root"
53
+ },
54
+ {
55
+ "value": "Resources",
56
+ "href": "#resources",
57
+ "depth": 3,
58
+ "numbering": [
59
+ 1,
60
+ 1,
61
+ 3
62
+ ],
63
+ "parent": "root"
64
+ }
65
+ ],
66
+ "layout": "app-frame",
67
+ "tabs": [
68
+ {
69
+ "key": "overview",
70
+ "title": "Overview",
71
+ "order": 0
72
+ },
73
+ {
74
+ "key": "dev",
75
+ "title": "Implementation",
76
+ "order": 3
77
+ }
78
+ ]
79
+ },
80
+ "mdx": "\n## Overview\n\nFormDialog is a pattern component that wraps the Dialog primitive plus two Buttons in a flat, opinionated API for the most common form-in-a-modal shape: a title, form content, two action callbacks, and optional state props. It is the recommended replacement for the Merchant Center Application Kit's `FormDialog`.\n\nThe pattern exposes a small, flat set of props — `title`, `children`, `onSave`, `onCancel`, plus optional state, label, and accessibility props — and delegates everything else (sizing, stacking, portaling, focus management, button styling) to the underlying `Dialog` and `Button` primitives.\n\n### When to use\n\n- Hosting an editable form inside a modal (create / edit a single entity, configure settings, edit a profile)\n- Any scenario that needs a save/cancel pair with an in-flight loading state and a unified cancel route across the cancel button and ambient dismiss affordances\n- Replacing app-kit's `FormDialog` during the migration\n\n### When not to use\n\n- Read-only informational dialogs without action buttons — use the `InfoDialog` pattern\n- Confirm/cancel dialogs without an editable form (e.g. \"Are you sure you want to delete this?\") — use the `ConfirmationDialog` pattern, which adds a destructive intent variant\n- Any scenario that needs a non-default size, custom dismissability behaviour, per-button `data-*` attributes, or a separate `onClose`/`onCancel` distinction — drop down to `Dialog` directly (see the escape hatch in the developer documentation)\n\n### Resources\n\nFor detailed guidance on the underlying primitives, consult the component guidelines:\n\n- [Dialog Guidelines](/components/feedback/dialog) — Variants, dismissability, sizing, placement, accessibility\n- [Button Guidelines](/components/forms/button) — Variants, color palettes, loading states\n- [Storybook](https://nimbus-storybook.vercel.app/?path=/docs/patterns-dialogs-formdialog--docs)\n",
81
+ "views": {
82
+ "overview": {
83
+ "mdx": "\n## Overview\n\nFormDialog is a pattern component that wraps the Dialog primitive plus two Buttons in a flat, opinionated API for the most common form-in-a-modal shape: a title, form content, two action callbacks, and optional state props. It is the recommended replacement for the Merchant Center Application Kit's `FormDialog`.\n\nThe pattern exposes a small, flat set of props — `title`, `children`, `onSave`, `onCancel`, plus optional state, label, and accessibility props — and delegates everything else (sizing, stacking, portaling, focus management, button styling) to the underlying `Dialog` and `Button` primitives.\n\n### When to use\n\n- Hosting an editable form inside a modal (create / edit a single entity, configure settings, edit a profile)\n- Any scenario that needs a save/cancel pair with an in-flight loading state and a unified cancel route across the cancel button and ambient dismiss affordances\n- Replacing app-kit's `FormDialog` during the migration\n\n### When not to use\n\n- Read-only informational dialogs without action buttons — use the `InfoDialog` pattern\n- Confirm/cancel dialogs without an editable form (e.g. \"Are you sure you want to delete this?\") — use the `ConfirmationDialog` pattern, which adds a destructive intent variant\n- Any scenario that needs a non-default size, custom dismissability behaviour, per-button `data-*` attributes, or a separate `onClose`/`onCancel` distinction — drop down to `Dialog` directly (see the escape hatch in the developer documentation)\n\n### Resources\n\nFor detailed guidance on the underlying primitives, consult the component guidelines:\n\n- [Dialog Guidelines](/components/feedback/dialog) — Variants, dismissability, sizing, placement, accessibility\n- [Button Guidelines](/components/forms/button) — Variants, color palettes, loading states\n- [Storybook](https://nimbus-storybook.vercel.app/?path=/docs/patterns-dialogs-formdialog--docs)\n",
84
+ "toc": [
85
+ {
86
+ "value": "Overview",
87
+ "href": "#overview",
88
+ "depth": 2,
89
+ "numbering": [
90
+ 1,
91
+ 1
92
+ ],
93
+ "parent": "root"
94
+ },
95
+ {
96
+ "value": "When to use",
97
+ "href": "#when-to-use",
98
+ "depth": 3,
99
+ "numbering": [
100
+ 1,
101
+ 1,
102
+ 1
103
+ ],
104
+ "parent": "root"
105
+ },
106
+ {
107
+ "value": "When not to use",
108
+ "href": "#when-not-to-use",
109
+ "depth": 3,
110
+ "numbering": [
111
+ 1,
112
+ 1,
113
+ 2
114
+ ],
115
+ "parent": "root"
116
+ },
117
+ {
118
+ "value": "Resources",
119
+ "href": "#resources",
120
+ "depth": 3,
121
+ "numbering": [
122
+ 1,
123
+ 1,
124
+ 3
125
+ ],
126
+ "parent": "root"
127
+ }
128
+ ]
129
+ },
130
+ "dev": {
131
+ "mdx": "\n## Getting started\n\n### Import\n\n```tsx\nimport { FormDialog, type FormDialogProps } from \"@commercetools/nimbus\";\n```\n\n### Basic usage\n\nThe simplest implementation with a string title and controlled open state:\n\n```jsx live-dev\nconst App = () => {\n const [isOpen, setIsOpen] = useState(false);\n const [name, setName] = useState(\"\");\n\n return (\n <>\n <Button onPress={() => setIsOpen(true)}>Edit display name</Button>\n <FormDialog\n title=\"Edit display name\"\n isOpen={isOpen}\n onOpenChange={setIsOpen}\n onSave={() => {\n console.log(\"saving\", name);\n setIsOpen(false);\n }}\n onCancel={() => setIsOpen(false)}\n >\n <FormField.Root>\n <FormField.Label>Display name</FormField.Label>\n <FormField.Input>\n <TextInput value={name} onChange={setName} />\n </FormField.Input>\n </FormField.Root>\n </FormDialog>\n </>\n );\n};\n```\n\n## Comparison: FormDialog vs manual Dialog composition\n\n**With FormDialog:**\n\n```tsx\n<FormDialog\n title=\"Edit profile\"\n isOpen={isOpen}\n onOpenChange={setIsOpen}\n onSave={handleSave}\n onCancel={() => setIsOpen(false)}\n isSaveDisabled={!isDirty}\n>\n <FormField.Root>\n <FormField.Label>Display name</FormField.Label>\n <FormField.Input>\n <TextInput value={name} onChange={setName} />\n </FormField.Input>\n </FormField.Root>\n</FormDialog>\n```\n\n**Manual composition:**\n\n```tsx\n<Dialog.Root isOpen={isOpen} onOpenChange={setIsOpen} isDismissable>\n <Dialog.Content>\n <Dialog.Header>\n <Dialog.Title>Edit profile</Dialog.Title>\n <Dialog.CloseTrigger />\n </Dialog.Header>\n <Dialog.Body>\n <FormField.Root>\n <FormField.Label>Display name</FormField.Label>\n <FormField.Input>\n <TextInput value={name} onChange={setName} />\n </FormField.Input>\n </FormField.Root>\n </Dialog.Body>\n <Dialog.Footer>\n <Button variant=\"outline\" onPress={() => setIsOpen(false)}>\n Cancel\n </Button>\n <Button\n variant=\"solid\"\n colorPalette=\"primary\"\n isDisabled={!isDirty}\n onPress={() => {\n handleSave();\n setIsOpen(false);\n }}\n >\n Save\n </Button>\n </Dialog.Footer>\n </Dialog.Content>\n</Dialog.Root>\n```\n\n### When to use which\n\n**Use FormDialog when:**\n\n- You need a save/cancel dialog hosting an editable form\n- You want localized default Save/Cancel labels without wiring `useIntl` boilerplate\n- You want a built-in loading lockout for async save handlers (with in-flight data-loss protection)\n- You're happy treating Escape, overlay click, and the X button as equivalent to clicking Cancel\n\n**Use Dialog directly when:**\n\n- You need a non-default size (e.g. wider for two-column form layouts)\n- You need per-button `data-*` attributes (test IDs, tracking)\n- You need to distinguish the cancel button click from ambient dismissals (Escape, overlay, X) in your handlers\n- You need a confirm/cancel dialog without an editable form — use `ConfirmationDialog` instead\n- You need a read-only informational dialog — use `InfoDialog` instead\n\n## Migrating from `merchant-center-application-kit`\n\nThe Nimbus `FormDialog` is intentionally smaller and more opinionated than the app-kit equivalent.\n\n| app-kit prop | Nimbus equivalent |\n| ------------------------------------------------------------- | -------------------------------------------------------------------------------- |\n| `isOpen`, `onClose` | `isOpen`, `onOpenChange` (close routes through `onCancel` for ambient dismissals) |\n| `title`, `aria-label` | unchanged |\n| `children` | unchanged |\n| `labelPrimary`, `labelSecondary` | `saveLabel`, `cancelLabel` |\n| `isPrimaryButtonDisabled` | `isSaveDisabled` |\n| `onCancel`, `onSecondaryButtonClick` | unified onto a single `onCancel` |\n| `onPrimaryButtonClick`, `onSubmit` | unified onto a single `onSave` |\n| `size` | dropped — drop down to `Dialog` for non-default sizes |\n| `zIndex` | dropped — Dialog primitive handles z-index stacking |\n| `dataAttributesPrimaryButton`, `dataAttributesSecondaryButton` | dropped — drop down to `Dialog` to add `data-*` attributes per button |\n| `getParentSelector` | dropped — React Aria handles portaling |\n| _no equivalent_ | new: `isSaveLoading` for async save lockout |\n| _no equivalent_ | new: `onSave` accepts `() => void \\| Promise<void>` for deferred close |\n\nThe most consequential semantic changes are:\n\n1. **`onClose` and `onCancel` are unified onto a single `onCancel`** that fires from every close path: the cancel button, Escape, overlay click, and the X button. If your app relies on the distinction, drop down to `Dialog` directly.\n2. **The pattern does NOT wrap `children` in a `<form>` element.** `onSave` is invoked from the save button's `onPress`, not from a form `submit` event. Consumers wanting Enter-to-submit semantics should wrap their fields in `<form onSubmit={handleSubmit}>` inside `children` and call `onSave` from `handleSubmit`.\n\n## Usage examples\n\n### Async save with loading lockout\n\nIf `onSave` returns a `Promise`, the dialog stays open while the promise is pending and closes automatically when it fulfills. Hold `isSaveLoading` true for the duration so the pattern can:\n\n- Show a spinner inside the save button\n- Disable both buttons\n- Suppress Escape, overlay click, and the X button (preventing accidental in-flight data loss)\n\n```jsx live-dev\nconst App = () => {\n const [isOpen, setIsOpen] = useState(false);\n const [isLoading, setIsLoading] = useState(false);\n const [name, setName] = useState(\"\");\n\n const handleSave = async () => {\n setIsLoading(true);\n try {\n await new Promise((resolve) => setTimeout(resolve, 1500));\n // Dialog closes automatically when the promise fulfills.\n } finally {\n setIsLoading(false);\n }\n };\n\n return (\n <>\n <Button onPress={() => setIsOpen(true)}>Edit profile</Button>\n <FormDialog\n title=\"Edit profile\"\n isOpen={isOpen}\n onOpenChange={setIsOpen}\n isSaveLoading={isLoading}\n onSave={handleSave}\n onCancel={() => setIsOpen(false)}\n >\n <FormField.Root>\n <FormField.Label>Display name</FormField.Label>\n <FormField.Input>\n <TextInput value={name} onChange={setName} />\n </FormField.Input>\n </FormField.Root>\n </FormDialog>\n </>\n );\n};\n```\n\nIf the promise **rejects**, the dialog stays open so the consumer can surface validation errors (e.g. server-side field errors as inline `FormField.Error`) and let the user correct and retry. Let the error propagate out of `onSave` to keep the dialog open — catching the error inside the handler resolves the promise successfully and closes the dialog.\n\nFor long-running saves, prefer closing the dialog optimistically and reporting progress via a Toast or a banner, rather than holding the dialog open with a spinner.\n\n### Disabled save\n\nUse `isSaveDisabled` to gate the save action on consumer-side validity (e.g. required form fields are empty or invalid).\n\n```jsx live-dev\nconst App = () => {\n const [isOpen, setIsOpen] = useState(false);\n const [name, setName] = useState(\"\");\n const isValid = name.trim().length > 0;\n\n return (\n <>\n <Button onPress={() => setIsOpen(true)}>New project</Button>\n <FormDialog\n title=\"Create project\"\n isOpen={isOpen}\n onOpenChange={setIsOpen}\n isSaveDisabled={!isValid}\n saveLabel=\"Create\"\n onSave={() => {\n console.log(\"creating\", name);\n setIsOpen(false);\n setName(\"\");\n }}\n onCancel={() => {\n setIsOpen(false);\n setName(\"\");\n }}\n >\n <FormField.Root>\n <FormField.Label>Project name</FormField.Label>\n <FormField.Input>\n <TextInput value={name} onChange={setName} />\n </FormField.Input>\n </FormField.Root>\n </FormDialog>\n </>\n );\n};\n```\n\n### Custom labels\n\n`saveLabel` and `cancelLabel` accept any `ReactNode`, so consumer-localized strings (e.g. from `intl.formatMessage(...)`) work directly:\n\n```tsx\n<FormDialog\n title={intl.formatMessage(messages.editProfileTitle)}\n saveLabel={intl.formatMessage(messages.editProfileSave)}\n cancelLabel={intl.formatMessage(messages.editProfileCancel)}\n isOpen={isOpen}\n onOpenChange={setIsOpen}\n onSave={handleSave}\n onCancel={() => setIsOpen(false)}\n>\n {/* form fields */}\n</FormDialog>\n```\n\n### Enter-to-submit via native `<form>`\n\nThe pattern does not wrap `children` in a native `<form>` element. To get Enter-to-submit and browser-native validation, wrap your fields in your own `<form onSubmit={...}>` and call `onSave` from the submit handler:\n\n```tsx\nconst handleSubmit = (event) => {\n event.preventDefault();\n handleSave();\n};\n\n<FormDialog\n title=\"Edit profile\"\n isOpen={isOpen}\n onOpenChange={setIsOpen}\n onSave={handleSave}\n onCancel={() => setIsOpen(false)}\n>\n <form onSubmit={handleSubmit}>\n <FormField.Root>\n <FormField.Label>Display name</FormField.Label>\n <FormField.Input>\n <TextInput value={name} onChange={setName} />\n </FormField.Input>\n </FormField.Root>\n </form>\n</FormDialog>;\n```\n\n### ReactNode title with aria-label\n\n`title` accepts any ReactNode, so you can compose a heading with an icon or badge alongside text. When the composed markup wouldn't form a meaningful accessible name, pass `aria-label`:\n\n```tsx\n<FormDialog\n title={\n <Flex alignItems=\"center\" gap=\"200\">\n <Text>Edit billing</Text>\n <Badge>Pro</Badge>\n </Flex>\n }\n aria-label=\"Edit billing\"\n isOpen={isOpen}\n onOpenChange={setIsOpen}\n onSave={handleSave}\n onCancel={() => setIsOpen(false)}\n>\n {/* form fields */}\n</FormDialog>\n```\n\n## Close affordances\n\nEvery close path other than the save button invokes `onCancel`:\n\n- **Cancel button** → `onCancel()` then `onOpenChange(false)`\n- **Save button** → `onSave()` then `onOpenChange(false)` (after the returned `Promise` fulfills, if any; rejected promises leave the dialog open). Does NOT invoke `onCancel`.\n- **Escape key** → `onCancel()` then `onOpenChange(false)`\n- **Overlay click** → `onCancel()` then `onOpenChange(false)`\n- **X button in header** → `onCancel()` then `onOpenChange(false)`\n\nWhile `isSaveLoading` is `true`, all four cancel paths are suppressed and neither callback fires.\n\n## Accessibility\n\nFormDialog inherits its accessibility guarantees from the Nimbus `Dialog` primitive:\n\n- `role=\"dialog\"` is exposed to assistive technology while open\n- Focus moves into the dialog on open and is trapped within it — typically landing on the first focusable form field\n- Focus is restored to the triggering element on close\n- When `title` is a string, the dialog's accessible name is derived from it automatically — no extra `aria-label` is required\n\nIf the `title` is a composed ReactNode, pass an explicit `aria-label` so the dialog has a meaningful accessible name.\n\n## Escape hatch\n\nIf you need a non-default size (e.g. wider for two-column forms), per-button `data-*` attributes, custom dismissability, or a separate `onClose`-vs-`onCancel` distinction, drop down to composing `Dialog` and `Button` directly:\n\n```jsx live-dev\nconst App = () => {\n const [isOpen, setIsOpen] = useState(false);\n const [name, setName] = useState(\"\");\n\n return (\n <>\n <Button onPress={() => setIsOpen(true)}>Open wide form</Button>\n <Dialog.Root\n isOpen={isOpen}\n onOpenChange={setIsOpen}\n isDismissable\n aria-label=\"Edit profile\"\n >\n <Dialog.Content size=\"lg\">\n <Dialog.Header>\n <Dialog.Title>Edit profile</Dialog.Title>\n <Dialog.CloseTrigger data-test-id=\"profile-close\" />\n </Dialog.Header>\n <Dialog.Body>\n <FormField.Root>\n <FormField.Label>Display name</FormField.Label>\n <FormField.Input>\n <TextInput value={name} onChange={setName} />\n </FormField.Input>\n </FormField.Root>\n </Dialog.Body>\n <Dialog.Footer>\n <Button\n variant=\"outline\"\n data-test-id=\"profile-cancel\"\n onPress={() => setIsOpen(false)}\n >\n Cancel\n </Button>\n <Button\n variant=\"solid\"\n colorPalette=\"primary\"\n data-test-id=\"profile-save\"\n onPress={() => setIsOpen(false)}\n >\n Save\n </Button>\n </Dialog.Footer>\n </Dialog.Content>\n </Dialog.Root>\n </>\n );\n};\n```\n\n## API reference\n\n<PropsTable id=\"FormDialog\" />\n\n## Testing your implementation\n\nThese examples demonstrate how to test your implementation when using FormDialog within your application. The component's internal behaviour is already covered by Nimbus — these tests help you verify your integration.\n\n### Basic Rendering\n\nVerify the FormDialog opens with form content and default localized button labels.\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport { userEvent } from \"@testing-library/user-event\";\nimport { useState } from \"react\";\nimport {\n FormDialog,\n FormField,\n NimbusProvider,\n TextInput,\n} from \"@commercetools/nimbus\";\n\ndescribe(\"FormDialog - Basic rendering\", () => {\n it(\"renders title, form content, and default Save/Cancel labels when isOpen is true\", () => {\n render(\n <NimbusProvider>\n <FormDialog\n title=\"Edit display name\"\n isOpen\n onSave={() => {}}\n onCancel={() => {}}\n >\n <FormField.Root>\n <FormField.Label>Display name</FormField.Label>\n <FormField.Input>\n <TextInput defaultValue=\"\" />\n </FormField.Input>\n </FormField.Root>\n </FormDialog>\n </NimbusProvider>\n );\n\n expect(\n screen.getByRole(\"heading\", { name: \"Edit display name\" })\n ).toBeInTheDocument();\n expect(screen.getByLabelText(\"Display name\")).toBeInTheDocument();\n expect(screen.getByRole(\"button\", { name: \"Save\" })).toBeInTheDocument();\n expect(screen.getByRole(\"button\", { name: \"Cancel\" })).toBeInTheDocument();\n });\n\n it(\"uses the string title as the dialog's accessible name\", () => {\n render(\n <NimbusProvider>\n <FormDialog\n title=\"Edit profile\"\n isOpen\n onSave={() => {}}\n onCancel={() => {}}\n >\n <FormField.Root>\n <FormField.Label>Display name</FormField.Label>\n <FormField.Input>\n <TextInput defaultValue=\"\" />\n </FormField.Input>\n </FormField.Root>\n </FormDialog>\n </NimbusProvider>\n );\n\n expect(screen.getByRole(\"dialog\")).toHaveAccessibleName(\"Edit profile\");\n });\n});\n```\n\n### Save and Cancel callbacks\n\nWire onSave and onCancel to consumer state to react to user choices.\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport { userEvent } from \"@testing-library/user-event\";\nimport { useState } from \"react\";\nimport {\n FormDialog,\n FormField,\n NimbusProvider,\n TextInput,\n} from \"@commercetools/nimbus\";\n\ndescribe(\"FormDialog - Save and Cancel callbacks\", () => {\n it(\"invokes onSave and closes the dialog when the save button is clicked\", async () => {\n const user = userEvent.setup();\n const handleSave = vi.fn();\n const handleCancel = vi.fn();\n\n const ControlledFormDialog = () => {\n const [isOpen, setIsOpen] = useState(true);\n return (\n <FormDialog\n title=\"Edit display name\"\n isOpen={isOpen}\n onOpenChange={setIsOpen}\n onSave={handleSave}\n onCancel={handleCancel}\n >\n <FormField.Root>\n <FormField.Label>Display name</FormField.Label>\n <FormField.Input>\n <TextInput defaultValue=\"\" />\n </FormField.Input>\n </FormField.Root>\n </FormDialog>\n );\n };\n\n render(\n <NimbusProvider>\n <ControlledFormDialog />\n </NimbusProvider>\n );\n\n await waitFor(() => expect(screen.getByRole(\"dialog\")).toBeInTheDocument());\n\n await user.click(screen.getByRole(\"button\", { name: \"Save\" }));\n\n expect(handleSave).toHaveBeenCalledTimes(1);\n expect(handleCancel).not.toHaveBeenCalled();\n await waitFor(() => {\n expect(screen.queryByRole(\"dialog\")).not.toBeInTheDocument();\n });\n });\n\n it(\"invokes onCancel and closes the dialog when the cancel button is clicked\", async () => {\n const user = userEvent.setup();\n const handleSave = vi.fn();\n const handleCancel = vi.fn();\n\n const ControlledFormDialog = () => {\n const [isOpen, setIsOpen] = useState(true);\n return (\n <FormDialog\n title=\"Edit display name\"\n isOpen={isOpen}\n onOpenChange={setIsOpen}\n onSave={handleSave}\n onCancel={handleCancel}\n >\n <FormField.Root>\n <FormField.Label>Display name</FormField.Label>\n <FormField.Input>\n <TextInput defaultValue=\"\" />\n </FormField.Input>\n </FormField.Root>\n </FormDialog>\n );\n };\n\n render(\n <NimbusProvider>\n <ControlledFormDialog />\n </NimbusProvider>\n );\n\n await waitFor(() => expect(screen.getByRole(\"dialog\")).toBeInTheDocument());\n\n await user.click(screen.getByRole(\"button\", { name: \"Cancel\" }));\n\n expect(handleCancel).toHaveBeenCalledTimes(1);\n expect(handleSave).not.toHaveBeenCalled();\n await waitFor(() => {\n expect(screen.queryByRole(\"dialog\")).not.toBeInTheDocument();\n });\n });\n});\n```\n\n### Save Disabled\n\nGate the save action behind consumer-side validity (e.g. required fields are empty).\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport { userEvent } from \"@testing-library/user-event\";\nimport { useState } from \"react\";\nimport {\n FormDialog,\n FormField,\n NimbusProvider,\n TextInput,\n} from \"@commercetools/nimbus\";\n\ndescribe(\"FormDialog - Save disabled\", () => {\n it(\"disables the save button when isSaveDisabled is true\", () => {\n render(\n <NimbusProvider>\n <FormDialog\n title=\"Edit profile\"\n isOpen\n isSaveDisabled\n onSave={() => {}}\n onCancel={() => {}}\n >\n <FormField.Root>\n <FormField.Label>Display name</FormField.Label>\n <FormField.Input>\n <TextInput defaultValue=\"\" />\n </FormField.Input>\n </FormField.Root>\n </FormDialog>\n </NimbusProvider>\n );\n\n expect(screen.getByRole(\"button\", { name: \"Save\" })).toBeDisabled();\n expect(screen.getByRole(\"button\", { name: \"Cancel\" })).toBeEnabled();\n });\n});\n```\n\n### Custom button labels\n\nOverride the default localized Save and Cancel labels via saveLabel and cancelLabel.\n\n```tsx\nimport { describe, it, expect, vi } from \"vitest\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport { userEvent } from \"@testing-library/user-event\";\nimport { useState } from \"react\";\nimport {\n FormDialog,\n FormField,\n NimbusProvider,\n TextInput,\n} from \"@commercetools/nimbus\";\n\ndescribe(\"FormDialog - Custom labels\", () => {\n it(\"renders custom saveLabel and cancelLabel when provided\", () => {\n render(\n <NimbusProvider>\n <FormDialog\n title=\"New project\"\n isOpen\n saveLabel=\"Create\"\n cancelLabel=\"Discard\"\n onSave={() => {}}\n onCancel={() => {}}\n >\n <FormField.Root>\n <FormField.Label>Project name</FormField.Label>\n <FormField.Input>\n <TextInput defaultValue=\"\" />\n </FormField.Input>\n </FormField.Root>\n </FormDialog>\n </NimbusProvider>\n );\n\n expect(screen.getByRole(\"button\", { name: \"Create\" })).toBeInTheDocument();\n expect(screen.getByRole(\"button\", { name: \"Discard\" })).toBeInTheDocument();\n });\n});\n```\n\n",
132
+ "toc": [
133
+ {
134
+ "value": "Getting started",
135
+ "href": "#getting-started",
136
+ "depth": 2,
137
+ "numbering": [
138
+ 1,
139
+ 1
140
+ ],
141
+ "parent": "root"
142
+ },
143
+ {
144
+ "value": "Import",
145
+ "href": "#import",
146
+ "depth": 3,
147
+ "numbering": [
148
+ 1,
149
+ 1,
150
+ 1
151
+ ],
152
+ "parent": "root"
153
+ },
154
+ {
155
+ "value": "Basic usage",
156
+ "href": "#basic-usage",
157
+ "depth": 3,
158
+ "numbering": [
159
+ 1,
160
+ 1,
161
+ 2
162
+ ],
163
+ "parent": "root"
164
+ },
165
+ {
166
+ "value": "Comparison: FormDialog vs manual Dialog composition",
167
+ "href": "#comparison-formdialog-vs-manual-dialog-composition",
168
+ "depth": 2,
169
+ "numbering": [
170
+ 1,
171
+ 2
172
+ ],
173
+ "parent": "root"
174
+ },
175
+ {
176
+ "value": "When to use which",
177
+ "href": "#when-to-use-which",
178
+ "depth": 3,
179
+ "numbering": [
180
+ 1,
181
+ 2,
182
+ 1
183
+ ],
184
+ "parent": "root"
185
+ },
186
+ {
187
+ "value": "Migrating from merchant-center-application-kit",
188
+ "href": "#migrating-from-merchant-center-application-kit",
189
+ "depth": 2,
190
+ "numbering": [
191
+ 1,
192
+ 3
193
+ ],
194
+ "parent": "root"
195
+ },
196
+ {
197
+ "value": "Usage examples",
198
+ "href": "#usage-examples",
199
+ "depth": 2,
200
+ "numbering": [
201
+ 1,
202
+ 4
203
+ ],
204
+ "parent": "root"
205
+ },
206
+ {
207
+ "value": "Async save with loading lockout",
208
+ "href": "#async-save-with-loading-lockout",
209
+ "depth": 3,
210
+ "numbering": [
211
+ 1,
212
+ 4,
213
+ 1
214
+ ],
215
+ "parent": "root"
216
+ },
217
+ {
218
+ "value": "Disabled save",
219
+ "href": "#disabled-save",
220
+ "depth": 3,
221
+ "numbering": [
222
+ 1,
223
+ 4,
224
+ 2
225
+ ],
226
+ "parent": "root"
227
+ },
228
+ {
229
+ "value": "Custom labels",
230
+ "href": "#custom-labels",
231
+ "depth": 3,
232
+ "numbering": [
233
+ 1,
234
+ 4,
235
+ 3
236
+ ],
237
+ "parent": "root"
238
+ },
239
+ {
240
+ "value": "Enter-to-submit via native <form>",
241
+ "href": "#enter-to-submit-via-native-form",
242
+ "depth": 3,
243
+ "numbering": [
244
+ 1,
245
+ 4,
246
+ 4
247
+ ],
248
+ "parent": "root"
249
+ },
250
+ {
251
+ "value": "ReactNode title with aria-label",
252
+ "href": "#reactnode-title-with-aria-label",
253
+ "depth": 3,
254
+ "numbering": [
255
+ 1,
256
+ 4,
257
+ 5
258
+ ],
259
+ "parent": "root"
260
+ },
261
+ {
262
+ "value": "Close affordances",
263
+ "href": "#close-affordances",
264
+ "depth": 2,
265
+ "numbering": [
266
+ 1,
267
+ 5
268
+ ],
269
+ "parent": "root"
270
+ },
271
+ {
272
+ "value": "Accessibility",
273
+ "href": "#accessibility",
274
+ "depth": 2,
275
+ "numbering": [
276
+ 1,
277
+ 6
278
+ ],
279
+ "parent": "root"
280
+ },
281
+ {
282
+ "value": "Escape hatch",
283
+ "href": "#escape-hatch",
284
+ "depth": 2,
285
+ "numbering": [
286
+ 1,
287
+ 7
288
+ ],
289
+ "parent": "root"
290
+ },
291
+ {
292
+ "value": "API reference",
293
+ "href": "#api-reference",
294
+ "depth": 2,
295
+ "numbering": [
296
+ 1,
297
+ 8
298
+ ],
299
+ "parent": "root"
300
+ },
301
+ {
302
+ "value": "Testing your implementation",
303
+ "href": "#testing-your-implementation",
304
+ "depth": 2,
305
+ "numbering": [
306
+ 1,
307
+ 9
308
+ ],
309
+ "parent": "root"
310
+ },
311
+ {
312
+ "value": "Basic Rendering",
313
+ "href": "#basic-rendering",
314
+ "depth": 3,
315
+ "numbering": [
316
+ 1,
317
+ 9,
318
+ 1
319
+ ],
320
+ "parent": "root"
321
+ },
322
+ {
323
+ "value": "Save and Cancel callbacks",
324
+ "href": "#save-and-cancel-callbacks",
325
+ "depth": 3,
326
+ "numbering": [
327
+ 1,
328
+ 9,
329
+ 2
330
+ ],
331
+ "parent": "root"
332
+ },
333
+ {
334
+ "value": "Save Disabled",
335
+ "href": "#save-disabled",
336
+ "depth": 3,
337
+ "numbering": [
338
+ 1,
339
+ 9,
340
+ 3
341
+ ],
342
+ "parent": "root"
343
+ },
344
+ {
345
+ "value": "Custom button labels",
346
+ "href": "#custom-button-labels",
347
+ "depth": 3,
348
+ "numbering": [
349
+ 1,
350
+ 9,
351
+ 4
352
+ ],
353
+ "parent": "root"
354
+ }
355
+ ]
356
+ }
357
+ }
358
+ }