@commercetools/nimbus-mcp 3.2.0 → 3.4.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 (223) hide show
  1. package/README.md +9 -1
  2. package/data/docs/route-manifest.json +1608 -143
  3. package/data/docs/routes/components-buttons-button.json +25 -2
  4. package/data/docs/routes/components-chat-chat-message-list.json +448 -0
  5. package/data/docs/routes/components-chat-chat-message.json +580 -0
  6. package/data/docs/routes/components-content-markdown.json +548 -0
  7. package/data/docs/routes/components-data-display-item-group.json +413 -0
  8. package/data/docs/routes/components-data-display-item.json +611 -0
  9. package/data/docs/routes/components-feedback-activity-indicator.json +396 -0
  10. package/data/docs/routes/components-feedback-skeleton.json +481 -0
  11. package/data/docs/routes/components-feedback-skeletoncircle.json +429 -0
  12. package/data/docs/routes/components-feedback-skeletontext.json +417 -0
  13. package/data/docs/routes/components-feedback-tooltip.json +23 -1
  14. package/data/docs/routes/components-inputs-drop-zone.json +562 -0
  15. package/data/docs/routes/components-inputs-file-trigger.json +421 -0
  16. package/data/docs/routes/components-inputs-range-slider.json +633 -0
  17. package/data/docs/routes/components-inputs-slider.json +611 -0
  18. package/data/docs/routes/components-layout-splitter.json +3 -3
  19. package/data/docs/routes/components-media-avatar.json +16 -5
  20. package/data/docs/routes/components-navigation-breadcrumbs.json +565 -0
  21. package/data/docs/routes/components-navigation-menu.json +1 -1
  22. package/data/docs/routes/components-navigation-tabnav.json +80 -14
  23. package/data/docs/routes/components-navigation-tabs.json +44 -11
  24. package/data/docs/routes/components-navigation-tree.json +445 -0
  25. package/data/docs/routes/home-getting-started-mcp-server-overview.json +2 -2
  26. package/data/docs/routes/home-getting-started-mcp-server-setup.json +2 -2
  27. package/data/docs/routes/icons.json +3 -3
  28. package/data/docs/routes/patterns-buttons-floating-action-button.json +392 -0
  29. package/data/docs/routes/patterns-buttons.json +58 -0
  30. package/data/docs/search-index.json +1 -1
  31. package/data/docs/types/AccordionContent.json +24 -24
  32. package/data/docs/types/AccordionHeader.json +88 -88
  33. package/data/docs/types/AccordionItem.json +20 -20
  34. package/data/docs/types/AccordionRoot.json +14 -14
  35. package/data/docs/types/ActivityIndicator.json +202 -0
  36. package/data/docs/types/AlertDescription.json +8 -8
  37. package/data/docs/types/AlertDismissButton.json +98 -81
  38. package/data/docs/types/AlertTitle.json +8 -8
  39. package/data/docs/types/Avatar.json +19 -2
  40. package/data/docs/types/Badge.json +2 -2
  41. package/data/docs/types/Body.json +6 -6
  42. package/data/docs/types/Box.json +6 -6
  43. package/data/docs/types/Breadcrumbs.json +12 -0
  44. package/data/docs/types/BreadcrumbsItem.json +296 -0
  45. package/data/docs/types/BreadcrumbsItemProps.json +9 -0
  46. package/data/docs/types/BreadcrumbsProps.json +9 -0
  47. package/data/docs/types/BreadcrumbsRoot.json +110 -0
  48. package/data/docs/types/Button.json +106 -89
  49. package/data/docs/types/Calendar.json +56 -56
  50. package/data/docs/types/Caption.json +6 -6
  51. package/data/docs/types/CardRoot.json +2 -2
  52. package/data/docs/types/Cell.json +6 -6
  53. package/data/docs/types/ChatMessage.json +12 -0
  54. package/data/docs/types/ChatMessageActions.json +42 -0
  55. package/data/docs/types/ChatMessageAvatar.json +140 -0
  56. package/data/docs/types/ChatMessageBody.json +42 -0
  57. package/data/docs/types/ChatMessageList.json +12 -0
  58. package/data/docs/types/ChatMessageListItem.json +42 -0
  59. package/data/docs/types/ChatMessageListRoot.json +78 -0
  60. package/data/docs/types/ChatMessageMeta.json +42 -0
  61. package/data/docs/types/ChatMessageRoot.json +119 -0
  62. package/data/docs/types/ChatMessageTyping.json +42 -0
  63. package/data/docs/types/Checkbox.json +86 -86
  64. package/data/docs/types/Code.json +11 -11
  65. package/data/docs/types/Column.json +6 -6
  66. package/data/docs/types/ColumnGroup.json +6 -6
  67. package/data/docs/types/ColumnHeader.json +6 -6
  68. package/data/docs/types/ComboBoxListBox.json +70 -70
  69. package/data/docs/types/ComboBoxOption.json +64 -64
  70. package/data/docs/types/ComboBoxPopover.json +91 -70
  71. package/data/docs/types/ComboBoxRoot.json +8 -8
  72. package/data/docs/types/ComboBoxSection.json +24 -24
  73. package/data/docs/types/ComboBoxTrigger.json +6 -6
  74. package/data/docs/types/DataTable.json +2 -2
  75. package/data/docs/types/DataTableBody.json +20 -20
  76. package/data/docs/types/DataTableHeader.json +25 -25
  77. package/data/docs/types/DataTableRoot.json +2 -2
  78. package/data/docs/types/DataTableTable.json +6 -6
  79. package/data/docs/types/DateInput.json +73 -73
  80. package/data/docs/types/DatePicker.json +58 -58
  81. package/data/docs/types/DateRangePicker.json +86 -86
  82. package/data/docs/types/DateRangePickerField.json +86 -86
  83. package/data/docs/types/DefaultPageRoot.json +2 -2
  84. package/data/docs/types/DialogCloseTrigger.json +96 -79
  85. package/data/docs/types/DraggableListField.json +88 -60
  86. package/data/docs/types/DraggableListItem.json +52 -52
  87. package/data/docs/types/DraggableListRoot.json +88 -60
  88. package/data/docs/types/DrawerCloseTrigger.json +96 -79
  89. package/data/docs/types/DropZone.json +161 -0
  90. package/data/docs/types/FieldErrors.json +2 -2
  91. package/data/docs/types/FileTrigger.json +130 -0
  92. package/data/docs/types/Flex.json +22 -22
  93. package/data/docs/types/FloatingActionButton.json +1036 -0
  94. package/data/docs/types/Footer.json +6 -6
  95. package/data/docs/types/FormActionBar.json +1 -1
  96. package/data/docs/types/FormFieldRoot.json +2 -2
  97. package/data/docs/types/Grid.json +24 -24
  98. package/data/docs/types/Group.json +12 -12
  99. package/data/docs/types/Header.json +6 -6
  100. package/data/docs/types/Heading.json +8 -8
  101. package/data/docs/types/Icon.json +4 -4
  102. package/data/docs/types/IconButton.json +106 -89
  103. package/data/docs/types/IconToggleButton.json +71 -71
  104. package/data/docs/types/Image.json +43 -43
  105. package/data/docs/types/Indicator.json +6 -6
  106. package/data/docs/types/InlineSvg.json +2 -2
  107. package/data/docs/types/Item.json +6 -6
  108. package/data/docs/types/ItemActions.json +27 -0
  109. package/data/docs/types/ItemContent.json +27 -0
  110. package/data/docs/types/ItemDescription.json +27 -0
  111. package/data/docs/types/ItemFooter.json +27 -0
  112. package/data/docs/types/ItemGroup.json +12 -0
  113. package/data/docs/types/ItemGroupRoot.json +27 -0
  114. package/data/docs/types/ItemGroupSeparator.json +27 -0
  115. package/data/docs/types/ItemHeader.json +27 -0
  116. package/data/docs/types/ItemMedia.json +56 -0
  117. package/data/docs/types/ItemRoot.json +239 -0
  118. package/data/docs/types/ItemTitle.json +27 -0
  119. package/data/docs/types/Kbd.json +8 -8
  120. package/data/docs/types/Link.json +14 -14
  121. package/data/docs/types/ListIndicator.json +6 -6
  122. package/data/docs/types/ListItem.json +6 -6
  123. package/data/docs/types/ListRoot.json +11 -11
  124. package/data/docs/types/LoadingSpinner.json +2 -2
  125. package/data/docs/types/MakeElementFocusable.json +18 -18
  126. package/data/docs/types/Markdown.json +183 -0
  127. package/data/docs/types/MenuItem.json +62 -62
  128. package/data/docs/types/MenuRoot.json +55 -55
  129. package/data/docs/types/MenuSection.json +30 -30
  130. package/data/docs/types/MenuSubmenuTrigger.json +4 -4
  131. package/data/docs/types/MenuTrigger.json +88 -88
  132. package/data/docs/types/MultilineTextInput.json +104 -104
  133. package/data/docs/types/MultilineTextInputField.json +102 -102
  134. package/data/docs/types/NumberInput.json +86 -86
  135. package/data/docs/types/NumberInputField.json +82 -82
  136. package/data/docs/types/PageContentColumn.json +6 -6
  137. package/data/docs/types/PageContentRoot.json +6 -6
  138. package/data/docs/types/PasswordInput.json +100 -100
  139. package/data/docs/types/PasswordInputField.json +100 -100
  140. package/data/docs/types/ProgressBar.json +13 -13
  141. package/data/docs/types/RadioInputOption.json +56 -56
  142. package/data/docs/types/RadioInputRoot.json +48 -48
  143. package/data/docs/types/RangeCalendar.json +58 -58
  144. package/data/docs/types/RangeSlider.json +337 -0
  145. package/data/docs/types/RangeSliderProps.json +9 -0
  146. package/data/docs/types/Region.json +6 -6
  147. package/data/docs/types/RegionTarget.json +6 -6
  148. package/data/docs/types/RichTextInput.json +2 -2
  149. package/data/docs/types/Root.json +11 -11
  150. package/data/docs/types/Row.json +6 -6
  151. package/data/docs/types/ScrollArea.json +2 -2
  152. package/data/docs/types/SearchInput.json +106 -106
  153. package/data/docs/types/SearchInputField.json +102 -102
  154. package/data/docs/types/SelectOption.json +54 -54
  155. package/data/docs/types/SelectOptionGroup.json +18 -18
  156. package/data/docs/types/SelectOptions.json +64 -64
  157. package/data/docs/types/SelectRoot.json +92 -92
  158. package/data/docs/types/Separator.json +4 -4
  159. package/data/docs/types/SimpleGrid.json +28 -28
  160. package/data/docs/types/Skeleton.json +129 -0
  161. package/data/docs/types/SkeletonCircle.json +103 -0
  162. package/data/docs/types/SkeletonCircleProps.json +9 -0
  163. package/data/docs/types/SkeletonProps.json +9 -0
  164. package/data/docs/types/SkeletonText.json +62 -0
  165. package/data/docs/types/SkeletonTextProps.json +9 -0
  166. package/data/docs/types/Slider.json +337 -0
  167. package/data/docs/types/SlidingIndicatorGeometry.json +9 -0
  168. package/data/docs/types/SlidingIndicatorRects.json +9 -0
  169. package/data/docs/types/SplitButton.json +16 -20
  170. package/data/docs/types/Stack.json +2 -2
  171. package/data/docs/types/StepsRoot.json +2 -2
  172. package/data/docs/types/Switch.json +34 -34
  173. package/data/docs/types/TabNavItem.json +5 -5
  174. package/data/docs/types/TabNavRoot.json +5 -5
  175. package/data/docs/types/TableBody.json +6 -6
  176. package/data/docs/types/TableCaption.json +6 -6
  177. package/data/docs/types/TableCell.json +6 -6
  178. package/data/docs/types/TableColumn.json +6 -6
  179. package/data/docs/types/TableColumnGroup.json +6 -6
  180. package/data/docs/types/TableColumnHeader.json +6 -6
  181. package/data/docs/types/TableFooter.json +6 -6
  182. package/data/docs/types/TableHeader.json +6 -6
  183. package/data/docs/types/TableRoot.json +19 -19
  184. package/data/docs/types/TableRow.json +6 -6
  185. package/data/docs/types/TableScrollArea.json +6 -6
  186. package/data/docs/types/TabsList.json +2 -2
  187. package/data/docs/types/TabsPanel.json +2 -2
  188. package/data/docs/types/TabsPanels.json +2 -2
  189. package/data/docs/types/TabsRoot.json +3 -3
  190. package/data/docs/types/TabsTab.json +2 -2
  191. package/data/docs/types/TagGroupRoot.json +24 -24
  192. package/data/docs/types/TagGroupTag.json +56 -56
  193. package/data/docs/types/TagGroupTagList.json +14 -14
  194. package/data/docs/types/Text.json +8 -8
  195. package/data/docs/types/TextInput.json +102 -102
  196. package/data/docs/types/TextInputField.json +100 -100
  197. package/data/docs/types/TimeInput.json +68 -68
  198. package/data/docs/types/ToggleButton.json +73 -73
  199. package/data/docs/types/ToggleButtonGroupButton.json +30 -30
  200. package/data/docs/types/ToggleButtonGroupRoot.json +20 -20
  201. package/data/docs/types/Toolbar.json +9 -9
  202. package/data/docs/types/TooltipContent.json +28 -28
  203. package/data/docs/types/TooltipRoot.json +16 -16
  204. package/data/docs/types/Tree.json +12 -0
  205. package/data/docs/types/TreeIndicator.json +828 -0
  206. package/data/docs/types/TreeItem.json +559 -0
  207. package/data/docs/types/TreeItemContent.json +27 -0
  208. package/data/docs/types/TreeRoot.json +636 -0
  209. package/data/docs/types/TreeSubTree.json +40 -0
  210. package/data/docs/types/VisuallyHidden.json +6 -6
  211. package/data/docs/types/createItemsFromCsvDrop.json +4 -4
  212. package/data/docs/types/createItemsFromDirectoryDrop.json +4 -4
  213. package/data/docs/types/createItemsFromFileDrop.json +4 -4
  214. package/data/docs/types/createItemsFromImageDrop.json +4 -4
  215. package/data/docs/types/createItemsFromJsonDrop.json +4 -4
  216. package/data/docs/types/manifest.json +50 -0
  217. package/data/docs/types/useRegion.json +2 -2
  218. package/data/docs/types/useSlidingIndicator.json +145 -0
  219. package/data/docs/types/useTree.json +267 -0
  220. package/data/tokens.json +21 -0
  221. package/data/uikit-tokens.json +894 -0
  222. package/dist/index.js +3298 -109
  223. package/package.json +8 -6
@@ -136,10 +136,10 @@
136
136
  }
137
137
  ]
138
138
  },
139
- "mdx": "\n## Overview\n\nThe Splitter lets people resize two adjacent regions by dragging the handle\nbetween them — a sidebar next to a content area, a list next to a detail view,\nan editor next to a preview. It is intentionally a **two-pane** primitive: the\nboundary is a single position, which keeps the interaction predictable and the\naccessibility model clean. Layouts with three or more regions are composed by\n**nesting** one Splitter inside another's pane.\n\n### Resources\n\nDeep dive into implementation details and access the Nimbus design library.\n\n- [Figma library](https://www.figma.com/design/gHbAJGfcrCv7f2bgzUQgHq/NIMBUS-Guidelines?node-id=1793-8790&m=dev)\n- [W3C ARIA window splitter pattern](https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/)\n\n## Variables\n\nGet familiar with the visual options.\n\n### Orientation\n\n`horizontal` (default) places the panes side by side with a vertical handle;\n`vertical` stacks them with a horizontal handle. The handle is keyboard operable\nin both orientations.\n\n```jsx live\nconst App = () => (\n <Stack direction=\"horizontal\" gap=\"400\">\n <Box height=\"240px\" width=\"320px\" borderWidth=\"25\" borderColor=\"neutral.6\">\n <Splitter.Root orientation=\"horizontal\" defaultSize={40}>\n <Splitter.Aside>\n <Box p=\"400\" bg=\"indigo.3\" h=\"100%\">\n <Text>Left</Text>\n </Box>\n </Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>\n <Box p=\"400\" bg=\"amber.3\" h=\"100%\">\n <Text>Right</Text>\n </Box>\n </Splitter.Main>\n </Splitter.Root>\n </Box>\n <Box height=\"240px\" width=\"320px\" borderWidth=\"25\" borderColor=\"neutral.6\">\n <Splitter.Root orientation=\"vertical\" defaultSize={40}>\n <Splitter.Aside>\n <Box p=\"400\" bg=\"teal.3\" h=\"100%\">\n <Text>Top</Text>\n </Box>\n </Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>\n <Box p=\"400\" bg=\"rose.3\" h=\"100%\">\n <Text>Bottom</Text>\n </Box>\n </Splitter.Main>\n </Splitter.Root>\n </Box>\n </Stack>\n);\n```\n\n### Collapse\n\nThe aside can be made collapsible so a region — usually a sidebar or panel — can\nbe hidden and brought back. Collapsing animates the boundary to the aside's\ncollapsed size: fully hidden by default, or a thin rail when a collapsed size is\nset. The freed space goes to the main pane. Surface a visible control (a button)\nto toggle it; on the handle, Enter toggles collapse and double-click restores\nthe default split.\n\n```jsx live\nconst App = () => {\n const [collapsed, setCollapsed] = useState(false);\n\n return (\n <Stack direction=\"column\" gap=\"300\">\n <Button alignSelf=\"start\" onPress={() => setCollapsed((c) => !c)}>\n {collapsed ? \"Show nav\" : \"Hide nav\"}\n </Button>\n <Box height=\"240px\" width=\"100%\" borderWidth=\"25\" borderColor=\"neutral.6\">\n <Splitter.Root\n defaultSize={30}\n minSize={12}\n maxSize={80}\n collapsible\n collapsed={collapsed}\n onCollapsedChange={setCollapsed}\n >\n <Splitter.Aside>\n <Box p=\"400\" bg=\"indigo.3\" h=\"100%\">\n <Text>Nav</Text>\n </Box>\n </Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>\n <Box p=\"400\" bg=\"amber.3\" h=\"100%\">\n <Text>Main</Text>\n </Box>\n </Splitter.Main>\n </Splitter.Root>\n </Box>\n </Stack>\n );\n};\n```\n\n## Guidelines\n\n### Best practices\n\n- **Two panes per Splitter.** Nest splitters for layouts with three or more\n regions; each nested splitter is independently focusable and announced to\n assistive technology as its own widget.\n- **Bound the aside.** Set `minSize` and `maxSize` so dragging can't shrink the\n aside — or, via `maxSize`, the main pane — below its usable content width. Pair\n the bounds with content that scrolls inside the pane.\n- **Use collapsing for chrome.** A navigation or panel the user may want hidden\n should be the `collapsible` aside; surface a visible control (a button) for the\n mouse, since double-click is reserved for restoring the default split.\n- **Persist the layout** when the split is part of the user's workflow, so it\n survives across sessions.\n\n### When to use\n\n> [!TIP]\\\n> Use when\n\n- Side-by-side content the user benefits from resizing themselves.\n- App shells that pair a flexible navigation with a main content area.\n- Master/detail views where the relative split is part of the workflow.\n\n### When not to use\n\n> [!CAUTION]\\\n> When not to use\n\n- Static two-column layouts — use a flex or grid layout directly.\n- Narrow, mobile-first viewports — dragging is awkward; use a Drawer or stack\n the regions vertically.\n- More than two peer regions in one splitter — nest splitters instead.\n",
139
+ "mdx": "\n## Overview\n\nThe Splitter lets people resize two adjacent regions by dragging the handle\nbetween them — a sidebar next to a content area, a list next to a detail view,\nan editor next to a preview. It is intentionally a **two-pane** primitive: the\nboundary is a single position, which keeps the interaction predictable and the\naccessibility model clean. Layouts with three or more regions are composed by\n**nesting** one Splitter inside another's pane.\n\n### Resources\n\nDeep dive into implementation details and access the Nimbus design library.\n\n- [Figma library](https://www.figma.com/design/gHbAJGfcrCv7f2bgzUQgHq/NIMBUS-Guidelines?node-id=1793-8790&m=dev)\n- [W3C ARIA window splitter pattern](https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/)\n\n## Variables\n\nGet familiar with the visual options.\n\n### Orientation\n\n`horizontal` (default) places the panes side by side with a vertical handle;\n`vertical` stacks them with a horizontal handle. The handle is keyboard operable\nin both orientations.\n\n```jsx live\nconst App = () => (\n <Stack direction=\"horizontal\" gap=\"400\">\n <Box height=\"240px\" width=\"320px\" borderWidth=\"25\" borderColor=\"neutral.6\">\n <Splitter.Root orientation=\"horizontal\" defaultSize={40}>\n <Splitter.Aside>\n <Box p=\"400\" bg=\"indigo.3\" h=\"100%\">\n <Text>Left</Text>\n </Box>\n </Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>\n <Box p=\"400\" bg=\"amber.3\" h=\"100%\">\n <Text>Right</Text>\n </Box>\n </Splitter.Main>\n </Splitter.Root>\n </Box>\n <Box height=\"240px\" width=\"320px\" borderWidth=\"25\" borderColor=\"neutral.6\">\n <Splitter.Root orientation=\"vertical\" defaultSize={40}>\n <Splitter.Aside>\n <Box p=\"400\" bg=\"teal.3\" h=\"100%\">\n <Text>Top</Text>\n </Box>\n </Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>\n <Box p=\"400\" bg=\"rose.3\" h=\"100%\">\n <Text>Bottom</Text>\n </Box>\n </Splitter.Main>\n </Splitter.Root>\n </Box>\n </Stack>\n);\n```\n\n### Collapse\n\nThe aside can be made collapsible so a region — usually a sidebar or panel — can\nbe hidden and brought back. Collapsing animates the boundary to the aside's\ncollapsed size: fully hidden by default, or a thin rail when a collapsed size is\nset. The freed space goes to the main pane.\n\nWhile **expanded**, the handle resizes (drag, arrows, Home/End), Enter collapses,\nand double-click restores the default split. While **collapsed**, the handle is a\n**keyboard-only toggle**: Enter expands it again. It advertises no mouse-resize\naffordances — there is no hover track and double-click does nothing — because\nresizing is locked until the aside is reopened.\n\nBecause `collapsedSize` defaults to `0` (the aside fully disappears, pinning the\nhandle to the container edge), a collapsed aside has **no visible mouse target to\nreopen it**. Always surface your own visible control — a button outside the\nsplitter that drives the `collapsed` prop (as below), or a trigger near the\ncollapsed aside — so mouse users can expand it.\n\n```jsx live\nconst App = () => {\n const [collapsed, setCollapsed] = useState(false);\n\n return (\n <Stack direction=\"column\" gap=\"300\">\n <Button alignSelf=\"start\" onPress={() => setCollapsed((c) => !c)}>\n {collapsed ? \"Show nav\" : \"Hide nav\"}\n </Button>\n <Box height=\"240px\" width=\"100%\" borderWidth=\"25\" borderColor=\"neutral.6\">\n <Splitter.Root\n defaultSize={30}\n minSize={12}\n maxSize={80}\n collapsible\n collapsed={collapsed}\n onCollapsedChange={setCollapsed}\n >\n <Splitter.Aside>\n <Box p=\"400\" bg=\"indigo.3\" h=\"100%\">\n <Text>Nav</Text>\n </Box>\n </Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>\n <Box p=\"400\" bg=\"amber.3\" h=\"100%\">\n <Text>Main</Text>\n </Box>\n </Splitter.Main>\n </Splitter.Root>\n </Box>\n </Stack>\n );\n};\n```\n\n## Guidelines\n\n### Best practices\n\n- **Two panes per Splitter.** Nest splitters for layouts with three or more\n regions; each nested splitter is independently focusable and announced to\n assistive technology as its own widget.\n- **Bound the aside.** Set `minSize` and `maxSize` so dragging can't shrink the\n aside — or, via `maxSize`, the main pane — below its usable content width. Pair\n the bounds with content that scrolls inside the pane.\n- **Use collapsing for chrome.** A navigation or panel the user may want hidden\n should be the `collapsible` aside; surface a visible control (a button) for the\n mouse, since double-click is reserved for restoring the default split.\n- **Persist the layout** when the split is part of the user's workflow, so it\n survives across sessions.\n\n### When to use\n\n> [!TIP]\\\n> Use when\n\n- Side-by-side content the user benefits from resizing themselves.\n- App shells that pair a flexible navigation with a main content area.\n- Master/detail views where the relative split is part of the workflow.\n\n### When not to use\n\n> [!CAUTION]\\\n> When not to use\n\n- Static two-column layouts — use a flex or grid layout directly.\n- Narrow, mobile-first viewports — dragging is awkward; use a Drawer or stack\n the regions vertically.\n- More than two peer regions in one splitter — nest splitters instead.\n",
140
140
  "views": {
141
141
  "overview": {
142
- "mdx": "\n## Overview\n\nThe Splitter lets people resize two adjacent regions by dragging the handle\nbetween them — a sidebar next to a content area, a list next to a detail view,\nan editor next to a preview. It is intentionally a **two-pane** primitive: the\nboundary is a single position, which keeps the interaction predictable and the\naccessibility model clean. Layouts with three or more regions are composed by\n**nesting** one Splitter inside another's pane.\n\n### Resources\n\nDeep dive into implementation details and access the Nimbus design library.\n\n- [Figma library](https://www.figma.com/design/gHbAJGfcrCv7f2bgzUQgHq/NIMBUS-Guidelines?node-id=1793-8790&m=dev)\n- [W3C ARIA window splitter pattern](https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/)\n\n## Variables\n\nGet familiar with the visual options.\n\n### Orientation\n\n`horizontal` (default) places the panes side by side with a vertical handle;\n`vertical` stacks them with a horizontal handle. The handle is keyboard operable\nin both orientations.\n\n```jsx live\nconst App = () => (\n <Stack direction=\"horizontal\" gap=\"400\">\n <Box height=\"240px\" width=\"320px\" borderWidth=\"25\" borderColor=\"neutral.6\">\n <Splitter.Root orientation=\"horizontal\" defaultSize={40}>\n <Splitter.Aside>\n <Box p=\"400\" bg=\"indigo.3\" h=\"100%\">\n <Text>Left</Text>\n </Box>\n </Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>\n <Box p=\"400\" bg=\"amber.3\" h=\"100%\">\n <Text>Right</Text>\n </Box>\n </Splitter.Main>\n </Splitter.Root>\n </Box>\n <Box height=\"240px\" width=\"320px\" borderWidth=\"25\" borderColor=\"neutral.6\">\n <Splitter.Root orientation=\"vertical\" defaultSize={40}>\n <Splitter.Aside>\n <Box p=\"400\" bg=\"teal.3\" h=\"100%\">\n <Text>Top</Text>\n </Box>\n </Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>\n <Box p=\"400\" bg=\"rose.3\" h=\"100%\">\n <Text>Bottom</Text>\n </Box>\n </Splitter.Main>\n </Splitter.Root>\n </Box>\n </Stack>\n);\n```\n\n### Collapse\n\nThe aside can be made collapsible so a region — usually a sidebar or panel — can\nbe hidden and brought back. Collapsing animates the boundary to the aside's\ncollapsed size: fully hidden by default, or a thin rail when a collapsed size is\nset. The freed space goes to the main pane. Surface a visible control (a button)\nto toggle it; on the handle, Enter toggles collapse and double-click restores\nthe default split.\n\n```jsx live\nconst App = () => {\n const [collapsed, setCollapsed] = useState(false);\n\n return (\n <Stack direction=\"column\" gap=\"300\">\n <Button alignSelf=\"start\" onPress={() => setCollapsed((c) => !c)}>\n {collapsed ? \"Show nav\" : \"Hide nav\"}\n </Button>\n <Box height=\"240px\" width=\"100%\" borderWidth=\"25\" borderColor=\"neutral.6\">\n <Splitter.Root\n defaultSize={30}\n minSize={12}\n maxSize={80}\n collapsible\n collapsed={collapsed}\n onCollapsedChange={setCollapsed}\n >\n <Splitter.Aside>\n <Box p=\"400\" bg=\"indigo.3\" h=\"100%\">\n <Text>Nav</Text>\n </Box>\n </Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>\n <Box p=\"400\" bg=\"amber.3\" h=\"100%\">\n <Text>Main</Text>\n </Box>\n </Splitter.Main>\n </Splitter.Root>\n </Box>\n </Stack>\n );\n};\n```\n\n## Guidelines\n\n### Best practices\n\n- **Two panes per Splitter.** Nest splitters for layouts with three or more\n regions; each nested splitter is independently focusable and announced to\n assistive technology as its own widget.\n- **Bound the aside.** Set `minSize` and `maxSize` so dragging can't shrink the\n aside — or, via `maxSize`, the main pane — below its usable content width. Pair\n the bounds with content that scrolls inside the pane.\n- **Use collapsing for chrome.** A navigation or panel the user may want hidden\n should be the `collapsible` aside; surface a visible control (a button) for the\n mouse, since double-click is reserved for restoring the default split.\n- **Persist the layout** when the split is part of the user's workflow, so it\n survives across sessions.\n\n### When to use\n\n> [!TIP]\\\n> Use when\n\n- Side-by-side content the user benefits from resizing themselves.\n- App shells that pair a flexible navigation with a main content area.\n- Master/detail views where the relative split is part of the workflow.\n\n### When not to use\n\n> [!CAUTION]\\\n> When not to use\n\n- Static two-column layouts — use a flex or grid layout directly.\n- Narrow, mobile-first viewports — dragging is awkward; use a Drawer or stack\n the regions vertically.\n- More than two peer regions in one splitter — nest splitters instead.\n",
142
+ "mdx": "\n## Overview\n\nThe Splitter lets people resize two adjacent regions by dragging the handle\nbetween them — a sidebar next to a content area, a list next to a detail view,\nan editor next to a preview. It is intentionally a **two-pane** primitive: the\nboundary is a single position, which keeps the interaction predictable and the\naccessibility model clean. Layouts with three or more regions are composed by\n**nesting** one Splitter inside another's pane.\n\n### Resources\n\nDeep dive into implementation details and access the Nimbus design library.\n\n- [Figma library](https://www.figma.com/design/gHbAJGfcrCv7f2bgzUQgHq/NIMBUS-Guidelines?node-id=1793-8790&m=dev)\n- [W3C ARIA window splitter pattern](https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/)\n\n## Variables\n\nGet familiar with the visual options.\n\n### Orientation\n\n`horizontal` (default) places the panes side by side with a vertical handle;\n`vertical` stacks them with a horizontal handle. The handle is keyboard operable\nin both orientations.\n\n```jsx live\nconst App = () => (\n <Stack direction=\"horizontal\" gap=\"400\">\n <Box height=\"240px\" width=\"320px\" borderWidth=\"25\" borderColor=\"neutral.6\">\n <Splitter.Root orientation=\"horizontal\" defaultSize={40}>\n <Splitter.Aside>\n <Box p=\"400\" bg=\"indigo.3\" h=\"100%\">\n <Text>Left</Text>\n </Box>\n </Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>\n <Box p=\"400\" bg=\"amber.3\" h=\"100%\">\n <Text>Right</Text>\n </Box>\n </Splitter.Main>\n </Splitter.Root>\n </Box>\n <Box height=\"240px\" width=\"320px\" borderWidth=\"25\" borderColor=\"neutral.6\">\n <Splitter.Root orientation=\"vertical\" defaultSize={40}>\n <Splitter.Aside>\n <Box p=\"400\" bg=\"teal.3\" h=\"100%\">\n <Text>Top</Text>\n </Box>\n </Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>\n <Box p=\"400\" bg=\"rose.3\" h=\"100%\">\n <Text>Bottom</Text>\n </Box>\n </Splitter.Main>\n </Splitter.Root>\n </Box>\n </Stack>\n);\n```\n\n### Collapse\n\nThe aside can be made collapsible so a region — usually a sidebar or panel — can\nbe hidden and brought back. Collapsing animates the boundary to the aside's\ncollapsed size: fully hidden by default, or a thin rail when a collapsed size is\nset. The freed space goes to the main pane.\n\nWhile **expanded**, the handle resizes (drag, arrows, Home/End), Enter collapses,\nand double-click restores the default split. While **collapsed**, the handle is a\n**keyboard-only toggle**: Enter expands it again. It advertises no mouse-resize\naffordances — there is no hover track and double-click does nothing — because\nresizing is locked until the aside is reopened.\n\nBecause `collapsedSize` defaults to `0` (the aside fully disappears, pinning the\nhandle to the container edge), a collapsed aside has **no visible mouse target to\nreopen it**. Always surface your own visible control — a button outside the\nsplitter that drives the `collapsed` prop (as below), or a trigger near the\ncollapsed aside — so mouse users can expand it.\n\n```jsx live\nconst App = () => {\n const [collapsed, setCollapsed] = useState(false);\n\n return (\n <Stack direction=\"column\" gap=\"300\">\n <Button alignSelf=\"start\" onPress={() => setCollapsed((c) => !c)}>\n {collapsed ? \"Show nav\" : \"Hide nav\"}\n </Button>\n <Box height=\"240px\" width=\"100%\" borderWidth=\"25\" borderColor=\"neutral.6\">\n <Splitter.Root\n defaultSize={30}\n minSize={12}\n maxSize={80}\n collapsible\n collapsed={collapsed}\n onCollapsedChange={setCollapsed}\n >\n <Splitter.Aside>\n <Box p=\"400\" bg=\"indigo.3\" h=\"100%\">\n <Text>Nav</Text>\n </Box>\n </Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>\n <Box p=\"400\" bg=\"amber.3\" h=\"100%\">\n <Text>Main</Text>\n </Box>\n </Splitter.Main>\n </Splitter.Root>\n </Box>\n </Stack>\n );\n};\n```\n\n## Guidelines\n\n### Best practices\n\n- **Two panes per Splitter.** Nest splitters for layouts with three or more\n regions; each nested splitter is independently focusable and announced to\n assistive technology as its own widget.\n- **Bound the aside.** Set `minSize` and `maxSize` so dragging can't shrink the\n aside — or, via `maxSize`, the main pane — below its usable content width. Pair\n the bounds with content that scrolls inside the pane.\n- **Use collapsing for chrome.** A navigation or panel the user may want hidden\n should be the `collapsible` aside; surface a visible control (a button) for the\n mouse, since double-click is reserved for restoring the default split.\n- **Persist the layout** when the split is part of the user's workflow, so it\n survives across sessions.\n\n### When to use\n\n> [!TIP]\\\n> Use when\n\n- Side-by-side content the user benefits from resizing themselves.\n- App shells that pair a flexible navigation with a main content area.\n- Master/detail views where the relative split is part of the workflow.\n\n### When not to use\n\n> [!CAUTION]\\\n> When not to use\n\n- Static two-column layouts — use a flex or grid layout directly.\n- Narrow, mobile-first viewports — dragging is awkward; use a Drawer or stack\n the regions vertically.\n- More than two peer regions in one splitter — nest splitters instead.\n",
143
143
  "toc": [
144
144
  {
145
145
  "value": "Overview",
@@ -277,7 +277,7 @@
277
277
  ]
278
278
  },
279
279
  "dev": {
280
- "mdx": "\n## Getting started\n\n### Import\n\n```tsx\nimport { Splitter, type SplitterRootProps } from \"@commercetools/nimbus\";\n```\n\n### Basic usage\n\n`Splitter.Root` wraps one `Splitter.Aside` and one `Splitter.Main` with a\n`Splitter.Handle` between them. `Splitter.Aside` is the configurable pane you\nsize; `Splitter.Main` takes the remaining space. The initial split is set with\n`defaultSize` — a single percentage that always refers to the aside (the main\npane is `100 − size`).\n\n```jsx live-dev\nconst App = () => (\n <Box height=\"xs\" width=\"100%\" borderWidth=\"25\" borderColor=\"neutral.6\">\n <Splitter.Root defaultSize={30} minSize={15} maxSize={75}>\n <Splitter.Aside>\n <Box p=\"400\" bg=\"indigo.3\" h=\"100%\">\n <Text>\n Drag the handle to resize. Tab to it, then use the arrow keys.\n </Text>\n </Box>\n </Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>\n <Box p=\"400\" bg=\"amber.3\" h=\"100%\">\n <Text>Main</Text>\n </Box>\n </Splitter.Main>\n </Splitter.Root>\n </Box>\n);\n```\n\n## Usage examples\n\n### Orientation\n\n`orientation=\"vertical\"` stacks the panes and makes ArrowUp / ArrowDown the\nactive keys. The handle's `aria-orientation` reflects this (W3C separator\nsemantics describe the boundary axis, not the layout axis).\n\n```jsx live-dev\nconst App = () => (\n <Box height=\"sm\" width=\"100%\" borderWidth=\"25\" borderColor=\"neutral.6\">\n <Splitter.Root orientation=\"vertical\" defaultSize={40}>\n <Splitter.Aside>\n <Box p=\"400\" bg=\"teal.3\" h=\"100%\">\n <Text>Aside</Text>\n </Box>\n </Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>\n <Box p=\"400\" bg=\"rose.3\" h=\"100%\">\n <Text>Main</Text>\n </Box>\n </Splitter.Main>\n </Splitter.Root>\n </Box>\n);\n```\n\n### Aside constraints\n\n`minSize` and `maxSize` bound the single aside dimension. `minSize` (default\n`0`) is the aside's floor; `maxSize` (default `100`) caps how far the aside can\ngrow — which in turn fixes the main pane's floor at `100 − maxSize`. Because\nthere is one boundary, this single `[minSize, maxSize]` window on the aside\nfully describes both sides; there is no main-specific prop.\n\n```jsx live-dev\nconst App = () => (\n <Box height=\"xs\" width=\"100%\" borderWidth=\"25\" borderColor=\"neutral.6\">\n <Splitter.Root defaultSize={30} minSize={15} maxSize={75}>\n <Splitter.Aside>\n <Box p=\"400\" bg=\"indigo.3\" h=\"100%\">\n <Text>Aside (min 15, max 75)</Text>\n </Box>\n </Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>\n <Box p=\"400\" bg=\"amber.3\" h=\"100%\">\n <Text>Main (floor 25)</Text>\n </Box>\n </Splitter.Main>\n </Splitter.Root>\n </Box>\n);\n```\n\n### Collapsible aside\n\nSet `collapsible` to let the aside collapse, and control its collapsed state\nwith the boolean `collapsed`. Because collapse is plain controlled state, any\ncontrol can drive it; Enter on the focused handle toggles it too.\n`onCollapsedChange` fires on every transition. Only the aside collapses.\n\nWhile the aside is collapsed the handle can't be resized (drag and the arrow\nkeys are inactive) — a collapsed aside sits below its `minSize`, so a resize\ncould only snap. Reopen it with the control, Enter, or a double-click; the\ncollapsed state holds until you explicitly leave it.\n\n```jsx live-dev\nconst App = () => {\n const [collapsed, setCollapsed] = useState(false);\n\n return (\n <Stack direction=\"column\" gap=\"300\">\n <Button alignSelf=\"start\" onPress={() => setCollapsed((c) => !c)}>\n {collapsed ? \"Show aside\" : \"Hide aside\"}\n </Button>\n <Box height=\"2xs\" width=\"100%\" borderWidth=\"25\" borderColor=\"neutral.6\">\n <Splitter.Root\n defaultSize={30}\n minSize={10}\n maxSize={80}\n collapsible\n collapsed={collapsed}\n onCollapsedChange={setCollapsed}\n >\n <Splitter.Aside>\n <Box p=\"400\" bg=\"indigo.3\" h=\"100%\">\n <Text>Aside</Text>\n </Box>\n </Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>\n <Box p=\"400\" bg=\"amber.3\" h=\"100%\">\n <Text>Main</Text>\n </Box>\n </Splitter.Main>\n </Splitter.Root>\n </Box>\n </Stack>\n );\n};\n```\n\n### Collapsing to a rail\n\nCollapse is a two-tier model. `minSize` is the floor for _dragging_ and the\narrow keys; `collapsedSize` is a distinct discrete state that collapse snaps to,\nbelow `minSize`. It defaults to `0` (the aside hides entirely) — set it to leave\na thin rail behind, e.g. an icon strip the user can click to expand.\n\n```jsx live-dev\nconst App = () => {\n const [collapsed, setCollapsed] = useState(false);\n\n return (\n <Stack direction=\"column\" gap=\"300\">\n <Button alignSelf=\"start\" onPress={() => setCollapsed((c) => !c)}>\n {collapsed ? \"Expand aside\" : \"Collapse aside to a rail\"}\n </Button>\n <Box height=\"2xs\" width=\"100%\" borderWidth=\"25\" borderColor=\"neutral.6\">\n <Splitter.Root\n defaultSize={30}\n minSize={15}\n maxSize={80}\n collapsible\n collapsedSize={6}\n collapsed={collapsed}\n onCollapsedChange={setCollapsed}\n >\n <Splitter.Aside>\n <Box p=\"400\" bg=\"indigo.3\" h=\"100%\">\n <Text>Aside</Text>\n </Box>\n </Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>\n <Box p=\"400\" bg=\"amber.3\" h=\"100%\">\n <Text>Main</Text>\n </Box>\n </Splitter.Main>\n </Splitter.Root>\n </Box>\n </Stack>\n );\n};\n```\n\n### Uncontrolled collapse\n\nWhen you don't need to drive collapse from outside the splitter, seed the\ninitial collapsed state with `defaultCollapsed` and let the splitter own it.\nEnter on the handle toggles it from there, and double-click restores the default\nsplit.\n\n```jsx live-dev\nconst App = () => (\n <Box height=\"3xs\" width=\"100%\" borderWidth=\"25\" borderColor=\"neutral.6\">\n <Splitter.Root\n defaultSize={30}\n minSize={10}\n maxSize={80}\n collapsible\n defaultCollapsed\n >\n <Splitter.Aside>\n <Box p=\"400\" bg=\"indigo.3\" h=\"100%\">\n <Text>Aside (starts collapsed)</Text>\n </Box>\n </Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>\n <Box p=\"400\" bg=\"amber.3\" h=\"100%\">\n <Text>Tab to the handle and press Enter to toggle</Text>\n </Box>\n </Splitter.Main>\n </Splitter.Root>\n </Box>\n);\n```\n\n### Keyboard step\n\nArrow keys move the boundary by `keyboardStep` percentage points (default `5`);\nHome / End jump it to the bounds. Accepts floats for finer control.\n\n```jsx live-dev\nconst App = () => (\n <Box height=\"3xs\" width=\"100%\" borderWidth=\"25\" borderColor=\"neutral.6\">\n <Splitter.Root defaultSize={30} keyboardStep={10}>\n <Splitter.Aside>\n <Box p=\"400\" bg=\"indigo.3\" h=\"100%\">\n <Text>\n Tab to the handle — each arrow press moves the boundary 10%\n </Text>\n </Box>\n </Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>\n <Box p=\"400\" bg=\"amber.3\" h=\"100%\">\n <Text>Main</Text>\n </Box>\n </Splitter.Main>\n </Splitter.Root>\n </Box>\n);\n```\n\n### Restoring the default split\n\nDouble-click the handle to restore the boundary to the sizes resolved on mount.\nSet `isDoubleClickDisabled` to turn that off; drag and keyboard stay active.\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"column\" gap=\"400\">\n <Box height=\"3xs\" width=\"100%\" borderWidth=\"25\" borderColor=\"neutral.6\">\n <Splitter.Root defaultSize={50}>\n <Splitter.Aside>\n <Box p=\"400\" bg=\"indigo.3\" h=\"100%\">\n <Text>Drag, then double-click the handle to restore</Text>\n </Box>\n </Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>\n <Box p=\"400\" bg=\"amber.3\" h=\"100%\" />\n </Splitter.Main>\n </Splitter.Root>\n </Box>\n <Box height=\"3xs\" width=\"100%\" borderWidth=\"25\" borderColor=\"neutral.6\">\n <Splitter.Root defaultSize={50} isDoubleClickDisabled>\n <Splitter.Aside>\n <Box p=\"400\" bg=\"teal.3\" h=\"100%\">\n <Text>Double-click restore disabled</Text>\n </Box>\n </Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>\n <Box p=\"400\" bg=\"rose.3\" h=\"100%\" />\n </Splitter.Main>\n </Splitter.Root>\n </Box>\n </Stack>\n);\n```\n\n### Disabled state\n\n`isDisabled` makes the whole splitter non-interactive: the handle leaves the tab\norder, gets `aria-disabled`, and ignores drag, keyboard, and collapse input.\n\n```jsx live-dev\nconst App = () => (\n <Box height=\"3xs\" width=\"100%\" borderWidth=\"25\" borderColor=\"neutral.6\">\n <Splitter.Root isDisabled defaultSize={30}>\n <Splitter.Aside>\n <Box p=\"400\" bg=\"indigo.3\" h=\"100%\">\n <Text>Aside</Text>\n </Box>\n </Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>\n <Box p=\"400\" bg=\"amber.3\" h=\"100%\">\n <Text>Main</Text>\n </Box>\n </Splitter.Main>\n </Splitter.Root>\n </Box>\n);\n```\n\n### Persistence\n\nSize is uncontrolled by default, so persist it in your app with any storage:\nhydrate `defaultSize` from stored state and write the settled value back in\n`onSizeChangeEnd` (it fires once per settled **resize** — drag end, each\nkeypress, double-click restore — so no debouncing is needed; collapse/expand are\nsignalled by `onCollapsedChange`, not here). A\nsingle `number` round-trips: the value `onSizeChangeEnd` emits is exactly the\nshape `defaultSize` accepts. Collapse persists through its controlled boolean\nstate.\n\n```tsx\n// `useLocalStorage` is any storage hook of your choosing.\nconst [size, setSize] = useLocalStorage(\"ide-layout\", 30);\nconst [collapsed, setCollapsed] = useLocalStorage(\"ide-collapsed\", false);\n\n<Splitter.Root\n defaultSize={size}\n onSizeChangeEnd={setSize}\n minSize={10}\n maxSize={80}\n collapsible\n collapsed={collapsed}\n onCollapsedChange={setCollapsed}\n>\n <Splitter.Aside>…</Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>…</Splitter.Main>\n</Splitter.Root>;\n```\n\n`onSizeChange` (live, every drag tick ~60Hz) is also available when you need a\nreal-time read-out; prefer `onSizeChangeEnd` for persistence.\n\n### Controlled size\n\nPass `size` instead of `defaultSize` to drive the layout from outside — useful\nfor responsive layouts that swap the proportion per breakpoint, since external\nchanges apply in place (no remount, so pane content keeps its scroll and focus).\nControl is settled, not live: drag and keyboard stay smooth from internal state\nand report once via `onSizeChangeEnd`. Wire that callback and feed the value\nback, or the splitter keeps the last interactive size and behaves as\nuncontrolled from then on.\n\n```tsx\nconst [size, setSize] = useState(30);\n\n<Splitter.Root size={size} onSizeChangeEnd={setSize}>\n <Splitter.Aside>…</Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>…</Splitter.Main>\n</Splitter.Root>;\n```\n\n### Pixel-precise sizes\n\nThe aside percentage carries full float precision end-to-end (nothing in the\nsize pipeline is rounded — only the handle's `aria-valuenow` is rounded, for\nassistive technology). To land on an exact pixel at a known container width,\npass the computed float — `250px` in an `800px` container is `31.25`.\n\n```jsx live-dev\nconst App = () => (\n <Box height=\"3xs\" width=\"100%\" borderWidth=\"25\" borderColor=\"neutral.6\">\n <Splitter.Root defaultSize={31.25}>\n <Splitter.Aside>\n <Box p=\"400\" bg=\"indigo.3\" h=\"100%\">\n <Text>31.25%</Text>\n </Box>\n </Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>\n <Box p=\"400\" bg=\"amber.3\" h=\"100%\">\n <Text>68.75%</Text>\n </Box>\n </Splitter.Main>\n </Splitter.Root>\n </Box>\n);\n```\n\n### Responsive pixel & token sizes with `useResponsiveSplitterSizes`\n\n`Splitter.Root` is percentage-native — it has no pixel code path. When you'd\nrather think in pixels (a fixed-width sidebar, an icon rail) or size per device,\nreach for the companion hook `useResponsiveSplitterSizes`. It is a **pixel/token\n→ percentage translator**: it measures the splitter's container, converts your\nconfig to the percentage the component wants, drives the controlled `size`\nchannel, and persists the result — all without the component gaining a pixel\npath.\n\nSpread its `rootProps` onto `Splitter.Root` and attach the `ref` it returns (the\n`ref` is required — the hook measures the container through it):\n\n```tsx\nconst { rootProps } = useResponsiveSplitterSizes({\n orientation: \"horizontal\",\n persistKey: \"app:main-splitter\",\n size: \"xs\", // 320px — size tokens resolve to pixels\n minSize: \"3xs\", // 224px\n maxSize: \"lg\", // 512px\n});\n\n<Splitter.Root {...rootProps} collapsible>\n <Splitter.Aside>…</Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>…</Splitter.Main>\n</Splitter.Root>;\n```\n\n**Units.** A value is a `number` (pixels), a size token (`3xs`–`8xl` or\n`breakpoint-sm`…`breakpoint-2xl`, resolving to pixels), or a `\"N%\"` string\n(passed through untranslated). Note the contrast with `Splitter.Root`'s own\n`size` / `minSize` / … props, which are **percentages**: through the hook, a\nbare number means pixels. Because the hook owns the full facade (`size` plus\n`minSize` / `maxSize` / `collapsedSize`), you don't hand-write percentages on\nthe root when you use it.\n\n**Responsive by container width.** Any dimension can be a map keyed by container\n**min-width thresholds** (pixels or tokens) — a min-width cascade resolved\nagainst the splitter's own width, not the viewport. The largest threshold `≤`\nthe measured width wins; the smallest entry also applies below it.\n\n```tsx\nconst { rootProps } = useResponsiveSplitterSizes({\n size: { 0: \"xs\", \"breakpoint-md\": \"30%\" }, // \"xs\" (320px) below 768px, 30% above\n});\n```\n\n**Persistence.** Pass a `persistKey` (and optionally a `storage` adapter,\ndefaulting to `localStorage`) and the hook stores the settled size **in pixels**\nper band, so a dragged `320px` re-pins to `320px` across reloads and container\nresizes. `collapsedSize` is static config and is never persisted; the latest\nexpanded size survives collapse and expand.\n\n### Three or more regions\n\nThe Splitter is two-pane by design. For additional regions, nest a Splitter\ninside a pane's children; each splitter owns its own state independently. The\ninner aside can sit on either side of its main pane — here it trails as a right\npanel.\n\n```jsx live-dev\nconst App = () => (\n <Box height=\"sm\" width=\"100%\" borderWidth=\"25\" borderColor=\"neutral.6\">\n <Splitter.Root defaultSize={25}>\n <Splitter.Aside>\n <Box p=\"400\" bg=\"indigo.3\" h=\"100%\">\n <Text>Aside (outer)</Text>\n </Box>\n </Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>\n <Splitter.Root defaultSize={35}>\n <Splitter.Main>\n <Box p=\"400\" bg=\"amber.3\" h=\"100%\">\n <Text>Main</Text>\n </Box>\n </Splitter.Main>\n <Splitter.Handle />\n <Splitter.Aside>\n <Box p=\"400\" bg=\"rose.3\" h=\"100%\">\n <Text>Aside (inner)</Text>\n </Box>\n </Splitter.Aside>\n </Splitter.Root>\n </Splitter.Main>\n </Splitter.Root>\n </Box>\n);\n```\n\n## Common patterns\n\n### IDE-style layout\n\nThe most common real-world composition: a persisted split (`defaultSize` +\n`onSizeChangeEnd`) with a `collapsible` aside that snaps to a rail\n(`collapsedSize`), driven by both a toolbar button and the handle, plus aside\nbounds and a specific handle label.\n\n```jsx live-dev\nconst App = () => {\n const [size, setSize] = useState(22);\n const [collapsed, setCollapsed] = useState(false);\n\n return (\n <Stack direction=\"column\" gap=\"300\">\n <Button alignSelf=\"start\" onPress={() => setCollapsed((c) => !c)}>\n {collapsed ? \"Show aside\" : \"Hide aside\"}\n </Button>\n <Box height=\"xs\" width=\"100%\" borderWidth=\"25\" borderColor=\"neutral.6\">\n <Splitter.Root\n defaultSize={size}\n onSizeChangeEnd={setSize}\n minSize={15}\n maxSize={70}\n collapsible\n collapsedSize={4}\n collapsed={collapsed}\n onCollapsedChange={setCollapsed}\n >\n <Splitter.Aside>\n <Box p=\"400\" bg=\"indigo.3\" h=\"100%\">\n <Text>Aside</Text>\n </Box>\n </Splitter.Aside>\n <Splitter.Handle aria-label=\"Resize aside\" />\n <Splitter.Main>\n <Box p=\"400\" bg=\"amber.3\" h=\"100%\">\n <Text>Aside</Text>\n </Box>\n </Splitter.Main>\n </Splitter.Root>\n </Box>\n </Stack>\n );\n};\n```\n\n### Vertical with persistence\n\nA vertical split with a persisted boundary and aside bounds so neither region\ncollapses to nothing while dragging.\n\n```jsx live-dev\nconst App = () => {\n const [size, setSize] = useState(60);\n\n return (\n <Box height=\"sm\" width=\"100%\" borderWidth=\"25\" borderColor=\"neutral.6\">\n <Splitter.Root\n orientation=\"vertical\"\n defaultSize={size}\n onSizeChangeEnd={setSize}\n minSize={25}\n maxSize={80}\n >\n <Splitter.Aside>\n <Box p=\"400\" bg=\"teal.3\" h=\"100%\">\n <Text>Aside</Text>\n </Box>\n </Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>\n <Box p=\"400\" bg=\"rose.3\" h=\"100%\">\n <Text>Main</Text>\n </Box>\n </Splitter.Main>\n </Splitter.Root>\n </Box>\n );\n};\n```\n\n## Component requirements\n\n### Structure\n\n- `Splitter.Root` must contain exactly one `Splitter.Aside` and one\n `Splitter.Main` with one `Splitter.Handle` between them; a development-time\n warning is emitted otherwise. The aside may be placed before or after the main\n pane (a leading or trailing panel) — `size` always refers to the aside.\n- All sizing and collapse configuration (`defaultSize` / `size`, `minSize`,\n `maxSize`, `collapsible`, `collapsedSize`) lives on `Splitter.Root`. Panes\n take only their content and an optional `id` for analytics/testing — nothing\n is configured on the pane itself.\n\n## Accessibility\n\nThe Splitter handles the W3C window-splitter semantics internally: the handle is\n`role=\"separator\"` with `aria-orientation`, `aria-valuenow` / `aria-valuemin` /\n`aria-valuemax`, `aria-valuetext`, and `aria-controls` pointing at the leading\npane. For the full conformance details, see the Accessibility tab.\n\n#### Labeling\n\nThe handle ships a localized default `aria-label` (\"Resize panes\"); override it\nper handle with `aria-label` or `aria-labelledby` for a more specific label:\n\n```tsx\n<Splitter.Handle aria-label={msg.format(\"resizeAside\")} />\n```\n\n#### Persistent ID\n\nIf your use case requires tracking and analytics, it is good practice to add a\n**persistent**, **unique** id to the handle:\n\n```tsx\nconst PERSISTENT_ID = \"ide-layout-splitter-handle\";\n\nexport const Example = () => (\n <Splitter.Root defaultSize={30}>\n <Splitter.Aside>…</Splitter.Aside>\n <Splitter.Handle id={PERSISTENT_ID} />\n <Splitter.Main>…</Splitter.Main>\n </Splitter.Root>\n);\n```\n\n#### Keyboard navigation\n\nWhen the handle has focus:\n\n- `Tab` / `Shift+Tab`: Move focus to/from the handle (in DOM order).\n- `ArrowLeft` / `ArrowRight` (horizontal) or `ArrowUp` / `ArrowDown` (vertical):\n Move the boundary by `keyboardStep` percentage points (inactive while the\n aside is collapsed).\n- `Home` / `End`: Jump the boundary to the aside's minimum / maximum (inactive\n while the aside is collapsed).\n- `Enter`: Toggle collapse of the aside (when `collapsible`).\n\n## API reference\n\n<PropsTable id=\"Splitter\" />\n\n## Testing your implementation\n\nThese examples demonstrate how to test your implementation when using Splitter\nin your application. As the component's internal functionality is already tested\nby Nimbus, these patterns help you verify your integration and\napplication-specific logic.\n\n### Basic Rendering\n\nMinimal Splitter — a configurable aside and a main pane —\n\n```tsx\nimport { describe, it, expect } from \"vitest\";\nimport { useState } from \"react\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { NimbusProvider, Splitter } from \"@commercetools/nimbus\";\n\ndescribe(\"Splitter - Basic rendering\", () => {\n it(\"renders an aside, a main pane, and one handle\", async () => {\n render(\n <NimbusProvider>\n <Splitter.Root defaultSize={30}>\n <Splitter.Aside>Aside</Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>Main</Splitter.Main>\n </Splitter.Root>\n </NimbusProvider>\n );\n\n const handle = await screen.findByRole(\"separator\");\n expect(handle).toBeInTheDocument();\n });\n});\n```\n\n### Persistence with any storage\n\nHydrate `defaultSize` from stored state and persist the\n\n```tsx\nimport { describe, it, expect } from \"vitest\";\nimport { useState } from \"react\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { NimbusProvider, Splitter } from \"@commercetools/nimbus\";\n\ndescribe(\"Splitter - persistence\", () => {\n it(\"hydrates from the stored size on first render\", async () => {\n // Stand-in for a `useLocalStorage`-style hook seeded from storage.\n const Demo = () => {\n const [size, setSize] = useState(25);\n return (\n <Splitter.Root\n defaultSize={size}\n onSizeChangeEnd={setSize}\n minSize={5}\n maxSize={95}\n >\n <Splitter.Aside>Aside</Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>Main</Splitter.Main>\n </Splitter.Root>\n );\n };\n\n render(\n <NimbusProvider>\n <Demo />\n </NimbusProvider>\n );\n\n const handle = await screen.findByRole(\"separator\");\n await waitFor(() => {\n expect(Number(handle.getAttribute(\"aria-valuenow\"))).toBe(25);\n });\n });\n});\n```\n\n### Controlled size from anywhere\n\nDrive the layout with the `size` prop and update it from\n\n```tsx\nimport { describe, it, expect } from \"vitest\";\nimport { useState } from \"react\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { NimbusProvider, Splitter } from \"@commercetools/nimbus\";\n\ndescribe(\"Splitter - controlled size\", () => {\n it(\"reflects an external size change in place\", async () => {\n const user = userEvent.setup();\n const Demo = () => {\n const [size, setSize] = useState(30);\n return (\n <>\n <button type=\"button\" onClick={() => setSize(60)}>\n widen-aside\n </button>\n <Splitter.Root\n size={size}\n onSizeChangeEnd={setSize}\n minSize={5}\n maxSize={95}\n >\n <Splitter.Aside>Aside</Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>Main</Splitter.Main>\n </Splitter.Root>\n </>\n );\n };\n\n render(\n <NimbusProvider>\n <Demo />\n </NimbusProvider>\n );\n\n const handle = await screen.findByRole(\"separator\");\n await waitFor(() => {\n expect(Number(handle.getAttribute(\"aria-valuenow\"))).toBe(30);\n });\n\n await user.click(screen.getByText(\"widen-aside\"));\n await waitFor(() => {\n expect(Number(handle.getAttribute(\"aria-valuenow\"))).toBe(60);\n });\n });\n});\n```\n\n### Controlled collapse from anywhere\n\nCollapse is plain controlled boolean state, so a button\n\n```tsx\nimport { describe, it, expect } from \"vitest\";\nimport { useState } from \"react\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { NimbusProvider, Splitter } from \"@commercetools/nimbus\";\n\ndescribe(\"Splitter - controlled collapse\", () => {\n it(\"collapses the aside from a button outside the subtree\", async () => {\n const user = userEvent.setup();\n const Demo = () => {\n const [collapsed, setCollapsed] = useState(false);\n return (\n <>\n <button type=\"button\" onClick={() => setCollapsed((c) => !c)}>\n toggle-aside\n </button>\n <Splitter.Root\n defaultSize={30}\n minSize={5}\n maxSize={95}\n collapsible\n collapsed={collapsed}\n onCollapsedChange={setCollapsed}\n >\n <Splitter.Aside>Aside</Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>Main</Splitter.Main>\n </Splitter.Root>\n </>\n );\n };\n\n render(\n <NimbusProvider>\n <Demo />\n </NimbusProvider>\n );\n\n await user.click(screen.getByText(\"toggle-aside\"));\n const handle = await screen.findByRole(\"separator\");\n await waitFor(() => {\n expect(Number(handle.getAttribute(\"aria-valuenow\"))).toBe(0);\n });\n });\n});\n```\n\n### Nested splitters for 3+ regions\n\nEach nested Splitter is an independent widget.\n\n```tsx\nimport { describe, it, expect } from \"vitest\";\nimport { useState } from \"react\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { NimbusProvider, Splitter } from \"@commercetools/nimbus\";\n\ndescribe(\"Splitter - Nested\", () => {\n it(\"nests inside a pane to express three regions\", async () => {\n render(\n <NimbusProvider>\n <Splitter.Root defaultSize={25}>\n <Splitter.Aside>Aside</Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>\n <Splitter.Root defaultSize={35}>\n <Splitter.Main>Main</Splitter.Main>\n <Splitter.Handle />\n <Splitter.Aside>Aside</Splitter.Aside>\n </Splitter.Root>\n </Splitter.Main>\n </Splitter.Root>\n </NimbusProvider>\n );\n\n const handles = await screen.findAllByRole(\"separator\");\n expect(handles).toHaveLength(2);\n });\n});\n```\n\n\n## Resources\n\n- [Storybook](https://nimbus-storybook.vercel.app/?path=/docs/components-splitter--docs)\n- [React Aria useSeparator](https://react-spectrum.adobe.com/react-aria/useSeparator.html)\n- [W3C ARIA window splitter pattern](https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/)\n",
280
+ "mdx": "\n## Getting started\n\n### Import\n\n```tsx\nimport { Splitter, type SplitterRootProps } from \"@commercetools/nimbus\";\n```\n\n### Basic usage\n\n`Splitter.Root` wraps one `Splitter.Aside` and one `Splitter.Main` with a\n`Splitter.Handle` between them. `Splitter.Aside` is the configurable pane you\nsize; `Splitter.Main` takes the remaining space. The initial split is set with\n`defaultSize` — a single percentage that always refers to the aside (the main\npane is `100 − size`).\n\n```jsx live-dev\nconst App = () => (\n <Box height=\"xs\" width=\"100%\" borderWidth=\"25\" borderColor=\"neutral.6\">\n <Splitter.Root defaultSize={30} minSize={15} maxSize={75}>\n <Splitter.Aside>\n <Box p=\"400\" bg=\"indigo.3\" h=\"100%\">\n <Text>\n Drag the handle to resize. Tab to it, then use the arrow keys.\n </Text>\n </Box>\n </Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>\n <Box p=\"400\" bg=\"amber.3\" h=\"100%\">\n <Text>Main</Text>\n </Box>\n </Splitter.Main>\n </Splitter.Root>\n </Box>\n);\n```\n\n## Usage examples\n\n### Orientation\n\n`orientation=\"vertical\"` stacks the panes and makes ArrowUp / ArrowDown the\nactive keys. The handle's `aria-orientation` reflects this (W3C separator\nsemantics describe the boundary axis, not the layout axis).\n\n```jsx live-dev\nconst App = () => (\n <Box height=\"sm\" width=\"100%\" borderWidth=\"25\" borderColor=\"neutral.6\">\n <Splitter.Root orientation=\"vertical\" defaultSize={40}>\n <Splitter.Aside>\n <Box p=\"400\" bg=\"teal.3\" h=\"100%\">\n <Text>Aside</Text>\n </Box>\n </Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>\n <Box p=\"400\" bg=\"rose.3\" h=\"100%\">\n <Text>Main</Text>\n </Box>\n </Splitter.Main>\n </Splitter.Root>\n </Box>\n);\n```\n\n### Aside constraints\n\n`minSize` and `maxSize` bound the single aside dimension. `minSize` (default\n`0`) is the aside's floor; `maxSize` (default `100`) caps how far the aside can\ngrow — which in turn fixes the main pane's floor at `100 − maxSize`. Because\nthere is one boundary, this single `[minSize, maxSize]` window on the aside\nfully describes both sides; there is no main-specific prop.\n\n```jsx live-dev\nconst App = () => (\n <Box height=\"xs\" width=\"100%\" borderWidth=\"25\" borderColor=\"neutral.6\">\n <Splitter.Root defaultSize={30} minSize={15} maxSize={75}>\n <Splitter.Aside>\n <Box p=\"400\" bg=\"indigo.3\" h=\"100%\">\n <Text>Aside (min 15, max 75)</Text>\n </Box>\n </Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>\n <Box p=\"400\" bg=\"amber.3\" h=\"100%\">\n <Text>Main (floor 25)</Text>\n </Box>\n </Splitter.Main>\n </Splitter.Root>\n </Box>\n);\n```\n\n### Collapsible aside\n\nSet `collapsible` to let the aside collapse, and control its collapsed state\nwith the boolean `collapsed`. Because collapse is plain controlled state, any\ncontrol can drive it; Enter on the focused handle toggles it too.\n`onCollapsedChange` fires on every transition. Only the aside collapses.\n\nWhile the aside is collapsed the handle can't be resized (drag and the arrow\nkeys are inactive) — a collapsed aside sits below its `minSize`, so a resize\ncould only snap. The collapsed handle is **keyboard-only**: it advertises no\nmouse-resize affordances (no hover track, and double-click does nothing), but\n**Enter** still expands it. Reopen it with Enter or your own control; the\ncollapsed state holds until you explicitly leave it. Since `collapsedSize`\ndefaults to `0` (the aside fully disappears), provide a visible control — a\nbutton driving `collapsed`, or a trigger near the collapsed aside — so mouse\nusers have a way to reopen it.\n\n```jsx live-dev\nconst App = () => {\n const [collapsed, setCollapsed] = useState(false);\n\n return (\n <Stack direction=\"column\" gap=\"300\">\n <Button alignSelf=\"start\" onPress={() => setCollapsed((c) => !c)}>\n {collapsed ? \"Show aside\" : \"Hide aside\"}\n </Button>\n <Box height=\"2xs\" width=\"100%\" borderWidth=\"25\" borderColor=\"neutral.6\">\n <Splitter.Root\n defaultSize={30}\n minSize={10}\n maxSize={80}\n collapsible\n collapsed={collapsed}\n onCollapsedChange={setCollapsed}\n >\n <Splitter.Aside>\n <Box p=\"400\" bg=\"indigo.3\" h=\"100%\">\n <Text>Aside</Text>\n </Box>\n </Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>\n <Box p=\"400\" bg=\"amber.3\" h=\"100%\">\n <Text>Main</Text>\n </Box>\n </Splitter.Main>\n </Splitter.Root>\n </Box>\n </Stack>\n );\n};\n```\n\n### Collapsing to a rail\n\nCollapse is a two-tier model. `minSize` is the floor for _dragging_ and the\narrow keys; `collapsedSize` is a distinct discrete state that collapse snaps to,\nbelow `minSize`. It defaults to `0` (the aside hides entirely) — set it to leave\na thin rail behind, e.g. an icon strip the user can click to expand.\n\n```jsx live-dev\nconst App = () => {\n const [collapsed, setCollapsed] = useState(false);\n\n return (\n <Stack direction=\"column\" gap=\"300\">\n <Button alignSelf=\"start\" onPress={() => setCollapsed((c) => !c)}>\n {collapsed ? \"Expand aside\" : \"Collapse aside to a rail\"}\n </Button>\n <Box height=\"2xs\" width=\"100%\" borderWidth=\"25\" borderColor=\"neutral.6\">\n <Splitter.Root\n defaultSize={30}\n minSize={15}\n maxSize={80}\n collapsible\n collapsedSize={6}\n collapsed={collapsed}\n onCollapsedChange={setCollapsed}\n >\n <Splitter.Aside>\n <Box p=\"400\" bg=\"indigo.3\" h=\"100%\">\n <Text>Aside</Text>\n </Box>\n </Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>\n <Box p=\"400\" bg=\"amber.3\" h=\"100%\">\n <Text>Main</Text>\n </Box>\n </Splitter.Main>\n </Splitter.Root>\n </Box>\n </Stack>\n );\n};\n```\n\n### Uncontrolled collapse\n\nWhen you don't need to drive collapse from outside the splitter, seed the\ninitial collapsed state with `defaultCollapsed` and let the splitter own it.\nEnter on the handle toggles it from there. While expanded, double-click restores\nthe default split; while collapsed, the handle is keyboard-only (Enter to\nexpand) and double-click does nothing.\n\n```jsx live-dev\nconst App = () => (\n <Box height=\"3xs\" width=\"100%\" borderWidth=\"25\" borderColor=\"neutral.6\">\n <Splitter.Root\n defaultSize={30}\n minSize={10}\n maxSize={80}\n collapsible\n defaultCollapsed\n >\n <Splitter.Aside>\n <Box p=\"400\" bg=\"indigo.3\" h=\"100%\">\n <Text>Aside (starts collapsed)</Text>\n </Box>\n </Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>\n <Box p=\"400\" bg=\"amber.3\" h=\"100%\">\n <Text>Tab to the handle and press Enter to toggle</Text>\n </Box>\n </Splitter.Main>\n </Splitter.Root>\n </Box>\n);\n```\n\n### Keyboard step\n\nArrow keys move the boundary by `keyboardStep` percentage points (default `5`);\nHome / End jump it to the bounds. Accepts floats for finer control.\n\n```jsx live-dev\nconst App = () => (\n <Box height=\"3xs\" width=\"100%\" borderWidth=\"25\" borderColor=\"neutral.6\">\n <Splitter.Root defaultSize={30} keyboardStep={10}>\n <Splitter.Aside>\n <Box p=\"400\" bg=\"indigo.3\" h=\"100%\">\n <Text>\n Tab to the handle — each arrow press moves the boundary 10%\n </Text>\n </Box>\n </Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>\n <Box p=\"400\" bg=\"amber.3\" h=\"100%\">\n <Text>Main</Text>\n </Box>\n </Splitter.Main>\n </Splitter.Root>\n </Box>\n);\n```\n\n### Restoring the default split\n\nDouble-click the handle to restore the boundary to the sizes resolved on mount.\nSet `isDoubleClickDisabled` to turn that off; drag and keyboard stay active.\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"column\" gap=\"400\">\n <Box height=\"3xs\" width=\"100%\" borderWidth=\"25\" borderColor=\"neutral.6\">\n <Splitter.Root defaultSize={50}>\n <Splitter.Aside>\n <Box p=\"400\" bg=\"indigo.3\" h=\"100%\">\n <Text>Drag, then double-click the handle to restore</Text>\n </Box>\n </Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>\n <Box p=\"400\" bg=\"amber.3\" h=\"100%\" />\n </Splitter.Main>\n </Splitter.Root>\n </Box>\n <Box height=\"3xs\" width=\"100%\" borderWidth=\"25\" borderColor=\"neutral.6\">\n <Splitter.Root defaultSize={50} isDoubleClickDisabled>\n <Splitter.Aside>\n <Box p=\"400\" bg=\"teal.3\" h=\"100%\">\n <Text>Double-click restore disabled</Text>\n </Box>\n </Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>\n <Box p=\"400\" bg=\"rose.3\" h=\"100%\" />\n </Splitter.Main>\n </Splitter.Root>\n </Box>\n </Stack>\n);\n```\n\n### Disabled state\n\n`isDisabled` makes the whole splitter non-interactive: the handle leaves the tab\norder, gets `aria-disabled`, and ignores drag, keyboard, and collapse input.\n\n```jsx live-dev\nconst App = () => (\n <Box height=\"3xs\" width=\"100%\" borderWidth=\"25\" borderColor=\"neutral.6\">\n <Splitter.Root isDisabled defaultSize={30}>\n <Splitter.Aside>\n <Box p=\"400\" bg=\"indigo.3\" h=\"100%\">\n <Text>Aside</Text>\n </Box>\n </Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>\n <Box p=\"400\" bg=\"amber.3\" h=\"100%\">\n <Text>Main</Text>\n </Box>\n </Splitter.Main>\n </Splitter.Root>\n </Box>\n);\n```\n\n### Persistence\n\nSize is uncontrolled by default, so persist it in your app with any storage:\nhydrate `defaultSize` from stored state and write the settled value back in\n`onSizeChangeEnd` (it fires once per settled **resize** — drag end, each\nkeypress, double-click restore — so no debouncing is needed; collapse/expand are\nsignalled by `onCollapsedChange`, not here). A\nsingle `number` round-trips: the value `onSizeChangeEnd` emits is exactly the\nshape `defaultSize` accepts. Collapse persists through its controlled boolean\nstate.\n\n```tsx\n// `useLocalStorage` is any storage hook of your choosing.\nconst [size, setSize] = useLocalStorage(\"ide-layout\", 30);\nconst [collapsed, setCollapsed] = useLocalStorage(\"ide-collapsed\", false);\n\n<Splitter.Root\n defaultSize={size}\n onSizeChangeEnd={setSize}\n minSize={10}\n maxSize={80}\n collapsible\n collapsed={collapsed}\n onCollapsedChange={setCollapsed}\n>\n <Splitter.Aside>…</Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>…</Splitter.Main>\n</Splitter.Root>;\n```\n\n`onSizeChange` (live, every drag tick ~60Hz) is also available when you need a\nreal-time read-out; prefer `onSizeChangeEnd` for persistence.\n\n### Controlled size\n\nPass `size` instead of `defaultSize` to drive the layout from outside — useful\nfor responsive layouts that swap the proportion per breakpoint, since external\nchanges apply in place (no remount, so pane content keeps its scroll and focus).\nControl is settled, not live: drag and keyboard stay smooth from internal state\nand report once via `onSizeChangeEnd`. Wire that callback and feed the value\nback, or the splitter keeps the last interactive size and behaves as\nuncontrolled from then on.\n\n```tsx\nconst [size, setSize] = useState(30);\n\n<Splitter.Root size={size} onSizeChangeEnd={setSize}>\n <Splitter.Aside>…</Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>…</Splitter.Main>\n</Splitter.Root>;\n```\n\n### Pixel-precise sizes\n\nThe aside percentage carries full float precision end-to-end (nothing in the\nsize pipeline is rounded — only the handle's `aria-valuenow` is rounded, for\nassistive technology). To land on an exact pixel at a known container width,\npass the computed float — `250px` in an `800px` container is `31.25`.\n\n```jsx live-dev\nconst App = () => (\n <Box height=\"3xs\" width=\"100%\" borderWidth=\"25\" borderColor=\"neutral.6\">\n <Splitter.Root defaultSize={31.25}>\n <Splitter.Aside>\n <Box p=\"400\" bg=\"indigo.3\" h=\"100%\">\n <Text>31.25%</Text>\n </Box>\n </Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>\n <Box p=\"400\" bg=\"amber.3\" h=\"100%\">\n <Text>68.75%</Text>\n </Box>\n </Splitter.Main>\n </Splitter.Root>\n </Box>\n);\n```\n\n### Responsive pixel & token sizes with `useResponsiveSplitterSizes`\n\n`Splitter.Root` is percentage-native — it has no pixel code path. When you'd\nrather think in pixels (a fixed-width sidebar, an icon rail) or size per device,\nreach for the companion hook `useResponsiveSplitterSizes`. It is a **pixel/token\n→ percentage translator**: it measures the splitter's container, converts your\nconfig to the percentage the component wants, drives the controlled `size`\nchannel, and persists the result — all without the component gaining a pixel\npath.\n\nSpread its `rootProps` onto `Splitter.Root` and attach the `ref` it returns (the\n`ref` is required — the hook measures the container through it):\n\n```tsx\nconst { rootProps } = useResponsiveSplitterSizes({\n orientation: \"horizontal\",\n persistKey: \"app:main-splitter\",\n size: \"xs\", // 320px — size tokens resolve to pixels\n minSize: \"3xs\", // 224px\n maxSize: \"lg\", // 512px\n});\n\n<Splitter.Root {...rootProps} collapsible>\n <Splitter.Aside>…</Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>…</Splitter.Main>\n</Splitter.Root>;\n```\n\n**Units.** A value is a `number` (pixels), a size token (`3xs`–`8xl` or\n`breakpoint-sm`…`breakpoint-2xl`, resolving to pixels), or a `\"N%\"` string\n(passed through untranslated). Note the contrast with `Splitter.Root`'s own\n`size` / `minSize` / … props, which are **percentages**: through the hook, a\nbare number means pixels. Because the hook owns the full facade (`size` plus\n`minSize` / `maxSize` / `collapsedSize`), you don't hand-write percentages on\nthe root when you use it.\n\n**Responsive by container width.** Any dimension can be a map keyed by container\n**min-width thresholds** (pixels or tokens) — a min-width cascade resolved\nagainst the splitter's own width, not the viewport. The largest threshold `≤`\nthe measured width wins; the smallest entry also applies below it.\n\n```tsx\nconst { rootProps } = useResponsiveSplitterSizes({\n size: { 0: \"xs\", \"breakpoint-md\": \"30%\" }, // \"xs\" (320px) below 768px, 30% above\n});\n```\n\n**Persistence.** Pass a `persistKey` (and optionally a `storage` adapter,\ndefaulting to `localStorage`) and the hook stores the settled size **in pixels**\nper band, so a dragged `320px` re-pins to `320px` across reloads and container\nresizes. `collapsedSize` is static config and is never persisted; the latest\nexpanded size survives collapse and expand.\n\n### Three or more regions\n\nThe Splitter is two-pane by design. For additional regions, nest a Splitter\ninside a pane's children; each splitter owns its own state independently. The\ninner aside can sit on either side of its main pane — here it trails as a right\npanel.\n\n```jsx live-dev\nconst App = () => (\n <Box height=\"sm\" width=\"100%\" borderWidth=\"25\" borderColor=\"neutral.6\">\n <Splitter.Root defaultSize={25}>\n <Splitter.Aside>\n <Box p=\"400\" bg=\"indigo.3\" h=\"100%\">\n <Text>Aside (outer)</Text>\n </Box>\n </Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>\n <Splitter.Root defaultSize={35}>\n <Splitter.Main>\n <Box p=\"400\" bg=\"amber.3\" h=\"100%\">\n <Text>Main</Text>\n </Box>\n </Splitter.Main>\n <Splitter.Handle />\n <Splitter.Aside>\n <Box p=\"400\" bg=\"rose.3\" h=\"100%\">\n <Text>Aside (inner)</Text>\n </Box>\n </Splitter.Aside>\n </Splitter.Root>\n </Splitter.Main>\n </Splitter.Root>\n </Box>\n);\n```\n\n## Common patterns\n\n### IDE-style layout\n\nThe most common real-world composition: a persisted split (`defaultSize` +\n`onSizeChangeEnd`) with a `collapsible` aside that snaps to a rail\n(`collapsedSize`), driven by both a toolbar button and the handle, plus aside\nbounds and a specific handle label.\n\n```jsx live-dev\nconst App = () => {\n const [size, setSize] = useState(22);\n const [collapsed, setCollapsed] = useState(false);\n\n return (\n <Stack direction=\"column\" gap=\"300\">\n <Button alignSelf=\"start\" onPress={() => setCollapsed((c) => !c)}>\n {collapsed ? \"Show aside\" : \"Hide aside\"}\n </Button>\n <Box height=\"xs\" width=\"100%\" borderWidth=\"25\" borderColor=\"neutral.6\">\n <Splitter.Root\n defaultSize={size}\n onSizeChangeEnd={setSize}\n minSize={15}\n maxSize={70}\n collapsible\n collapsedSize={4}\n collapsed={collapsed}\n onCollapsedChange={setCollapsed}\n >\n <Splitter.Aside>\n <Box p=\"400\" bg=\"indigo.3\" h=\"100%\">\n <Text>Aside</Text>\n </Box>\n </Splitter.Aside>\n <Splitter.Handle aria-label=\"Resize aside\" />\n <Splitter.Main>\n <Box p=\"400\" bg=\"amber.3\" h=\"100%\">\n <Text>Aside</Text>\n </Box>\n </Splitter.Main>\n </Splitter.Root>\n </Box>\n </Stack>\n );\n};\n```\n\n### Vertical with persistence\n\nA vertical split with a persisted boundary and aside bounds so neither region\ncollapses to nothing while dragging.\n\n```jsx live-dev\nconst App = () => {\n const [size, setSize] = useState(60);\n\n return (\n <Box height=\"sm\" width=\"100%\" borderWidth=\"25\" borderColor=\"neutral.6\">\n <Splitter.Root\n orientation=\"vertical\"\n defaultSize={size}\n onSizeChangeEnd={setSize}\n minSize={25}\n maxSize={80}\n >\n <Splitter.Aside>\n <Box p=\"400\" bg=\"teal.3\" h=\"100%\">\n <Text>Aside</Text>\n </Box>\n </Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>\n <Box p=\"400\" bg=\"rose.3\" h=\"100%\">\n <Text>Main</Text>\n </Box>\n </Splitter.Main>\n </Splitter.Root>\n </Box>\n );\n};\n```\n\n## Component requirements\n\n### Structure\n\n- `Splitter.Root` must contain exactly one `Splitter.Aside` and one\n `Splitter.Main` with one `Splitter.Handle` between them; a development-time\n warning is emitted otherwise. The aside may be placed before or after the main\n pane (a leading or trailing panel) — `size` always refers to the aside.\n- All sizing and collapse configuration (`defaultSize` / `size`, `minSize`,\n `maxSize`, `collapsible`, `collapsedSize`) lives on `Splitter.Root`. Panes\n take only their content and an optional `id` for analytics/testing — nothing\n is configured on the pane itself.\n\n## Accessibility\n\nThe Splitter handles the W3C window-splitter semantics internally: the handle is\n`role=\"separator\"` with `aria-orientation`, `aria-valuenow` / `aria-valuemin` /\n`aria-valuemax`, `aria-valuetext`, and `aria-controls` pointing at the leading\npane. For the full conformance details, see the Accessibility tab.\n\n#### Labeling\n\nThe handle ships a localized default `aria-label` (\"Resize panes\"); override it\nper handle with `aria-label` or `aria-labelledby` for a more specific label:\n\n```tsx\n<Splitter.Handle aria-label={msg.format(\"resizeAside\")} />\n```\n\n#### Persistent ID\n\nIf your use case requires tracking and analytics, it is good practice to add a\n**persistent**, **unique** id to the handle:\n\n```tsx\nconst PERSISTENT_ID = \"ide-layout-splitter-handle\";\n\nexport const Example = () => (\n <Splitter.Root defaultSize={30}>\n <Splitter.Aside>…</Splitter.Aside>\n <Splitter.Handle id={PERSISTENT_ID} />\n <Splitter.Main>…</Splitter.Main>\n </Splitter.Root>\n);\n```\n\n#### Keyboard navigation\n\nWhen the handle has focus:\n\n- `Tab` / `Shift+Tab`: Move focus to/from the handle (in DOM order).\n- `ArrowLeft` / `ArrowRight` (horizontal) or `ArrowUp` / `ArrowDown` (vertical):\n Move the boundary by `keyboardStep` percentage points (inactive while the\n aside is collapsed).\n- `Home` / `End`: Jump the boundary to the aside's minimum / maximum (inactive\n while the aside is collapsed).\n- `Enter`: Toggle collapse of the aside (when `collapsible`).\n\n## API reference\n\n<PropsTable id=\"Splitter\" />\n\n## Testing your implementation\n\nThese examples demonstrate how to test your implementation when using Splitter\nin your application. As the component's internal functionality is already tested\nby Nimbus, these patterns help you verify your integration and\napplication-specific logic.\n\n### Basic Rendering\n\nMinimal Splitter — a configurable aside and a main pane —\n\n```tsx\nimport { describe, it, expect } from \"vitest\";\nimport { useState } from \"react\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { NimbusProvider, Splitter } from \"@commercetools/nimbus\";\n\ndescribe(\"Splitter - Basic rendering\", () => {\n it(\"renders an aside, a main pane, and one handle\", async () => {\n render(\n <NimbusProvider>\n <Splitter.Root defaultSize={30}>\n <Splitter.Aside>Aside</Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>Main</Splitter.Main>\n </Splitter.Root>\n </NimbusProvider>\n );\n\n const handle = await screen.findByRole(\"separator\");\n expect(handle).toBeInTheDocument();\n });\n});\n```\n\n### Persistence with any storage\n\nHydrate `defaultSize` from stored state and persist the\n\n```tsx\nimport { describe, it, expect } from \"vitest\";\nimport { useState } from \"react\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { NimbusProvider, Splitter } from \"@commercetools/nimbus\";\n\ndescribe(\"Splitter - persistence\", () => {\n it(\"hydrates from the stored size on first render\", async () => {\n // Stand-in for a `useLocalStorage`-style hook seeded from storage.\n const Demo = () => {\n const [size, setSize] = useState(25);\n return (\n <Splitter.Root\n defaultSize={size}\n onSizeChangeEnd={setSize}\n minSize={5}\n maxSize={95}\n >\n <Splitter.Aside>Aside</Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>Main</Splitter.Main>\n </Splitter.Root>\n );\n };\n\n render(\n <NimbusProvider>\n <Demo />\n </NimbusProvider>\n );\n\n const handle = await screen.findByRole(\"separator\");\n await waitFor(() => {\n expect(Number(handle.getAttribute(\"aria-valuenow\"))).toBe(25);\n });\n });\n});\n```\n\n### Controlled size from anywhere\n\nDrive the layout with the `size` prop and update it from\n\n```tsx\nimport { describe, it, expect } from \"vitest\";\nimport { useState } from \"react\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { NimbusProvider, Splitter } from \"@commercetools/nimbus\";\n\ndescribe(\"Splitter - controlled size\", () => {\n it(\"reflects an external size change in place\", async () => {\n const user = userEvent.setup();\n const Demo = () => {\n const [size, setSize] = useState(30);\n return (\n <>\n <button type=\"button\" onClick={() => setSize(60)}>\n widen-aside\n </button>\n <Splitter.Root\n size={size}\n onSizeChangeEnd={setSize}\n minSize={5}\n maxSize={95}\n >\n <Splitter.Aside>Aside</Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>Main</Splitter.Main>\n </Splitter.Root>\n </>\n );\n };\n\n render(\n <NimbusProvider>\n <Demo />\n </NimbusProvider>\n );\n\n const handle = await screen.findByRole(\"separator\");\n await waitFor(() => {\n expect(Number(handle.getAttribute(\"aria-valuenow\"))).toBe(30);\n });\n\n await user.click(screen.getByText(\"widen-aside\"));\n await waitFor(() => {\n expect(Number(handle.getAttribute(\"aria-valuenow\"))).toBe(60);\n });\n });\n});\n```\n\n### Controlled collapse from anywhere\n\nCollapse is plain controlled boolean state, so a button\n\n```tsx\nimport { describe, it, expect } from \"vitest\";\nimport { useState } from \"react\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { NimbusProvider, Splitter } from \"@commercetools/nimbus\";\n\ndescribe(\"Splitter - controlled collapse\", () => {\n it(\"collapses the aside from a button outside the subtree\", async () => {\n const user = userEvent.setup();\n const Demo = () => {\n const [collapsed, setCollapsed] = useState(false);\n return (\n <>\n <button type=\"button\" onClick={() => setCollapsed((c) => !c)}>\n toggle-aside\n </button>\n <Splitter.Root\n defaultSize={30}\n minSize={5}\n maxSize={95}\n collapsible\n collapsed={collapsed}\n onCollapsedChange={setCollapsed}\n >\n <Splitter.Aside>Aside</Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>Main</Splitter.Main>\n </Splitter.Root>\n </>\n );\n };\n\n render(\n <NimbusProvider>\n <Demo />\n </NimbusProvider>\n );\n\n await user.click(screen.getByText(\"toggle-aside\"));\n const handle = await screen.findByRole(\"separator\");\n await waitFor(() => {\n expect(Number(handle.getAttribute(\"aria-valuenow\"))).toBe(0);\n });\n });\n});\n```\n\n### Nested splitters for 3+ regions\n\nEach nested Splitter is an independent widget.\n\n```tsx\nimport { describe, it, expect } from \"vitest\";\nimport { useState } from \"react\";\nimport { render, screen, waitFor } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { NimbusProvider, Splitter } from \"@commercetools/nimbus\";\n\ndescribe(\"Splitter - Nested\", () => {\n it(\"nests inside a pane to express three regions\", async () => {\n render(\n <NimbusProvider>\n <Splitter.Root defaultSize={25}>\n <Splitter.Aside>Aside</Splitter.Aside>\n <Splitter.Handle />\n <Splitter.Main>\n <Splitter.Root defaultSize={35}>\n <Splitter.Main>Main</Splitter.Main>\n <Splitter.Handle />\n <Splitter.Aside>Aside</Splitter.Aside>\n </Splitter.Root>\n </Splitter.Main>\n </Splitter.Root>\n </NimbusProvider>\n );\n\n const handles = await screen.findAllByRole(\"separator\");\n expect(handles).toHaveLength(2);\n });\n});\n```\n\n\n## Resources\n\n- [Storybook](https://nimbus-storybook.vercel.app/?path=/docs/components-splitter--docs)\n- [React Aria useSeparator](https://react-spectrum.adobe.com/react-aria/useSeparator.html)\n- [W3C ARIA window splitter pattern](https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/)\n",
281
281
  "toc": [
282
282
  {
283
283
  "value": "Getting started",
@@ -206,7 +206,7 @@
206
206
  ]
207
207
  },
208
208
  "dev": {
209
- "mdx": "\n## Getting started\n\n### Import\n\n```tsx\nimport { Avatar, type AvatarProps } from \"@commercetools/nimbus\";\n```\n\n### Basic usage\n\nThe simplest implementation displays an avatar with initials derived from first\nand last names:\n\n```jsx live-dev\nconst App = () => (\n <Avatar firstName=\"John\" lastName=\"Doe\" />\n)\n```\n\n## Usage examples\n\n### Size options\n\nThe `2xs`, `xs`, and `md` size variants are available to match your interface\ndensity:\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"row\" gap=\"400\" alignItems=\"center\">\n <Avatar firstName=\"John\" lastName=\"Doe\" size=\"2xs\" />\n <Avatar firstName=\"John\" lastName=\"Doe\" size=\"xs\" />\n <Avatar firstName=\"John\" lastName=\"Doe\" size=\"md\" />\n </Stack>\n)\n```\n\n### Color palettes\n\nAvatars support different color palettes to convey semantic meaning or match\nyour design theme:\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"row\" gap=\"400\" alignItems=\"center\">\n <Avatar firstName=\"John\" lastName=\"Doe\" colorPalette=\"primary\" />\n <Avatar firstName=\"Jane\" lastName=\"Smith\" colorPalette=\"positive\" />\n <Avatar firstName=\"Alex\" lastName=\"Chen\" colorPalette=\"info\" />\n <Avatar firstName=\"Maria\" lastName=\"Garcia\" colorPalette=\"critical\" />\n <Avatar firstName=\"Sam\" lastName=\"Wilson\" colorPalette=\"cyan\" />\n </Stack>\n)\n```\n\n### With image\n\nWhen a `src` prop is provided, the Avatar displays the image. If the image fails\nto load, it automatically falls back to initials:\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"row\" gap=\"400\" alignItems=\"center\">\n <Avatar\n firstName=\"Jane\"\n lastName=\"Smith\"\n src=\"https://i.pravatar.cc/150?img=1\"\n alt=\"Jane Smith's profile picture\"\n />\n <Avatar\n firstName=\"Alex\"\n lastName=\"Johnson\"\n src=\"https://i.pravatar.cc/150?img=2\"\n alt=\"Alex Johnson's profile picture\"\n />\n </Stack>\n)\n```\n\n### Initials fallback\n\nThe Avatar automatically extracts and displays initials from the provided first\nand last names. This serves as a fallback when no image is available or when the\nimage fails to load:\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"row\" gap=\"400\">\n <Avatar\n firstName=\"Maria\"\n lastName=\"Garcia\"\n src=\"https://www.gravatar.com/avatar/thisWill404?s=200&d=404\" />\n <Avatar\n firstName=\"Chen\"\n lastName=\"Wei\"\n src=\"https://www.gravatar.com/avatar/thisWill404?s=200&d=404\" />\n <Avatar\n firstName=\"Aisha\"\n lastName=\"Patel\"\n src=\"https://www.gravatar.com/avatar/thisWill404?s=200&d=404\" />\n </Stack>\n)\n```\n\n### Missing or partial names\n\n`firstName` and `lastName` are both **optional**. The Avatar handles missing,\nempty, and whitespace-only values defensively:\n\n- If only one name is usable (after trimming whitespace), a single initial\n is rendered.\n- If neither name yields a usable character, a generic `Person` icon is\n rendered as the fallback and a localized generic `aria-label`\n (`\"User avatar\"`) is applied.\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"row\" gap=\"400\" alignItems=\"center\">\n <Avatar firstName=\"John\" /> {/* renders \"J\" */}\n <Avatar lastName=\"Doe\" /> {/* renders \"D\" */}\n <Avatar firstName=\"\" lastName=\"\" /> {/* renders the Person icon */}\n <Avatar /> {/* renders the Person icon */}\n </Stack>\n)\n```\n\nThis is intended for cases where user records have incomplete name data —\ncommon in legacy systems and partial profiles. The component's TypeScript\ncontract permits `undefined` so call sites do not need non-null assertions\nor empty-string fallbacks.\n\nInitials extraction is also Unicode codepoint-safe (emoji and astral-plane\ncharacters are not split mid-surrogate) and trim-aware (leading/trailing\nwhitespace is discarded before extracting the first character).\n\n## Component requirements\n\n## Accessibility\n\nThe Avatar component handles most accessibility requirements internally,\nincluding automatic labeling with the user's full name.\n\n- **Labeling**: The component automatically generates an internationalized\n accessible label from the `firstName` and `lastName` props. When providing an\n image via the `src` prop, always include an `alt` attribute for screen\n readers.\n- **Role**: Renders as a `<figure>` element with an `aria-label` that includes\n the full name (e.g., \"Avatar image for John Doe\").\n\nIf your use case requires tracking and analytics for this component, it is good\npractice to add a **persistent**, **unique** id to the component:\n\n```tsx\nconst PERSISTENT_ID = \"example-avatar\";\n\nexport const Example = () => (\n <Avatar id={PERSISTENT_ID} firstName=\"John\" lastName=\"Doe\" />\n);\n```\n\n#### Keyboard navigation\n\nAvatars are non-interactive elements and do not receive focus or support\nkeyboard interaction by default. If you need to make an avatar interactive\n(e.g., clickable), wrap it in a button or interactive element:\n\n```tsx\n<button onClick={handleClick} aria-label=\"View profile\">\n <Avatar firstName=\"John\" lastName=\"Doe\" />\n</button>\n```\n\n## API reference\n\n<PropsTable id=\"Avatar\" />\n\n## Common patterns\n\n### User profile header\n\nCombine Avatar with text components to create a user profile header:\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"row\" gap=\"400\" alignItems=\"center\">\n <Avatar\n firstName=\"Sarah\"\n lastName=\"Johnson\"\n src=\"https://i.pravatar.cc/150?img=5\"\n size=\"md\"\n />\n <Stack direction=\"column\" gap=\"100\">\n <Text fontSize=\"md\" fontWeight=\"600\">Sarah Johnson</Text>\n <Text fontSize=\"sm\" color=\"neutral.11\">Product Manager</Text>\n </Stack>\n </Stack>\n)\n```\n\n### Comment or message list\n\nDisplay avatars alongside user content in comments or messages:\n\n```jsx live-dev\nconst App = () => {\n const comments = [\n { id: 1, author: { firstName: \"Alex\", lastName: \"Chen\" }, text: \"Great work on this feature!\", time: \"2 hours ago\" },\n { id: 2, author: { firstName: \"Maria\", lastName: \"Garcia\" }, text: \"I agree, this is really helpful.\", time: \"1 hour ago\" },\n { id: 3, author: { firstName: \"James\", lastName: \"Wilson\" }, text: \"Looking forward to the next update.\", time: \"30 minutes ago\" },\n ];\n\n return (\n <Stack direction=\"column\" gap=\"600\">\n {comments.map((comment) => (\n <Stack key={comment.id} direction=\"row\" gap=\"400\">\n <Avatar\n firstName={comment.author.firstName}\n lastName={comment.author.lastName}\n size=\"xs\"\n />\n <Stack direction=\"column\" gap=\"100\" flex=\"1\">\n <Stack direction=\"row\" gap=\"200\" alignItems=\"baseline\">\n <Text fontSize=\"sm\" fontWeight=\"600\">\n {comment.author.firstName} {comment.author.lastName}\n </Text>\n <Text fontSize=\"xs\" color=\"neutral.11\">{comment.time}</Text>\n </Stack>\n <Text fontSize=\"sm\">{comment.text}</Text>\n </Stack>\n </Stack>\n ))}\n </Stack>\n );\n}\n```\n\n## Testing your implementation\n\nThese examples demonstrate how to test your implementation when using Avatar\nwithin your application. As the component's internal functionality is already\ntested by Nimbus, these patterns help you verify your integration and\napplication-specific logic.\n\n### Basic rendering tests\n\nVerify the Avatar renders with expected elements and labels\n\n```tsx\nimport { describe, it, expect } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport { Avatar, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"Avatar - Basic rendering\", () => {\n it(\"renders avatar with initials\", () => {\n render(\n <NimbusProvider>\n <Avatar firstName=\"John\" lastName=\"Doe\" />\n </NimbusProvider>\n );\n\n expect(screen.getByRole(\"figure\")).toBeInTheDocument();\n expect(screen.getByLabelText(/John Doe/i)).toBeInTheDocument();\n expect(screen.getByText(\"JD\")).toBeInTheDocument();\n });\n\n it(\"renders avatar with image\", () => {\n render(\n <NimbusProvider>\n <Avatar\n firstName=\"Jane\"\n lastName=\"Smith\"\n src=\"https://example.com/avatar.jpg\"\n alt=\"Jane Smith profile\"\n />\n </NimbusProvider>\n );\n\n const image = screen.getByAltText(\"Jane Smith profile\");\n expect(image).toBeInTheDocument();\n expect(image).toHaveAttribute(\"src\", \"https://example.com/avatar.jpg\");\n });\n});\n```\n\n### Size variant tests\n\nTest different size options\n\n```tsx\nimport { describe, it, expect } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport { Avatar, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"Avatar - Size variants\", () => {\n it(\"renders different sizes correctly\", () => {\n const { rerender } = render(\n <NimbusProvider>\n <Avatar firstName=\"John\" lastName=\"Doe\" size=\"2xs\" />\n </NimbusProvider>\n );\n\n let avatar = screen.getByRole(\"figure\");\n expect(avatar).toBeInTheDocument();\n\n rerender(\n <NimbusProvider>\n <Avatar firstName=\"John\" lastName=\"Doe\" size=\"xs\" />\n </NimbusProvider>\n );\n\n avatar = screen.getByRole(\"figure\");\n expect(avatar).toBeInTheDocument();\n\n rerender(\n <NimbusProvider>\n <Avatar firstName=\"John\" lastName=\"Doe\" size=\"md\" />\n </NimbusProvider>\n );\n\n avatar = screen.getByRole(\"figure\");\n expect(avatar).toBeInTheDocument();\n });\n});\n```\n\n### Accessibility tests\n\nVerify accessibility attributes and labeling\n\n```tsx\nimport { describe, it, expect } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport { Avatar, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"Avatar - Accessibility\", () => {\n it(\"has correct aria-label with full name\", () => {\n render(\n <NimbusProvider>\n <Avatar firstName=\"Maria\" lastName=\"Garcia\" />\n </NimbusProvider>\n );\n\n const avatar = screen.getByLabelText(/Maria Garcia/i);\n expect(avatar).toBeInTheDocument();\n expect(avatar.tagName).toBe(\"FIGURE\");\n });\n\n it(\"applies custom id for tracking\", () => {\n const PERSISTENT_ID = \"test-avatar-id\";\n\n render(\n <NimbusProvider>\n <Avatar id={PERSISTENT_ID} firstName=\"John\" lastName=\"Doe\" />\n </NimbusProvider>\n );\n\n const avatar = screen.getByRole(\"figure\");\n expect(avatar).toHaveAttribute(\"id\", PERSISTENT_ID);\n });\n});\n```\n\n### Missing-name fallback tests\n\nVerify the Avatar renders a generic icon and label when\n\n```tsx\nimport { describe, it, expect } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport { Avatar, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"Avatar - Missing names\", () => {\n it(\"renders the Person icon and generic aria-label when both names are missing\", () => {\n render(\n <NimbusProvider>\n <Avatar />\n </NimbusProvider>\n );\n\n const avatar = screen.getByRole(\"figure\");\n // Generic localized aria-label (\"Generic user avatar\" in English)\n expect(avatar).toHaveAttribute(\"aria-label\", \"Generic user avatar\");\n // Person icon is rendered as the visual fallback\n expect(avatar.querySelector(\"svg\")).not.toBeNull();\n // No initials text is rendered\n expect(avatar.textContent?.trim()).toBe(\"\");\n });\n\n it(\"renders a single initial when only firstName is provided\", () => {\n render(\n <NimbusProvider>\n <Avatar firstName=\"John\" />\n </NimbusProvider>\n );\n\n const avatar = screen.getByRole(\"figure\");\n expect(avatar.textContent?.trim()).toBe(\"J\");\n expect(avatar.querySelector(\"svg\")).toBeNull();\n });\n});\n```\n\n\n## Resources\n\n- [Storybook](https://nimbus-storybook.vercel.app/?path=/docs/components-avatar--docs)\n",
209
+ "mdx": "\n## Getting started\n\n### Import\n\n```tsx\nimport { Avatar, type AvatarProps } from \"@commercetools/nimbus\";\n```\n\n### Basic usage\n\nThe simplest implementation displays an avatar with initials derived from first\nand last names:\n\n```jsx live-dev\nconst App = () => (\n <Avatar firstName=\"John\" lastName=\"Doe\" />\n)\n```\n\n## Usage examples\n\n### Size options\n\nThe `2xs`, `xs`, and `md` size variants are available to match your interface\ndensity:\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"row\" gap=\"400\" alignItems=\"center\">\n <Avatar firstName=\"John\" lastName=\"Doe\" size=\"2xs\" />\n <Avatar firstName=\"John\" lastName=\"Doe\" size=\"xs\" />\n <Avatar firstName=\"John\" lastName=\"Doe\" size=\"md\" />\n </Stack>\n)\n```\n\n### Style variants\n\nTwo visual variants are available. `subtle` (the default) uses a soft tinted\nbackground, while `solid` fills the avatar with the palette's solid color —\nmatching the equivalent Button variant:\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"row\" gap=\"400\" alignItems=\"center\">\n <Avatar firstName=\"John\" lastName=\"Doe\" variant=\"subtle\" />\n <Avatar firstName=\"John\" lastName=\"Doe\" variant=\"solid\" />\n </Stack>\n)\n```\n\n### Color palettes\n\nAvatars support different color palettes to convey semantic meaning or match\nyour design theme:\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"row\" gap=\"400\" alignItems=\"center\">\n <Avatar firstName=\"John\" lastName=\"Doe\" colorPalette=\"primary\" />\n <Avatar firstName=\"Jane\" lastName=\"Smith\" colorPalette=\"positive\" />\n <Avatar firstName=\"Alex\" lastName=\"Chen\" colorPalette=\"info\" />\n <Avatar firstName=\"Maria\" lastName=\"Garcia\" colorPalette=\"critical\" />\n <Avatar firstName=\"Sam\" lastName=\"Wilson\" colorPalette=\"cyan\" />\n </Stack>\n)\n```\n\n### With image\n\nWhen a `src` prop is provided, the Avatar displays the image. If the image fails\nto load, it automatically falls back to initials:\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"row\" gap=\"400\" alignItems=\"center\">\n <Avatar\n firstName=\"Jane\"\n lastName=\"Smith\"\n src=\"https://i.pravatar.cc/150?img=1\"\n alt=\"Jane Smith's profile picture\"\n />\n <Avatar\n firstName=\"Alex\"\n lastName=\"Johnson\"\n src=\"https://i.pravatar.cc/150?img=2\"\n alt=\"Alex Johnson's profile picture\"\n />\n </Stack>\n)\n```\n\n### Initials fallback\n\nThe Avatar automatically extracts and displays initials from the provided first\nand last names. This serves as a fallback when no image is available or when the\nimage fails to load:\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"row\" gap=\"400\">\n <Avatar\n firstName=\"Maria\"\n lastName=\"Garcia\"\n src=\"https://www.gravatar.com/avatar/thisWill404?s=200&d=404\" />\n <Avatar\n firstName=\"Chen\"\n lastName=\"Wei\"\n src=\"https://www.gravatar.com/avatar/thisWill404?s=200&d=404\" />\n <Avatar\n firstName=\"Aisha\"\n lastName=\"Patel\"\n src=\"https://www.gravatar.com/avatar/thisWill404?s=200&d=404\" />\n </Stack>\n)\n```\n\n### Missing or partial names\n\n`firstName` and `lastName` are both **optional**. The Avatar handles missing,\nempty, and whitespace-only values defensively:\n\n- If only one name is usable (after trimming whitespace), a single initial\n is rendered.\n- If neither name yields a usable character, a generic `Person` icon is\n rendered as the fallback and a localized generic `aria-label`\n (`\"User avatar\"`) is applied.\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"row\" gap=\"400\" alignItems=\"center\">\n <Avatar firstName=\"John\" /> {/* renders \"J\" */}\n <Avatar lastName=\"Doe\" /> {/* renders \"D\" */}\n <Avatar firstName=\"\" lastName=\"\" /> {/* renders the Person icon */}\n <Avatar /> {/* renders the Person icon */}\n </Stack>\n)\n```\n\nThis is intended for cases where user records have incomplete name data —\ncommon in legacy systems and partial profiles. The component's TypeScript\ncontract permits `undefined` so call sites do not need non-null assertions\nor empty-string fallbacks.\n\nInitials extraction is also Unicode codepoint-safe (emoji and astral-plane\ncharacters are not split mid-surrogate) and trim-aware (leading/trailing\nwhitespace is discarded before extracting the first character).\n\n## Component requirements\n\n## Accessibility\n\nThe Avatar component handles most accessibility requirements internally,\nincluding automatic labeling with the user's full name.\n\n- **Labeling**: The component automatically generates an internationalized\n accessible label from the `firstName` and `lastName` props. When providing an\n image via the `src` prop, always include an `alt` attribute for screen\n readers.\n- **Role**: Renders as a `<figure>` element with an `aria-label` that includes\n the full name (e.g., \"Avatar image for John Doe\").\n\nIf your use case requires tracking and analytics for this component, it is good\npractice to add a **persistent**, **unique** id to the component:\n\n```tsx\nconst PERSISTENT_ID = \"example-avatar\";\n\nexport const Example = () => (\n <Avatar id={PERSISTENT_ID} firstName=\"John\" lastName=\"Doe\" />\n);\n```\n\n#### Keyboard navigation\n\nAvatars are non-interactive elements and do not receive focus or support\nkeyboard interaction by default. If you need to make an avatar interactive\n(e.g., clickable), wrap it in a button or interactive element:\n\n```tsx\n<button onClick={handleClick} aria-label=\"View profile\">\n <Avatar firstName=\"John\" lastName=\"Doe\" />\n</button>\n```\n\n## API reference\n\n<PropsTable id=\"Avatar\" />\n\n## Common patterns\n\n### User profile header\n\nCombine Avatar with text components to create a user profile header:\n\n```jsx live-dev\nconst App = () => (\n <Stack direction=\"row\" gap=\"400\" alignItems=\"center\">\n <Avatar\n firstName=\"Sarah\"\n lastName=\"Johnson\"\n src=\"https://i.pravatar.cc/150?img=5\"\n size=\"md\"\n />\n <Stack direction=\"column\" gap=\"100\">\n <Text fontSize=\"md\" fontWeight=\"600\">Sarah Johnson</Text>\n <Text fontSize=\"sm\" color=\"neutral.11\">Product Manager</Text>\n </Stack>\n </Stack>\n)\n```\n\n### Comment or message list\n\nDisplay avatars alongside user content in comments or messages:\n\n```jsx live-dev\nconst App = () => {\n const comments = [\n { id: 1, author: { firstName: \"Alex\", lastName: \"Chen\" }, text: \"Great work on this feature!\", time: \"2 hours ago\" },\n { id: 2, author: { firstName: \"Maria\", lastName: \"Garcia\" }, text: \"I agree, this is really helpful.\", time: \"1 hour ago\" },\n { id: 3, author: { firstName: \"James\", lastName: \"Wilson\" }, text: \"Looking forward to the next update.\", time: \"30 minutes ago\" },\n ];\n\n return (\n <Stack direction=\"column\" gap=\"600\">\n {comments.map((comment) => (\n <Stack key={comment.id} direction=\"row\" gap=\"400\">\n <Avatar\n firstName={comment.author.firstName}\n lastName={comment.author.lastName}\n size=\"xs\"\n />\n <Stack direction=\"column\" gap=\"100\" flex=\"1\">\n <Stack direction=\"row\" gap=\"200\" alignItems=\"baseline\">\n <Text fontSize=\"sm\" fontWeight=\"600\">\n {comment.author.firstName} {comment.author.lastName}\n </Text>\n <Text fontSize=\"xs\" color=\"neutral.11\">{comment.time}</Text>\n </Stack>\n <Text fontSize=\"sm\">{comment.text}</Text>\n </Stack>\n </Stack>\n ))}\n </Stack>\n );\n}\n```\n\n## Testing your implementation\n\nThese examples demonstrate how to test your implementation when using Avatar\nwithin your application. As the component's internal functionality is already\ntested by Nimbus, these patterns help you verify your integration and\napplication-specific logic.\n\n### Basic rendering tests\n\nVerify the Avatar renders with expected elements and labels\n\n```tsx\nimport { describe, it, expect } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport { Avatar, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"Avatar - Basic rendering\", () => {\n it(\"renders avatar with initials\", () => {\n render(\n <NimbusProvider>\n <Avatar firstName=\"John\" lastName=\"Doe\" />\n </NimbusProvider>\n );\n\n expect(screen.getByRole(\"figure\")).toBeInTheDocument();\n expect(screen.getByLabelText(/John Doe/i)).toBeInTheDocument();\n expect(screen.getByText(\"JD\")).toBeInTheDocument();\n });\n\n it(\"renders avatar with image\", () => {\n render(\n <NimbusProvider>\n <Avatar\n firstName=\"Jane\"\n lastName=\"Smith\"\n src=\"https://example.com/avatar.jpg\"\n alt=\"Jane Smith profile\"\n />\n </NimbusProvider>\n );\n\n const image = screen.getByAltText(\"Jane Smith profile\");\n expect(image).toBeInTheDocument();\n expect(image).toHaveAttribute(\"src\", \"https://example.com/avatar.jpg\");\n });\n});\n```\n\n### Size variant tests\n\nTest different size options\n\n```tsx\nimport { describe, it, expect } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport { Avatar, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"Avatar - Size variants\", () => {\n it(\"renders different sizes correctly\", () => {\n const { rerender } = render(\n <NimbusProvider>\n <Avatar firstName=\"John\" lastName=\"Doe\" size=\"2xs\" />\n </NimbusProvider>\n );\n\n let avatar = screen.getByRole(\"figure\");\n expect(avatar).toBeInTheDocument();\n\n rerender(\n <NimbusProvider>\n <Avatar firstName=\"John\" lastName=\"Doe\" size=\"xs\" />\n </NimbusProvider>\n );\n\n avatar = screen.getByRole(\"figure\");\n expect(avatar).toBeInTheDocument();\n\n rerender(\n <NimbusProvider>\n <Avatar firstName=\"John\" lastName=\"Doe\" size=\"md\" />\n </NimbusProvider>\n );\n\n avatar = screen.getByRole(\"figure\");\n expect(avatar).toBeInTheDocument();\n });\n});\n```\n\n### Accessibility tests\n\nVerify accessibility attributes and labeling\n\n```tsx\nimport { describe, it, expect } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport { Avatar, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"Avatar - Accessibility\", () => {\n it(\"has correct aria-label with full name\", () => {\n render(\n <NimbusProvider>\n <Avatar firstName=\"Maria\" lastName=\"Garcia\" />\n </NimbusProvider>\n );\n\n const avatar = screen.getByLabelText(/Maria Garcia/i);\n expect(avatar).toBeInTheDocument();\n expect(avatar.tagName).toBe(\"FIGURE\");\n });\n\n it(\"applies custom id for tracking\", () => {\n const PERSISTENT_ID = \"test-avatar-id\";\n\n render(\n <NimbusProvider>\n <Avatar id={PERSISTENT_ID} firstName=\"John\" lastName=\"Doe\" />\n </NimbusProvider>\n );\n\n const avatar = screen.getByRole(\"figure\");\n expect(avatar).toHaveAttribute(\"id\", PERSISTENT_ID);\n });\n});\n```\n\n### Missing-name fallback tests\n\nVerify the Avatar renders a generic icon and label when\n\n```tsx\nimport { describe, it, expect } from \"vitest\";\nimport { render, screen } from \"@testing-library/react\";\nimport { Avatar, NimbusProvider } from \"@commercetools/nimbus\";\n\ndescribe(\"Avatar - Missing names\", () => {\n it(\"renders the Person icon and generic aria-label when both names are missing\", () => {\n render(\n <NimbusProvider>\n <Avatar />\n </NimbusProvider>\n );\n\n const avatar = screen.getByRole(\"figure\");\n // Generic localized aria-label (\"Generic user avatar\" in English)\n expect(avatar).toHaveAttribute(\"aria-label\", \"Generic user avatar\");\n // Person icon is rendered as the visual fallback\n expect(avatar.querySelector(\"svg\")).not.toBeNull();\n // No initials text is rendered\n expect(avatar.textContent?.trim()).toBe(\"\");\n });\n\n it(\"renders a single initial when only firstName is provided\", () => {\n render(\n <NimbusProvider>\n <Avatar firstName=\"John\" />\n </NimbusProvider>\n );\n\n const avatar = screen.getByRole(\"figure\");\n expect(avatar.textContent?.trim()).toBe(\"J\");\n expect(avatar.querySelector(\"svg\")).toBeNull();\n });\n});\n```\n\n\n## Resources\n\n- [Storybook](https://nimbus-storybook.vercel.app/?path=/docs/components-avatar--docs)\n",
210
210
  "toc": [
211
211
  {
212
212
  "value": "Getting started",
@@ -261,6 +261,17 @@
261
261
  ],
262
262
  "parent": "root"
263
263
  },
264
+ {
265
+ "value": "Style variants",
266
+ "href": "#style-variants",
267
+ "depth": 3,
268
+ "numbering": [
269
+ 1,
270
+ 2,
271
+ 2
272
+ ],
273
+ "parent": "root"
274
+ },
264
275
  {
265
276
  "value": "Color palettes",
266
277
  "href": "#color-palettes",
@@ -268,7 +279,7 @@
268
279
  "numbering": [
269
280
  1,
270
281
  2,
271
- 2
282
+ 3
272
283
  ],
273
284
  "parent": "root"
274
285
  },
@@ -279,7 +290,7 @@
279
290
  "numbering": [
280
291
  1,
281
292
  2,
282
- 3
293
+ 4
283
294
  ],
284
295
  "parent": "root"
285
296
  },
@@ -290,7 +301,7 @@
290
301
  "numbering": [
291
302
  1,
292
303
  2,
293
- 4
304
+ 5
294
305
  ],
295
306
  "parent": "root"
296
307
  },
@@ -301,7 +312,7 @@
301
312
  "numbering": [
302
313
  1,
303
314
  2,
304
- 5
315
+ 6
305
316
  ],
306
317
  "parent": "root"
307
318
  },