@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.
- package/AI-PRIMER.md +341 -0
- package/README.md +262 -2
- package/docs/components/Accordion.md +114 -0
- package/docs/components/AgentAvatar.md +58 -0
- package/docs/components/Alert.md +81 -0
- package/docs/components/AppLayout.md +70 -0
- package/docs/components/Avatar.md +49 -0
- package/docs/components/AvatarGroup.md +47 -0
- package/docs/components/Badge.md +60 -0
- package/docs/components/Banner.md +89 -0
- package/docs/components/BrandIcon.md +32 -0
- package/docs/components/Breadcrumb.md +72 -0
- package/docs/components/Button.md +107 -0
- package/docs/components/ButtonGroup.md +78 -0
- package/docs/components/Calendar.md +158 -0
- package/docs/components/Card.md +165 -0
- package/docs/components/CatalogPicker.md +83 -0
- package/docs/components/Chart.md +114 -0
- package/docs/components/ChatBubble.md +55 -0
- package/docs/components/ChatLauncher.md +97 -0
- package/docs/components/Checkbox.md +55 -0
- package/docs/components/CircularProgress.md +53 -0
- package/docs/components/Cluster.md +96 -0
- package/docs/components/Code.md +35 -0
- package/docs/components/CodeBlock.md +67 -0
- package/docs/components/ColorPicker.md +115 -0
- package/docs/components/ConfirmationPopover.md +70 -0
- package/docs/components/Constrain.md +39 -0
- package/docs/components/CurrencySelect.md +57 -0
- package/docs/components/CursorPagination.md +43 -0
- package/docs/components/DashboardCanvas.md +79 -0
- package/docs/components/DashboardWidget.md +131 -0
- package/docs/components/DataTable.md +88 -0
- package/docs/components/DatePicker.md +68 -0
- package/docs/components/DateRangePicker.md +71 -0
- package/docs/components/DateStrip.md +56 -0
- package/docs/components/DefinitionList.md +82 -0
- package/docs/components/Divider.md +57 -0
- package/docs/components/Dot.md +26 -0
- package/docs/components/Drawer.md +94 -0
- package/docs/components/DropdownMenu.md +299 -0
- package/docs/components/EmojiPicker.md +84 -0
- package/docs/components/EmptyState.md +55 -0
- package/docs/components/EntityChip.md +102 -0
- package/docs/components/ErrorState.md +72 -0
- package/docs/components/Field.md +91 -0
- package/docs/components/FileUpload.md +109 -0
- package/docs/components/FilterChip.md +88 -0
- package/docs/components/FlowCanvas.md +108 -0
- package/docs/components/FormRow.md +31 -0
- package/docs/components/FormSection.md +37 -0
- package/docs/components/Grid.md +103 -0
- package/docs/components/Highlight.md +75 -0
- package/docs/components/HighlightedText.md +50 -0
- package/docs/components/IconPicker.md +134 -0
- package/docs/components/IconTile.md +30 -0
- package/docs/components/Image.md +84 -0
- package/docs/components/ImageCrop.md +163 -0
- package/docs/components/Indent.md +31 -0
- package/docs/components/InlineDatePicker.md +58 -0
- package/docs/components/InlineDateRangePicker.md +57 -0
- package/docs/components/Input.md +54 -0
- package/docs/components/Kanban.md +122 -0
- package/docs/components/Kbd.md +41 -0
- package/docs/components/Lightbox.md +105 -0
- package/docs/components/Link.md +65 -0
- package/docs/components/LinkCard.md +41 -0
- package/docs/components/LiquidEditor.md +84 -0
- package/docs/components/List.md +68 -0
- package/docs/components/LiveRegion.md +29 -0
- package/docs/components/LocaleProvider.md +25 -0
- package/docs/components/Logo.md +41 -0
- package/docs/components/Masonry.md +48 -0
- package/docs/components/MediaTile.md +95 -0
- package/docs/components/Modal.md +107 -0
- package/docs/components/MoneyInput.md +62 -0
- package/docs/components/OptionsPicker.md +123 -0
- package/docs/components/OtpInput.md +65 -0
- package/docs/components/Page.md +35 -0
- package/docs/components/PageHeader.md +133 -0
- package/docs/components/Pagination.md +44 -0
- package/docs/components/Palette.md +32 -0
- package/docs/components/PasswordInput.md +45 -0
- package/docs/components/PasswordStrengthMeter.md +27 -0
- package/docs/components/PersonDisplay.md +153 -0
- package/docs/components/PhoneInput.md +54 -0
- package/docs/components/PillMenu.md +91 -0
- package/docs/components/Popover.md +127 -0
- package/docs/components/Progress.md +56 -0
- package/docs/components/QrCode.md +34 -0
- package/docs/components/Radio.md +37 -0
- package/docs/components/RadioGroup.md +56 -0
- package/docs/components/Rail.md +191 -0
- package/docs/components/RelativeDateInput.md +114 -0
- package/docs/components/RichText.md +44 -0
- package/docs/components/RichTextEditor.md +112 -0
- package/docs/components/Screen.md +37 -0
- package/docs/components/ScrollArea.md +39 -0
- package/docs/components/Select.md +131 -0
- package/docs/components/SettingRow.md +98 -0
- package/docs/components/Skeleton.md +68 -0
- package/docs/components/Slider.md +92 -0
- package/docs/components/SlotGrid.md +55 -0
- package/docs/components/SocialButton.md +31 -0
- package/docs/components/Sortable.md +127 -0
- package/docs/components/SortableGroup.md +52 -0
- package/docs/components/Sparkline.md +51 -0
- package/docs/components/Split.md +76 -0
- package/docs/components/Stack.md +59 -0
- package/docs/components/StagePath.md +66 -0
- package/docs/components/StatGroup.md +61 -0
- package/docs/components/StatTile.md +58 -0
- package/docs/components/Sticky.md +69 -0
- package/docs/components/Switch.md +89 -0
- package/docs/components/Table.md +184 -0
- package/docs/components/TableOfContents.md +76 -0
- package/docs/components/Tabs.md +94 -0
- package/docs/components/Text.md +63 -0
- package/docs/components/Textarea.md +59 -0
- package/docs/components/Thread.md +52 -0
- package/docs/components/TimeField.md +70 -0
- package/docs/components/Timeline.md +57 -0
- package/docs/components/Title.md +54 -0
- package/docs/components/ToastViewport.md +61 -0
- package/docs/components/Tooltip.md +63 -0
- package/docs/components/TopBar.md +214 -0
- package/docs/components/Tour.md +85 -0
- package/docs/components/TreeView.md +113 -0
- package/docs/components/VisuallyHidden.md +43 -0
- package/docs/components/WidgetPreview.md +54 -0
- package/docs/components/useBelowBreakpoint.md +11 -0
- package/docs/components/useMonth.md +18 -0
- package/docs/setup.md +51 -0
- package/docs/theming.md +95 -0
- package/docs/tokens.md +123 -0
- package/docs/transient-state.md +56 -0
- package/package.json +82 -4
- package/src/_meta/manifest.ts +293 -0
- package/src/app/AppProvider.tsx +78 -0
- package/src/app/index.ts +3 -0
- package/src/app/themeTokens.ts +95 -0
- package/src/calendar/dateMath.ts +114 -0
- package/src/calendar/formatters.ts +75 -0
- package/src/calendar/index.ts +39 -0
- package/src/calendar/types.ts +36 -0
- package/src/calendar/useAgenda.ts +70 -0
- package/src/calendar/useDay.ts +51 -0
- package/src/calendar/useMonth.ts +82 -0
- package/src/calendar/useWeek.ts +62 -0
- package/src/calendar/weekInfo.ts +83 -0
- package/src/components/Accordion/Accordion.module.scss +304 -0
- package/src/components/Accordion/Accordion.tokens.scss +64 -0
- package/src/components/Accordion/Accordion.tsx +272 -0
- package/src/components/Accordion/AccordionContent.tsx +36 -0
- package/src/components/Accordion/AccordionItem.tsx +66 -0
- package/src/components/Accordion/AccordionTrigger.tsx +121 -0
- package/src/components/Accordion/context.ts +47 -0
- package/src/components/Accordion/index.ts +14 -0
- package/src/components/Alert/Alert.module.scss +92 -0
- package/src/components/Alert/Alert.tokens.scss +60 -0
- package/src/components/Alert/Alert.tsx +136 -0
- package/src/components/Alert/index.ts +2 -0
- package/src/components/AppLayout/AppLayout.module.scss +130 -0
- package/src/components/AppLayout/AppLayout.tokens.scss +27 -0
- package/src/components/AppLayout/AppLayout.tsx +211 -0
- package/src/components/AppLayout/index.ts +2 -0
- package/src/components/Avatar/AgentAvatar.module.scss +117 -0
- package/src/components/Avatar/AgentAvatar.tsx +122 -0
- package/src/components/Avatar/Avatar.module.scss +181 -0
- package/src/components/Avatar/Avatar.tokens.scss +107 -0
- package/src/components/Avatar/Avatar.tsx +210 -0
- package/src/components/Avatar/AvatarGroup.module.scss +128 -0
- package/src/components/Avatar/AvatarGroup.tsx +119 -0
- package/src/components/Avatar/AvatarGroupContext.tsx +20 -0
- package/src/components/Avatar/index.ts +6 -0
- package/src/components/Badge/Badge.module.scss +124 -0
- package/src/components/Badge/Badge.tokens.scss +74 -0
- package/src/components/Badge/Badge.tsx +164 -0
- package/src/components/Badge/index.ts +2 -0
- package/src/components/Banner/Banner.module.scss +73 -0
- package/src/components/Banner/Banner.tokens.scss +51 -0
- package/src/components/Banner/Banner.tsx +138 -0
- package/src/components/Banner/index.ts +2 -0
- package/src/components/BrandIcon/BrandIcon.module.scss +5 -0
- package/src/components/BrandIcon/BrandIcon.tsx +99 -0
- package/src/components/BrandIcon/index.ts +2 -0
- package/src/components/Breadcrumb/Breadcrumb.module.scss +32 -0
- package/src/components/Breadcrumb/Breadcrumb.tokens.scss +9 -0
- package/src/components/Breadcrumb/Breadcrumb.tsx +146 -0
- package/src/components/Breadcrumb/index.ts +2 -0
- package/src/components/Button/Button.module.scss +167 -0
- package/src/components/Button/Button.tokens.scss +90 -0
- package/src/components/Button/Button.tsx +205 -0
- package/src/components/Button/index.ts +2 -0
- package/src/components/ButtonGroup/ButtonGroup.module.scss +134 -0
- package/src/components/ButtonGroup/ButtonGroup.tokens.scss +42 -0
- package/src/components/ButtonGroup/ButtonGroup.tsx +278 -0
- package/src/components/ButtonGroup/ButtonGroupItem.tsx +74 -0
- package/src/components/ButtonGroup/context.ts +45 -0
- package/src/components/ButtonGroup/index.ts +2 -0
- package/src/components/Calendar/AgendaView.module.scss +231 -0
- package/src/components/Calendar/AgendaView.tsx +214 -0
- package/src/components/Calendar/AllDayBand.module.scss +18 -0
- package/src/components/Calendar/AllDayBand.tsx +75 -0
- package/src/components/Calendar/Calendar.module.scss +23 -0
- package/src/components/Calendar/Calendar.tokens.scss +262 -0
- package/src/components/Calendar/Calendar.tsx +370 -0
- package/src/components/Calendar/DayCell.module.scss +30 -0
- package/src/components/Calendar/DayCell.tsx +50 -0
- package/src/components/Calendar/DayView.module.scss +10 -0
- package/src/components/Calendar/DayView.tsx +163 -0
- package/src/components/Calendar/EventChip.module.scss +171 -0
- package/src/components/Calendar/EventChip.tsx +115 -0
- package/src/components/Calendar/HourGrid.module.scss +123 -0
- package/src/components/Calendar/HourGrid.tsx +381 -0
- package/src/components/Calendar/MonthView.module.scss +99 -0
- package/src/components/Calendar/MonthView.tsx +296 -0
- package/src/components/Calendar/TimedEvent.module.scss +205 -0
- package/src/components/Calendar/TimedEvent.tsx +247 -0
- package/src/components/Calendar/ViewSwitcher.module.scss +3 -0
- package/src/components/Calendar/ViewSwitcher.tsx +47 -0
- package/src/components/Calendar/WeekView.module.scss +10 -0
- package/src/components/Calendar/WeekView.tsx +137 -0
- package/src/components/Calendar/eventColor.ts +60 -0
- package/src/components/Calendar/index.ts +18 -0
- package/src/components/Calendar/types.ts +451 -0
- package/src/components/Calendar/useEventDrag.ts +783 -0
- package/src/components/Calendar/utils.ts +622 -0
- package/src/components/Card/Card.module.scss +264 -0
- package/src/components/Card/Card.tokens.scss +55 -0
- package/src/components/Card/Card.tsx +170 -0
- package/src/components/Card/CardBody.tsx +45 -0
- package/src/components/Card/CardHeader.tsx +113 -0
- package/src/components/Card/CardList.tsx +24 -0
- package/src/components/Card/CardListRow.tsx +24 -0
- package/src/components/Card/index.ts +7 -0
- package/src/components/CatalogPicker/CatalogPicker.module.scss +193 -0
- package/src/components/CatalogPicker/CatalogPicker.tokens.scss +26 -0
- package/src/components/CatalogPicker/CatalogPicker.tsx +339 -0
- package/src/components/CatalogPicker/index.ts +2 -0
- package/src/components/Chart/Chart.module.scss +287 -0
- package/src/components/Chart/Chart.tokens.scss +63 -0
- package/src/components/Chart/Chart.tsx +607 -0
- package/src/components/Chart/ChartLegend.tsx +99 -0
- package/src/components/Chart/ChartTable.tsx +62 -0
- package/src/components/Chart/ChartTooltip.tsx +91 -0
- package/src/components/Chart/chartModel.ts +689 -0
- package/src/components/Chart/index.ts +2 -0
- package/src/components/ChatBubble/ChatBubble.module.scss +93 -0
- package/src/components/ChatBubble/ChatBubble.tokens.scss +17 -0
- package/src/components/ChatBubble/ChatBubble.tsx +61 -0
- package/src/components/ChatBubble/ChatBubbleList.tsx +27 -0
- package/src/components/ChatBubble/index.ts +3 -0
- package/src/components/ChatLauncher/ChatLauncher.module.scss +68 -0
- package/src/components/ChatLauncher/ChatLauncher.tokens.scss +27 -0
- package/src/components/ChatLauncher/ChatLauncher.tsx +202 -0
- package/src/components/ChatLauncher/index.ts +2 -0
- package/src/components/ChatLauncher/isAtBottom.ts +12 -0
- package/src/components/Checkbox/Checkbox.module.scss +147 -0
- package/src/components/Checkbox/Checkbox.tokens.scss +44 -0
- package/src/components/Checkbox/Checkbox.tsx +188 -0
- package/src/components/Checkbox/index.ts +2 -0
- package/src/components/CircularProgress/CircularProgress.module.scss +133 -0
- package/src/components/CircularProgress/CircularProgress.tokens.scss +40 -0
- package/src/components/CircularProgress/CircularProgress.tsx +139 -0
- package/src/components/CircularProgress/index.ts +7 -0
- package/src/components/Cluster/Cluster.module.scss +145 -0
- package/src/components/Cluster/Cluster.tokens.scss +11 -0
- package/src/components/Cluster/Cluster.tsx +215 -0
- package/src/components/Cluster/index.ts +2 -0
- package/src/components/Code/Code.module.scss +28 -0
- package/src/components/Code/Code.tokens.scss +15 -0
- package/src/components/Code/Code.tsx +44 -0
- package/src/components/Code/index.ts +2 -0
- package/src/components/CodeBlock/CodeBlock.module.scss +146 -0
- package/src/components/CodeBlock/CodeBlock.tokens.scss +27 -0
- package/src/components/CodeBlock/CodeBlock.tsx +274 -0
- package/src/components/CodeBlock/highlight.ts +179 -0
- package/src/components/CodeBlock/index.ts +8 -0
- package/src/components/CodeBlock/sampleSync.ts +42 -0
- package/src/components/ColorPicker/ColorPicker.module.scss +286 -0
- package/src/components/ColorPicker/ColorPicker.tokens.scss +60 -0
- package/src/components/ColorPicker/ColorPicker.tsx +367 -0
- package/src/components/ColorPicker/ColorPickerPanel.tsx +265 -0
- package/src/components/ColorPicker/SVSquare.tsx +189 -0
- package/src/components/ColorPicker/colorMath.ts +141 -0
- package/src/components/ColorPicker/index.ts +6 -0
- package/src/components/ConfirmationPopover/ConfirmationPopover.module.scss +39 -0
- package/src/components/ConfirmationPopover/ConfirmationPopover.tokens.scss +13 -0
- package/src/components/ConfirmationPopover/ConfirmationPopover.tsx +395 -0
- package/src/components/ConfirmationPopover/index.ts +2 -0
- package/src/components/Constrain/Constrain.module.scss +99 -0
- package/src/components/Constrain/Constrain.tsx +79 -0
- package/src/components/Constrain/index.ts +2 -0
- package/src/components/CurrencySelect/CurrencySelect.module.scss +7 -0
- package/src/components/CurrencySelect/CurrencySelect.tsx +167 -0
- package/src/components/CurrencySelect/index.ts +3 -0
- package/src/components/CursorPagination/CursorPagination.module.scss +69 -0
- package/src/components/CursorPagination/CursorPagination.tokens.scss +32 -0
- package/src/components/CursorPagination/CursorPagination.tsx +121 -0
- package/src/components/CursorPagination/index.ts +2 -0
- package/src/components/DashboardCanvas/DashboardCanvas.module.scss +360 -0
- package/src/components/DashboardCanvas/DashboardCanvas.tokens.scss +48 -0
- package/src/components/DashboardCanvas/DashboardCanvas.tsx +1270 -0
- package/src/components/DashboardCanvas/DashboardCanvasItem.tsx +140 -0
- package/src/components/DashboardCanvas/DashboardCanvasSection.tsx +174 -0
- package/src/components/DashboardCanvas/engine.ts +363 -0
- package/src/components/DashboardCanvas/index.ts +9 -0
- package/src/components/DashboardWidget/DashboardWidget.module.scss +100 -0
- package/src/components/DashboardWidget/DashboardWidget.tokens.scss +16 -0
- package/src/components/DashboardWidget/DashboardWidget.tsx +188 -0
- package/src/components/DashboardWidget/index.ts +7 -0
- package/src/components/DataTable/BodyRow.tsx +197 -0
- package/src/components/DataTable/ColumnVisibilityTrigger.module.scss +7 -0
- package/src/components/DataTable/ColumnVisibilityTrigger.tsx +105 -0
- package/src/components/DataTable/DataTable.module.scss +511 -0
- package/src/components/DataTable/DataTable.tokens.scss +80 -0
- package/src/components/DataTable/DataTable.tsx +734 -0
- package/src/components/DataTable/HeaderCell.module.scss +215 -0
- package/src/components/DataTable/HeaderCell.tsx +440 -0
- package/src/components/DataTable/columnShift.ts +117 -0
- package/src/components/DataTable/index.ts +23 -0
- package/src/components/DataTable/pinStyle.ts +47 -0
- package/src/components/DataTable/reorderColumns.ts +66 -0
- package/src/components/DataTable/types.ts +289 -0
- package/src/components/DataTable/useColumnDragShift.ts +216 -0
- package/src/components/DataTable/useDataTable.ts +372 -0
- package/src/components/DataTable/useResizeHandle.ts +97 -0
- package/src/components/DatePicker/DatePicker.module.scss +180 -0
- package/src/components/DatePicker/DatePicker.tokens.scss +152 -0
- package/src/components/DatePicker/DatePicker.tsx +664 -0
- package/src/components/DatePicker/DatePickerGrid.module.scss +172 -0
- package/src/components/DatePicker/DatePickerGrid.tsx +341 -0
- package/src/components/DatePicker/InlineDatePicker.module.scss +32 -0
- package/src/components/DatePicker/InlineDatePicker.tsx +201 -0
- package/src/components/DatePicker/index.ts +4 -0
- package/src/components/DatePicker/utils.ts +364 -0
- package/src/components/DateRangePicker/DateRangePicker.module.scss +237 -0
- package/src/components/DateRangePicker/DateRangePicker.tokens.scss +78 -0
- package/src/components/DateRangePicker/DateRangePicker.tsx +720 -0
- package/src/components/DateRangePicker/InlineDateRangePicker.module.scss +94 -0
- package/src/components/DateRangePicker/InlineDateRangePicker.tsx +376 -0
- package/src/components/DateRangePicker/index.ts +5 -0
- package/src/components/DateRangePicker/utils.ts +151 -0
- package/src/components/DateStrip/DateStrip.module.scss +78 -0
- package/src/components/DateStrip/DateStrip.tokens.scss +23 -0
- package/src/components/DateStrip/DateStrip.tsx +205 -0
- package/src/components/DateStrip/index.ts +2 -0
- package/src/components/DefinitionList/DefinitionList.module.scss +102 -0
- package/src/components/DefinitionList/DefinitionList.tokens.scss +29 -0
- package/src/components/DefinitionList/DefinitionList.tsx +198 -0
- package/src/components/DefinitionList/index.ts +9 -0
- package/src/components/Divider/Divider.module.scss +144 -0
- package/src/components/Divider/Divider.tokens.scss +21 -0
- package/src/components/Divider/Divider.tsx +103 -0
- package/src/components/Divider/index.ts +2 -0
- package/src/components/Dot/Dot.module.scss +40 -0
- package/src/components/Dot/Dot.tokens.scss +4 -0
- package/src/components/Dot/Dot.tsx +45 -0
- package/src/components/Dot/index.ts +2 -0
- package/src/components/Drawer/Body.tsx +23 -0
- package/src/components/Drawer/Close.tsx +35 -0
- package/src/components/Drawer/Content.tsx +93 -0
- package/src/components/Drawer/Drawer.module.scss +232 -0
- package/src/components/Drawer/Drawer.tokens.scss +38 -0
- package/src/components/Drawer/Drawer.tsx +23 -0
- package/src/components/Drawer/DrawerRoot.tsx +190 -0
- package/src/components/Drawer/Footer.tsx +24 -0
- package/src/components/Drawer/Header.tsx +64 -0
- package/src/components/Drawer/Overlay.tsx +82 -0
- package/src/components/Drawer/context.ts +44 -0
- package/src/components/Drawer/index.ts +12 -0
- package/src/components/Drawer/useDragToClose.ts +149 -0
- package/src/components/DropdownMenu/CheckboxItem.tsx +144 -0
- package/src/components/DropdownMenu/Content.tsx +419 -0
- package/src/components/DropdownMenu/DropdownMenu.module.scss +280 -0
- package/src/components/DropdownMenu/DropdownMenu.tokens.scss +88 -0
- package/src/components/DropdownMenu/DropdownMenu.tsx +43 -0
- package/src/components/DropdownMenu/Group.tsx +76 -0
- package/src/components/DropdownMenu/Item.tsx +148 -0
- package/src/components/DropdownMenu/ItemIndicator.tsx +30 -0
- package/src/components/DropdownMenu/Radio.tsx +178 -0
- package/src/components/DropdownMenu/Root.tsx +100 -0
- package/src/components/DropdownMenu/Sub.tsx +332 -0
- package/src/components/DropdownMenu/Trigger.tsx +76 -0
- package/src/components/DropdownMenu/context.ts +65 -0
- package/src/components/DropdownMenu/index.ts +20 -0
- package/src/components/EmojiPicker/EmojiPicker.module.scss +84 -0
- package/src/components/EmojiPicker/EmojiPicker.tokens.scss +24 -0
- package/src/components/EmojiPicker/EmojiPicker.tsx +341 -0
- package/src/components/EmojiPicker/emojiData.ts +245 -0
- package/src/components/EmojiPicker/index.ts +3 -0
- package/src/components/EmptyState/EmptyState.module.scss +102 -0
- package/src/components/EmptyState/EmptyState.tokens.scss +28 -0
- package/src/components/EmptyState/EmptyState.tsx +112 -0
- package/src/components/EmptyState/index.ts +7 -0
- package/src/components/EntityChip/EntityChip.module.scss +275 -0
- package/src/components/EntityChip/EntityChip.tokens.scss +66 -0
- package/src/components/EntityChip/EntityChip.tsx +649 -0
- package/src/components/EntityChip/index.ts +10 -0
- package/src/components/ErrorState/ErrorState.module.scss +105 -0
- package/src/components/ErrorState/ErrorState.tokens.scss +32 -0
- package/src/components/ErrorState/ErrorState.tsx +125 -0
- package/src/components/ErrorState/index.ts +8 -0
- package/src/components/Field/Field.module.scss +46 -0
- package/src/components/Field/Field.tsx +185 -0
- package/src/components/Field/index.ts +2 -0
- package/src/components/FileUpload/FileUpload.module.scss +187 -0
- package/src/components/FileUpload/FileUpload.tokens.scss +81 -0
- package/src/components/FileUpload/FileUpload.tsx +569 -0
- package/src/components/FileUpload/formatBytes.ts +34 -0
- package/src/components/FileUpload/iconForFile.ts +40 -0
- package/src/components/FileUpload/index.ts +2 -0
- package/src/components/FilterChip/FilterChip.module.scss +61 -0
- package/src/components/FilterChip/FilterChip.tokens.scss +25 -0
- package/src/components/FilterChip/FilterChip.tsx +208 -0
- package/src/components/FilterChip/index.ts +5 -0
- package/src/components/FlowCanvas/FlowCanvas.module.scss +329 -0
- package/src/components/FlowCanvas/FlowCanvas.tokens.scss +45 -0
- package/src/components/FlowCanvas/FlowCanvas.tsx +1867 -0
- package/src/components/FlowCanvas/FlowControls.tsx +46 -0
- package/src/components/FlowCanvas/FlowEdge.tsx +58 -0
- package/src/components/FlowCanvas/FlowNode.tsx +76 -0
- package/src/components/FlowCanvas/edgePath.ts +85 -0
- package/src/components/FlowCanvas/index.ts +4 -0
- package/src/components/FlowCanvas/layout.ts +148 -0
- package/src/components/FlowCanvas/spatialNav.ts +76 -0
- package/src/components/FlowCanvas/types.ts +59 -0
- package/src/components/FlowCanvas/useViewport.ts +122 -0
- package/src/components/FormRow/FormRow.module.scss +2 -0
- package/src/components/FormRow/FormRow.tsx +37 -0
- package/src/components/FormRow/index.ts +2 -0
- package/src/components/FormSection/FormSection.module.scss +24 -0
- package/src/components/FormSection/FormSection.tsx +46 -0
- package/src/components/FormSection/index.ts +2 -0
- package/src/components/Grid/Grid.module.scss +162 -0
- package/src/components/Grid/Grid.tokens.scss +13 -0
- package/src/components/Grid/Grid.tsx +226 -0
- package/src/components/Grid/GridItem.tsx +77 -0
- package/src/components/Grid/index.ts +11 -0
- package/src/components/Highlight/Highlight.module.scss +43 -0
- package/src/components/Highlight/Highlight.tokens.scss +35 -0
- package/src/components/Highlight/Highlight.tsx +164 -0
- package/src/components/Highlight/HighlightOverlay.tsx +100 -0
- package/src/components/Highlight/index.ts +2 -0
- package/src/components/HighlightedText/HighlightedText.module.scss +11 -0
- package/src/components/HighlightedText/HighlightedText.tokens.scss +9 -0
- package/src/components/HighlightedText/HighlightedText.tsx +50 -0
- package/src/components/HighlightedText/highlightRanges.ts +44 -0
- package/src/components/HighlightedText/index.ts +2 -0
- package/src/components/IconPicker/IconPicker.module.scss +175 -0
- package/src/components/IconPicker/IconPicker.tokens.scss +40 -0
- package/src/components/IconPicker/IconPicker.tsx +629 -0
- package/src/components/IconPicker/index.ts +9 -0
- package/src/components/IconTile/IconTile.module.scss +57 -0
- package/src/components/IconTile/IconTile.tokens.scss +16 -0
- package/src/components/IconTile/IconTile.tsx +91 -0
- package/src/components/IconTile/index.ts +2 -0
- package/src/components/Image/Image.module.scss +266 -0
- package/src/components/Image/Image.tokens.scss +55 -0
- package/src/components/Image/Image.tsx +342 -0
- package/src/components/Image/index.ts +2 -0
- package/src/components/ImageCrop/ImageCrop.module.scss +274 -0
- package/src/components/ImageCrop/ImageCrop.tokens.scss +33 -0
- package/src/components/ImageCrop/ImageCrop.tsx +542 -0
- package/src/components/ImageCrop/cropGeometry.ts +120 -0
- package/src/components/ImageCrop/extractCropBlob.ts +140 -0
- package/src/components/ImageCrop/index.ts +11 -0
- package/src/components/ImageCrop/useCropPreview.ts +161 -0
- package/src/components/Indent/Indent.module.scss +33 -0
- package/src/components/Indent/Indent.tokens.scss +10 -0
- package/src/components/Indent/Indent.tsx +46 -0
- package/src/components/Indent/index.ts +2 -0
- package/src/components/Input/Input.module.scss +99 -0
- package/src/components/Input/Input.tokens.scss +54 -0
- package/src/components/Input/Input.tsx +105 -0
- package/src/components/Input/Suggestions.tsx +260 -0
- package/src/components/Input/index.ts +2 -0
- package/src/components/Kanban/Kanban.module.scss +39 -0
- package/src/components/Kanban/Kanban.tokens.scss +15 -0
- package/src/components/Kanban/Kanban.tsx +951 -0
- package/src/components/Kanban/index.ts +7 -0
- package/src/components/Kbd/Kbd.module.scss +47 -0
- package/src/components/Kbd/Kbd.tokens.scss +26 -0
- package/src/components/Kbd/Kbd.tsx +73 -0
- package/src/components/Kbd/index.ts +2 -0
- package/src/components/Lightbox/Lightbox.module.scss +283 -0
- package/src/components/Lightbox/Lightbox.tokens.scss +36 -0
- package/src/components/Lightbox/Lightbox.tsx +506 -0
- package/src/components/Lightbox/index.ts +2 -0
- package/src/components/Link/Link.module.scss +78 -0
- package/src/components/Link/Link.tokens.scss +28 -0
- package/src/components/Link/Link.tsx +114 -0
- package/src/components/Link/index.ts +2 -0
- package/src/components/LinkCard/LinkCard.module.scss +92 -0
- package/src/components/LinkCard/LinkCard.tokens.scss +9 -0
- package/src/components/LinkCard/LinkCard.tsx +72 -0
- package/src/components/LinkCard/index.ts +2 -0
- package/src/components/LiquidEditor/AutocompleteMenu.tsx +133 -0
- package/src/components/LiquidEditor/HighlightLayer.tsx +53 -0
- package/src/components/LiquidEditor/InsertVariableMenu.tsx +81 -0
- package/src/components/LiquidEditor/LiquidEditor.module.scss +339 -0
- package/src/components/LiquidEditor/LiquidEditor.tokens.scss +81 -0
- package/src/components/LiquidEditor/LiquidEditor.tsx +412 -0
- package/src/components/LiquidEditor/PreviewPane.tsx +29 -0
- package/src/components/LiquidEditor/caretCoordinates.ts +97 -0
- package/src/components/LiquidEditor/index.ts +7 -0
- package/src/components/LiquidEditor/liquidFilters.ts +23 -0
- package/src/components/LiquidEditor/liquidTokenizer.ts +235 -0
- package/src/components/LiquidEditor/types.ts +101 -0
- package/src/components/LiquidEditor/useLiquidAutocomplete.ts +178 -0
- package/src/components/List/List.module.scss +105 -0
- package/src/components/List/List.tokens.scss +42 -0
- package/src/components/List/List.tsx +124 -0
- package/src/components/List/index.ts +9 -0
- package/src/components/LiveRegion/LiveRegion.module.scss +3 -0
- package/src/components/LiveRegion/LiveRegion.tsx +116 -0
- package/src/components/LiveRegion/index.ts +2 -0
- package/src/components/Logo/Logo.module.scss +139 -0
- package/src/components/Logo/Logo.tokens.scss +14 -0
- package/src/components/Logo/Logo.tsx +126 -0
- package/src/components/Logo/index.ts +2 -0
- package/src/components/Masonry/Masonry.module.scss +42 -0
- package/src/components/Masonry/Masonry.tokens.scss +8 -0
- package/src/components/Masonry/Masonry.tsx +136 -0
- package/src/components/Masonry/index.ts +2 -0
- package/src/components/Masonry/masonryUtils.ts +42 -0
- package/src/components/MediaTile/MediaTile.module.scss +183 -0
- package/src/components/MediaTile/MediaTile.tokens.scss +60 -0
- package/src/components/MediaTile/MediaTile.tsx +208 -0
- package/src/components/MediaTile/index.ts +8 -0
- package/src/components/Modal/Body.tsx +27 -0
- package/src/components/Modal/Close.tsx +45 -0
- package/src/components/Modal/Content.tsx +98 -0
- package/src/components/Modal/Footer.tsx +28 -0
- package/src/components/Modal/Header.tsx +51 -0
- package/src/components/Modal/Modal.module.scss +262 -0
- package/src/components/Modal/Modal.tokens.scss +50 -0
- package/src/components/Modal/Modal.tsx +23 -0
- package/src/components/Modal/ModalRoot.tsx +299 -0
- package/src/components/Modal/Overlay.tsx +101 -0
- package/src/components/Modal/context.ts +60 -0
- package/src/components/Modal/index.ts +11 -0
- package/src/components/MoneyInput/MoneyInput.module.scss +57 -0
- package/src/components/MoneyInput/MoneyInput.tsx +433 -0
- package/src/components/MoneyInput/index.ts +2 -0
- package/src/components/OptionsPicker/OptionsPicker.module.scss +216 -0
- package/src/components/OptionsPicker/OptionsPicker.tokens.scss +68 -0
- package/src/components/OptionsPicker/OptionsPicker.tsx +936 -0
- package/src/components/OptionsPicker/index.ts +9 -0
- package/src/components/OtpInput/OtpInput.module.scss +69 -0
- package/src/components/OtpInput/OtpInput.tokens.scss +45 -0
- package/src/components/OtpInput/OtpInput.tsx +368 -0
- package/src/components/OtpInput/index.ts +2 -0
- package/src/components/Page/Page.module.scss +30 -0
- package/src/components/Page/Page.tokens.scss +11 -0
- package/src/components/Page/Page.tsx +53 -0
- package/src/components/Page/index.ts +2 -0
- package/src/components/PageHeader/PageHeader.module.scss +197 -0
- package/src/components/PageHeader/PageHeader.tokens.scss +41 -0
- package/src/components/PageHeader/PageHeader.tsx +310 -0
- package/src/components/PageHeader/index.ts +20 -0
- package/src/components/Pagination/Pagination.module.scss +117 -0
- package/src/components/Pagination/Pagination.tokens.scss +54 -0
- package/src/components/Pagination/Pagination.tsx +165 -0
- package/src/components/Pagination/index.ts +4 -0
- package/src/components/Pagination/paginationRange.ts +72 -0
- package/src/components/PasswordInput/PasswordInput.module.scss +120 -0
- package/src/components/PasswordInput/PasswordInput.tokens.scss +44 -0
- package/src/components/PasswordInput/PasswordInput.tsx +210 -0
- package/src/components/PasswordInput/index.ts +2 -0
- package/src/components/PasswordStrengthMeter/PasswordStrengthMeter.module.scss +80 -0
- package/src/components/PasswordStrengthMeter/PasswordStrengthMeter.tokens.scss +40 -0
- package/src/components/PasswordStrengthMeter/PasswordStrengthMeter.tsx +96 -0
- package/src/components/PasswordStrengthMeter/index.ts +2 -0
- package/src/components/PersonDisplay/PersonDisplay.module.scss +180 -0
- package/src/components/PersonDisplay/PersonDisplay.tokens.scss +35 -0
- package/src/components/PersonDisplay/PersonDisplay.tsx +420 -0
- package/src/components/PersonDisplay/index.ts +10 -0
- package/src/components/PhoneInput/PhoneInput.module.scss +23 -0
- package/src/components/PhoneInput/PhoneInput.tsx +271 -0
- package/src/components/PhoneInput/index.ts +4 -0
- package/src/components/PhoneInput/phone.ts +128 -0
- package/src/components/PillMenu/PillMenu.module.scss +117 -0
- package/src/components/PillMenu/PillMenu.tokens.scss +22 -0
- package/src/components/PillMenu/PillMenu.tsx +324 -0
- package/src/components/PillMenu/index.ts +2 -0
- package/src/components/Popover/Anchor.tsx +37 -0
- package/src/components/Popover/Close.tsx +45 -0
- package/src/components/Popover/Content.tsx +213 -0
- package/src/components/Popover/Heading.tsx +33 -0
- package/src/components/Popover/Popover.module.scss +177 -0
- package/src/components/Popover/Popover.tokens.scss +22 -0
- package/src/components/Popover/Popover.tsx +23 -0
- package/src/components/Popover/PopoverRoot.tsx +75 -0
- package/src/components/Popover/Trigger.tsx +86 -0
- package/src/components/Popover/context.ts +22 -0
- package/src/components/Popover/index.ts +7 -0
- package/src/components/Progress/Progress.module.scss +110 -0
- package/src/components/Progress/Progress.tokens.scss +44 -0
- package/src/components/Progress/Progress.tsx +129 -0
- package/src/components/Progress/index.ts +2 -0
- package/src/components/QrCode/QrCode.module.scss +97 -0
- package/src/components/QrCode/QrCode.tokens.scss +17 -0
- package/src/components/QrCode/QrCode.tsx +233 -0
- package/src/components/QrCode/index.ts +2 -0
- package/src/components/QrCode/qr.ts +161 -0
- package/src/components/Radio/Radio.module.scss +175 -0
- package/src/components/Radio/Radio.tokens.scss +59 -0
- package/src/components/Radio/Radio.tsx +127 -0
- package/src/components/Radio/RadioGroup.module.scss +42 -0
- package/src/components/Radio/RadioGroup.tsx +120 -0
- package/src/components/Radio/RadioGroupContext.tsx +30 -0
- package/src/components/Radio/index.ts +4 -0
- package/src/components/Rail/Rail.module.scss +510 -0
- package/src/components/Rail/Rail.tokens.scss +92 -0
- package/src/components/Rail/Rail.tsx +287 -0
- package/src/components/Rail/RailCollapseToggle.tsx +57 -0
- package/src/components/Rail/RailFooter.tsx +17 -0
- package/src/components/Rail/RailGroup.tsx +728 -0
- package/src/components/Rail/RailGroupContext.ts +9 -0
- package/src/components/Rail/RailHeader.tsx +23 -0
- package/src/components/Rail/RailItem.tsx +119 -0
- package/src/components/Rail/RailSection.tsx +39 -0
- package/src/components/Rail/RailSpacer.tsx +19 -0
- package/src/components/Rail/index.ts +9 -0
- package/src/components/RelativeDateInput/RelativeDateInput.module.scss +15 -0
- package/src/components/RelativeDateInput/RelativeDateInput.tokens.scss +6 -0
- package/src/components/RelativeDateInput/RelativeDateInput.tsx +474 -0
- package/src/components/RelativeDateInput/index.ts +10 -0
- package/src/components/RelativeDateInput/relativeDate.ts +127 -0
- package/src/components/RichText/RichText.module.scss +5 -0
- package/src/components/RichText/RichText.tsx +70 -0
- package/src/components/RichText/RichTextAttachment.module.scss +82 -0
- package/src/components/RichText/RichTextAttachment.tsx +122 -0
- package/src/components/RichText/_prose.scss +79 -0
- package/src/components/RichText/attachmentIcon.tsx +24 -0
- package/src/components/RichText/engine/attachment.ts +154 -0
- package/src/components/RichText/engine/autolink.ts +64 -0
- package/src/components/RichText/engine/blockUnit.ts +174 -0
- package/src/components/RichText/engine/colorMarks.ts +85 -0
- package/src/components/RichText/engine/escape.ts +11 -0
- package/src/components/RichText/engine/fromHtml.ts +353 -0
- package/src/components/RichText/engine/fromMarkdown.ts +20 -0
- package/src/components/RichText/engine/inlines.ts +82 -0
- package/src/components/RichText/engine/listDepths.ts +32 -0
- package/src/components/RichText/engine/marks.ts +80 -0
- package/src/components/RichText/engine/mdToHtml.ts +178 -0
- package/src/components/RichText/engine/model.ts +157 -0
- package/src/components/RichText/engine/position.ts +118 -0
- package/src/components/RichText/engine/renderDoc.tsx +395 -0
- package/src/components/RichText/engine/renderLink.ts +15 -0
- package/src/components/RichText/engine/renderMention.ts +16 -0
- package/src/components/RichText/engine/safeHref.ts +22 -0
- package/src/components/RichText/engine/toHtml.ts +154 -0
- package/src/components/RichText/engine/toMarkdown.ts +97 -0
- package/src/components/RichText/engine/transforms.ts +434 -0
- package/src/components/RichText/imageMaxHeightStyle.ts +10 -0
- package/src/components/RichText/index.ts +31 -0
- package/src/components/RichTextEditor/RichTextAttachmentConfig.tsx +264 -0
- package/src/components/RichTextEditor/RichTextBlockControls.tsx +365 -0
- package/src/components/RichTextEditor/RichTextBlockMenu.tsx +162 -0
- package/src/components/RichTextEditor/RichTextColorMenu.tsx +115 -0
- package/src/components/RichTextEditor/RichTextEditor.module.scss +298 -0
- package/src/components/RichTextEditor/RichTextEditor.tsx +1910 -0
- package/src/components/RichTextEditor/RichTextImageResizer.tsx +131 -0
- package/src/components/RichTextEditor/RichTextLinkEditor.tsx +151 -0
- package/src/components/RichTextEditor/RichTextMentionMenu.tsx +99 -0
- package/src/components/RichTextEditor/RichTextToolbar.tsx +379 -0
- package/src/components/RichTextEditor/autolinkInput.ts +65 -0
- package/src/components/RichTextEditor/blockDrop.ts +9 -0
- package/src/components/RichTextEditor/blockReflow.ts +62 -0
- package/src/components/RichTextEditor/commands.ts +300 -0
- package/src/components/RichTextEditor/history.ts +93 -0
- package/src/components/RichTextEditor/icons.tsx +152 -0
- package/src/components/RichTextEditor/imageSize.ts +70 -0
- package/src/components/RichTextEditor/index.ts +5 -0
- package/src/components/RichTextEditor/input.ts +209 -0
- package/src/components/RichTextEditor/inputRules.ts +71 -0
- package/src/components/RichTextEditor/links.ts +103 -0
- package/src/components/RichTextEditor/mentionContext.ts +37 -0
- package/src/components/RichTextEditor/mentions.ts +28 -0
- package/src/components/RichTextEditor/preventSelectionLoss.ts +15 -0
- package/src/components/RichTextEditor/selection.ts +415 -0
- package/src/components/RichTextEditor/shortcuts.ts +44 -0
- package/src/components/RichTextEditor/useAnchoredFloating.ts +50 -0
- package/src/components/RichTextEditor/useAttachmentConfig.ts +134 -0
- package/src/components/RichTextEditor/useDismissOnOutsidePointerDown.ts +26 -0
- package/src/components/RichTextEditor/useLinkEditor.ts +107 -0
- package/src/components/RichTextEditor/useMention.ts +226 -0
- package/src/components/RichTextEditor/useOverlayRect.ts +73 -0
- package/src/components/RichTextEditor/useUpload.ts +246 -0
- package/src/components/Screen/Screen.module.scss +46 -0
- package/src/components/Screen/Screen.tokens.scss +20 -0
- package/src/components/Screen/Screen.tsx +97 -0
- package/src/components/Screen/index.ts +2 -0
- package/src/components/ScrollArea/ScrollArea.module.scss +39 -0
- package/src/components/ScrollArea/ScrollArea.tokens.scss +15 -0
- package/src/components/ScrollArea/ScrollArea.tsx +154 -0
- package/src/components/ScrollArea/index.ts +2 -0
- package/src/components/Select/Chip.tsx +45 -0
- package/src/components/Select/Empty.tsx +39 -0
- package/src/components/Select/Error.tsx +51 -0
- package/src/components/Select/HiddenInputs.tsx +44 -0
- package/src/components/Select/Listbox.tsx +557 -0
- package/src/components/Select/Loading.tsx +30 -0
- package/src/components/Select/Select.module.scss +512 -0
- package/src/components/Select/Select.tokens.scss +140 -0
- package/src/components/Select/Select.tsx +685 -0
- package/src/components/Select/Trigger.tsx +952 -0
- package/src/components/Select/context.ts +101 -0
- package/src/components/Select/emptyStateText.ts +19 -0
- package/src/components/Select/index.ts +9 -0
- package/src/components/Select/useSelectState.ts +157 -0
- package/src/components/Select/utils-types.ts +7 -0
- package/src/components/Select/utils.ts +122 -0
- package/src/components/SettingRow/SettingRow.module.scss +166 -0
- package/src/components/SettingRow/SettingRow.tokens.scss +37 -0
- package/src/components/SettingRow/SettingRow.tsx +242 -0
- package/src/components/SettingRow/index.ts +7 -0
- package/src/components/Skeleton/Skeleton.module.scss +55 -0
- package/src/components/Skeleton/Skeleton.tokens.scss +17 -0
- package/src/components/Skeleton/Skeleton.tsx +131 -0
- package/src/components/Skeleton/index.ts +4 -0
- package/src/components/Skeleton/useSkeletonVisibility.ts +95 -0
- package/src/components/Slider/Slider.module.scss +293 -0
- package/src/components/Slider/Slider.tokens.scss +98 -0
- package/src/components/Slider/Slider.tsx +624 -0
- package/src/components/Slider/index.ts +9 -0
- package/src/components/SlotGrid/SlotGrid.module.scss +61 -0
- package/src/components/SlotGrid/SlotGrid.tokens.scss +21 -0
- package/src/components/SlotGrid/SlotGrid.tsx +111 -0
- package/src/components/SlotGrid/index.ts +2 -0
- package/src/components/SocialButton/SocialButton.module.scss +4 -0
- package/src/components/SocialButton/SocialButton.tsx +40 -0
- package/src/components/SocialButton/index.ts +2 -0
- package/src/components/Sortable/Sortable.module.scss +177 -0
- package/src/components/Sortable/Sortable.tokens.scss +27 -0
- package/src/components/Sortable/Sortable.tsx +572 -0
- package/src/components/Sortable/containerAwareCollision.ts +125 -0
- package/src/components/Sortable/index.ts +13 -0
- package/src/components/SortableGroup/SortableGroup.module.scss +23 -0
- package/src/components/SortableGroup/SortableGroup.tsx +315 -0
- package/src/components/SortableGroup/index.ts +4 -0
- package/src/components/SortableGroup/moveSortableItem.ts +39 -0
- package/src/components/Sparkline/Sparkline.module.scss +28 -0
- package/src/components/Sparkline/Sparkline.tokens.scss +7 -0
- package/src/components/Sparkline/Sparkline.tsx +133 -0
- package/src/components/Sparkline/index.ts +2 -0
- package/src/components/Sparkline/sparklineModel.ts +119 -0
- package/src/components/Split/Split.module.scss +158 -0
- package/src/components/Split/Split.tokens.scss +10 -0
- package/src/components/Split/Split.tsx +231 -0
- package/src/components/Split/index.ts +2 -0
- package/src/components/Stack/Stack.module.scss +67 -0
- package/src/components/Stack/Stack.tokens.scss +11 -0
- package/src/components/Stack/Stack.tsx +100 -0
- package/src/components/Stack/index.ts +2 -0
- package/src/components/StagePath/StagePath.module.scss +255 -0
- package/src/components/StagePath/StagePath.tokens.scss +43 -0
- package/src/components/StagePath/StagePath.tsx +176 -0
- package/src/components/StagePath/index.ts +2 -0
- package/src/components/StatGroup/StatGroup.module.scss +6 -0
- package/src/components/StatGroup/StatGroup.tsx +70 -0
- package/src/components/StatGroup/StatGroupContext.ts +10 -0
- package/src/components/StatGroup/StatTile.module.scss +99 -0
- package/src/components/StatGroup/StatTile.tokens.scss +35 -0
- package/src/components/StatGroup/StatTile.tsx +143 -0
- package/src/components/StatGroup/index.ts +4 -0
- package/src/components/Sticky/Sticky.module.scss +98 -0
- package/src/components/Sticky/Sticky.tokens.scss +17 -0
- package/src/components/Sticky/Sticky.tsx +103 -0
- package/src/components/Sticky/index.ts +2 -0
- package/src/components/Switch/Switch.module.scss +182 -0
- package/src/components/Switch/Switch.tokens.scss +59 -0
- package/src/components/Switch/Switch.tsx +214 -0
- package/src/components/Switch/index.ts +2 -0
- package/src/components/Table/Table.module.scss +308 -0
- package/src/components/Table/Table.tokens.scss +79 -0
- package/src/components/Table/Table.tsx +381 -0
- package/src/components/Table/index.ts +15 -0
- package/src/components/TableOfContents/TableOfContents.module.scss +83 -0
- package/src/components/TableOfContents/TableOfContents.tokens.scss +26 -0
- package/src/components/TableOfContents/TableOfContents.tsx +192 -0
- package/src/components/TableOfContents/activeSection.ts +18 -0
- package/src/components/TableOfContents/index.ts +2 -0
- package/src/components/Tabs/Tabs.module.scss +359 -0
- package/src/components/Tabs/Tabs.tokens.scss +50 -0
- package/src/components/Tabs/Tabs.tsx +538 -0
- package/src/components/Tabs/index.ts +2 -0
- package/src/components/Text/Text.module.scss +115 -0
- package/src/components/Text/Text.tokens.scss +55 -0
- package/src/components/Text/Text.tsx +197 -0
- package/src/components/Text/index.ts +2 -0
- package/src/components/Textarea/Textarea.module.scss +94 -0
- package/src/components/Textarea/Textarea.tokens.scss +48 -0
- package/src/components/Textarea/Textarea.tsx +246 -0
- package/src/components/Textarea/index.ts +2 -0
- package/src/components/Thread/Thread.module.scss +161 -0
- package/src/components/Thread/Thread.tokens.scss +32 -0
- package/src/components/Thread/Thread.tsx +176 -0
- package/src/components/Thread/index.ts +2 -0
- package/src/components/TimeField/TimeField.module.scss +232 -0
- package/src/components/TimeField/TimeField.tsx +793 -0
- package/src/components/TimeField/index.ts +2 -0
- package/src/components/Timeline/Timeline.module.scss +58 -0
- package/src/components/Timeline/Timeline.tokens.scss +13 -0
- package/src/components/Timeline/Timeline.tsx +53 -0
- package/src/components/Timeline/index.ts +2 -0
- package/src/components/Title/Title.module.scss +84 -0
- package/src/components/Title/Title.tokens.scss +37 -0
- package/src/components/Title/Title.tsx +150 -0
- package/src/components/Title/index.ts +2 -0
- package/src/components/Toast/Toast.module.scss +299 -0
- package/src/components/Toast/Toast.tokens.scss +65 -0
- package/src/components/Toast/Toast.tsx +122 -0
- package/src/components/Toast/ToastViewport.tsx +166 -0
- package/src/components/Toast/api.ts +175 -0
- package/src/components/Toast/index.ts +5 -0
- package/src/components/Toast/store.ts +133 -0
- package/src/components/Toast/useToastTimer.ts +94 -0
- package/src/components/Tooltip/Tooltip.module.scss +99 -0
- package/src/components/Tooltip/Tooltip.tokens.scss +23 -0
- package/src/components/Tooltip/Tooltip.tsx +341 -0
- package/src/components/Tooltip/index.ts +2 -0
- package/src/components/TopBar/TopBar.module.scss +235 -0
- package/src/components/TopBar/TopBar.tokens.scss +79 -0
- package/src/components/TopBar/TopBar.tsx +87 -0
- package/src/components/TopBar/TopBarEnd.tsx +20 -0
- package/src/components/TopBar/TopBarIconButton.tsx +86 -0
- package/src/components/TopBar/TopBarSearch.tsx +319 -0
- package/src/components/TopBar/TopBarSearchPanel.tsx +268 -0
- package/src/components/TopBar/TopBarStart.tsx +20 -0
- package/src/components/TopBar/index.ts +11 -0
- package/src/components/Tour/Spotlight.tsx +121 -0
- package/src/components/Tour/Tour.module.scss +210 -0
- package/src/components/Tour/Tour.tokens.scss +58 -0
- package/src/components/Tour/Tour.tsx +617 -0
- package/src/components/Tour/index.ts +2 -0
- package/src/components/Tour/useTourTarget.ts +115 -0
- package/src/components/TreeView/TreeView.module.scss +268 -0
- package/src/components/TreeView/TreeView.tokens.scss +36 -0
- package/src/components/TreeView/TreeView.tsx +536 -0
- package/src/components/TreeView/index.ts +3 -0
- package/src/components/TreeView/treeModel.ts +224 -0
- package/src/components/VisuallyHidden/VisuallyHidden.module.scss +7 -0
- package/src/components/VisuallyHidden/VisuallyHidden.tsx +29 -0
- package/src/components/VisuallyHidden/index.ts +2 -0
- package/src/components/WidgetPreview/WidgetPreview.module.scss +11 -0
- package/src/components/WidgetPreview/WidgetPreview.tokens.scss +26 -0
- package/src/components/WidgetPreview/WidgetPreview.tsx +36 -0
- package/src/components/WidgetPreview/WidgetShape.module.scss +140 -0
- package/src/components/WidgetPreview/WidgetShape.tsx +159 -0
- package/src/components/WidgetPreview/index.ts +2 -0
- package/src/components/_internal/Trend.tsx +27 -0
- package/src/components/_internal/choiceTile.scss +74 -0
- package/src/components/_internal/collapse.scss +10 -0
- package/src/components/_internal/collapse.ts +58 -0
- package/src/components/_internal/currency.ts +257 -0
- package/src/components/_internal/dragAnnouncements.ts +200 -0
- package/src/components/_internal/fieldWiring.ts +225 -0
- package/src/components/_internal/gridSpan.ts +54 -0
- package/src/components/_internal/isoDay.ts +35 -0
- package/src/components/_internal/letterKey.ts +26 -0
- package/src/components/_internal/overlay/focusRestore.ts +21 -0
- package/src/components/_internal/overlay/index.ts +12 -0
- package/src/components/_internal/overlay/tourHold.ts +23 -0
- package/src/components/_internal/overlay/useFocusTrap.ts +120 -0
- package/src/components/_internal/overlay/useInOverlay.ts +81 -0
- package/src/components/_internal/overlay/useOverlayStack.ts +232 -0
- package/src/components/_internal/overlay/useScrollLock.ts +112 -0
- package/src/components/_internal/refs.ts +48 -0
- package/src/components/_internal/statusColor.ts +26 -0
- package/src/components/_internal/timeUtils.ts +23 -0
- package/src/components/_internal/useAsyncOptions.ts +145 -0
- package/src/components/_internal/useClippedTooltip.ts +50 -0
- package/src/components/_internal/useControllableState.ts +50 -0
- package/src/components.manifest.json +1141 -0
- package/src/hooks/useBelowBreakpoint.ts +58 -0
- package/src/i18n/I18nProvider.tsx +52 -0
- package/src/i18n/LocaleProvider.tsx +17 -0
- package/src/i18n/en.ts +648 -0
- package/src/i18n/format.ts +71 -0
- package/src/i18n/index.ts +13 -0
- package/src/i18n/messages.ts +1275 -0
- package/src/i18n/ru.ts +677 -0
- package/src/i18n/useLocale.ts +19 -0
- package/src/i18n/useTranslation.ts +58 -0
- package/src/index.ts +913 -0
- package/src/palette/index.ts +2 -0
- package/src/palette/palette.ts +108 -0
- package/src/styles/dark.scss +4 -0
- package/src/styles/global.scss +7 -0
- package/src/styles/mixins.scss +143 -0
- package/src/styles/reset.scss +78 -0
- package/src/styles/tokens.scss +18 -0
- 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
|
+
);
|