@eocrm/design-system 0.0.0-stage → 0.6.17

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 (897) hide show
  1. package/AI-PRIMER.md +341 -0
  2. package/README.md +262 -2
  3. package/docs/components/Accordion.md +114 -0
  4. package/docs/components/AgentAvatar.md +58 -0
  5. package/docs/components/Alert.md +81 -0
  6. package/docs/components/AppLayout.md +70 -0
  7. package/docs/components/Avatar.md +49 -0
  8. package/docs/components/AvatarGroup.md +47 -0
  9. package/docs/components/Badge.md +60 -0
  10. package/docs/components/Banner.md +89 -0
  11. package/docs/components/BrandIcon.md +32 -0
  12. package/docs/components/Breadcrumb.md +72 -0
  13. package/docs/components/Button.md +107 -0
  14. package/docs/components/ButtonGroup.md +78 -0
  15. package/docs/components/Calendar.md +158 -0
  16. package/docs/components/Card.md +165 -0
  17. package/docs/components/CatalogPicker.md +83 -0
  18. package/docs/components/Chart.md +114 -0
  19. package/docs/components/ChatBubble.md +55 -0
  20. package/docs/components/ChatLauncher.md +97 -0
  21. package/docs/components/Checkbox.md +55 -0
  22. package/docs/components/CircularProgress.md +53 -0
  23. package/docs/components/Cluster.md +96 -0
  24. package/docs/components/Code.md +35 -0
  25. package/docs/components/CodeBlock.md +67 -0
  26. package/docs/components/ColorPicker.md +115 -0
  27. package/docs/components/ConfirmationPopover.md +70 -0
  28. package/docs/components/Constrain.md +39 -0
  29. package/docs/components/CurrencySelect.md +57 -0
  30. package/docs/components/CursorPagination.md +43 -0
  31. package/docs/components/DashboardCanvas.md +79 -0
  32. package/docs/components/DashboardWidget.md +131 -0
  33. package/docs/components/DataTable.md +88 -0
  34. package/docs/components/DatePicker.md +68 -0
  35. package/docs/components/DateRangePicker.md +71 -0
  36. package/docs/components/DateStrip.md +56 -0
  37. package/docs/components/DefinitionList.md +82 -0
  38. package/docs/components/Divider.md +57 -0
  39. package/docs/components/Dot.md +26 -0
  40. package/docs/components/Drawer.md +94 -0
  41. package/docs/components/DropdownMenu.md +299 -0
  42. package/docs/components/EmojiPicker.md +84 -0
  43. package/docs/components/EmptyState.md +55 -0
  44. package/docs/components/EntityChip.md +102 -0
  45. package/docs/components/ErrorState.md +72 -0
  46. package/docs/components/Field.md +91 -0
  47. package/docs/components/FileUpload.md +109 -0
  48. package/docs/components/FilterChip.md +88 -0
  49. package/docs/components/FlowCanvas.md +108 -0
  50. package/docs/components/FormRow.md +31 -0
  51. package/docs/components/FormSection.md +37 -0
  52. package/docs/components/Grid.md +103 -0
  53. package/docs/components/Highlight.md +75 -0
  54. package/docs/components/HighlightedText.md +50 -0
  55. package/docs/components/IconPicker.md +134 -0
  56. package/docs/components/IconTile.md +30 -0
  57. package/docs/components/Image.md +84 -0
  58. package/docs/components/ImageCrop.md +163 -0
  59. package/docs/components/Indent.md +31 -0
  60. package/docs/components/InlineDatePicker.md +58 -0
  61. package/docs/components/InlineDateRangePicker.md +57 -0
  62. package/docs/components/Input.md +54 -0
  63. package/docs/components/Kanban.md +122 -0
  64. package/docs/components/Kbd.md +41 -0
  65. package/docs/components/Lightbox.md +105 -0
  66. package/docs/components/Link.md +65 -0
  67. package/docs/components/LinkCard.md +41 -0
  68. package/docs/components/LiquidEditor.md +84 -0
  69. package/docs/components/List.md +68 -0
  70. package/docs/components/LiveRegion.md +29 -0
  71. package/docs/components/LocaleProvider.md +25 -0
  72. package/docs/components/Logo.md +41 -0
  73. package/docs/components/Masonry.md +48 -0
  74. package/docs/components/MediaTile.md +95 -0
  75. package/docs/components/Modal.md +107 -0
  76. package/docs/components/MoneyInput.md +62 -0
  77. package/docs/components/OptionsPicker.md +123 -0
  78. package/docs/components/OtpInput.md +65 -0
  79. package/docs/components/Page.md +35 -0
  80. package/docs/components/PageHeader.md +133 -0
  81. package/docs/components/Pagination.md +44 -0
  82. package/docs/components/Palette.md +32 -0
  83. package/docs/components/PasswordInput.md +45 -0
  84. package/docs/components/PasswordStrengthMeter.md +27 -0
  85. package/docs/components/PersonDisplay.md +153 -0
  86. package/docs/components/PhoneInput.md +54 -0
  87. package/docs/components/PillMenu.md +91 -0
  88. package/docs/components/Popover.md +127 -0
  89. package/docs/components/Progress.md +56 -0
  90. package/docs/components/QrCode.md +34 -0
  91. package/docs/components/Radio.md +37 -0
  92. package/docs/components/RadioGroup.md +56 -0
  93. package/docs/components/Rail.md +191 -0
  94. package/docs/components/RelativeDateInput.md +114 -0
  95. package/docs/components/RichText.md +44 -0
  96. package/docs/components/RichTextEditor.md +112 -0
  97. package/docs/components/Screen.md +37 -0
  98. package/docs/components/ScrollArea.md +39 -0
  99. package/docs/components/Select.md +131 -0
  100. package/docs/components/SettingRow.md +98 -0
  101. package/docs/components/Skeleton.md +68 -0
  102. package/docs/components/Slider.md +92 -0
  103. package/docs/components/SlotGrid.md +55 -0
  104. package/docs/components/SocialButton.md +31 -0
  105. package/docs/components/Sortable.md +127 -0
  106. package/docs/components/SortableGroup.md +52 -0
  107. package/docs/components/Sparkline.md +51 -0
  108. package/docs/components/Split.md +76 -0
  109. package/docs/components/Stack.md +59 -0
  110. package/docs/components/StagePath.md +66 -0
  111. package/docs/components/StatGroup.md +61 -0
  112. package/docs/components/StatTile.md +58 -0
  113. package/docs/components/Sticky.md +69 -0
  114. package/docs/components/Switch.md +89 -0
  115. package/docs/components/Table.md +184 -0
  116. package/docs/components/TableOfContents.md +76 -0
  117. package/docs/components/Tabs.md +94 -0
  118. package/docs/components/Text.md +63 -0
  119. package/docs/components/Textarea.md +59 -0
  120. package/docs/components/Thread.md +52 -0
  121. package/docs/components/TimeField.md +70 -0
  122. package/docs/components/Timeline.md +57 -0
  123. package/docs/components/Title.md +54 -0
  124. package/docs/components/ToastViewport.md +61 -0
  125. package/docs/components/Tooltip.md +63 -0
  126. package/docs/components/TopBar.md +214 -0
  127. package/docs/components/Tour.md +85 -0
  128. package/docs/components/TreeView.md +113 -0
  129. package/docs/components/VisuallyHidden.md +43 -0
  130. package/docs/components/WidgetPreview.md +54 -0
  131. package/docs/components/useBelowBreakpoint.md +11 -0
  132. package/docs/components/useMonth.md +18 -0
  133. package/docs/setup.md +51 -0
  134. package/docs/theming.md +95 -0
  135. package/docs/tokens.md +123 -0
  136. package/docs/transient-state.md +56 -0
  137. package/package.json +82 -4
  138. package/src/_meta/manifest.ts +293 -0
  139. package/src/app/AppProvider.tsx +78 -0
  140. package/src/app/index.ts +3 -0
  141. package/src/app/themeTokens.ts +95 -0
  142. package/src/calendar/dateMath.ts +114 -0
  143. package/src/calendar/formatters.ts +75 -0
  144. package/src/calendar/index.ts +39 -0
  145. package/src/calendar/types.ts +36 -0
  146. package/src/calendar/useAgenda.ts +70 -0
  147. package/src/calendar/useDay.ts +51 -0
  148. package/src/calendar/useMonth.ts +82 -0
  149. package/src/calendar/useWeek.ts +62 -0
  150. package/src/calendar/weekInfo.ts +83 -0
  151. package/src/components/Accordion/Accordion.module.scss +304 -0
  152. package/src/components/Accordion/Accordion.tokens.scss +64 -0
  153. package/src/components/Accordion/Accordion.tsx +272 -0
  154. package/src/components/Accordion/AccordionContent.tsx +36 -0
  155. package/src/components/Accordion/AccordionItem.tsx +66 -0
  156. package/src/components/Accordion/AccordionTrigger.tsx +121 -0
  157. package/src/components/Accordion/context.ts +47 -0
  158. package/src/components/Accordion/index.ts +14 -0
  159. package/src/components/Alert/Alert.module.scss +92 -0
  160. package/src/components/Alert/Alert.tokens.scss +60 -0
  161. package/src/components/Alert/Alert.tsx +136 -0
  162. package/src/components/Alert/index.ts +2 -0
  163. package/src/components/AppLayout/AppLayout.module.scss +130 -0
  164. package/src/components/AppLayout/AppLayout.tokens.scss +27 -0
  165. package/src/components/AppLayout/AppLayout.tsx +211 -0
  166. package/src/components/AppLayout/index.ts +2 -0
  167. package/src/components/Avatar/AgentAvatar.module.scss +117 -0
  168. package/src/components/Avatar/AgentAvatar.tsx +122 -0
  169. package/src/components/Avatar/Avatar.module.scss +181 -0
  170. package/src/components/Avatar/Avatar.tokens.scss +107 -0
  171. package/src/components/Avatar/Avatar.tsx +210 -0
  172. package/src/components/Avatar/AvatarGroup.module.scss +128 -0
  173. package/src/components/Avatar/AvatarGroup.tsx +119 -0
  174. package/src/components/Avatar/AvatarGroupContext.tsx +20 -0
  175. package/src/components/Avatar/index.ts +6 -0
  176. package/src/components/Badge/Badge.module.scss +124 -0
  177. package/src/components/Badge/Badge.tokens.scss +74 -0
  178. package/src/components/Badge/Badge.tsx +164 -0
  179. package/src/components/Badge/index.ts +2 -0
  180. package/src/components/Banner/Banner.module.scss +73 -0
  181. package/src/components/Banner/Banner.tokens.scss +51 -0
  182. package/src/components/Banner/Banner.tsx +138 -0
  183. package/src/components/Banner/index.ts +2 -0
  184. package/src/components/BrandIcon/BrandIcon.module.scss +5 -0
  185. package/src/components/BrandIcon/BrandIcon.tsx +99 -0
  186. package/src/components/BrandIcon/index.ts +2 -0
  187. package/src/components/Breadcrumb/Breadcrumb.module.scss +32 -0
  188. package/src/components/Breadcrumb/Breadcrumb.tokens.scss +9 -0
  189. package/src/components/Breadcrumb/Breadcrumb.tsx +146 -0
  190. package/src/components/Breadcrumb/index.ts +2 -0
  191. package/src/components/Button/Button.module.scss +167 -0
  192. package/src/components/Button/Button.tokens.scss +90 -0
  193. package/src/components/Button/Button.tsx +205 -0
  194. package/src/components/Button/index.ts +2 -0
  195. package/src/components/ButtonGroup/ButtonGroup.module.scss +134 -0
  196. package/src/components/ButtonGroup/ButtonGroup.tokens.scss +42 -0
  197. package/src/components/ButtonGroup/ButtonGroup.tsx +278 -0
  198. package/src/components/ButtonGroup/ButtonGroupItem.tsx +74 -0
  199. package/src/components/ButtonGroup/context.ts +45 -0
  200. package/src/components/ButtonGroup/index.ts +2 -0
  201. package/src/components/Calendar/AgendaView.module.scss +231 -0
  202. package/src/components/Calendar/AgendaView.tsx +214 -0
  203. package/src/components/Calendar/AllDayBand.module.scss +18 -0
  204. package/src/components/Calendar/AllDayBand.tsx +75 -0
  205. package/src/components/Calendar/Calendar.module.scss +23 -0
  206. package/src/components/Calendar/Calendar.tokens.scss +262 -0
  207. package/src/components/Calendar/Calendar.tsx +370 -0
  208. package/src/components/Calendar/DayCell.module.scss +30 -0
  209. package/src/components/Calendar/DayCell.tsx +50 -0
  210. package/src/components/Calendar/DayView.module.scss +10 -0
  211. package/src/components/Calendar/DayView.tsx +163 -0
  212. package/src/components/Calendar/EventChip.module.scss +171 -0
  213. package/src/components/Calendar/EventChip.tsx +115 -0
  214. package/src/components/Calendar/HourGrid.module.scss +123 -0
  215. package/src/components/Calendar/HourGrid.tsx +381 -0
  216. package/src/components/Calendar/MonthView.module.scss +99 -0
  217. package/src/components/Calendar/MonthView.tsx +296 -0
  218. package/src/components/Calendar/TimedEvent.module.scss +205 -0
  219. package/src/components/Calendar/TimedEvent.tsx +247 -0
  220. package/src/components/Calendar/ViewSwitcher.module.scss +3 -0
  221. package/src/components/Calendar/ViewSwitcher.tsx +47 -0
  222. package/src/components/Calendar/WeekView.module.scss +10 -0
  223. package/src/components/Calendar/WeekView.tsx +137 -0
  224. package/src/components/Calendar/eventColor.ts +60 -0
  225. package/src/components/Calendar/index.ts +18 -0
  226. package/src/components/Calendar/types.ts +451 -0
  227. package/src/components/Calendar/useEventDrag.ts +783 -0
  228. package/src/components/Calendar/utils.ts +622 -0
  229. package/src/components/Card/Card.module.scss +264 -0
  230. package/src/components/Card/Card.tokens.scss +55 -0
  231. package/src/components/Card/Card.tsx +170 -0
  232. package/src/components/Card/CardBody.tsx +45 -0
  233. package/src/components/Card/CardHeader.tsx +113 -0
  234. package/src/components/Card/CardList.tsx +24 -0
  235. package/src/components/Card/CardListRow.tsx +24 -0
  236. package/src/components/Card/index.ts +7 -0
  237. package/src/components/CatalogPicker/CatalogPicker.module.scss +193 -0
  238. package/src/components/CatalogPicker/CatalogPicker.tokens.scss +26 -0
  239. package/src/components/CatalogPicker/CatalogPicker.tsx +339 -0
  240. package/src/components/CatalogPicker/index.ts +2 -0
  241. package/src/components/Chart/Chart.module.scss +287 -0
  242. package/src/components/Chart/Chart.tokens.scss +63 -0
  243. package/src/components/Chart/Chart.tsx +607 -0
  244. package/src/components/Chart/ChartLegend.tsx +99 -0
  245. package/src/components/Chart/ChartTable.tsx +62 -0
  246. package/src/components/Chart/ChartTooltip.tsx +91 -0
  247. package/src/components/Chart/chartModel.ts +689 -0
  248. package/src/components/Chart/index.ts +2 -0
  249. package/src/components/ChatBubble/ChatBubble.module.scss +93 -0
  250. package/src/components/ChatBubble/ChatBubble.tokens.scss +17 -0
  251. package/src/components/ChatBubble/ChatBubble.tsx +61 -0
  252. package/src/components/ChatBubble/ChatBubbleList.tsx +27 -0
  253. package/src/components/ChatBubble/index.ts +3 -0
  254. package/src/components/ChatLauncher/ChatLauncher.module.scss +68 -0
  255. package/src/components/ChatLauncher/ChatLauncher.tokens.scss +27 -0
  256. package/src/components/ChatLauncher/ChatLauncher.tsx +202 -0
  257. package/src/components/ChatLauncher/index.ts +2 -0
  258. package/src/components/ChatLauncher/isAtBottom.ts +12 -0
  259. package/src/components/Checkbox/Checkbox.module.scss +147 -0
  260. package/src/components/Checkbox/Checkbox.tokens.scss +44 -0
  261. package/src/components/Checkbox/Checkbox.tsx +188 -0
  262. package/src/components/Checkbox/index.ts +2 -0
  263. package/src/components/CircularProgress/CircularProgress.module.scss +133 -0
  264. package/src/components/CircularProgress/CircularProgress.tokens.scss +40 -0
  265. package/src/components/CircularProgress/CircularProgress.tsx +139 -0
  266. package/src/components/CircularProgress/index.ts +7 -0
  267. package/src/components/Cluster/Cluster.module.scss +145 -0
  268. package/src/components/Cluster/Cluster.tokens.scss +11 -0
  269. package/src/components/Cluster/Cluster.tsx +215 -0
  270. package/src/components/Cluster/index.ts +2 -0
  271. package/src/components/Code/Code.module.scss +28 -0
  272. package/src/components/Code/Code.tokens.scss +15 -0
  273. package/src/components/Code/Code.tsx +44 -0
  274. package/src/components/Code/index.ts +2 -0
  275. package/src/components/CodeBlock/CodeBlock.module.scss +146 -0
  276. package/src/components/CodeBlock/CodeBlock.tokens.scss +27 -0
  277. package/src/components/CodeBlock/CodeBlock.tsx +274 -0
  278. package/src/components/CodeBlock/highlight.ts +179 -0
  279. package/src/components/CodeBlock/index.ts +8 -0
  280. package/src/components/CodeBlock/sampleSync.ts +42 -0
  281. package/src/components/ColorPicker/ColorPicker.module.scss +286 -0
  282. package/src/components/ColorPicker/ColorPicker.tokens.scss +60 -0
  283. package/src/components/ColorPicker/ColorPicker.tsx +367 -0
  284. package/src/components/ColorPicker/ColorPickerPanel.tsx +265 -0
  285. package/src/components/ColorPicker/SVSquare.tsx +189 -0
  286. package/src/components/ColorPicker/colorMath.ts +141 -0
  287. package/src/components/ColorPicker/index.ts +6 -0
  288. package/src/components/ConfirmationPopover/ConfirmationPopover.module.scss +39 -0
  289. package/src/components/ConfirmationPopover/ConfirmationPopover.tokens.scss +13 -0
  290. package/src/components/ConfirmationPopover/ConfirmationPopover.tsx +395 -0
  291. package/src/components/ConfirmationPopover/index.ts +2 -0
  292. package/src/components/Constrain/Constrain.module.scss +99 -0
  293. package/src/components/Constrain/Constrain.tsx +79 -0
  294. package/src/components/Constrain/index.ts +2 -0
  295. package/src/components/CurrencySelect/CurrencySelect.module.scss +7 -0
  296. package/src/components/CurrencySelect/CurrencySelect.tsx +167 -0
  297. package/src/components/CurrencySelect/index.ts +3 -0
  298. package/src/components/CursorPagination/CursorPagination.module.scss +69 -0
  299. package/src/components/CursorPagination/CursorPagination.tokens.scss +32 -0
  300. package/src/components/CursorPagination/CursorPagination.tsx +121 -0
  301. package/src/components/CursorPagination/index.ts +2 -0
  302. package/src/components/DashboardCanvas/DashboardCanvas.module.scss +360 -0
  303. package/src/components/DashboardCanvas/DashboardCanvas.tokens.scss +48 -0
  304. package/src/components/DashboardCanvas/DashboardCanvas.tsx +1270 -0
  305. package/src/components/DashboardCanvas/DashboardCanvasItem.tsx +140 -0
  306. package/src/components/DashboardCanvas/DashboardCanvasSection.tsx +174 -0
  307. package/src/components/DashboardCanvas/engine.ts +363 -0
  308. package/src/components/DashboardCanvas/index.ts +9 -0
  309. package/src/components/DashboardWidget/DashboardWidget.module.scss +100 -0
  310. package/src/components/DashboardWidget/DashboardWidget.tokens.scss +16 -0
  311. package/src/components/DashboardWidget/DashboardWidget.tsx +188 -0
  312. package/src/components/DashboardWidget/index.ts +7 -0
  313. package/src/components/DataTable/BodyRow.tsx +197 -0
  314. package/src/components/DataTable/ColumnVisibilityTrigger.module.scss +7 -0
  315. package/src/components/DataTable/ColumnVisibilityTrigger.tsx +105 -0
  316. package/src/components/DataTable/DataTable.module.scss +511 -0
  317. package/src/components/DataTable/DataTable.tokens.scss +80 -0
  318. package/src/components/DataTable/DataTable.tsx +734 -0
  319. package/src/components/DataTable/HeaderCell.module.scss +215 -0
  320. package/src/components/DataTable/HeaderCell.tsx +440 -0
  321. package/src/components/DataTable/columnShift.ts +117 -0
  322. package/src/components/DataTable/index.ts +23 -0
  323. package/src/components/DataTable/pinStyle.ts +47 -0
  324. package/src/components/DataTable/reorderColumns.ts +66 -0
  325. package/src/components/DataTable/types.ts +289 -0
  326. package/src/components/DataTable/useColumnDragShift.ts +216 -0
  327. package/src/components/DataTable/useDataTable.ts +372 -0
  328. package/src/components/DataTable/useResizeHandle.ts +97 -0
  329. package/src/components/DatePicker/DatePicker.module.scss +180 -0
  330. package/src/components/DatePicker/DatePicker.tokens.scss +152 -0
  331. package/src/components/DatePicker/DatePicker.tsx +664 -0
  332. package/src/components/DatePicker/DatePickerGrid.module.scss +172 -0
  333. package/src/components/DatePicker/DatePickerGrid.tsx +341 -0
  334. package/src/components/DatePicker/InlineDatePicker.module.scss +32 -0
  335. package/src/components/DatePicker/InlineDatePicker.tsx +201 -0
  336. package/src/components/DatePicker/index.ts +4 -0
  337. package/src/components/DatePicker/utils.ts +364 -0
  338. package/src/components/DateRangePicker/DateRangePicker.module.scss +237 -0
  339. package/src/components/DateRangePicker/DateRangePicker.tokens.scss +78 -0
  340. package/src/components/DateRangePicker/DateRangePicker.tsx +720 -0
  341. package/src/components/DateRangePicker/InlineDateRangePicker.module.scss +94 -0
  342. package/src/components/DateRangePicker/InlineDateRangePicker.tsx +376 -0
  343. package/src/components/DateRangePicker/index.ts +5 -0
  344. package/src/components/DateRangePicker/utils.ts +151 -0
  345. package/src/components/DateStrip/DateStrip.module.scss +78 -0
  346. package/src/components/DateStrip/DateStrip.tokens.scss +23 -0
  347. package/src/components/DateStrip/DateStrip.tsx +205 -0
  348. package/src/components/DateStrip/index.ts +2 -0
  349. package/src/components/DefinitionList/DefinitionList.module.scss +102 -0
  350. package/src/components/DefinitionList/DefinitionList.tokens.scss +29 -0
  351. package/src/components/DefinitionList/DefinitionList.tsx +198 -0
  352. package/src/components/DefinitionList/index.ts +9 -0
  353. package/src/components/Divider/Divider.module.scss +144 -0
  354. package/src/components/Divider/Divider.tokens.scss +21 -0
  355. package/src/components/Divider/Divider.tsx +103 -0
  356. package/src/components/Divider/index.ts +2 -0
  357. package/src/components/Dot/Dot.module.scss +40 -0
  358. package/src/components/Dot/Dot.tokens.scss +4 -0
  359. package/src/components/Dot/Dot.tsx +45 -0
  360. package/src/components/Dot/index.ts +2 -0
  361. package/src/components/Drawer/Body.tsx +23 -0
  362. package/src/components/Drawer/Close.tsx +35 -0
  363. package/src/components/Drawer/Content.tsx +93 -0
  364. package/src/components/Drawer/Drawer.module.scss +232 -0
  365. package/src/components/Drawer/Drawer.tokens.scss +38 -0
  366. package/src/components/Drawer/Drawer.tsx +23 -0
  367. package/src/components/Drawer/DrawerRoot.tsx +190 -0
  368. package/src/components/Drawer/Footer.tsx +24 -0
  369. package/src/components/Drawer/Header.tsx +64 -0
  370. package/src/components/Drawer/Overlay.tsx +82 -0
  371. package/src/components/Drawer/context.ts +44 -0
  372. package/src/components/Drawer/index.ts +12 -0
  373. package/src/components/Drawer/useDragToClose.ts +149 -0
  374. package/src/components/DropdownMenu/CheckboxItem.tsx +144 -0
  375. package/src/components/DropdownMenu/Content.tsx +419 -0
  376. package/src/components/DropdownMenu/DropdownMenu.module.scss +280 -0
  377. package/src/components/DropdownMenu/DropdownMenu.tokens.scss +88 -0
  378. package/src/components/DropdownMenu/DropdownMenu.tsx +43 -0
  379. package/src/components/DropdownMenu/Group.tsx +76 -0
  380. package/src/components/DropdownMenu/Item.tsx +148 -0
  381. package/src/components/DropdownMenu/ItemIndicator.tsx +30 -0
  382. package/src/components/DropdownMenu/Radio.tsx +178 -0
  383. package/src/components/DropdownMenu/Root.tsx +100 -0
  384. package/src/components/DropdownMenu/Sub.tsx +332 -0
  385. package/src/components/DropdownMenu/Trigger.tsx +76 -0
  386. package/src/components/DropdownMenu/context.ts +65 -0
  387. package/src/components/DropdownMenu/index.ts +20 -0
  388. package/src/components/EmojiPicker/EmojiPicker.module.scss +84 -0
  389. package/src/components/EmojiPicker/EmojiPicker.tokens.scss +24 -0
  390. package/src/components/EmojiPicker/EmojiPicker.tsx +341 -0
  391. package/src/components/EmojiPicker/emojiData.ts +245 -0
  392. package/src/components/EmojiPicker/index.ts +3 -0
  393. package/src/components/EmptyState/EmptyState.module.scss +102 -0
  394. package/src/components/EmptyState/EmptyState.tokens.scss +28 -0
  395. package/src/components/EmptyState/EmptyState.tsx +112 -0
  396. package/src/components/EmptyState/index.ts +7 -0
  397. package/src/components/EntityChip/EntityChip.module.scss +275 -0
  398. package/src/components/EntityChip/EntityChip.tokens.scss +66 -0
  399. package/src/components/EntityChip/EntityChip.tsx +649 -0
  400. package/src/components/EntityChip/index.ts +10 -0
  401. package/src/components/ErrorState/ErrorState.module.scss +105 -0
  402. package/src/components/ErrorState/ErrorState.tokens.scss +32 -0
  403. package/src/components/ErrorState/ErrorState.tsx +125 -0
  404. package/src/components/ErrorState/index.ts +8 -0
  405. package/src/components/Field/Field.module.scss +46 -0
  406. package/src/components/Field/Field.tsx +185 -0
  407. package/src/components/Field/index.ts +2 -0
  408. package/src/components/FileUpload/FileUpload.module.scss +187 -0
  409. package/src/components/FileUpload/FileUpload.tokens.scss +81 -0
  410. package/src/components/FileUpload/FileUpload.tsx +569 -0
  411. package/src/components/FileUpload/formatBytes.ts +34 -0
  412. package/src/components/FileUpload/iconForFile.ts +40 -0
  413. package/src/components/FileUpload/index.ts +2 -0
  414. package/src/components/FilterChip/FilterChip.module.scss +61 -0
  415. package/src/components/FilterChip/FilterChip.tokens.scss +25 -0
  416. package/src/components/FilterChip/FilterChip.tsx +208 -0
  417. package/src/components/FilterChip/index.ts +5 -0
  418. package/src/components/FlowCanvas/FlowCanvas.module.scss +329 -0
  419. package/src/components/FlowCanvas/FlowCanvas.tokens.scss +45 -0
  420. package/src/components/FlowCanvas/FlowCanvas.tsx +1867 -0
  421. package/src/components/FlowCanvas/FlowControls.tsx +46 -0
  422. package/src/components/FlowCanvas/FlowEdge.tsx +58 -0
  423. package/src/components/FlowCanvas/FlowNode.tsx +76 -0
  424. package/src/components/FlowCanvas/edgePath.ts +85 -0
  425. package/src/components/FlowCanvas/index.ts +4 -0
  426. package/src/components/FlowCanvas/layout.ts +148 -0
  427. package/src/components/FlowCanvas/spatialNav.ts +76 -0
  428. package/src/components/FlowCanvas/types.ts +59 -0
  429. package/src/components/FlowCanvas/useViewport.ts +122 -0
  430. package/src/components/FormRow/FormRow.module.scss +2 -0
  431. package/src/components/FormRow/FormRow.tsx +37 -0
  432. package/src/components/FormRow/index.ts +2 -0
  433. package/src/components/FormSection/FormSection.module.scss +24 -0
  434. package/src/components/FormSection/FormSection.tsx +46 -0
  435. package/src/components/FormSection/index.ts +2 -0
  436. package/src/components/Grid/Grid.module.scss +162 -0
  437. package/src/components/Grid/Grid.tokens.scss +13 -0
  438. package/src/components/Grid/Grid.tsx +226 -0
  439. package/src/components/Grid/GridItem.tsx +77 -0
  440. package/src/components/Grid/index.ts +11 -0
  441. package/src/components/Highlight/Highlight.module.scss +43 -0
  442. package/src/components/Highlight/Highlight.tokens.scss +35 -0
  443. package/src/components/Highlight/Highlight.tsx +164 -0
  444. package/src/components/Highlight/HighlightOverlay.tsx +100 -0
  445. package/src/components/Highlight/index.ts +2 -0
  446. package/src/components/HighlightedText/HighlightedText.module.scss +11 -0
  447. package/src/components/HighlightedText/HighlightedText.tokens.scss +9 -0
  448. package/src/components/HighlightedText/HighlightedText.tsx +50 -0
  449. package/src/components/HighlightedText/highlightRanges.ts +44 -0
  450. package/src/components/HighlightedText/index.ts +2 -0
  451. package/src/components/IconPicker/IconPicker.module.scss +175 -0
  452. package/src/components/IconPicker/IconPicker.tokens.scss +40 -0
  453. package/src/components/IconPicker/IconPicker.tsx +629 -0
  454. package/src/components/IconPicker/index.ts +9 -0
  455. package/src/components/IconTile/IconTile.module.scss +57 -0
  456. package/src/components/IconTile/IconTile.tokens.scss +16 -0
  457. package/src/components/IconTile/IconTile.tsx +91 -0
  458. package/src/components/IconTile/index.ts +2 -0
  459. package/src/components/Image/Image.module.scss +266 -0
  460. package/src/components/Image/Image.tokens.scss +55 -0
  461. package/src/components/Image/Image.tsx +342 -0
  462. package/src/components/Image/index.ts +2 -0
  463. package/src/components/ImageCrop/ImageCrop.module.scss +274 -0
  464. package/src/components/ImageCrop/ImageCrop.tokens.scss +33 -0
  465. package/src/components/ImageCrop/ImageCrop.tsx +542 -0
  466. package/src/components/ImageCrop/cropGeometry.ts +120 -0
  467. package/src/components/ImageCrop/extractCropBlob.ts +140 -0
  468. package/src/components/ImageCrop/index.ts +11 -0
  469. package/src/components/ImageCrop/useCropPreview.ts +161 -0
  470. package/src/components/Indent/Indent.module.scss +33 -0
  471. package/src/components/Indent/Indent.tokens.scss +10 -0
  472. package/src/components/Indent/Indent.tsx +46 -0
  473. package/src/components/Indent/index.ts +2 -0
  474. package/src/components/Input/Input.module.scss +99 -0
  475. package/src/components/Input/Input.tokens.scss +54 -0
  476. package/src/components/Input/Input.tsx +105 -0
  477. package/src/components/Input/Suggestions.tsx +260 -0
  478. package/src/components/Input/index.ts +2 -0
  479. package/src/components/Kanban/Kanban.module.scss +39 -0
  480. package/src/components/Kanban/Kanban.tokens.scss +15 -0
  481. package/src/components/Kanban/Kanban.tsx +951 -0
  482. package/src/components/Kanban/index.ts +7 -0
  483. package/src/components/Kbd/Kbd.module.scss +47 -0
  484. package/src/components/Kbd/Kbd.tokens.scss +26 -0
  485. package/src/components/Kbd/Kbd.tsx +73 -0
  486. package/src/components/Kbd/index.ts +2 -0
  487. package/src/components/Lightbox/Lightbox.module.scss +283 -0
  488. package/src/components/Lightbox/Lightbox.tokens.scss +36 -0
  489. package/src/components/Lightbox/Lightbox.tsx +506 -0
  490. package/src/components/Lightbox/index.ts +2 -0
  491. package/src/components/Link/Link.module.scss +78 -0
  492. package/src/components/Link/Link.tokens.scss +28 -0
  493. package/src/components/Link/Link.tsx +114 -0
  494. package/src/components/Link/index.ts +2 -0
  495. package/src/components/LinkCard/LinkCard.module.scss +92 -0
  496. package/src/components/LinkCard/LinkCard.tokens.scss +9 -0
  497. package/src/components/LinkCard/LinkCard.tsx +72 -0
  498. package/src/components/LinkCard/index.ts +2 -0
  499. package/src/components/LiquidEditor/AutocompleteMenu.tsx +133 -0
  500. package/src/components/LiquidEditor/HighlightLayer.tsx +53 -0
  501. package/src/components/LiquidEditor/InsertVariableMenu.tsx +81 -0
  502. package/src/components/LiquidEditor/LiquidEditor.module.scss +339 -0
  503. package/src/components/LiquidEditor/LiquidEditor.tokens.scss +81 -0
  504. package/src/components/LiquidEditor/LiquidEditor.tsx +412 -0
  505. package/src/components/LiquidEditor/PreviewPane.tsx +29 -0
  506. package/src/components/LiquidEditor/caretCoordinates.ts +97 -0
  507. package/src/components/LiquidEditor/index.ts +7 -0
  508. package/src/components/LiquidEditor/liquidFilters.ts +23 -0
  509. package/src/components/LiquidEditor/liquidTokenizer.ts +235 -0
  510. package/src/components/LiquidEditor/types.ts +101 -0
  511. package/src/components/LiquidEditor/useLiquidAutocomplete.ts +178 -0
  512. package/src/components/List/List.module.scss +105 -0
  513. package/src/components/List/List.tokens.scss +42 -0
  514. package/src/components/List/List.tsx +124 -0
  515. package/src/components/List/index.ts +9 -0
  516. package/src/components/LiveRegion/LiveRegion.module.scss +3 -0
  517. package/src/components/LiveRegion/LiveRegion.tsx +116 -0
  518. package/src/components/LiveRegion/index.ts +2 -0
  519. package/src/components/Logo/Logo.module.scss +139 -0
  520. package/src/components/Logo/Logo.tokens.scss +14 -0
  521. package/src/components/Logo/Logo.tsx +126 -0
  522. package/src/components/Logo/index.ts +2 -0
  523. package/src/components/Masonry/Masonry.module.scss +42 -0
  524. package/src/components/Masonry/Masonry.tokens.scss +8 -0
  525. package/src/components/Masonry/Masonry.tsx +136 -0
  526. package/src/components/Masonry/index.ts +2 -0
  527. package/src/components/Masonry/masonryUtils.ts +42 -0
  528. package/src/components/MediaTile/MediaTile.module.scss +183 -0
  529. package/src/components/MediaTile/MediaTile.tokens.scss +60 -0
  530. package/src/components/MediaTile/MediaTile.tsx +208 -0
  531. package/src/components/MediaTile/index.ts +8 -0
  532. package/src/components/Modal/Body.tsx +27 -0
  533. package/src/components/Modal/Close.tsx +45 -0
  534. package/src/components/Modal/Content.tsx +98 -0
  535. package/src/components/Modal/Footer.tsx +28 -0
  536. package/src/components/Modal/Header.tsx +51 -0
  537. package/src/components/Modal/Modal.module.scss +262 -0
  538. package/src/components/Modal/Modal.tokens.scss +50 -0
  539. package/src/components/Modal/Modal.tsx +23 -0
  540. package/src/components/Modal/ModalRoot.tsx +299 -0
  541. package/src/components/Modal/Overlay.tsx +101 -0
  542. package/src/components/Modal/context.ts +60 -0
  543. package/src/components/Modal/index.ts +11 -0
  544. package/src/components/MoneyInput/MoneyInput.module.scss +57 -0
  545. package/src/components/MoneyInput/MoneyInput.tsx +433 -0
  546. package/src/components/MoneyInput/index.ts +2 -0
  547. package/src/components/OptionsPicker/OptionsPicker.module.scss +216 -0
  548. package/src/components/OptionsPicker/OptionsPicker.tokens.scss +68 -0
  549. package/src/components/OptionsPicker/OptionsPicker.tsx +936 -0
  550. package/src/components/OptionsPicker/index.ts +9 -0
  551. package/src/components/OtpInput/OtpInput.module.scss +69 -0
  552. package/src/components/OtpInput/OtpInput.tokens.scss +45 -0
  553. package/src/components/OtpInput/OtpInput.tsx +368 -0
  554. package/src/components/OtpInput/index.ts +2 -0
  555. package/src/components/Page/Page.module.scss +30 -0
  556. package/src/components/Page/Page.tokens.scss +11 -0
  557. package/src/components/Page/Page.tsx +53 -0
  558. package/src/components/Page/index.ts +2 -0
  559. package/src/components/PageHeader/PageHeader.module.scss +197 -0
  560. package/src/components/PageHeader/PageHeader.tokens.scss +41 -0
  561. package/src/components/PageHeader/PageHeader.tsx +310 -0
  562. package/src/components/PageHeader/index.ts +20 -0
  563. package/src/components/Pagination/Pagination.module.scss +117 -0
  564. package/src/components/Pagination/Pagination.tokens.scss +54 -0
  565. package/src/components/Pagination/Pagination.tsx +165 -0
  566. package/src/components/Pagination/index.ts +4 -0
  567. package/src/components/Pagination/paginationRange.ts +72 -0
  568. package/src/components/PasswordInput/PasswordInput.module.scss +120 -0
  569. package/src/components/PasswordInput/PasswordInput.tokens.scss +44 -0
  570. package/src/components/PasswordInput/PasswordInput.tsx +210 -0
  571. package/src/components/PasswordInput/index.ts +2 -0
  572. package/src/components/PasswordStrengthMeter/PasswordStrengthMeter.module.scss +80 -0
  573. package/src/components/PasswordStrengthMeter/PasswordStrengthMeter.tokens.scss +40 -0
  574. package/src/components/PasswordStrengthMeter/PasswordStrengthMeter.tsx +96 -0
  575. package/src/components/PasswordStrengthMeter/index.ts +2 -0
  576. package/src/components/PersonDisplay/PersonDisplay.module.scss +180 -0
  577. package/src/components/PersonDisplay/PersonDisplay.tokens.scss +35 -0
  578. package/src/components/PersonDisplay/PersonDisplay.tsx +420 -0
  579. package/src/components/PersonDisplay/index.ts +10 -0
  580. package/src/components/PhoneInput/PhoneInput.module.scss +23 -0
  581. package/src/components/PhoneInput/PhoneInput.tsx +271 -0
  582. package/src/components/PhoneInput/index.ts +4 -0
  583. package/src/components/PhoneInput/phone.ts +128 -0
  584. package/src/components/PillMenu/PillMenu.module.scss +117 -0
  585. package/src/components/PillMenu/PillMenu.tokens.scss +22 -0
  586. package/src/components/PillMenu/PillMenu.tsx +324 -0
  587. package/src/components/PillMenu/index.ts +2 -0
  588. package/src/components/Popover/Anchor.tsx +37 -0
  589. package/src/components/Popover/Close.tsx +45 -0
  590. package/src/components/Popover/Content.tsx +213 -0
  591. package/src/components/Popover/Heading.tsx +33 -0
  592. package/src/components/Popover/Popover.module.scss +177 -0
  593. package/src/components/Popover/Popover.tokens.scss +22 -0
  594. package/src/components/Popover/Popover.tsx +23 -0
  595. package/src/components/Popover/PopoverRoot.tsx +75 -0
  596. package/src/components/Popover/Trigger.tsx +86 -0
  597. package/src/components/Popover/context.ts +22 -0
  598. package/src/components/Popover/index.ts +7 -0
  599. package/src/components/Progress/Progress.module.scss +110 -0
  600. package/src/components/Progress/Progress.tokens.scss +44 -0
  601. package/src/components/Progress/Progress.tsx +129 -0
  602. package/src/components/Progress/index.ts +2 -0
  603. package/src/components/QrCode/QrCode.module.scss +97 -0
  604. package/src/components/QrCode/QrCode.tokens.scss +17 -0
  605. package/src/components/QrCode/QrCode.tsx +233 -0
  606. package/src/components/QrCode/index.ts +2 -0
  607. package/src/components/QrCode/qr.ts +161 -0
  608. package/src/components/Radio/Radio.module.scss +175 -0
  609. package/src/components/Radio/Radio.tokens.scss +59 -0
  610. package/src/components/Radio/Radio.tsx +127 -0
  611. package/src/components/Radio/RadioGroup.module.scss +42 -0
  612. package/src/components/Radio/RadioGroup.tsx +120 -0
  613. package/src/components/Radio/RadioGroupContext.tsx +30 -0
  614. package/src/components/Radio/index.ts +4 -0
  615. package/src/components/Rail/Rail.module.scss +510 -0
  616. package/src/components/Rail/Rail.tokens.scss +92 -0
  617. package/src/components/Rail/Rail.tsx +287 -0
  618. package/src/components/Rail/RailCollapseToggle.tsx +57 -0
  619. package/src/components/Rail/RailFooter.tsx +17 -0
  620. package/src/components/Rail/RailGroup.tsx +728 -0
  621. package/src/components/Rail/RailGroupContext.ts +9 -0
  622. package/src/components/Rail/RailHeader.tsx +23 -0
  623. package/src/components/Rail/RailItem.tsx +119 -0
  624. package/src/components/Rail/RailSection.tsx +39 -0
  625. package/src/components/Rail/RailSpacer.tsx +19 -0
  626. package/src/components/Rail/index.ts +9 -0
  627. package/src/components/RelativeDateInput/RelativeDateInput.module.scss +15 -0
  628. package/src/components/RelativeDateInput/RelativeDateInput.tokens.scss +6 -0
  629. package/src/components/RelativeDateInput/RelativeDateInput.tsx +474 -0
  630. package/src/components/RelativeDateInput/index.ts +10 -0
  631. package/src/components/RelativeDateInput/relativeDate.ts +127 -0
  632. package/src/components/RichText/RichText.module.scss +5 -0
  633. package/src/components/RichText/RichText.tsx +70 -0
  634. package/src/components/RichText/RichTextAttachment.module.scss +82 -0
  635. package/src/components/RichText/RichTextAttachment.tsx +122 -0
  636. package/src/components/RichText/_prose.scss +79 -0
  637. package/src/components/RichText/attachmentIcon.tsx +24 -0
  638. package/src/components/RichText/engine/attachment.ts +154 -0
  639. package/src/components/RichText/engine/autolink.ts +64 -0
  640. package/src/components/RichText/engine/blockUnit.ts +174 -0
  641. package/src/components/RichText/engine/colorMarks.ts +85 -0
  642. package/src/components/RichText/engine/escape.ts +11 -0
  643. package/src/components/RichText/engine/fromHtml.ts +353 -0
  644. package/src/components/RichText/engine/fromMarkdown.ts +20 -0
  645. package/src/components/RichText/engine/inlines.ts +82 -0
  646. package/src/components/RichText/engine/listDepths.ts +32 -0
  647. package/src/components/RichText/engine/marks.ts +80 -0
  648. package/src/components/RichText/engine/mdToHtml.ts +178 -0
  649. package/src/components/RichText/engine/model.ts +157 -0
  650. package/src/components/RichText/engine/position.ts +118 -0
  651. package/src/components/RichText/engine/renderDoc.tsx +395 -0
  652. package/src/components/RichText/engine/renderLink.ts +15 -0
  653. package/src/components/RichText/engine/renderMention.ts +16 -0
  654. package/src/components/RichText/engine/safeHref.ts +22 -0
  655. package/src/components/RichText/engine/toHtml.ts +154 -0
  656. package/src/components/RichText/engine/toMarkdown.ts +97 -0
  657. package/src/components/RichText/engine/transforms.ts +434 -0
  658. package/src/components/RichText/imageMaxHeightStyle.ts +10 -0
  659. package/src/components/RichText/index.ts +31 -0
  660. package/src/components/RichTextEditor/RichTextAttachmentConfig.tsx +264 -0
  661. package/src/components/RichTextEditor/RichTextBlockControls.tsx +365 -0
  662. package/src/components/RichTextEditor/RichTextBlockMenu.tsx +162 -0
  663. package/src/components/RichTextEditor/RichTextColorMenu.tsx +115 -0
  664. package/src/components/RichTextEditor/RichTextEditor.module.scss +298 -0
  665. package/src/components/RichTextEditor/RichTextEditor.tsx +1910 -0
  666. package/src/components/RichTextEditor/RichTextImageResizer.tsx +131 -0
  667. package/src/components/RichTextEditor/RichTextLinkEditor.tsx +151 -0
  668. package/src/components/RichTextEditor/RichTextMentionMenu.tsx +99 -0
  669. package/src/components/RichTextEditor/RichTextToolbar.tsx +379 -0
  670. package/src/components/RichTextEditor/autolinkInput.ts +65 -0
  671. package/src/components/RichTextEditor/blockDrop.ts +9 -0
  672. package/src/components/RichTextEditor/blockReflow.ts +62 -0
  673. package/src/components/RichTextEditor/commands.ts +300 -0
  674. package/src/components/RichTextEditor/history.ts +93 -0
  675. package/src/components/RichTextEditor/icons.tsx +152 -0
  676. package/src/components/RichTextEditor/imageSize.ts +70 -0
  677. package/src/components/RichTextEditor/index.ts +5 -0
  678. package/src/components/RichTextEditor/input.ts +209 -0
  679. package/src/components/RichTextEditor/inputRules.ts +71 -0
  680. package/src/components/RichTextEditor/links.ts +103 -0
  681. package/src/components/RichTextEditor/mentionContext.ts +37 -0
  682. package/src/components/RichTextEditor/mentions.ts +28 -0
  683. package/src/components/RichTextEditor/preventSelectionLoss.ts +15 -0
  684. package/src/components/RichTextEditor/selection.ts +415 -0
  685. package/src/components/RichTextEditor/shortcuts.ts +44 -0
  686. package/src/components/RichTextEditor/useAnchoredFloating.ts +50 -0
  687. package/src/components/RichTextEditor/useAttachmentConfig.ts +134 -0
  688. package/src/components/RichTextEditor/useDismissOnOutsidePointerDown.ts +26 -0
  689. package/src/components/RichTextEditor/useLinkEditor.ts +107 -0
  690. package/src/components/RichTextEditor/useMention.ts +226 -0
  691. package/src/components/RichTextEditor/useOverlayRect.ts +73 -0
  692. package/src/components/RichTextEditor/useUpload.ts +246 -0
  693. package/src/components/Screen/Screen.module.scss +46 -0
  694. package/src/components/Screen/Screen.tokens.scss +20 -0
  695. package/src/components/Screen/Screen.tsx +97 -0
  696. package/src/components/Screen/index.ts +2 -0
  697. package/src/components/ScrollArea/ScrollArea.module.scss +39 -0
  698. package/src/components/ScrollArea/ScrollArea.tokens.scss +15 -0
  699. package/src/components/ScrollArea/ScrollArea.tsx +154 -0
  700. package/src/components/ScrollArea/index.ts +2 -0
  701. package/src/components/Select/Chip.tsx +45 -0
  702. package/src/components/Select/Empty.tsx +39 -0
  703. package/src/components/Select/Error.tsx +51 -0
  704. package/src/components/Select/HiddenInputs.tsx +44 -0
  705. package/src/components/Select/Listbox.tsx +557 -0
  706. package/src/components/Select/Loading.tsx +30 -0
  707. package/src/components/Select/Select.module.scss +512 -0
  708. package/src/components/Select/Select.tokens.scss +140 -0
  709. package/src/components/Select/Select.tsx +685 -0
  710. package/src/components/Select/Trigger.tsx +952 -0
  711. package/src/components/Select/context.ts +101 -0
  712. package/src/components/Select/emptyStateText.ts +19 -0
  713. package/src/components/Select/index.ts +9 -0
  714. package/src/components/Select/useSelectState.ts +157 -0
  715. package/src/components/Select/utils-types.ts +7 -0
  716. package/src/components/Select/utils.ts +122 -0
  717. package/src/components/SettingRow/SettingRow.module.scss +166 -0
  718. package/src/components/SettingRow/SettingRow.tokens.scss +37 -0
  719. package/src/components/SettingRow/SettingRow.tsx +242 -0
  720. package/src/components/SettingRow/index.ts +7 -0
  721. package/src/components/Skeleton/Skeleton.module.scss +55 -0
  722. package/src/components/Skeleton/Skeleton.tokens.scss +17 -0
  723. package/src/components/Skeleton/Skeleton.tsx +131 -0
  724. package/src/components/Skeleton/index.ts +4 -0
  725. package/src/components/Skeleton/useSkeletonVisibility.ts +95 -0
  726. package/src/components/Slider/Slider.module.scss +293 -0
  727. package/src/components/Slider/Slider.tokens.scss +98 -0
  728. package/src/components/Slider/Slider.tsx +624 -0
  729. package/src/components/Slider/index.ts +9 -0
  730. package/src/components/SlotGrid/SlotGrid.module.scss +61 -0
  731. package/src/components/SlotGrid/SlotGrid.tokens.scss +21 -0
  732. package/src/components/SlotGrid/SlotGrid.tsx +111 -0
  733. package/src/components/SlotGrid/index.ts +2 -0
  734. package/src/components/SocialButton/SocialButton.module.scss +4 -0
  735. package/src/components/SocialButton/SocialButton.tsx +40 -0
  736. package/src/components/SocialButton/index.ts +2 -0
  737. package/src/components/Sortable/Sortable.module.scss +177 -0
  738. package/src/components/Sortable/Sortable.tokens.scss +27 -0
  739. package/src/components/Sortable/Sortable.tsx +572 -0
  740. package/src/components/Sortable/containerAwareCollision.ts +125 -0
  741. package/src/components/Sortable/index.ts +13 -0
  742. package/src/components/SortableGroup/SortableGroup.module.scss +23 -0
  743. package/src/components/SortableGroup/SortableGroup.tsx +315 -0
  744. package/src/components/SortableGroup/index.ts +4 -0
  745. package/src/components/SortableGroup/moveSortableItem.ts +39 -0
  746. package/src/components/Sparkline/Sparkline.module.scss +28 -0
  747. package/src/components/Sparkline/Sparkline.tokens.scss +7 -0
  748. package/src/components/Sparkline/Sparkline.tsx +133 -0
  749. package/src/components/Sparkline/index.ts +2 -0
  750. package/src/components/Sparkline/sparklineModel.ts +119 -0
  751. package/src/components/Split/Split.module.scss +158 -0
  752. package/src/components/Split/Split.tokens.scss +10 -0
  753. package/src/components/Split/Split.tsx +231 -0
  754. package/src/components/Split/index.ts +2 -0
  755. package/src/components/Stack/Stack.module.scss +67 -0
  756. package/src/components/Stack/Stack.tokens.scss +11 -0
  757. package/src/components/Stack/Stack.tsx +100 -0
  758. package/src/components/Stack/index.ts +2 -0
  759. package/src/components/StagePath/StagePath.module.scss +255 -0
  760. package/src/components/StagePath/StagePath.tokens.scss +43 -0
  761. package/src/components/StagePath/StagePath.tsx +176 -0
  762. package/src/components/StagePath/index.ts +2 -0
  763. package/src/components/StatGroup/StatGroup.module.scss +6 -0
  764. package/src/components/StatGroup/StatGroup.tsx +70 -0
  765. package/src/components/StatGroup/StatGroupContext.ts +10 -0
  766. package/src/components/StatGroup/StatTile.module.scss +99 -0
  767. package/src/components/StatGroup/StatTile.tokens.scss +35 -0
  768. package/src/components/StatGroup/StatTile.tsx +143 -0
  769. package/src/components/StatGroup/index.ts +4 -0
  770. package/src/components/Sticky/Sticky.module.scss +98 -0
  771. package/src/components/Sticky/Sticky.tokens.scss +17 -0
  772. package/src/components/Sticky/Sticky.tsx +103 -0
  773. package/src/components/Sticky/index.ts +2 -0
  774. package/src/components/Switch/Switch.module.scss +182 -0
  775. package/src/components/Switch/Switch.tokens.scss +59 -0
  776. package/src/components/Switch/Switch.tsx +214 -0
  777. package/src/components/Switch/index.ts +2 -0
  778. package/src/components/Table/Table.module.scss +308 -0
  779. package/src/components/Table/Table.tokens.scss +79 -0
  780. package/src/components/Table/Table.tsx +381 -0
  781. package/src/components/Table/index.ts +15 -0
  782. package/src/components/TableOfContents/TableOfContents.module.scss +83 -0
  783. package/src/components/TableOfContents/TableOfContents.tokens.scss +26 -0
  784. package/src/components/TableOfContents/TableOfContents.tsx +192 -0
  785. package/src/components/TableOfContents/activeSection.ts +18 -0
  786. package/src/components/TableOfContents/index.ts +2 -0
  787. package/src/components/Tabs/Tabs.module.scss +359 -0
  788. package/src/components/Tabs/Tabs.tokens.scss +50 -0
  789. package/src/components/Tabs/Tabs.tsx +538 -0
  790. package/src/components/Tabs/index.ts +2 -0
  791. package/src/components/Text/Text.module.scss +115 -0
  792. package/src/components/Text/Text.tokens.scss +55 -0
  793. package/src/components/Text/Text.tsx +197 -0
  794. package/src/components/Text/index.ts +2 -0
  795. package/src/components/Textarea/Textarea.module.scss +94 -0
  796. package/src/components/Textarea/Textarea.tokens.scss +48 -0
  797. package/src/components/Textarea/Textarea.tsx +246 -0
  798. package/src/components/Textarea/index.ts +2 -0
  799. package/src/components/Thread/Thread.module.scss +161 -0
  800. package/src/components/Thread/Thread.tokens.scss +32 -0
  801. package/src/components/Thread/Thread.tsx +176 -0
  802. package/src/components/Thread/index.ts +2 -0
  803. package/src/components/TimeField/TimeField.module.scss +232 -0
  804. package/src/components/TimeField/TimeField.tsx +793 -0
  805. package/src/components/TimeField/index.ts +2 -0
  806. package/src/components/Timeline/Timeline.module.scss +58 -0
  807. package/src/components/Timeline/Timeline.tokens.scss +13 -0
  808. package/src/components/Timeline/Timeline.tsx +53 -0
  809. package/src/components/Timeline/index.ts +2 -0
  810. package/src/components/Title/Title.module.scss +84 -0
  811. package/src/components/Title/Title.tokens.scss +37 -0
  812. package/src/components/Title/Title.tsx +150 -0
  813. package/src/components/Title/index.ts +2 -0
  814. package/src/components/Toast/Toast.module.scss +299 -0
  815. package/src/components/Toast/Toast.tokens.scss +65 -0
  816. package/src/components/Toast/Toast.tsx +122 -0
  817. package/src/components/Toast/ToastViewport.tsx +166 -0
  818. package/src/components/Toast/api.ts +175 -0
  819. package/src/components/Toast/index.ts +5 -0
  820. package/src/components/Toast/store.ts +133 -0
  821. package/src/components/Toast/useToastTimer.ts +94 -0
  822. package/src/components/Tooltip/Tooltip.module.scss +99 -0
  823. package/src/components/Tooltip/Tooltip.tokens.scss +23 -0
  824. package/src/components/Tooltip/Tooltip.tsx +341 -0
  825. package/src/components/Tooltip/index.ts +2 -0
  826. package/src/components/TopBar/TopBar.module.scss +235 -0
  827. package/src/components/TopBar/TopBar.tokens.scss +79 -0
  828. package/src/components/TopBar/TopBar.tsx +87 -0
  829. package/src/components/TopBar/TopBarEnd.tsx +20 -0
  830. package/src/components/TopBar/TopBarIconButton.tsx +86 -0
  831. package/src/components/TopBar/TopBarSearch.tsx +319 -0
  832. package/src/components/TopBar/TopBarSearchPanel.tsx +268 -0
  833. package/src/components/TopBar/TopBarStart.tsx +20 -0
  834. package/src/components/TopBar/index.ts +11 -0
  835. package/src/components/Tour/Spotlight.tsx +121 -0
  836. package/src/components/Tour/Tour.module.scss +210 -0
  837. package/src/components/Tour/Tour.tokens.scss +58 -0
  838. package/src/components/Tour/Tour.tsx +617 -0
  839. package/src/components/Tour/index.ts +2 -0
  840. package/src/components/Tour/useTourTarget.ts +115 -0
  841. package/src/components/TreeView/TreeView.module.scss +268 -0
  842. package/src/components/TreeView/TreeView.tokens.scss +36 -0
  843. package/src/components/TreeView/TreeView.tsx +536 -0
  844. package/src/components/TreeView/index.ts +3 -0
  845. package/src/components/TreeView/treeModel.ts +224 -0
  846. package/src/components/VisuallyHidden/VisuallyHidden.module.scss +7 -0
  847. package/src/components/VisuallyHidden/VisuallyHidden.tsx +29 -0
  848. package/src/components/VisuallyHidden/index.ts +2 -0
  849. package/src/components/WidgetPreview/WidgetPreview.module.scss +11 -0
  850. package/src/components/WidgetPreview/WidgetPreview.tokens.scss +26 -0
  851. package/src/components/WidgetPreview/WidgetPreview.tsx +36 -0
  852. package/src/components/WidgetPreview/WidgetShape.module.scss +140 -0
  853. package/src/components/WidgetPreview/WidgetShape.tsx +159 -0
  854. package/src/components/WidgetPreview/index.ts +2 -0
  855. package/src/components/_internal/Trend.tsx +27 -0
  856. package/src/components/_internal/choiceTile.scss +74 -0
  857. package/src/components/_internal/collapse.scss +10 -0
  858. package/src/components/_internal/collapse.ts +58 -0
  859. package/src/components/_internal/currency.ts +257 -0
  860. package/src/components/_internal/dragAnnouncements.ts +200 -0
  861. package/src/components/_internal/fieldWiring.ts +225 -0
  862. package/src/components/_internal/gridSpan.ts +54 -0
  863. package/src/components/_internal/isoDay.ts +35 -0
  864. package/src/components/_internal/letterKey.ts +26 -0
  865. package/src/components/_internal/overlay/focusRestore.ts +21 -0
  866. package/src/components/_internal/overlay/index.ts +12 -0
  867. package/src/components/_internal/overlay/tourHold.ts +23 -0
  868. package/src/components/_internal/overlay/useFocusTrap.ts +120 -0
  869. package/src/components/_internal/overlay/useInOverlay.ts +81 -0
  870. package/src/components/_internal/overlay/useOverlayStack.ts +232 -0
  871. package/src/components/_internal/overlay/useScrollLock.ts +112 -0
  872. package/src/components/_internal/refs.ts +48 -0
  873. package/src/components/_internal/statusColor.ts +26 -0
  874. package/src/components/_internal/timeUtils.ts +23 -0
  875. package/src/components/_internal/useAsyncOptions.ts +145 -0
  876. package/src/components/_internal/useClippedTooltip.ts +50 -0
  877. package/src/components/_internal/useControllableState.ts +50 -0
  878. package/src/components.manifest.json +1141 -0
  879. package/src/hooks/useBelowBreakpoint.ts +58 -0
  880. package/src/i18n/I18nProvider.tsx +52 -0
  881. package/src/i18n/LocaleProvider.tsx +17 -0
  882. package/src/i18n/en.ts +648 -0
  883. package/src/i18n/format.ts +71 -0
  884. package/src/i18n/index.ts +13 -0
  885. package/src/i18n/messages.ts +1275 -0
  886. package/src/i18n/ru.ts +677 -0
  887. package/src/i18n/useLocale.ts +19 -0
  888. package/src/i18n/useTranslation.ts +58 -0
  889. package/src/index.ts +913 -0
  890. package/src/palette/index.ts +2 -0
  891. package/src/palette/palette.ts +108 -0
  892. package/src/styles/dark.scss +4 -0
  893. package/src/styles/global.scss +7 -0
  894. package/src/styles/mixins.scss +143 -0
  895. package/src/styles/reset.scss +78 -0
  896. package/src/styles/tokens.scss +18 -0
  897. package/src/styles/typography.scss +44 -0
@@ -0,0 +1,1270 @@
1
+ import { Fragment, forwardRef, useCallback, useEffect, useId, useRef, useState } from 'react';
2
+ import type {
3
+ FocusEvent as ReactFocusEvent,
4
+ HTMLAttributes,
5
+ KeyboardEvent as ReactKeyboardEvent,
6
+ PointerEvent as ReactPointerEvent,
7
+ ReactNode,
8
+ } from 'react';
9
+ import clsx from 'clsx';
10
+ import { useTranslation } from '../../i18n/useTranslation';
11
+ import { Accordion } from '../Accordion';
12
+ import type { CollapseBreakpoint } from '../_internal/collapse';
13
+ import { overlayStack, useFloatingSurface } from '../_internal/overlay';
14
+ import { mergeAriaDescribedby, mergeRefs, sanitizeId } from '../_internal/refs';
15
+ import {
16
+ DASHBOARD_COLUMNS,
17
+ applyMove,
18
+ applyResize,
19
+ cellFromPoint,
20
+ clampGhost,
21
+ insertionCell,
22
+ reorderSection,
23
+ toggleSection,
24
+ } from './engine';
25
+ import type {
26
+ ContainerRef,
27
+ DashboardCanvasValue,
28
+ DashboardItemConstraints,
29
+ DashboardPlacement,
30
+ } from './engine';
31
+ import { DashboardCanvasItem, sortByPosition } from './DashboardCanvasItem';
32
+ import type { CanvasGestures, ResizeEdge } from './DashboardCanvasItem';
33
+ import { DashboardCanvasSection } from './DashboardCanvasSection';
34
+ import styles from './DashboardCanvas.module.scss';
35
+
36
+ /**
37
+ * Per-item size constraints lookup passed to {@link DashboardCanvas}. Either a
38
+ * plain record keyed by item id, or a function for computed/dynamic limits.
39
+ * An item with no entry (and no matching key) gets `minW: 1`, `minH: 1`, no
40
+ * `maxH`.
41
+ */
42
+ export type DashboardCanvasConstraintsProp =
43
+ | Record<string, DashboardItemConstraints>
44
+ | ((id: string | number) => DashboardItemConstraints | undefined);
45
+
46
+ export interface DashboardCanvasProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> {
47
+ /**
48
+ * The controlled layout: top-level items plus an ordered array of
49
+ * collapsible sections. `DashboardCanvas` never mutates this value — every
50
+ * change flows out through `onChange`.
51
+ */
52
+ value: DashboardCanvasValue;
53
+ /**
54
+ * Fires once per completed gesture — a drop, a resize end, a collapse
55
+ * toggle, a section reorder, or a cross-container move — with the whole
56
+ * next `value`. Omit for a read-only or fully static canvas; without it,
57
+ * drags preview live but nothing persists on drop.
58
+ */
59
+ onChange?: (next: DashboardCanvasValue) => void;
60
+ /**
61
+ * Renders the body of an item by id. Called for every item on every
62
+ * render — including items inside a COLLAPSED section (its body stays
63
+ * mounted and `inert`, not unmounted, so Accordion's own collapse
64
+ * animation has real content to animate) — and once more for the dragged
65
+ * item mid-gesture (the cursor-following ghost). Keep it pure; a widget
66
+ * that fetches on mount pays that cost even while its section is
67
+ * collapsed. The cell itself is a bare positioned box with no
68
+ * background/border/radius/shadow of its own and no minimum height beyond
69
+ * its `h` row-span — that chrome belongs here, in whatever this returns
70
+ * (typically a `<Card>`); size the content to the cell, or accept that a
71
+ * shorter widget in a taller cell shows bare (dotted, in edit mode) space
72
+ * below it.
73
+ */
74
+ renderItem: (id: string | number) => ReactNode;
75
+ /**
76
+ * Optional extra controls in a section's header (a title editor, a
77
+ * `DropdownMenu` trigger) rendered by section id, to the right of the
78
+ * title. Keep it small — the header row is not a general-purpose toolbar.
79
+ * Pointer presses inside the slot never start a band-reorder drag.
80
+ */
81
+ renderSectionHeader?: (id: string | number) => ReactNode;
82
+ /**
83
+ * Id of a section whose band to highlight (scrolls into view, then a ring
84
+ * with outward waves, same as `Highlight`) — e.g. a section the user just added. Collapsed bands
85
+ * highlight too. Does not move focus. Clear it in `onSectionHighlightDone`;
86
+ * setting the same id again after that re-triggers.
87
+ */
88
+ highlightedSectionId?: string | number;
89
+ /** Fires once the highlight run on `highlightedSectionId` has finished. */
90
+ onSectionHighlightDone?: () => void;
91
+ /** Per-item size constraints, consulted when clamping resize gestures. */
92
+ constraints?: DashboardCanvasConstraintsProp;
93
+ /**
94
+ * Column count of every container grid (top level and each section's
95
+ * sub-grid). All placement/resize bounds clamp against it. Minimum 1;
96
+ * fractional or sub-1 values are the consumer's bug, not validated.
97
+ *
98
+ * Layouts are NOT converted between column counts — a `value` saved at one
99
+ * `columns` renders at half width when the count doubles. Schemas written
100
+ * for the earlier 12-column grid (e.g. the eocrm layout-v2 spec) either
101
+ * pass `columns={12}` to match, or migrate their saved placements to 24.
102
+ * @default 24
103
+ */
104
+ columns?: number;
105
+ /**
106
+ * Container width at (inclusive) and below which every container grid
107
+ * re-templates to one column and pointer + keyboard editing turns off:
108
+ * `sm` 480px / `md` 640px / `lg` 768px, measured against the canvas's OWN
109
+ * width (container query), not the viewport. Section collapse toggles keep
110
+ * working at any width. @default 'md'
111
+ */
112
+ stackBelow?: CollapseBreakpoint;
113
+ /**
114
+ * View-only mode: identical geometry, but no drag/resize wiring, no resize
115
+ * handles, and no keyboard editing — the canvas renders `value` and lets
116
+ * section collapse toggles keep working. The canvas also disables editing
117
+ * on its own, without this prop, once its own width drops to or below the
118
+ * `stackBelow` breakpoint (single-column stack) — `readOnly` is for a
119
+ * consumer-chosen view mode, the width gate is automatic. @default false
120
+ */
121
+ readOnly?: boolean;
122
+ }
123
+
124
+ const TOP_CONTAINER: ContainerRef = { kind: 'top' };
125
+
126
+ /** Per-breakpoint stack class (#318 pattern) — gates the @container step rules. */
127
+ const stackClass: Record<CollapseBreakpoint, string> = {
128
+ sm: styles.stackSm,
129
+ md: styles.stackMd,
130
+ lg: styles.stackLg,
131
+ };
132
+
133
+ /** Stable registry key for a container (object identity is per-render). */
134
+ function containerKey(cref: ContainerRef): string {
135
+ return cref.kind === 'top' ? 'top' : `s:${String(cref.id)}`;
136
+ }
137
+
138
+ /** The items of one container within a value (engine's private lookup, re-derived). */
139
+ function itemsOf(value: DashboardCanvasValue, cref: ContainerRef): DashboardPlacement[] {
140
+ return (
141
+ (cref.kind === 'top'
142
+ ? value.items
143
+ : value.sections.find((section) => section.id === cref.id)?.items) ?? []
144
+ );
145
+ }
146
+
147
+ /**
148
+ * A collapsed section's body is never a drop target — its `Accordion.Content`
149
+ * stays mounted (CSS height animation, not a DOM unmount) so this must be
150
+ * checked explicitly rather than relying on the container being unregistered.
151
+ */
152
+ function isCollapsedSection(value: DashboardCanvasValue, cref: ContainerRef): boolean {
153
+ return cref.kind === 'section' && !!value.sections.find((s) => s.id === cref.id)?.collapsed;
154
+ }
155
+
156
+ /** Last occupied row end (max `y + h`) of a container, `excludeId` left out. */
157
+ function bottomOf(
158
+ value: DashboardCanvasValue,
159
+ cref: ContainerRef,
160
+ excludeId?: string | number,
161
+ ): number {
162
+ return itemsOf(value, cref).reduce(
163
+ (max, p) => (p.id === excludeId ? max : Math.max(max, p.y + p.h)),
164
+ 0,
165
+ );
166
+ }
167
+
168
+ /**
169
+ * Screen-px a pointer must travel before a press arms as a drag (Sortable's
170
+ * activation-distance precedent) — presses and clicks inside item bodies and
171
+ * section headers pass through untouched below it.
172
+ */
173
+ const DRAG_ACTIVATION_PX = 5;
174
+
175
+ /**
176
+ * Fallback row unit when `grid-auto-rows` doesn't resolve to a px length
177
+ * (jsdom, where the square-cell `cqw` calc in `--dashboard-canvas-row` never
178
+ * evaluates). Mirrors `--dashboard-canvas-row-stacked` (`--space-12`, 48px).
179
+ */
180
+ const FALLBACK_ROW_PX = 48;
181
+
182
+ /**
183
+ * Editing-gate width thresholds (px) — JS mirror of the `@container
184
+ * dashboard-canvas (max-width: …)` step rules in DashboardCanvas.module.scss
185
+ * (`_internal/collapse.scss` constants). `max-width` is INCLUSIVE, so a width
186
+ * exactly at the threshold already single-columns — the gate below must match
187
+ * with `<=`, not `<`. SCSS constants aren't readable from TS, so these
188
+ * literals and those are kept in sync by hand, same as the
189
+ * `FALLBACK_ROW_PX`/`--dashboard-canvas-row-stacked` pairing above.
190
+ */
191
+ const STACK_GATE_PX: Record<CollapseBreakpoint, number> = { sm: 480, md: 640, lg: 768 };
192
+
193
+ /** Live px geometry of one container grid, re-measured per pointermove. */
194
+ function measureGrid(el: HTMLElement, columns: number) {
195
+ const rect = el.getBoundingClientRect();
196
+ const style = getComputedStyle(el);
197
+ const gap = Number.parseFloat(style.columnGap) || 0;
198
+ const rowHeight = Number.parseFloat(style.gridAutoRows) || FALLBACK_ROW_PX;
199
+ const colWidth = (rect.width - gap * (columns - 1)) / columns;
200
+ return { rect, colWidth, rowHeight, gap };
201
+ }
202
+
203
+ /**
204
+ * Capture always targets the ROOT: item elements REMOUNT when a preview moves
205
+ * them across containers, and a remount implicitly releases their capture —
206
+ * the pointerup would then never arrive and the drag would stick (FlowCanvas
207
+ * root-capture precedent). jsdom throws on inactive pointers, hence the guard.
208
+ */
209
+ function capturePointer(el: Element | null, pointerId: number) {
210
+ try {
211
+ el?.setPointerCapture(pointerId);
212
+ } catch {
213
+ /* jsdom */
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Portal roots of the stack-registered blocking overlays (Modal / Drawer /
219
+ * Lightbox — the ones that inert the background). A canvas inside one of
220
+ * these is that overlay's CONTENT and keeps editing while it's open; a canvas
221
+ * outside all of them is background whenever the stack is non-empty (#362).
222
+ */
223
+ const OVERLAY_HOST_SELECTOR =
224
+ '[data-modal-portal-root], [data-drawer-portal-root], [data-lightbox-portal-root]';
225
+
226
+ // In-flight gesture bookkeeping lives in a ref (FlowCanvas precedent): the
227
+ // pointer stream mutates it without re-render churn; `live` state below is
228
+ // the render-facing projection, discarded wholesale on cancel.
229
+ interface MoveDrag {
230
+ kind: 'move';
231
+ pointerId: number;
232
+ id: string | number;
233
+ from: ContainerRef;
234
+ homeX: number;
235
+ homeY: number;
236
+ startX: number;
237
+ startY: number;
238
+ /** Pointer offset inside the item at pointerdown — keeps the ghost under the grab point. */
239
+ grabDX: number;
240
+ grabDY: number;
241
+ ghostW: number;
242
+ ghostH: number;
243
+ moved: boolean;
244
+ target: ContainerRef;
245
+ targetKey: string;
246
+ cell: { x: number; y: number } | null;
247
+ previewKey: string | null;
248
+ preview: DashboardCanvasValue | null;
249
+ }
250
+ interface ResizeDrag {
251
+ kind: 'resize';
252
+ pointerId: number;
253
+ id: string | number;
254
+ container: ContainerRef;
255
+ containerKey: string;
256
+ edge: ResizeEdge;
257
+ startX: number;
258
+ startY: number;
259
+ startW: number;
260
+ startH: number;
261
+ lastW: number;
262
+ lastH: number;
263
+ preview: DashboardCanvasValue | null;
264
+ }
265
+ interface SectionDrag {
266
+ kind: 'section';
267
+ pointerId: number;
268
+ id: string | number;
269
+ fromIndex: number;
270
+ startX: number;
271
+ startY: number;
272
+ moved: boolean;
273
+ /** Insertion slot among ALL bands (0..N), indicator position. */
274
+ slot: number;
275
+ }
276
+ type DragState = MoveDrag | ResizeDrag | SectionDrag;
277
+
278
+ /**
279
+ * Keyboard pick-up state (Enter/Space on a focused item). `x`/`y` track the
280
+ * REQUESTED cell exactly like a pointer drag tracks the cursor cell — the
281
+ * engine's compaction may render the item elsewhere, but requested tracking
282
+ * is what lets repeated ArrowDown presses tunnel past the compaction pull
283
+ * and reach the next container. Committed via the same applyMove call and
284
+ * `commit` choke point as the pointer path.
285
+ */
286
+ interface KeyboardPick {
287
+ id: string | number;
288
+ from: ContainerRef;
289
+ homeX: number;
290
+ homeY: number;
291
+ /** Item dims, for clamping the requested x against the columns bound. */
292
+ w: number;
293
+ container: ContainerRef;
294
+ x: number;
295
+ y: number;
296
+ /** Engine preview of dropping right now — the rendered layout while picked. */
297
+ preview: DashboardCanvasValue;
298
+ }
299
+
300
+ /** Render-facing gesture projection: the engine preview IS the rendered layout. */
301
+ type LiveState =
302
+ | {
303
+ kind: 'move';
304
+ id: string | number;
305
+ value: DashboardCanvasValue;
306
+ /** Dragged item's row span — sizes the trailing drop space. */
307
+ h: number;
308
+ /** `containerKey` of the hovered drop target — only it gets the trailing space. */
309
+ target: string;
310
+ /** Canvas-relative px box for the cursor-following ghost. */
311
+ ghost: { left: number; top: number; width: number; height: number };
312
+ }
313
+ | { kind: 'resize'; id: string | number; value: DashboardCanvasValue }
314
+ | { kind: 'section'; id: string | number; slot: number };
315
+
316
+ /**
317
+ * Datadog-style 2D snap-grid dashboard: draggable, resizable items and collapsible sections on a configurable-column grid. Always controlled.
318
+ * @see docs/components/DashboardCanvas.md
319
+ */
320
+ export const DashboardCanvas = forwardRef<HTMLDivElement, DashboardCanvasProps>(
321
+ function DashboardCanvas(
322
+ {
323
+ value,
324
+ onChange,
325
+ renderItem,
326
+ renderSectionHeader,
327
+ highlightedSectionId,
328
+ onSectionHighlightDone,
329
+ constraints,
330
+ columns = DASHBOARD_COLUMNS,
331
+ stackBelow = 'md',
332
+ readOnly = false,
333
+ className,
334
+ style,
335
+ 'aria-describedby': ariaDescribedby,
336
+ onPointerMove: onPointerMoveProp,
337
+ onPointerUp: onPointerUpProp,
338
+ onPointerCancel: onPointerCancelProp,
339
+ onLostPointerCapture: onLostPointerCaptureProp,
340
+ onBlur: onBlurProp,
341
+ ...rest
342
+ },
343
+ ref,
344
+ ) {
345
+ const t = useTranslation();
346
+
347
+ const rootRef = useRef<HTMLDivElement | null>(null);
348
+ const containersRef = useRef(new Map<string, { el: HTMLElement; cref: ContainerRef }>());
349
+ const bandsRef = useRef(new Map<string | number, HTMLElement>());
350
+ const dragRef = useRef<DragState | null>(null);
351
+ const [live, setLive] = useState<LiveState | null>(null);
352
+ const [pick, setPick] = useState<KeyboardPick | null>(null);
353
+ // Below-md editing gate. Starts wide: jsdom has no ResizeObserver, and a
354
+ // freshly-mounted (possibly still-hidden) root reports 0 width — both
355
+ // stay wide so existing gesture tests keep exercising the full surface
356
+ // (FlowCanvas's "no RO / zero size -> untrustworthy" precedent).
357
+ const [isNarrow, setIsNarrow] = useState(false);
358
+ const editingEnabled = !readOnly && !isNarrow;
359
+ const uid = sanitizeId(useId());
360
+ const instructionsId = `dashboard-instructions-${uid}`;
361
+
362
+ // Nonce-keyed live region (FlowCanvas precedent): the nonce forces a DOM
363
+ // mutation for back-to-back identical messages, and keys a child <span>
364
+ // so the region node itself stays stable across announcements.
365
+ const [announcement, setAnnouncement] = useState({ text: '', nonce: 0 });
366
+ const announce = useCallback(
367
+ (message: string) => setAnnouncement((prev) => ({ text: message, nonce: prev.nonce + 1 })),
368
+ [],
369
+ );
370
+
371
+ // A keyboard step can remount or DOM-reorder the focused element (a
372
+ // cross-container move re-parents the item; a band reorder re-orders the
373
+ // section nodes), silently dropping focus to <body> and stranding the
374
+ // keyboard user. Handlers flag what to re-focus; the post-commit effect
375
+ // below restores it (FlowCanvas focus-reclaim precedent).
376
+ const reclaimRef = useRef<{ kind: 'item' | 'section'; id: string | number } | null>(null);
377
+
378
+ const setContainerEl = useCallback((cref: ContainerRef, el: HTMLElement | null) => {
379
+ const key = containerKey(cref);
380
+ if (el) containersRef.current.set(key, { el, cref });
381
+ else containersRef.current.delete(key);
382
+ }, []);
383
+ const setBandEl = useCallback((id: string | number, el: HTMLElement | null) => {
384
+ if (el) bandsRef.current.set(id, el);
385
+ else bandsRef.current.delete(id);
386
+ }, []);
387
+
388
+ const constraintsFor = (id: string | number): DashboardItemConstraints | undefined =>
389
+ typeof constraints === 'function' ? constraints(id) : constraints?.[String(id)];
390
+
391
+ // Single choke point for EVERY gesture commit: the engine ops return
392
+ // `value` by reference when the gesture resolves to the current layout
393
+ // (clamped resize, unmoved band, vanished id), so one identity check
394
+ // kills every no-op onChange.
395
+ const commit = (next: DashboardCanvasValue) => {
396
+ if (next !== value) onChange?.(next);
397
+ };
398
+
399
+ const handleToggleSection = (sectionId: string | number) => {
400
+ // Collapse stays active in readOnly AND below the narrow-editing
401
+ // threshold — it's navigation, not editing.
402
+ commit(toggleSection(value, sectionId));
403
+ };
404
+
405
+ const highlightKnown = value.sections.some((s) => s.id === highlightedSectionId);
406
+ useEffect(() => {
407
+ if (
408
+ highlightedSectionId != null &&
409
+ !highlightKnown &&
410
+ process.env.NODE_ENV !== 'production'
411
+ ) {
412
+ console.warn(
413
+ `[DashboardCanvas] highlightedSectionId "${highlightedSectionId}" matches no section.`,
414
+ );
415
+ }
416
+ }, [highlightedSectionId, highlightKnown]);
417
+
418
+ // Sections are composed from a single shared `<Accordion type="multiple">`
419
+ // (one Item per band) — `value` mirrors which bands are OPEN (inverse of
420
+ // `collapsed`). Reads `value`, not the render-facing `shown` (computed
421
+ // below): collapsed never changes mid-gesture, and `value` avoids a
422
+ // declaration-order dependency on `shown`. Accordion always hands back
423
+ // the WHOLE next open-ids array on a click, never which id changed;
424
+ // exactly one differs per click (its own toggle is the only way this
425
+ // fires), so the symmetric difference recovers it.
426
+ const openSectionIds = value.sections
427
+ .filter((section) => !section.collapsed)
428
+ .map((section) => String(section.id));
429
+ const handleSectionOpenChange = (next: string[]) => {
430
+ const toggledId =
431
+ next.find((id) => !openSectionIds.includes(id)) ??
432
+ openSectionIds.find((id) => !next.includes(id));
433
+ const section = value.sections.find((s) => String(s.id) === toggledId);
434
+ if (section) handleToggleSection(section.id);
435
+ };
436
+
437
+ // A Modal/Popover/DropdownMenu rendered inside renderItem portals its DOM
438
+ // to document.body, but its events bubble through the REACT tree into the
439
+ // item's and root's handlers — the press physically landed on the
440
+ // overlay's backdrop or panel, never the canvas (the backdrop DOES win
441
+ // hit-testing; FlowCanvas #290 is the same class on its background pan).
442
+ // A target outside the canvas root is never a gesture (#362).
443
+ const isPortalEvent = (event: ReactPointerEvent) =>
444
+ !(event.target instanceof Node) || !rootRef.current?.contains(event.target);
445
+
446
+ // A blocking overlay is open and this canvas is not inside any overlay
447
+ // portal — the canvas is inert background, so neither a pointer press nor
448
+ // a keyboard pick may arm (#362).
449
+ const underOpenOverlay = () =>
450
+ overlayStack.topDepth() > 0 && !rootRef.current?.closest(OVERLAY_HOST_SELECTOR);
451
+
452
+ // A pending press has no capture (capture waits for the 5px arm), so a
453
+ // pointerup swallowed by an overlay OUTSIDE the React tree never reaches
454
+ // the root handlers and would leave dragRef stale — hijacking later
455
+ // hovers into a buttonless drag (a mouse reuses its pointerId) or
456
+ // blocking arming outright (touch ids retire) (#363). Window CAPTURE
457
+ // listeners see the physical release before any stopPropagation below;
458
+ // clearing an UNMOVED press is behavior-preserving (unmoved presses
459
+ // commit nothing), and a moved drag is capture-owned — its own root
460
+ // handlers clear the ref before this check could matter.
461
+ const armPendingRelease = (drag: MoveDrag | SectionDrag) => {
462
+ const release = (event: PointerEvent) => {
463
+ // Only the press's OWN pointer resolves it — a stray second pointer
464
+ // (palm/touch) releasing elsewhere must neither clear a still-live
465
+ // press nor detach these listeners (that would reopen the swallowed-
466
+ // release hole for the own pointer).
467
+ if (event.pointerId !== drag.pointerId) return;
468
+ window.removeEventListener('pointerup', release, true);
469
+ window.removeEventListener('pointercancel', release, true);
470
+ if (dragRef.current === drag && !drag.moved) dragRef.current = null;
471
+ };
472
+ window.addEventListener('pointerup', release, true);
473
+ window.addEventListener('pointercancel', release, true);
474
+ };
475
+
476
+ // --- gesture starts (single `editingEnabled` choke point: readOnly prop OR narrow width) ---
477
+ const onMovePointerDown = (
478
+ event: ReactPointerEvent<HTMLDivElement>,
479
+ placement: DashboardPlacement,
480
+ container: ContainerRef,
481
+ ) => {
482
+ // One gesture at a time: a second pointer going down mid-drag must not
483
+ // overwrite the state (FlowCanvas discipline), and a keyboard pick owns
484
+ // the canvas until it drops or cancels. No preventDefault — clicks
485
+ // inside item bodies pass through until the drag arms.
486
+ if (!editingEnabled || event.button !== 0 || dragRef.current || pick) return;
487
+ if (isPortalEvent(event) || underOpenOverlay()) return;
488
+ const el = event.currentTarget;
489
+ const rect = el.getBoundingClientRect();
490
+ const drag: MoveDrag = {
491
+ kind: 'move',
492
+ pointerId: event.pointerId,
493
+ id: placement.id,
494
+ from: container,
495
+ homeX: placement.x,
496
+ homeY: placement.y,
497
+ startX: event.clientX,
498
+ startY: event.clientY,
499
+ grabDX: event.clientX - rect.left,
500
+ grabDY: event.clientY - rect.top,
501
+ ghostW: rect.width,
502
+ ghostH: rect.height,
503
+ moved: false,
504
+ target: container,
505
+ targetKey: containerKey(container),
506
+ cell: null,
507
+ previewKey: null,
508
+ preview: null,
509
+ };
510
+ dragRef.current = drag;
511
+ armPendingRelease(drag);
512
+ // No capture yet — capture retargets the eventual click to the root,
513
+ // so it must wait until the drag arms (clicks pass through until then).
514
+ };
515
+
516
+ const onResizePointerDown = (
517
+ event: ReactPointerEvent<HTMLDivElement>,
518
+ placement: DashboardPlacement,
519
+ container: ContainerRef,
520
+ edge: ResizeEdge,
521
+ ) => {
522
+ if (!editingEnabled || event.button !== 0 || dragRef.current || pick) return;
523
+ if (isPortalEvent(event) || underOpenOverlay()) return;
524
+ // The handle sits inside the move-drag surface — never arm both.
525
+ event.stopPropagation();
526
+ dragRef.current = {
527
+ kind: 'resize',
528
+ pointerId: event.pointerId,
529
+ id: placement.id,
530
+ container,
531
+ containerKey: containerKey(container),
532
+ edge,
533
+ startX: event.clientX,
534
+ startY: event.clientY,
535
+ startW: placement.w,
536
+ startH: placement.h,
537
+ lastW: placement.w,
538
+ lastH: placement.h,
539
+ preview: null,
540
+ };
541
+ // Handles have no click semantics — capture immediately (on the root).
542
+ capturePointer(rootRef.current, event.pointerId);
543
+ };
544
+
545
+ const onHeaderPointerDown = (
546
+ event: ReactPointerEvent<HTMLDivElement>,
547
+ sectionId: string | number,
548
+ ) => {
549
+ if (!editingEnabled || event.button !== 0 || dragRef.current || pick) return;
550
+ if (isPortalEvent(event) || underOpenOverlay()) return;
551
+ // Called only from the section's dedicated grip handle (see
552
+ // DashboardCanvasSection's remarks) — no target-based exclusion needed,
553
+ // the toggle button and the renderSectionHeader extras never wire this.
554
+ const fromIndex = value.sections.findIndex((section) => section.id === sectionId);
555
+ if (fromIndex === -1) return;
556
+ const drag: SectionDrag = {
557
+ kind: 'section',
558
+ pointerId: event.pointerId,
559
+ id: sectionId,
560
+ fromIndex,
561
+ startX: event.clientX,
562
+ startY: event.clientY,
563
+ moved: false,
564
+ slot: fromIndex,
565
+ };
566
+ dragRef.current = drag;
567
+ armPendingRelease(drag);
568
+ };
569
+
570
+ // --- pointer stream (root-level; capture retargeting still bubbles here) ---
571
+ const handleRootPointerMove = (event: ReactPointerEvent<HTMLDivElement>) => {
572
+ onPointerMoveProp?.(event);
573
+ const drag = dragRef.current;
574
+ if (!drag || event.pointerId !== drag.pointerId) return;
575
+
576
+ if (drag.kind === 'move') {
577
+ if (!drag.moved) {
578
+ // Portal-bubbled moves never ARM: an overlay opened on the press
579
+ // itself (before the 5px threshold) must not have its backdrop
580
+ // moves promote the pending drag. Once armed, capture retargets
581
+ // the stream to the root, so this can't misfire mid-drag.
582
+ if (
583
+ isPortalEvent(event) ||
584
+ Math.abs(event.clientX - drag.startX) + Math.abs(event.clientY - drag.startY) <
585
+ DRAG_ACTIVATION_PX
586
+ ) {
587
+ return;
588
+ }
589
+ drag.moved = true;
590
+ capturePointer(rootRef.current, event.pointerId);
591
+ }
592
+ // Drop target = the registered container grid under the pointer.
593
+ // Collapsed section bodies stay MOUNTED (Accordion.Content animates,
594
+ // never unmounts) and so stay registered — isCollapsedSection skips
595
+ // them explicitly here so their bands still can't be targets; in
596
+ // dead zones (headers, gaps, outside) the last hovered container
597
+ // keeps the drop.
598
+ for (const entry of containersRef.current.values()) {
599
+ if (isCollapsedSection(value, entry.cref)) continue;
600
+ const r = entry.el.getBoundingClientRect();
601
+ if (
602
+ event.clientX >= r.left &&
603
+ event.clientX <= r.right &&
604
+ event.clientY >= r.top &&
605
+ event.clientY <= r.bottom
606
+ ) {
607
+ drag.target = entry.cref;
608
+ drag.targetKey = containerKey(entry.cref);
609
+ break;
610
+ }
611
+ }
612
+ const entry = containersRef.current.get(drag.targetKey);
613
+ if (!entry) return;
614
+ const { rect, colWidth, rowHeight, gap } = measureGrid(entry.el, columns);
615
+ // Snap by the ghost's origin (pointer minus grab offset), not the
616
+ // pointer itself — grabbing a wide item mid-body must not yank its
617
+ // left edge to the cursor column.
618
+ const originX = event.clientX - drag.grabDX;
619
+ const originY = event.clientY - drag.grabDY;
620
+ const origin = cellFromPoint(originX, originY, rect, colWidth, rowHeight, gap, columns);
621
+ // #694: over another widget the POINTER picks insert-before/after
622
+ // (hit-tested against the ORIGINAL value, never the preview, so the
623
+ // preview can't flip the half under a still pointer).
624
+ const colPitch = colWidth + gap;
625
+ const rowPitch = rowHeight + gap;
626
+ const dragged = itemsOf(value, drag.from).find((p) => p.id === drag.id);
627
+ const cell =
628
+ dragged && colPitch > 0 && rowPitch > 0
629
+ ? insertionCell(
630
+ itemsOf(value, drag.target),
631
+ drag.id,
632
+ dragged.w,
633
+ (event.clientX - rect.left) / colPitch,
634
+ (event.clientY - rect.top) / rowPitch,
635
+ origin,
636
+ columns,
637
+ )
638
+ : origin;
639
+ if (
640
+ !drag.preview ||
641
+ drag.previewKey !== drag.targetKey ||
642
+ drag.cell?.x !== cell.x ||
643
+ drag.cell?.y !== cell.y
644
+ ) {
645
+ // The preview IS the engine result of the hypothetical drop — the
646
+ // exact value a pointerup right now would commit.
647
+ drag.preview = applyMove(value, drag.from, drag.target, drag.id, cell.x, cell.y, columns);
648
+ drag.previewKey = drag.targetKey;
649
+ drag.cell = cell;
650
+ }
651
+ const rootRect = rootRef.current?.getBoundingClientRect();
652
+ // #695: keep the ghost inside the root so it can't grow a scrollbar.
653
+ const ghost = clampGhost(
654
+ originX - (rootRect?.left ?? 0),
655
+ originY - (rootRect?.top ?? 0),
656
+ drag.ghostW,
657
+ drag.ghostH,
658
+ rootRect?.width ?? Infinity,
659
+ rootRect?.height ?? Infinity,
660
+ );
661
+ setLive({
662
+ kind: 'move',
663
+ id: drag.id,
664
+ value: drag.preview,
665
+ h: dragged?.h ?? 0,
666
+ target: drag.targetKey,
667
+ ghost: { ...ghost, width: drag.ghostW, height: drag.ghostH },
668
+ });
669
+ return;
670
+ }
671
+
672
+ if (drag.kind === 'resize') {
673
+ const entry = containersRef.current.get(drag.containerKey);
674
+ if (!entry) return;
675
+ const { colWidth, rowHeight, gap } = measureGrid(entry.el, columns);
676
+ const dw =
677
+ drag.edge === 's' ? 0 : Math.round((event.clientX - drag.startX) / (colWidth + gap));
678
+ const dh =
679
+ drag.edge === 'e' ? 0 : Math.round((event.clientY - drag.startY) / (rowHeight + gap));
680
+ const w = drag.startW + dw;
681
+ const h = drag.startH + dh;
682
+ if (!drag.preview || w !== drag.lastW || h !== drag.lastH) {
683
+ drag.lastW = w;
684
+ drag.lastH = h;
685
+ drag.preview = applyResize(
686
+ value,
687
+ drag.container,
688
+ drag.id,
689
+ w,
690
+ h,
691
+ constraintsFor(drag.id),
692
+ columns,
693
+ );
694
+ }
695
+ setLive({ kind: 'resize', id: drag.id, value: drag.preview });
696
+ return;
697
+ }
698
+
699
+ // Section band reorder — vertical only.
700
+ if (!drag.moved) {
701
+ // Same portal-arming guard as the move branch above.
702
+ if (
703
+ isPortalEvent(event) ||
704
+ Math.abs(event.clientX - drag.startX) + Math.abs(event.clientY - drag.startY) <
705
+ DRAG_ACTIVATION_PX
706
+ ) {
707
+ return;
708
+ }
709
+ drag.moved = true;
710
+ capturePointer(rootRef.current, event.pointerId);
711
+ }
712
+ // Insertion slot = how many band midpoints the pointer has passed.
713
+ let slot = 0;
714
+ for (const section of value.sections) {
715
+ const el = bandsRef.current.get(section.id);
716
+ if (!el) continue;
717
+ const r = el.getBoundingClientRect();
718
+ if (event.clientY > r.top + r.height / 2) slot += 1;
719
+ }
720
+ drag.slot = slot;
721
+ setLive({ kind: 'section', id: drag.id, slot });
722
+ };
723
+
724
+ const handleRootPointerUp = (event: ReactPointerEvent<HTMLDivElement>) => {
725
+ onPointerUpProp?.(event);
726
+ const drag = dragRef.current;
727
+ if (!drag || event.pointerId !== drag.pointerId) return;
728
+ // End-of-gesture cleanup ALWAYS runs (FlowCanvas precedent) so a drag
729
+ // can never get stuck; the commit below is the conditional part.
730
+ dragRef.current = null;
731
+ setLive(null);
732
+ // Commits recompute from the CURRENT `value` closure, never the cached
733
+ // preview — a controlled update mid-gesture must not be reverted.
734
+ if (drag.kind === 'move') {
735
+ if (!drag.moved || !drag.cell) return;
736
+ // No-op drop: same container, same cell → the gesture changed
737
+ // nothing, even if committing would compact an uncompacted value.
738
+ if (
739
+ containerKey(drag.from) === drag.targetKey &&
740
+ drag.cell.x === drag.homeX &&
741
+ drag.cell.y === drag.homeY
742
+ ) {
743
+ return;
744
+ }
745
+ commit(
746
+ applyMove(value, drag.from, drag.target, drag.id, drag.cell.x, drag.cell.y, columns),
747
+ );
748
+ return;
749
+ }
750
+ if (drag.kind === 'resize') {
751
+ // Requested dims unchanged → bail before applyResize, so a no-op
752
+ // gesture can't commit a compaction of an untouched value.
753
+ if (drag.lastW === drag.startW && drag.lastH === drag.startH) return;
754
+ commit(
755
+ applyResize(
756
+ value,
757
+ drag.container,
758
+ drag.id,
759
+ drag.lastW,
760
+ drag.lastH,
761
+ constraintsFor(drag.id),
762
+ columns,
763
+ ),
764
+ );
765
+ return;
766
+ }
767
+ if (!drag.moved) return;
768
+ // Slot counts ALL bands including the dragged one, so slots past it
769
+ // shift down by one when it's spliced out.
770
+ const toIndex = drag.slot > drag.fromIndex ? drag.slot - 1 : drag.slot;
771
+ commit(reorderSection(value, drag.id, toIndex));
772
+ };
773
+
774
+ const handleRootPointerCancel = (event: ReactPointerEvent<HTMLDivElement>) => {
775
+ onPointerCancelProp?.(event);
776
+ const drag = dragRef.current;
777
+ if (!drag || event.pointerId !== drag.pointerId) return;
778
+ // The system aborted the gesture — restore `value`, commit nothing.
779
+ dragRef.current = null;
780
+ setLive(null);
781
+ };
782
+
783
+ const handleLostPointerCapture = (event: ReactPointerEvent<HTMLDivElement>) => {
784
+ onLostPointerCaptureProp?.(event);
785
+ // Safety net: a normal release fires this AFTER pointerup already
786
+ // cleared the state; still holding a drag here means the capture died
787
+ // unexpectedly — abandon the gesture instead of leaving it stuck.
788
+ const drag = dragRef.current;
789
+ if (!drag || event.pointerId !== drag.pointerId) return;
790
+ dragRef.current = null;
791
+ setLive(null);
792
+ };
793
+
794
+ // --- keyboard editing — same engine calls + commit choke point as pointer ---
795
+
796
+ const cancelPick = () => {
797
+ setPick(null);
798
+ announce(t('dashboardCanvas.cancelled'));
799
+ };
800
+
801
+ /** One arrow step of a picked item; crossing a container edge moves bands. */
802
+ const stepPick = (current: KeyboardPick, key: string) => {
803
+ const dx = key === 'ArrowRight' ? 1 : key === 'ArrowLeft' ? -1 : 0;
804
+ const dy = key === 'ArrowDown' ? 1 : key === 'ArrowUp' ? -1 : 0;
805
+ // Keyboard targets = expanded containers in band order; collapsed
806
+ // sections are skipped (pointer drop-target parity).
807
+ const order: ContainerRef[] = [
808
+ TOP_CONTAINER,
809
+ ...value.sections
810
+ .filter((section) => !section.collapsed)
811
+ .map((section): ContainerRef => ({ kind: 'section', id: section.id })),
812
+ ];
813
+ const idx = order.findIndex((cref) => containerKey(cref) === containerKey(current.container));
814
+ let container = current.container;
815
+ let crossed: ContainerRef | null = null;
816
+ const x = Math.min(Math.max(current.x + dx, 0), columns - current.w);
817
+ let y = current.y + dy;
818
+ if (idx !== -1 && dy > 0 && y > bottomOf(current.preview, container, current.id)) {
819
+ // Below the container's last occupied row: enter the next band at its
820
+ // top, or clamp at the bottom when there is none.
821
+ const next = order[idx + 1];
822
+ if (next) {
823
+ container = next;
824
+ crossed = next;
825
+ y = 0;
826
+ } else {
827
+ y = bottomOf(current.preview, container, current.id);
828
+ }
829
+ } else if (idx !== -1 && dy < 0 && y < 0) {
830
+ // Above the top row: enter the previous band at its bottom.
831
+ const prev = order[idx - 1];
832
+ if (prev) {
833
+ container = prev;
834
+ crossed = prev;
835
+ y = bottomOf(current.preview, prev, current.id);
836
+ } else {
837
+ y = 0;
838
+ }
839
+ } else {
840
+ y = Math.max(y, 0);
841
+ }
842
+ const preview = applyMove(value, current.from, container, current.id, x, y, columns);
843
+ reclaimRef.current = { kind: 'item', id: current.id };
844
+ setPick({ ...current, container, x, y, preview });
845
+ if (crossed) {
846
+ announce(
847
+ crossed.kind === 'top'
848
+ ? t('dashboardCanvas.enteredTopLevel')
849
+ : t('dashboardCanvas.enteredSection', {
850
+ // `||`: the id tail is here so the announcement always names
851
+ // something; `title: ''` would have announced "Entered ".
852
+ title:
853
+ value.sections.find((section) => section.id === crossed.id)?.title ||
854
+ String(crossed.id),
855
+ }),
856
+ );
857
+ } else {
858
+ announce(
859
+ t('dashboardCanvas.movedTo', {
860
+ x: x + 1,
861
+ y: y + 1,
862
+ container:
863
+ container.kind === 'section'
864
+ ? value.sections.find((section) => section.id === container.id)?.title
865
+ : undefined,
866
+ }),
867
+ );
868
+ }
869
+ };
870
+
871
+ const onItemKeyDown = (
872
+ event: ReactKeyboardEvent<HTMLDivElement>,
873
+ placement: DashboardPlacement,
874
+ container: ContainerRef,
875
+ ) => {
876
+ // Only keys aimed at the cell itself — interactive widget content keeps
877
+ // its own keystrokes (FlowCanvas key-target discipline).
878
+ if (event.target !== event.currentTarget) return;
879
+ const { key } = event;
880
+ const activate = key === 'Enter' || key === ' ';
881
+ if (pick) {
882
+ if (pick.id !== placement.id) return; // one pick at a time
883
+ if (key === 'Escape') {
884
+ // Layered dismiss: this Escape ends the pick, never a host modal.
885
+ event.preventDefault();
886
+ event.stopPropagation();
887
+ cancelPick();
888
+ return;
889
+ }
890
+ if (activate) {
891
+ event.preventDefault();
892
+ setPick(null);
893
+ // Same-cell drop mirrors the pointer no-op guard: nothing changed,
894
+ // even if committing would compact an uncompacted value.
895
+ if (
896
+ !(
897
+ containerKey(pick.from) === containerKey(pick.container) &&
898
+ pick.x === pick.homeX &&
899
+ pick.y === pick.homeY
900
+ )
901
+ ) {
902
+ commit(applyMove(value, pick.from, pick.container, pick.id, pick.x, pick.y, columns));
903
+ }
904
+ announce(t('dashboardCanvas.dropped'));
905
+ return;
906
+ }
907
+ if (key.startsWith('Arrow')) {
908
+ event.preventDefault();
909
+ stepPick(pick, key);
910
+ }
911
+ return;
912
+ }
913
+ // A blocking overlay above the canvas gates the whole keyboard editing
914
+ // surface, not just the pick: `inert` already makes the cells
915
+ // unfocusable in the normal page case — this catches programmatic-focus
916
+ // edges (#362). The exemption is "inside ANY overlay host", so a canvas
917
+ // in a Drawer under a stacked Modal can still arm a fresh keyboard pick
918
+ // (pointer can't reach it; the mid-gesture abort covers post-arm opens)
919
+ // — deliberate residual.
920
+ if (underOpenOverlay()) return;
921
+ if (activate) {
922
+ event.preventDefault(); // Space must not scroll the page
923
+ setPick({
924
+ id: placement.id,
925
+ from: container,
926
+ container,
927
+ homeX: placement.x,
928
+ homeY: placement.y,
929
+ w: placement.w,
930
+ x: placement.x,
931
+ y: placement.y,
932
+ preview: value,
933
+ });
934
+ announce(t('dashboardCanvas.pickedUp'));
935
+ return;
936
+ }
937
+ if (event.shiftKey && key.startsWith('Arrow')) {
938
+ event.preventDefault();
939
+ const w = placement.w + (key === 'ArrowRight' ? 1 : key === 'ArrowLeft' ? -1 : 0);
940
+ const h = placement.h + (key === 'ArrowDown' ? 1 : key === 'ArrowUp' ? -1 : 0);
941
+ // Committed per keypress — no resize mode (FlowCanvas nudge precedent).
942
+ const next = applyResize(
943
+ value,
944
+ container,
945
+ placement.id,
946
+ w,
947
+ h,
948
+ constraintsFor(placement.id),
949
+ columns,
950
+ );
951
+ commit(next);
952
+ // Announce the engine-clamped size: at a constraint the layout does
953
+ // not change, but the user still needs the "you're at the limit" echo.
954
+ const result = itemsOf(next, container).find((p) => p.id === placement.id) ?? placement;
955
+ announce(t('dashboardCanvas.resized', { w: result.w, h: result.h }));
956
+ }
957
+ };
958
+
959
+ const onHeaderKeyDown = (
960
+ event: ReactKeyboardEvent<HTMLButtonElement>,
961
+ sectionId: string | number,
962
+ ) => {
963
+ if (!event.shiftKey || (event.key !== 'ArrowUp' && event.key !== 'ArrowDown')) return;
964
+ // Same keyboard-editing gate as onItemKeyDown (#362) — the focus-reclaim
965
+ // effect programmatically focuses this very trigger, so inert alone
966
+ // can't be relied on to keep keys out.
967
+ if (underOpenOverlay()) return;
968
+ // Composed onto Accordion.Trigger's own onKeyDown (it runs ours first);
969
+ // the target is always the trigger button itself — extras and the grip
970
+ // handle live outside it, so there's nothing to exclude here anymore.
971
+ const fromIndex = value.sections.findIndex((section) => section.id === sectionId);
972
+ if (fromIndex === -1) return;
973
+ event.preventDefault();
974
+ const next = reorderSection(
975
+ value,
976
+ sectionId,
977
+ fromIndex + (event.key === 'ArrowDown' ? 1 : -1),
978
+ );
979
+ if (next === value) return; // clamped at the edge — nothing moved
980
+ reclaimRef.current = { kind: 'section', id: sectionId };
981
+ commit(next);
982
+ announce(
983
+ t('dashboardCanvas.sectionMoved', {
984
+ title: value.sections[fromIndex].title,
985
+ position: next.sections.findIndex((section) => section.id === sectionId) + 1,
986
+ }),
987
+ );
988
+ };
989
+
990
+ // Focus reclaim. No dependency array — the flag is only ever raised
991
+ // alongside a state update, and the check costs a ref read. When focus
992
+ // survived (same element, still connected) this is a no-op.
993
+ useEffect(() => {
994
+ const target = reclaimRef.current;
995
+ if (!target) return;
996
+ reclaimRef.current = null;
997
+ const root = rootRef.current;
998
+ if (!root) return;
999
+ const active = document.activeElement;
1000
+ if (
1001
+ active &&
1002
+ active !== document.body &&
1003
+ active !== document.documentElement &&
1004
+ active.isConnected &&
1005
+ root.contains(active)
1006
+ ) {
1007
+ return;
1008
+ }
1009
+ // Attribute-value scan instead of a selector: ids are consumer strings
1010
+ // and CSS-escaping them buys nothing at this element count.
1011
+ const el =
1012
+ target.kind === 'item'
1013
+ ? Array.from(root.querySelectorAll<HTMLElement>('[data-dc-item]')).find(
1014
+ (node) => node.getAttribute('data-dc-item') === String(target.id),
1015
+ )
1016
+ : Array.from(root.querySelectorAll<HTMLElement>('[data-dc-section]'))
1017
+ .find((node) => node.getAttribute('data-dc-section') === String(target.id))
1018
+ ?.querySelector<HTMLElement>('button[aria-expanded]');
1019
+ el?.focus({ preventScroll: true });
1020
+ });
1021
+
1022
+ // Focus leaving the picked item cancels the pick — Tab is never trapped
1023
+ // (WCAG 2.1.2) and an unfocused pick would be uncancellable (Escape is
1024
+ // handled on the item). The reclaim flag distinguishes the transient
1025
+ // blur of a mid-step remount/reorder from a real departure.
1026
+ const handleRootBlur = (event: ReactFocusEvent<HTMLDivElement>) => {
1027
+ onBlurProp?.(event);
1028
+ if (!pick || reclaimRef.current) return;
1029
+ const target = event.target as HTMLElement;
1030
+ if (target.getAttribute?.('data-dc-item') === String(pick.id)) cancelPick();
1031
+ };
1032
+
1033
+ // Escape cancels an armed gesture and restores the current value. An
1034
+ // armed drag or keyboard pick is an Escape-consuming mode — registering
1035
+ // it as a floating surface makes a host Modal/Drawer yield that Escape
1036
+ // (FlowCanvas #282). The pick's own Escape lands on the focused item.
1037
+ const dragging = live != null;
1038
+ useFloatingSurface(dragging || pick != null);
1039
+ // Window-level Escape: cancels a pointer drag wherever focus sits, and
1040
+ // is the belt-and-braces exit for a pick whose item lost focus (the
1041
+ // item-level handler consumes Escape first when focused).
1042
+ useEffect(() => {
1043
+ if (!dragging && !pick) return undefined;
1044
+ const onKeyDown = (event: KeyboardEvent) => {
1045
+ if (event.key !== 'Escape') return;
1046
+ dragRef.current = null;
1047
+ setLive(null);
1048
+ if (pick) cancelPick();
1049
+ };
1050
+ window.addEventListener('keydown', onKeyDown);
1051
+ return () => window.removeEventListener('keydown', onKeyDown);
1052
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- cancelPick is re-created per render but only calls setters + announce
1053
+ }, [dragging, pick]);
1054
+
1055
+ // JS mirror of the CSS `@container dashboard-canvas (max-width: …)`
1056
+ // stack rule for the chosen breakpoint — width alone gates editing
1057
+ // (below it, gestures + keyboard editing turn off; collapse toggles are
1058
+ // unaffected). typeof-guard for jsdom; a reported 0 width means the root
1059
+ // isn't laid out yet (hidden mount), which stays wide for the same
1060
+ // reason FlowCanvas's node ResizeObserver treats 0×0 as unmeasured
1061
+ // rather than "small." Re-created when `stackBelow` changes — observe()
1062
+ // re-reports the current size, so the gate re-evaluates without a
1063
+ // resize.
1064
+ useEffect(() => {
1065
+ const root = rootRef.current;
1066
+ if (!root || typeof ResizeObserver === 'undefined') return undefined;
1067
+ const threshold = STACK_GATE_PX[stackBelow];
1068
+ const observer = new ResizeObserver((entries) => {
1069
+ const width = entries[0]?.contentRect.width ?? 0;
1070
+ setIsNarrow(width > 0 && width <= threshold);
1071
+ });
1072
+ observer.observe(root);
1073
+ return () => observer.disconnect();
1074
+ }, [stackBelow]);
1075
+
1076
+ // An overlay opening mid-gesture aborts it (#362): pointer capture keeps
1077
+ // the move stream flowing to the root even after a backdrop covers the
1078
+ // canvas, so without this the drag would keep going under the modal.
1079
+ // Depth is compared against the stack at gesture start, so a canvas
1080
+ // already INSIDE an open overlay is unaffected by its own host; a close
1081
+ // (depth shrinking) never aborts. Same restore as Escape.
1082
+ useEffect(() => {
1083
+ if (!dragging && !pick) return undefined;
1084
+ const baseDepth = overlayStack.topDepth();
1085
+ return overlayStack._subscribe(() => {
1086
+ if (overlayStack.topDepth() <= baseDepth) return;
1087
+ dragRef.current = null;
1088
+ setLive(null);
1089
+ if (pick) cancelPick();
1090
+ });
1091
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- cancelPick is re-created per render but only calls setters + announce
1092
+ }, [dragging, pick]);
1093
+
1094
+ // readOnly OR the narrow-width gate flipping editing off mid-gesture
1095
+ // aborts it — same restore as Escape.
1096
+ useEffect(() => {
1097
+ if (editingEnabled) return;
1098
+ dragRef.current = null;
1099
+ setLive(null);
1100
+ if (pick) {
1101
+ setPick(null);
1102
+ announce(t('dashboardCanvas.cancelled'));
1103
+ }
1104
+ // Deliberately keyed on the two raw gate inputs (not the derived
1105
+ // `editingEnabled`, recomputed every render); the closure is from the
1106
+ // render where either flipped, so `pick` is current.
1107
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- keyed on the raw gate inputs, see above
1108
+ }, [readOnly, isNarrow]);
1109
+
1110
+ // An external value change mid-pick invalidates the pick's home/from
1111
+ // coordinates (and can delete the picked item outright, which would
1112
+ // strand the canvas: every pointerdown is gated on `pick` and the
1113
+ // Escape handler lives on the now-gone item) — abort, same discipline
1114
+ // as the readOnly flip. Identity-guarded, no dependency array: a drop
1115
+ // clears `pick` before onChange in the same batch, so our own commits
1116
+ // never trip this.
1117
+ const prevValueRef = useRef(value);
1118
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- identity-guarded, runs after every commit by design
1119
+ useEffect(() => {
1120
+ if (prevValueRef.current === value) return;
1121
+ prevValueRef.current = value;
1122
+ if (!pick) return;
1123
+ setPick(null);
1124
+ announce(t('dashboardCanvas.cancelled'));
1125
+ });
1126
+
1127
+ // Render EXACTLY what the engine returned for the in-flight gesture —
1128
+ // never parallel geometry. Section band order only changes on commit.
1129
+ const shown =
1130
+ live != null && live.kind !== 'section' ? live.value : pick != null ? pick.preview : value;
1131
+ const gestures: CanvasGestures = {
1132
+ // The item/section internals only care whether editing is off, not
1133
+ // WHY — readOnly and narrow-width both render identically (no
1134
+ // handles, not editing-focusable, gestures inert).
1135
+ readOnly: !editingEnabled,
1136
+ movingId: live?.kind === 'move' ? live.id : null,
1137
+ resizingId: live?.kind === 'resize' ? live.id : null,
1138
+ pickedId: pick?.id ?? null,
1139
+ instructionsId,
1140
+ onMovePointerDown,
1141
+ onResizePointerDown,
1142
+ onItemKeyDown,
1143
+ };
1144
+
1145
+ // Band insertion indicator position (slot among N+1 gaps), suppressed
1146
+ // when dropping there would be a no-op.
1147
+ let bandSlot: number | null = null;
1148
+ if (live?.kind === 'section') {
1149
+ const fromIndex = value.sections.findIndex((section) => section.id === live.id);
1150
+ const toIndex = live.slot > fromIndex ? live.slot - 1 : live.slot;
1151
+ if (toIndex !== fromIndex) bandSlot = live.slot;
1152
+ }
1153
+ const bandIndicator = (
1154
+ <div className={styles.bandIndicator} data-dc-band-indicator aria-hidden="true" />
1155
+ );
1156
+
1157
+ return (
1158
+ // {...rest} last so consumer overrides win (Pattern A); the pointer
1159
+ // handlers are extracted above and re-invoked inside ours.
1160
+ <div
1161
+ ref={mergeRefs(ref, rootRef)}
1162
+ role="group"
1163
+ aria-label={t('dashboardCanvas.canvas')}
1164
+ aria-describedby={mergeAriaDescribedby(ariaDescribedby, instructionsId)}
1165
+ className={clsx(styles.canvas, stackClass[stackBelow], className)}
1166
+ // Consumer style spreads last so its own custom properties win.
1167
+ style={{
1168
+ ['--dc-cols' as string]: columns,
1169
+ // #696: trailing drop space height, only during a move-drag.
1170
+ ...(live?.kind === 'move' && { ['--dc-trailing-rows' as string]: live.h }),
1171
+ ...style,
1172
+ }}
1173
+ data-readonly={readOnly ? '' : undefined}
1174
+ data-narrow={isNarrow ? '' : undefined}
1175
+ data-dragging={dragging ? '' : undefined}
1176
+ onPointerMove={handleRootPointerMove}
1177
+ onPointerUp={handleRootPointerUp}
1178
+ onPointerCancel={handleRootPointerCancel}
1179
+ onLostPointerCapture={handleLostPointerCapture}
1180
+ onBlur={handleRootBlur}
1181
+ {...rest}
1182
+ >
1183
+ {/* .sizer is the square-cell measuring box: an unnamed size container
1184
+ whose content-box width IS the grid's width, so the `cqw` in
1185
+ `--dashboard-canvas-row` resolves to it (an element can't resolve
1186
+ cq units against itself — the root's own container only serves the
1187
+ named stack queries). */}
1188
+ <div className={styles.sizer}>
1189
+ <div
1190
+ className={styles.container}
1191
+ data-dc-container="top"
1192
+ // Intentional one-time shift: padding follows the target, so bands below
1193
+ // move once when the drag crosses into another container.
1194
+ data-dc-drop-target={live?.kind === 'move' && live.target === 'top' ? '' : undefined}
1195
+ data-dc-empty={shown.items.length === 0 ? '' : undefined}
1196
+ ref={(el) => setContainerEl(TOP_CONTAINER, el)}
1197
+ >
1198
+ {sortByPosition(shown.items).map((item) => (
1199
+ <DashboardCanvasItem
1200
+ key={item.id}
1201
+ placement={item}
1202
+ container={TOP_CONTAINER}
1203
+ gestures={gestures}
1204
+ >
1205
+ {renderItem(item.id)}
1206
+ </DashboardCanvasItem>
1207
+ ))}
1208
+ </div>
1209
+ </div>
1210
+ {shown.sections.length > 0 && (
1211
+ <Accordion
1212
+ type="multiple"
1213
+ value={openSectionIds}
1214
+ onValueChange={handleSectionOpenChange}
1215
+ gap="md"
1216
+ indicatorSide="left"
1217
+ >
1218
+ {shown.sections.map((section, index) => (
1219
+ <Fragment key={section.id}>
1220
+ {bandSlot === index && bandIndicator}
1221
+ <DashboardCanvasSection
1222
+ section={section}
1223
+ renderItem={renderItem}
1224
+ renderSectionHeader={renderSectionHeader}
1225
+ gestures={gestures}
1226
+ dragging={live?.kind === 'section' && live.id === section.id}
1227
+ dropTarget={
1228
+ live?.kind === 'move' &&
1229
+ live.target === containerKey({ kind: 'section', id: section.id })
1230
+ }
1231
+ onHeaderPointerDown={onHeaderPointerDown}
1232
+ onHeaderKeyDown={onHeaderKeyDown}
1233
+ setContainerEl={setContainerEl}
1234
+ setBandEl={setBandEl}
1235
+ highlighted={section.id === highlightedSectionId}
1236
+ onHighlightDone={onSectionHighlightDone}
1237
+ />
1238
+ </Fragment>
1239
+ ))}
1240
+ {bandSlot === shown.sections.length && bandIndicator}
1241
+ </Accordion>
1242
+ )}
1243
+ {live?.kind === 'move' && (
1244
+ <div
1245
+ className={styles.ghost}
1246
+ data-dc-ghost
1247
+ aria-hidden="true"
1248
+ style={{
1249
+ width: live.ghost.width,
1250
+ height: live.ghost.height,
1251
+ transform: `translate(${live.ghost.left}px, ${live.ghost.top}px)`,
1252
+ }}
1253
+ >
1254
+ {renderItem(live.id)}
1255
+ </div>
1256
+ )}
1257
+ <div id={instructionsId} className={styles.srOnly}>
1258
+ {t(
1259
+ editingEnabled
1260
+ ? 'dashboardCanvas.instructions'
1261
+ : 'dashboardCanvas.instructionsReadOnly',
1262
+ )}
1263
+ </div>
1264
+ <div role="status" className={styles.srOnly}>
1265
+ <span key={announcement.nonce}>{announcement.text}</span>
1266
+ </div>
1267
+ </div>
1268
+ );
1269
+ },
1270
+ );