From 6ec259a0e71174651bae95d4628138bf6fd68742 Mon Sep 17 00:00:00 2001
From: kj_sh604
Date: Sun, 15 Mar 2026 16:19:35 -0400
Subject: refactor: packages/
---
packages/excalidraw/components/Actions.scss | 93 +
packages/excalidraw/components/Actions.tsx | 478 +
.../excalidraw/components/ActiveConfirmDialog.tsx | 35 +
packages/excalidraw/components/App.tsx | 11180 +++++++++++++++++++
packages/excalidraw/components/Avatar.scss | 7 +
packages/excalidraw/components/Avatar.tsx | 41 +
.../components/BraveMeasureTextError.tsx | 43 +
packages/excalidraw/components/Button.scss | 7 +
packages/excalidraw/components/Button.tsx | 44 +
packages/excalidraw/components/ButtonIcon.scss | 12 +
packages/excalidraw/components/ButtonIcon.tsx | 37 +
packages/excalidraw/components/ButtonIconCycle.tsx | 29 +
.../excalidraw/components/ButtonIconSelect.tsx | 58 +
packages/excalidraw/components/ButtonSelect.tsx | 30 +
packages/excalidraw/components/ButtonSeparator.tsx | 10 +
packages/excalidraw/components/Card.scss | 57 +
packages/excalidraw/components/Card.tsx | 28 +
packages/excalidraw/components/CheckboxItem.scss | 91 +
packages/excalidraw/components/CheckboxItem.tsx | 36 +
.../components/ColorPicker/ColorInput.tsx | 130 +
.../components/ColorPicker/ColorPicker.scss | 441 +
.../components/ColorPicker/ColorPicker.tsx | 246 +
.../components/ColorPicker/CustomColorList.tsx | 63 +
.../components/ColorPicker/HotkeyLabel.tsx | 29 +
.../excalidraw/components/ColorPicker/Picker.tsx | 178 +
.../components/ColorPicker/PickerColorList.tsx | 91 +
.../components/ColorPicker/PickerHeading.tsx | 7 +
.../components/ColorPicker/ShadeList.tsx | 105 +
.../excalidraw/components/ColorPicker/TopPicks.tsx | 65 +
.../components/ColorPicker/colorPickerUtils.ts | 133 +
.../components/ColorPicker/keyboardNavHandlers.ts | 286 +
.../components/CommandPalette/CommandPalette.scss | 137 +
.../components/CommandPalette/CommandPalette.tsx | 956 ++
.../CommandPalette/defaultCommandPaletteItems.ts | 11 +
.../excalidraw/components/CommandPalette/types.ts | 26 +
packages/excalidraw/components/ConfirmDialog.scss | 11 +
packages/excalidraw/components/ConfirmDialog.tsx | 78 +
packages/excalidraw/components/ContextMenu.scss | 98 +
packages/excalidraw/components/ContextMenu.tsx | 128 +
packages/excalidraw/components/DarkModeToggle.tsx | 52 +
.../excalidraw/components/DefaultSidebar.test.tsx | 144 +
packages/excalidraw/components/DefaultSidebar.tsx | 121 +
.../DiagramToCodePlugin/DiagramToCodePlugin.tsx | 17 +
packages/excalidraw/components/Dialog.scss | 54 +
packages/excalidraw/components/Dialog.tsx | 134 +
.../excalidraw/components/DialogActionButton.scss | 47 +
.../excalidraw/components/DialogActionButton.tsx | 46 +
.../excalidraw/components/ElementLinkDialog.scss | 87 +
.../excalidraw/components/ElementLinkDialog.tsx | 174 +
packages/excalidraw/components/ErrorDialog.tsx | 40 +
packages/excalidraw/components/ExcalidrawLogo.scss | 73 +
packages/excalidraw/components/ExcalidrawLogo.tsx | 69 +
packages/excalidraw/components/ExportDialog.scss | 129 +
packages/excalidraw/components/EyeDropper.scss | 48 +
packages/excalidraw/components/EyeDropper.tsx | 235 +
packages/excalidraw/components/FilledButton.scss | 317 +
packages/excalidraw/components/FilledButton.tsx | 114 +
.../excalidraw/components/FixedSideContainer.scss | 39 +
.../excalidraw/components/FixedSideContainer.tsx | 26 +
.../components/FollowMode/FollowMode.scss | 59 +
.../components/FollowMode/FollowMode.tsx | 42 +
.../components/FontPicker/FontPicker.scss | 15 +
.../components/FontPicker/FontPicker.tsx | 110 +
.../components/FontPicker/FontPickerList.tsx | 272 +
.../components/FontPicker/FontPickerTrigger.tsx | 38 +
.../components/FontPicker/keyboardNavHandlers.ts | 66 +
packages/excalidraw/components/HandButton.tsx | 32 +
packages/excalidraw/components/HelpButton.tsx | 20 +
packages/excalidraw/components/HelpDialog.scss | 130 +
packages/excalidraw/components/HelpDialog.tsx | 503 +
packages/excalidraw/components/HintViewer.scss | 38 +
packages/excalidraw/components/HintViewer.tsx | 194 +
packages/excalidraw/components/IconPicker.scss | 109 +
packages/excalidraw/components/IconPicker.tsx | 239 +
.../excalidraw/components/ImageExportDialog.scss | 175 +
.../excalidraw/components/ImageExportDialog.tsx | 407 +
packages/excalidraw/components/InitializeApp.tsx | 28 +
packages/excalidraw/components/InlineIcon.tsx | 15 +
packages/excalidraw/components/Island.scss | 16 +
packages/excalidraw/components/Island.tsx | 23 +
.../excalidraw/components/JSONExportDialog.tsx | 136 +
.../excalidraw/components/LaserPointerButton.tsx | 41 +
packages/excalidraw/components/LayerUI.scss | 119 +
packages/excalidraw/components/LayerUI.tsx | 607 +
packages/excalidraw/components/LibraryMenu.scss | 150 +
packages/excalidraw/components/LibraryMenu.tsx | 290 +
.../components/LibraryMenuBrowseButton.tsx | 31 +
.../components/LibraryMenuControlButtons.tsx | 33 +
.../components/LibraryMenuHeaderContent.tsx | 321 +
.../excalidraw/components/LibraryMenuItems.scss | 99 +
.../excalidraw/components/LibraryMenuItems.tsx | 342 +
.../excalidraw/components/LibraryMenuSection.tsx | 78 +
packages/excalidraw/components/LibraryUnit.scss | 185 +
packages/excalidraw/components/LibraryUnit.tsx | 108 +
packages/excalidraw/components/LoadingMessage.tsx | 40 +
packages/excalidraw/components/LockButton.tsx | 48 +
packages/excalidraw/components/MagicButton.tsx | 39 +
packages/excalidraw/components/MobileMenu.tsx | 211 +
packages/excalidraw/components/Modal.scss | 136 +
packages/excalidraw/components/Modal.tsx | 65 +
.../OverwriteConfirm/OverwriteConfirm.scss | 126 +
.../OverwriteConfirm/OverwriteConfirm.tsx | 74 +
.../OverwriteConfirm/OverwriteConfirmActions.tsx | 85 +
.../OverwriteConfirm/OverwriteConfirmState.ts | 45 +
packages/excalidraw/components/Paragraph.tsx | 10 +
.../excalidraw/components/PasteChartDialog.scss | 46 +
.../excalidraw/components/PasteChartDialog.tsx | 136 +
packages/excalidraw/components/PenModeButton.tsx | 46 +
packages/excalidraw/components/Popover.scss | 8 +
packages/excalidraw/components/Popover.tsx | 152 +
packages/excalidraw/components/ProjectName.scss | 25 +
packages/excalidraw/components/ProjectName.tsx | 57 +
.../excalidraw/components/PropertiesPopover.tsx | 96 +
packages/excalidraw/components/PublishLibrary.scss | 172 +
packages/excalidraw/components/PublishLibrary.tsx | 540 +
packages/excalidraw/components/QuickSearch.scss | 48 +
packages/excalidraw/components/QuickSearch.tsx | 28 +
packages/excalidraw/components/RadioGroup.scss | 91 +
packages/excalidraw/components/RadioGroup.tsx | 45 +
packages/excalidraw/components/Range.scss | 56 +
packages/excalidraw/components/Range.tsx | 65 +
packages/excalidraw/components/SVGLayer.scss | 24 +
packages/excalidraw/components/SVGLayer.tsx | 33 +
packages/excalidraw/components/ScrollableList.scss | 21 +
packages/excalidraw/components/ScrollableList.tsx | 24 +
packages/excalidraw/components/SearchMenu.scss | 110 +
packages/excalidraw/components/SearchMenu.tsx | 713 ++
packages/excalidraw/components/Section.tsx | 28 +
.../excalidraw/components/ShareableLinkDialog.scss | 91 +
.../excalidraw/components/ShareableLinkDialog.tsx | 80 +
.../excalidraw/components/Sidebar/Sidebar.scss | 176 +
.../excalidraw/components/Sidebar/Sidebar.test.tsx | 393 +
packages/excalidraw/components/Sidebar/Sidebar.tsx | 213 +
.../components/Sidebar/SidebarHeader.tsx | 57 +
.../excalidraw/components/Sidebar/SidebarTab.tsx | 18 +
.../components/Sidebar/SidebarTabTrigger.tsx | 26 +
.../components/Sidebar/SidebarTabTriggers.tsx | 16 +
.../excalidraw/components/Sidebar/SidebarTabs.tsx | 36 +
.../components/Sidebar/SidebarTrigger.scss | 38 +
.../components/Sidebar/SidebarTrigger.tsx | 45 +
packages/excalidraw/components/Sidebar/common.ts | 42 +
.../components/Sidebar/siderbar.test.helpers.tsx | 42 +
packages/excalidraw/components/Spinner.scss | 49 +
packages/excalidraw/components/Spinner.tsx | 43 +
packages/excalidraw/components/Stack.scss | 19 +
packages/excalidraw/components/Stack.tsx | 62 +
packages/excalidraw/components/Stats/Angle.tsx | 95 +
.../excalidraw/components/Stats/CanvasGrid.tsx | 67 +
.../excalidraw/components/Stats/Collapsible.tsx | 46 +
packages/excalidraw/components/Stats/Dimension.tsx | 272 +
.../excalidraw/components/Stats/DragInput.scss | 76 +
packages/excalidraw/components/Stats/DragInput.tsx | 355 +
packages/excalidraw/components/Stats/FontSize.tsx | 99 +
.../excalidraw/components/Stats/MultiAngle.tsx | 136 +
.../excalidraw/components/Stats/MultiDimension.tsx | 401 +
.../excalidraw/components/Stats/MultiFontSize.tsx | 164 +
.../excalidraw/components/Stats/MultiPosition.tsx | 270 +
packages/excalidraw/components/Stats/Position.tsx | 214 +
packages/excalidraw/components/Stats/Stats.scss | 72 +
packages/excalidraw/components/Stats/index.tsx | 434 +
.../excalidraw/components/Stats/stats.test.tsx | 724 ++
packages/excalidraw/components/Stats/utils.ts | 219 +
packages/excalidraw/components/Switch.scss | 118 +
packages/excalidraw/components/Switch.tsx | 38 +
.../components/TTDDialog/MermaidToExcalidraw.scss | 10 +
.../components/TTDDialog/MermaidToExcalidraw.tsx | 132 +
.../excalidraw/components/TTDDialog/TTDDialog.scss | 315 +
.../excalidraw/components/TTDDialog/TTDDialog.tsx | 394 +
.../components/TTDDialog/TTDDialogInput.tsx | 53 +
.../components/TTDDialog/TTDDialogOutput.tsx | 39 +
.../components/TTDDialog/TTDDialogPanel.tsx | 63 +
.../components/TTDDialog/TTDDialogPanels.tsx | 5 +
.../TTDDialog/TTDDialogSubmitShortcut.tsx | 14 +
.../components/TTDDialog/TTDDialogTab.tsx | 17 +
.../components/TTDDialog/TTDDialogTabTrigger.tsx | 21 +
.../components/TTDDialog/TTDDialogTabTriggers.tsx | 13 +
.../components/TTDDialog/TTDDialogTabs.tsx | 55 +
.../components/TTDDialog/TTDDialogTrigger.tsx | 35 +
packages/excalidraw/components/TTDDialog/common.ts | 161 +
packages/excalidraw/components/TextField.scss | 123 +
packages/excalidraw/components/TextField.tsx | 112 +
packages/excalidraw/components/TextInput.scss | 7 +
packages/excalidraw/components/Toast.scss | 49 +
packages/excalidraw/components/Toast.tsx | 63 +
packages/excalidraw/components/ToolButton.tsx | 206 +
packages/excalidraw/components/ToolIcon.scss | 199 +
packages/excalidraw/components/Toolbar.scss | 50 +
packages/excalidraw/components/Tooltip.scss | 47 +
packages/excalidraw/components/Tooltip.tsx | 119 +
packages/excalidraw/components/Trans.test.tsx | 72 +
packages/excalidraw/components/Trans.tsx | 170 +
packages/excalidraw/components/UserList.scss | 160 +
packages/excalidraw/components/UserList.tsx | 293 +
.../components/__snapshots__/App.test.tsx.snap | 50 +
.../components/canvases/InteractiveCanvas.tsx | 240 +
.../components/canvases/NewElementCanvas.tsx | 56 +
.../components/canvases/StaticCanvas.tsx | 141 +
packages/excalidraw/components/canvases/index.tsx | 4 +
.../components/dropdownMenu/DropdownMenu.scss | 218 +
.../components/dropdownMenu/DropdownMenu.test.tsx | 26 +
.../components/dropdownMenu/DropdownMenu.tsx | 43 +
.../dropdownMenu/DropdownMenuContent.tsx | 88 +
.../components/dropdownMenu/DropdownMenuGroup.tsx | 23 +
.../components/dropdownMenu/DropdownMenuItem.tsx | 123 +
.../dropdownMenu/DropdownMenuItemContent.tsx | 28 +
.../dropdownMenu/DropdownMenuItemContentRadio.tsx | 51 +
.../dropdownMenu/DropdownMenuItemCustom.tsx | 25 +
.../dropdownMenu/DropdownMenuItemLink.tsx | 49 +
.../dropdownMenu/DropdownMenuSeparator.tsx | 14 +
.../dropdownMenu/DropdownMenuTrigger.tsx | 40 +
.../excalidraw/components/dropdownMenu/common.ts | 38 +
.../components/dropdownMenu/dropdownMenuUtils.ts | 35 +
packages/excalidraw/components/footer/Footer.tsx | 95 +
.../excalidraw/components/footer/FooterCenter.scss | 11 +
.../excalidraw/components/footer/FooterCenter.tsx | 24 +
.../components/hoc/withInternalFallback.test.tsx | 101 +
.../components/hoc/withInternalFallback.tsx | 75 +
.../excalidraw/components/hyperlink/Hyperlink.scss | 70 +
.../excalidraw/components/hyperlink/Hyperlink.tsx | 480 +
.../excalidraw/components/hyperlink/helpers.ts | 99 +
packages/excalidraw/components/icons.tsx | 2222 ++++
.../LiveCollaborationTrigger.scss | 66 +
.../LiveCollaborationTrigger.tsx | 42 +
.../components/main-menu/DefaultItems.scss | 21 +
.../components/main-menu/DefaultItems.tsx | 391 +
.../excalidraw/components/main-menu/MainMenu.tsx | 84 +
.../welcome-screen/WelcomeScreen.Center.tsx | 195 +
.../welcome-screen/WelcomeScreen.Hints.tsx | 52 +
.../components/welcome-screen/WelcomeScreen.scss | 272 +
.../components/welcome-screen/WelcomeScreen.tsx | 26 +
230 files changed, 39876 insertions(+)
create mode 100644 packages/excalidraw/components/Actions.scss
create mode 100644 packages/excalidraw/components/Actions.tsx
create mode 100644 packages/excalidraw/components/ActiveConfirmDialog.tsx
create mode 100644 packages/excalidraw/components/App.tsx
create mode 100644 packages/excalidraw/components/Avatar.scss
create mode 100644 packages/excalidraw/components/Avatar.tsx
create mode 100644 packages/excalidraw/components/BraveMeasureTextError.tsx
create mode 100644 packages/excalidraw/components/Button.scss
create mode 100644 packages/excalidraw/components/Button.tsx
create mode 100644 packages/excalidraw/components/ButtonIcon.scss
create mode 100644 packages/excalidraw/components/ButtonIcon.tsx
create mode 100644 packages/excalidraw/components/ButtonIconCycle.tsx
create mode 100644 packages/excalidraw/components/ButtonIconSelect.tsx
create mode 100644 packages/excalidraw/components/ButtonSelect.tsx
create mode 100644 packages/excalidraw/components/ButtonSeparator.tsx
create mode 100644 packages/excalidraw/components/Card.scss
create mode 100644 packages/excalidraw/components/Card.tsx
create mode 100644 packages/excalidraw/components/CheckboxItem.scss
create mode 100644 packages/excalidraw/components/CheckboxItem.tsx
create mode 100644 packages/excalidraw/components/ColorPicker/ColorInput.tsx
create mode 100644 packages/excalidraw/components/ColorPicker/ColorPicker.scss
create mode 100644 packages/excalidraw/components/ColorPicker/ColorPicker.tsx
create mode 100644 packages/excalidraw/components/ColorPicker/CustomColorList.tsx
create mode 100644 packages/excalidraw/components/ColorPicker/HotkeyLabel.tsx
create mode 100644 packages/excalidraw/components/ColorPicker/Picker.tsx
create mode 100644 packages/excalidraw/components/ColorPicker/PickerColorList.tsx
create mode 100644 packages/excalidraw/components/ColorPicker/PickerHeading.tsx
create mode 100644 packages/excalidraw/components/ColorPicker/ShadeList.tsx
create mode 100644 packages/excalidraw/components/ColorPicker/TopPicks.tsx
create mode 100644 packages/excalidraw/components/ColorPicker/colorPickerUtils.ts
create mode 100644 packages/excalidraw/components/ColorPicker/keyboardNavHandlers.ts
create mode 100644 packages/excalidraw/components/CommandPalette/CommandPalette.scss
create mode 100644 packages/excalidraw/components/CommandPalette/CommandPalette.tsx
create mode 100644 packages/excalidraw/components/CommandPalette/defaultCommandPaletteItems.ts
create mode 100644 packages/excalidraw/components/CommandPalette/types.ts
create mode 100644 packages/excalidraw/components/ConfirmDialog.scss
create mode 100644 packages/excalidraw/components/ConfirmDialog.tsx
create mode 100644 packages/excalidraw/components/ContextMenu.scss
create mode 100644 packages/excalidraw/components/ContextMenu.tsx
create mode 100644 packages/excalidraw/components/DarkModeToggle.tsx
create mode 100644 packages/excalidraw/components/DefaultSidebar.test.tsx
create mode 100644 packages/excalidraw/components/DefaultSidebar.tsx
create mode 100644 packages/excalidraw/components/DiagramToCodePlugin/DiagramToCodePlugin.tsx
create mode 100644 packages/excalidraw/components/Dialog.scss
create mode 100644 packages/excalidraw/components/Dialog.tsx
create mode 100644 packages/excalidraw/components/DialogActionButton.scss
create mode 100644 packages/excalidraw/components/DialogActionButton.tsx
create mode 100644 packages/excalidraw/components/ElementLinkDialog.scss
create mode 100644 packages/excalidraw/components/ElementLinkDialog.tsx
create mode 100644 packages/excalidraw/components/ErrorDialog.tsx
create mode 100644 packages/excalidraw/components/ExcalidrawLogo.scss
create mode 100644 packages/excalidraw/components/ExcalidrawLogo.tsx
create mode 100644 packages/excalidraw/components/ExportDialog.scss
create mode 100644 packages/excalidraw/components/EyeDropper.scss
create mode 100644 packages/excalidraw/components/EyeDropper.tsx
create mode 100644 packages/excalidraw/components/FilledButton.scss
create mode 100644 packages/excalidraw/components/FilledButton.tsx
create mode 100644 packages/excalidraw/components/FixedSideContainer.scss
create mode 100644 packages/excalidraw/components/FixedSideContainer.tsx
create mode 100644 packages/excalidraw/components/FollowMode/FollowMode.scss
create mode 100644 packages/excalidraw/components/FollowMode/FollowMode.tsx
create mode 100644 packages/excalidraw/components/FontPicker/FontPicker.scss
create mode 100644 packages/excalidraw/components/FontPicker/FontPicker.tsx
create mode 100644 packages/excalidraw/components/FontPicker/FontPickerList.tsx
create mode 100644 packages/excalidraw/components/FontPicker/FontPickerTrigger.tsx
create mode 100644 packages/excalidraw/components/FontPicker/keyboardNavHandlers.ts
create mode 100644 packages/excalidraw/components/HandButton.tsx
create mode 100644 packages/excalidraw/components/HelpButton.tsx
create mode 100644 packages/excalidraw/components/HelpDialog.scss
create mode 100644 packages/excalidraw/components/HelpDialog.tsx
create mode 100644 packages/excalidraw/components/HintViewer.scss
create mode 100644 packages/excalidraw/components/HintViewer.tsx
create mode 100644 packages/excalidraw/components/IconPicker.scss
create mode 100644 packages/excalidraw/components/IconPicker.tsx
create mode 100644 packages/excalidraw/components/ImageExportDialog.scss
create mode 100644 packages/excalidraw/components/ImageExportDialog.tsx
create mode 100644 packages/excalidraw/components/InitializeApp.tsx
create mode 100644 packages/excalidraw/components/InlineIcon.tsx
create mode 100644 packages/excalidraw/components/Island.scss
create mode 100644 packages/excalidraw/components/Island.tsx
create mode 100644 packages/excalidraw/components/JSONExportDialog.tsx
create mode 100644 packages/excalidraw/components/LaserPointerButton.tsx
create mode 100644 packages/excalidraw/components/LayerUI.scss
create mode 100644 packages/excalidraw/components/LayerUI.tsx
create mode 100644 packages/excalidraw/components/LibraryMenu.scss
create mode 100644 packages/excalidraw/components/LibraryMenu.tsx
create mode 100644 packages/excalidraw/components/LibraryMenuBrowseButton.tsx
create mode 100644 packages/excalidraw/components/LibraryMenuControlButtons.tsx
create mode 100644 packages/excalidraw/components/LibraryMenuHeaderContent.tsx
create mode 100644 packages/excalidraw/components/LibraryMenuItems.scss
create mode 100644 packages/excalidraw/components/LibraryMenuItems.tsx
create mode 100644 packages/excalidraw/components/LibraryMenuSection.tsx
create mode 100644 packages/excalidraw/components/LibraryUnit.scss
create mode 100644 packages/excalidraw/components/LibraryUnit.tsx
create mode 100644 packages/excalidraw/components/LoadingMessage.tsx
create mode 100644 packages/excalidraw/components/LockButton.tsx
create mode 100644 packages/excalidraw/components/MagicButton.tsx
create mode 100644 packages/excalidraw/components/MobileMenu.tsx
create mode 100644 packages/excalidraw/components/Modal.scss
create mode 100644 packages/excalidraw/components/Modal.tsx
create mode 100644 packages/excalidraw/components/OverwriteConfirm/OverwriteConfirm.scss
create mode 100644 packages/excalidraw/components/OverwriteConfirm/OverwriteConfirm.tsx
create mode 100644 packages/excalidraw/components/OverwriteConfirm/OverwriteConfirmActions.tsx
create mode 100644 packages/excalidraw/components/OverwriteConfirm/OverwriteConfirmState.ts
create mode 100644 packages/excalidraw/components/Paragraph.tsx
create mode 100644 packages/excalidraw/components/PasteChartDialog.scss
create mode 100644 packages/excalidraw/components/PasteChartDialog.tsx
create mode 100644 packages/excalidraw/components/PenModeButton.tsx
create mode 100644 packages/excalidraw/components/Popover.scss
create mode 100644 packages/excalidraw/components/Popover.tsx
create mode 100644 packages/excalidraw/components/ProjectName.scss
create mode 100644 packages/excalidraw/components/ProjectName.tsx
create mode 100644 packages/excalidraw/components/PropertiesPopover.tsx
create mode 100644 packages/excalidraw/components/PublishLibrary.scss
create mode 100644 packages/excalidraw/components/PublishLibrary.tsx
create mode 100644 packages/excalidraw/components/QuickSearch.scss
create mode 100644 packages/excalidraw/components/QuickSearch.tsx
create mode 100644 packages/excalidraw/components/RadioGroup.scss
create mode 100644 packages/excalidraw/components/RadioGroup.tsx
create mode 100644 packages/excalidraw/components/Range.scss
create mode 100644 packages/excalidraw/components/Range.tsx
create mode 100644 packages/excalidraw/components/SVGLayer.scss
create mode 100644 packages/excalidraw/components/SVGLayer.tsx
create mode 100644 packages/excalidraw/components/ScrollableList.scss
create mode 100644 packages/excalidraw/components/ScrollableList.tsx
create mode 100644 packages/excalidraw/components/SearchMenu.scss
create mode 100644 packages/excalidraw/components/SearchMenu.tsx
create mode 100644 packages/excalidraw/components/Section.tsx
create mode 100644 packages/excalidraw/components/ShareableLinkDialog.scss
create mode 100644 packages/excalidraw/components/ShareableLinkDialog.tsx
create mode 100644 packages/excalidraw/components/Sidebar/Sidebar.scss
create mode 100644 packages/excalidraw/components/Sidebar/Sidebar.test.tsx
create mode 100644 packages/excalidraw/components/Sidebar/Sidebar.tsx
create mode 100644 packages/excalidraw/components/Sidebar/SidebarHeader.tsx
create mode 100644 packages/excalidraw/components/Sidebar/SidebarTab.tsx
create mode 100644 packages/excalidraw/components/Sidebar/SidebarTabTrigger.tsx
create mode 100644 packages/excalidraw/components/Sidebar/SidebarTabTriggers.tsx
create mode 100644 packages/excalidraw/components/Sidebar/SidebarTabs.tsx
create mode 100644 packages/excalidraw/components/Sidebar/SidebarTrigger.scss
create mode 100644 packages/excalidraw/components/Sidebar/SidebarTrigger.tsx
create mode 100644 packages/excalidraw/components/Sidebar/common.ts
create mode 100644 packages/excalidraw/components/Sidebar/siderbar.test.helpers.tsx
create mode 100644 packages/excalidraw/components/Spinner.scss
create mode 100644 packages/excalidraw/components/Spinner.tsx
create mode 100644 packages/excalidraw/components/Stack.scss
create mode 100644 packages/excalidraw/components/Stack.tsx
create mode 100644 packages/excalidraw/components/Stats/Angle.tsx
create mode 100644 packages/excalidraw/components/Stats/CanvasGrid.tsx
create mode 100644 packages/excalidraw/components/Stats/Collapsible.tsx
create mode 100644 packages/excalidraw/components/Stats/Dimension.tsx
create mode 100644 packages/excalidraw/components/Stats/DragInput.scss
create mode 100644 packages/excalidraw/components/Stats/DragInput.tsx
create mode 100644 packages/excalidraw/components/Stats/FontSize.tsx
create mode 100644 packages/excalidraw/components/Stats/MultiAngle.tsx
create mode 100644 packages/excalidraw/components/Stats/MultiDimension.tsx
create mode 100644 packages/excalidraw/components/Stats/MultiFontSize.tsx
create mode 100644 packages/excalidraw/components/Stats/MultiPosition.tsx
create mode 100644 packages/excalidraw/components/Stats/Position.tsx
create mode 100644 packages/excalidraw/components/Stats/Stats.scss
create mode 100644 packages/excalidraw/components/Stats/index.tsx
create mode 100644 packages/excalidraw/components/Stats/stats.test.tsx
create mode 100644 packages/excalidraw/components/Stats/utils.ts
create mode 100644 packages/excalidraw/components/Switch.scss
create mode 100644 packages/excalidraw/components/Switch.tsx
create mode 100644 packages/excalidraw/components/TTDDialog/MermaidToExcalidraw.scss
create mode 100644 packages/excalidraw/components/TTDDialog/MermaidToExcalidraw.tsx
create mode 100644 packages/excalidraw/components/TTDDialog/TTDDialog.scss
create mode 100644 packages/excalidraw/components/TTDDialog/TTDDialog.tsx
create mode 100644 packages/excalidraw/components/TTDDialog/TTDDialogInput.tsx
create mode 100644 packages/excalidraw/components/TTDDialog/TTDDialogOutput.tsx
create mode 100644 packages/excalidraw/components/TTDDialog/TTDDialogPanel.tsx
create mode 100644 packages/excalidraw/components/TTDDialog/TTDDialogPanels.tsx
create mode 100644 packages/excalidraw/components/TTDDialog/TTDDialogSubmitShortcut.tsx
create mode 100644 packages/excalidraw/components/TTDDialog/TTDDialogTab.tsx
create mode 100644 packages/excalidraw/components/TTDDialog/TTDDialogTabTrigger.tsx
create mode 100644 packages/excalidraw/components/TTDDialog/TTDDialogTabTriggers.tsx
create mode 100644 packages/excalidraw/components/TTDDialog/TTDDialogTabs.tsx
create mode 100644 packages/excalidraw/components/TTDDialog/TTDDialogTrigger.tsx
create mode 100644 packages/excalidraw/components/TTDDialog/common.ts
create mode 100644 packages/excalidraw/components/TextField.scss
create mode 100644 packages/excalidraw/components/TextField.tsx
create mode 100644 packages/excalidraw/components/TextInput.scss
create mode 100644 packages/excalidraw/components/Toast.scss
create mode 100644 packages/excalidraw/components/Toast.tsx
create mode 100644 packages/excalidraw/components/ToolButton.tsx
create mode 100644 packages/excalidraw/components/ToolIcon.scss
create mode 100644 packages/excalidraw/components/Toolbar.scss
create mode 100644 packages/excalidraw/components/Tooltip.scss
create mode 100644 packages/excalidraw/components/Tooltip.tsx
create mode 100644 packages/excalidraw/components/Trans.test.tsx
create mode 100644 packages/excalidraw/components/Trans.tsx
create mode 100644 packages/excalidraw/components/UserList.scss
create mode 100644 packages/excalidraw/components/UserList.tsx
create mode 100644 packages/excalidraw/components/__snapshots__/App.test.tsx.snap
create mode 100644 packages/excalidraw/components/canvases/InteractiveCanvas.tsx
create mode 100644 packages/excalidraw/components/canvases/NewElementCanvas.tsx
create mode 100644 packages/excalidraw/components/canvases/StaticCanvas.tsx
create mode 100644 packages/excalidraw/components/canvases/index.tsx
create mode 100644 packages/excalidraw/components/dropdownMenu/DropdownMenu.scss
create mode 100644 packages/excalidraw/components/dropdownMenu/DropdownMenu.test.tsx
create mode 100644 packages/excalidraw/components/dropdownMenu/DropdownMenu.tsx
create mode 100644 packages/excalidraw/components/dropdownMenu/DropdownMenuContent.tsx
create mode 100644 packages/excalidraw/components/dropdownMenu/DropdownMenuGroup.tsx
create mode 100644 packages/excalidraw/components/dropdownMenu/DropdownMenuItem.tsx
create mode 100644 packages/excalidraw/components/dropdownMenu/DropdownMenuItemContent.tsx
create mode 100644 packages/excalidraw/components/dropdownMenu/DropdownMenuItemContentRadio.tsx
create mode 100644 packages/excalidraw/components/dropdownMenu/DropdownMenuItemCustom.tsx
create mode 100644 packages/excalidraw/components/dropdownMenu/DropdownMenuItemLink.tsx
create mode 100644 packages/excalidraw/components/dropdownMenu/DropdownMenuSeparator.tsx
create mode 100644 packages/excalidraw/components/dropdownMenu/DropdownMenuTrigger.tsx
create mode 100644 packages/excalidraw/components/dropdownMenu/common.ts
create mode 100644 packages/excalidraw/components/dropdownMenu/dropdownMenuUtils.ts
create mode 100644 packages/excalidraw/components/footer/Footer.tsx
create mode 100644 packages/excalidraw/components/footer/FooterCenter.scss
create mode 100644 packages/excalidraw/components/footer/FooterCenter.tsx
create mode 100644 packages/excalidraw/components/hoc/withInternalFallback.test.tsx
create mode 100644 packages/excalidraw/components/hoc/withInternalFallback.tsx
create mode 100644 packages/excalidraw/components/hyperlink/Hyperlink.scss
create mode 100644 packages/excalidraw/components/hyperlink/Hyperlink.tsx
create mode 100644 packages/excalidraw/components/hyperlink/helpers.ts
create mode 100644 packages/excalidraw/components/icons.tsx
create mode 100644 packages/excalidraw/components/live-collaboration/LiveCollaborationTrigger.scss
create mode 100644 packages/excalidraw/components/live-collaboration/LiveCollaborationTrigger.tsx
create mode 100644 packages/excalidraw/components/main-menu/DefaultItems.scss
create mode 100644 packages/excalidraw/components/main-menu/DefaultItems.tsx
create mode 100644 packages/excalidraw/components/main-menu/MainMenu.tsx
create mode 100644 packages/excalidraw/components/welcome-screen/WelcomeScreen.Center.tsx
create mode 100644 packages/excalidraw/components/welcome-screen/WelcomeScreen.Hints.tsx
create mode 100644 packages/excalidraw/components/welcome-screen/WelcomeScreen.scss
create mode 100644 packages/excalidraw/components/welcome-screen/WelcomeScreen.tsx
(limited to 'packages/excalidraw/components')
diff --git a/packages/excalidraw/components/Actions.scss b/packages/excalidraw/components/Actions.scss
new file mode 100644
index 0000000..5826628
--- /dev/null
+++ b/packages/excalidraw/components/Actions.scss
@@ -0,0 +1,93 @@
+.zoom-actions,
+.undo-redo-buttons {
+ background-color: var(--island-bg-color);
+ border-radius: var(--border-radius-lg);
+ box-shadow: 0 0 0 1px var(--color-surface-lowest);
+}
+
+.zoom-button,
+.undo-redo-buttons button {
+ border-radius: 0 !important;
+ background-color: var(--color-surface-low) !important;
+ font-size: 0.875rem !important;
+ width: var(--lg-button-size);
+ height: var(--lg-button-size);
+
+ svg {
+ width: var(--lg-icon-size) !important;
+ height: var(--lg-icon-size) !important;
+ }
+
+ .ToolIcon__icon {
+ width: 100%;
+ height: 100%;
+ }
+}
+
+.reset-zoom-button {
+ border-left: 0 !important;
+ border-right: 0 !important;
+ padding: 0 0.625rem !important;
+ width: 3.75rem !important;
+ justify-content: center;
+ color: var(--text-primary-color);
+}
+
+.zoom-out-button {
+ border-top-left-radius: var(--border-radius-lg) !important;
+ border-bottom-left-radius: var(--border-radius-lg) !important;
+
+ :root[dir="rtl"] & {
+ transform: scaleX(-1);
+ }
+
+ .ToolIcon__icon {
+ border-top-right-radius: 0 !important;
+ border-bottom-right-radius: 0 !important;
+ }
+}
+
+.zoom-in-button {
+ border-top-right-radius: var(--border-radius-lg) !important;
+ border-bottom-right-radius: var(--border-radius-lg) !important;
+
+ :root[dir="rtl"] & {
+ transform: scaleX(-1);
+ }
+
+ .ToolIcon__icon {
+ border-top-left-radius: 0 !important;
+ border-bottom-left-radius: 0 !important;
+ }
+}
+
+.undo-redo-buttons {
+ .undo-button-container button {
+ border-top-left-radius: var(--border-radius-lg) !important;
+ border-bottom-left-radius: var(--border-radius-lg) !important;
+ border-right: 0 !important;
+
+ :root[dir="rtl"] & {
+ transform: scaleX(-1);
+ }
+
+ .ToolIcon__icon {
+ border-top-right-radius: 0 !important;
+ border-bottom-right-radius: 0 !important;
+ }
+ }
+
+ .redo-button-container button {
+ border-top-right-radius: var(--border-radius-lg) !important;
+ border-bottom-right-radius: var(--border-radius-lg) !important;
+
+ :root[dir="rtl"] & {
+ transform: scaleX(-1);
+ }
+
+ .ToolIcon__icon {
+ border-top-left-radius: 0 !important;
+ border-bottom-left-radius: 0 !important;
+ }
+ }
+}
diff --git a/packages/excalidraw/components/Actions.tsx b/packages/excalidraw/components/Actions.tsx
new file mode 100644
index 0000000..cd9120f
--- /dev/null
+++ b/packages/excalidraw/components/Actions.tsx
@@ -0,0 +1,478 @@
+import { useState } from "react";
+import type { ActionManager } from "../actions/manager";
+import type {
+ ExcalidrawElement,
+ ExcalidrawElementType,
+ NonDeletedElementsMap,
+ NonDeletedSceneElementsMap,
+} from "../element/types";
+import { t } from "../i18n";
+import { useDevice } from "./App";
+import {
+ canChangeRoundness,
+ canHaveArrowheads,
+ getTargetElements,
+ hasBackground,
+ hasStrokeStyle,
+ hasStrokeWidth,
+} from "../scene";
+import { SHAPES } from "../shapes";
+import type { AppClassProperties, AppProps, UIAppState, Zoom } from "../types";
+import { capitalizeString, isTransparent } from "../utils";
+import Stack from "./Stack";
+import { ToolButton } from "./ToolButton";
+import { hasStrokeColor, toolIsArrow } from "../scene/comparisons";
+import { trackEvent } from "../analytics";
+import {
+ hasBoundTextElement,
+ isElbowArrow,
+ isImageElement,
+ isLinearElement,
+ isTextElement,
+} from "../element/typeChecks";
+import clsx from "clsx";
+import { actionToggleZenMode } from "../actions";
+import { Tooltip } from "./Tooltip";
+import {
+ shouldAllowVerticalAlign,
+ suppportsHorizontalAlign,
+} from "../element/textElement";
+
+import "./Actions.scss";
+import DropdownMenu from "./dropdownMenu/DropdownMenu";
+import {
+ EmbedIcon,
+ extraToolsIcon,
+ frameToolIcon,
+ mermaidLogoIcon,
+ laserPointerToolIcon,
+ MagicIcon,
+} from "./icons";
+import { KEYS } from "../keys";
+import { useTunnels } from "../context/tunnels";
+import { CLASSES } from "../constants";
+import { alignActionsPredicate } from "../actions/actionAlign";
+
+export const canChangeStrokeColor = (
+ appState: UIAppState,
+ targetElements: ExcalidrawElement[],
+) => {
+ let commonSelectedType: ExcalidrawElementType | null =
+ targetElements[0]?.type || null;
+
+ for (const element of targetElements) {
+ if (element.type !== commonSelectedType) {
+ commonSelectedType = null;
+ break;
+ }
+ }
+
+ return (
+ (hasStrokeColor(appState.activeTool.type) &&
+ appState.activeTool.type !== "image" &&
+ commonSelectedType !== "image" &&
+ commonSelectedType !== "frame" &&
+ commonSelectedType !== "magicframe") ||
+ targetElements.some((element) => hasStrokeColor(element.type))
+ );
+};
+
+export const canChangeBackgroundColor = (
+ appState: UIAppState,
+ targetElements: ExcalidrawElement[],
+) => {
+ return (
+ hasBackground(appState.activeTool.type) ||
+ targetElements.some((element) => hasBackground(element.type))
+ );
+};
+
+export const SelectedShapeActions = ({
+ appState,
+ elementsMap,
+ renderAction,
+ app,
+}: {
+ appState: UIAppState;
+ elementsMap: NonDeletedElementsMap | NonDeletedSceneElementsMap;
+ renderAction: ActionManager["renderAction"];
+ app: AppClassProperties;
+}) => {
+ const targetElements = getTargetElements(elementsMap, appState);
+
+ let isSingleElementBoundContainer = false;
+ if (
+ targetElements.length === 2 &&
+ (hasBoundTextElement(targetElements[0]) ||
+ hasBoundTextElement(targetElements[1]))
+ ) {
+ isSingleElementBoundContainer = true;
+ }
+ const isEditingTextOrNewElement = Boolean(
+ appState.editingTextElement || appState.newElement,
+ );
+ const device = useDevice();
+ const isRTL = document.documentElement.getAttribute("dir") === "rtl";
+
+ const showFillIcons =
+ (hasBackground(appState.activeTool.type) &&
+ !isTransparent(appState.currentItemBackgroundColor)) ||
+ targetElements.some(
+ (element) =>
+ hasBackground(element.type) && !isTransparent(element.backgroundColor),
+ );
+
+ const showLinkIcon =
+ targetElements.length === 1 || isSingleElementBoundContainer;
+
+ const showLineEditorAction =
+ !appState.editingLinearElement &&
+ targetElements.length === 1 &&
+ isLinearElement(targetElements[0]) &&
+ !isElbowArrow(targetElements[0]);
+
+ const showCropEditorAction =
+ !appState.croppingElementId &&
+ targetElements.length === 1 &&
+ isImageElement(targetElements[0]);
+
+ const showAlignActions =
+ !isSingleElementBoundContainer && alignActionsPredicate(appState, app);
+
+ return (
+
+
+ {canChangeStrokeColor(appState, targetElements) &&
+ renderAction("changeStrokeColor")}
+
+ {canChangeBackgroundColor(appState, targetElements) && (
+
{renderAction("changeBackgroundColor")}
+ )}
+ {showFillIcons && renderAction("changeFillStyle")}
+
+ {(hasStrokeWidth(appState.activeTool.type) ||
+ targetElements.some((element) => hasStrokeWidth(element.type))) &&
+ renderAction("changeStrokeWidth")}
+
+ {(appState.activeTool.type === "freedraw" ||
+ targetElements.some((element) => element.type === "freedraw")) &&
+ renderAction("changeStrokeShape")}
+
+ {(hasStrokeStyle(appState.activeTool.type) ||
+ targetElements.some((element) => hasStrokeStyle(element.type))) && (
+ <>
+ {renderAction("changeStrokeStyle")}
+ {renderAction("changeSloppiness")}
+ >
+ )}
+
+ {(canChangeRoundness(appState.activeTool.type) ||
+ targetElements.some((element) => canChangeRoundness(element.type))) && (
+ <>{renderAction("changeRoundness")}>
+ )}
+
+ {(toolIsArrow(appState.activeTool.type) ||
+ targetElements.some((element) => toolIsArrow(element.type))) && (
+ <>{renderAction("changeArrowType")}>
+ )}
+
+ {(appState.activeTool.type === "text" ||
+ targetElements.some(isTextElement)) && (
+ <>
+ {renderAction("changeFontFamily")}
+ {renderAction("changeFontSize")}
+ {(appState.activeTool.type === "text" ||
+ suppportsHorizontalAlign(targetElements, elementsMap)) &&
+ renderAction("changeTextAlign")}
+ >
+ )}
+
+ {shouldAllowVerticalAlign(targetElements, elementsMap) &&
+ renderAction("changeVerticalAlign")}
+ {(canHaveArrowheads(appState.activeTool.type) ||
+ targetElements.some((element) => canHaveArrowheads(element.type))) && (
+ <>{renderAction("changeArrowhead")}>
+ )}
+
+ {renderAction("changeOpacity")}
+
+
+
+ {showAlignActions && !isSingleElementBoundContainer && (
+
+ )}
+ {!isEditingTextOrNewElement && targetElements.length > 0 && (
+
+ )}
+
+ );
+};
+
+export const ShapesSwitcher = ({
+ activeTool,
+ appState,
+ app,
+ UIOptions,
+}: {
+ activeTool: UIAppState["activeTool"];
+ appState: UIAppState;
+ app: AppClassProperties;
+ UIOptions: AppProps["UIOptions"];
+}) => {
+ const [isExtraToolsMenuOpen, setIsExtraToolsMenuOpen] = useState(false);
+
+ const frameToolSelected = activeTool.type === "frame";
+ const laserToolSelected = activeTool.type === "laser";
+ const embeddableToolSelected = activeTool.type === "embeddable";
+
+ const { TTDDialogTriggerTunnel } = useTunnels();
+
+ return (
+ <>
+ {SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => {
+ if (
+ UIOptions.tools?.[
+ value as Extract
+ ] === false
+ ) {
+ return null;
+ }
+
+ const label = t(`toolBar.${value}`);
+ const letter =
+ key && capitalizeString(typeof key === "string" ? key : key[0]);
+ const shortcut = letter
+ ? `${letter} ${t("helpDialog.or")} ${numericKey}`
+ : `${numericKey}`;
+ return (
+ {
+ if (!appState.penDetected && pointerType === "pen") {
+ app.togglePenMode(true);
+ }
+ }}
+ onChange={({ pointerType }) => {
+ if (appState.activeTool.type !== value) {
+ trackEvent("toolbar", value, "ui");
+ }
+ if (value === "image") {
+ app.setActiveTool({
+ type: value,
+ insertOnCanvasDirectly: pointerType !== "mouse",
+ });
+ } else {
+ app.setActiveTool({ type: value });
+ }
+ }}
+ />
+ );
+ })}
+
+
+
+ setIsExtraToolsMenuOpen(!isExtraToolsMenuOpen)}
+ title={t("toolBar.extraTools")}
+ >
+ {extraToolsIcon}
+
+ setIsExtraToolsMenuOpen(false)}
+ onSelect={() => setIsExtraToolsMenuOpen(false)}
+ className="App-toolbar__extra-tools-dropdown"
+ >
+ app.setActiveTool({ type: "frame" })}
+ icon={frameToolIcon}
+ shortcut={KEYS.F.toLocaleUpperCase()}
+ data-testid="toolbar-frame"
+ selected={frameToolSelected}
+ >
+ {t("toolBar.frame")}
+
+ app.setActiveTool({ type: "embeddable" })}
+ icon={EmbedIcon}
+ data-testid="toolbar-embeddable"
+ selected={embeddableToolSelected}
+ >
+ {t("toolBar.embeddable")}
+
+ app.setActiveTool({ type: "laser" })}
+ icon={laserPointerToolIcon}
+ data-testid="toolbar-laser"
+ selected={laserToolSelected}
+ shortcut={KEYS.K.toLocaleUpperCase()}
+ >
+ {t("toolBar.laser")}
+
+
+ Generate
+
+ {app.props.aiEnabled !== false && }
+ app.setOpenDialog({ name: "ttd", tab: "mermaid" })}
+ icon={mermaidLogoIcon}
+ data-testid="toolbar-embeddable"
+ >
+ {t("toolBar.mermaidToExcalidraw")}
+
+ {app.props.aiEnabled !== false && app.plugins.diagramToCode && (
+ <>
+ app.onMagicframeToolSelect()}
+ icon={MagicIcon}
+ data-testid="toolbar-magicframe"
+ >
+ {t("toolBar.magicframe")}
+ AI
+
+ >
+ )}
+
+
+ >
+ );
+};
+
+export const ZoomActions = ({
+ renderAction,
+ zoom,
+}: {
+ renderAction: ActionManager["renderAction"];
+ zoom: Zoom;
+}) => (
+
+
+ {renderAction("zoomOut")}
+ {renderAction("resetZoom")}
+ {renderAction("zoomIn")}
+
+
+);
+
+export const UndoRedoActions = ({
+ renderAction,
+ className,
+}: {
+ renderAction: ActionManager["renderAction"];
+ className?: string;
+}) => (
+
+
+ {renderAction("undo")}
+
+
+ {renderAction("redo")}
+
+
+);
+
+export const ExitZenModeAction = ({
+ actionManager,
+ showExitZenModeBtn,
+}: {
+ actionManager: ActionManager;
+ showExitZenModeBtn: boolean;
+}) => (
+
+);
+
+export const FinalizeAction = ({
+ renderAction,
+ className,
+}: {
+ renderAction: ActionManager["renderAction"];
+ className?: string;
+}) => (
+
+ {renderAction("finalize", { size: "small" })}
+
+);
diff --git a/packages/excalidraw/components/ActiveConfirmDialog.tsx b/packages/excalidraw/components/ActiveConfirmDialog.tsx
new file mode 100644
index 0000000..699fbc6
--- /dev/null
+++ b/packages/excalidraw/components/ActiveConfirmDialog.tsx
@@ -0,0 +1,35 @@
+import { actionClearCanvas } from "../actions";
+import { t } from "../i18n";
+import { atom, useAtom } from "../editor-jotai";
+import { useExcalidrawActionManager } from "./App";
+import ConfirmDialog from "./ConfirmDialog";
+
+export const activeConfirmDialogAtom = atom<"clearCanvas" | null>(null);
+
+export const ActiveConfirmDialog = () => {
+ const [activeConfirmDialog, setActiveConfirmDialog] = useAtom(
+ activeConfirmDialogAtom,
+ );
+ const actionManager = useExcalidrawActionManager();
+
+ if (!activeConfirmDialog) {
+ return null;
+ }
+
+ if (activeConfirmDialog === "clearCanvas") {
+ return (
+ {
+ actionManager.executeAction(actionClearCanvas);
+ setActiveConfirmDialog(null);
+ }}
+ onCancel={() => setActiveConfirmDialog(null)}
+ title={t("clearCanvasDialog.title")}
+ >
+ {t("alerts.clearReset")}
+
+ );
+ }
+
+ return null;
+};
diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx
new file mode 100644
index 0000000..dc6d287
--- /dev/null
+++ b/packages/excalidraw/components/App.tsx
@@ -0,0 +1,11180 @@
+import React, { useContext } from "react";
+import { flushSync } from "react-dom";
+
+import type { RoughCanvas } from "roughjs/bin/canvas";
+import rough from "roughjs/bin/rough";
+import clsx from "clsx";
+import { nanoid } from "nanoid";
+import {
+ actionAddToLibrary,
+ actionBringForward,
+ actionBringToFront,
+ actionCopy,
+ actionCopyAsPng,
+ actionCopyAsSvg,
+ copyText,
+ actionCopyStyles,
+ actionCut,
+ actionDeleteSelected,
+ actionDuplicateSelection,
+ actionFinalize,
+ actionFlipHorizontal,
+ actionFlipVertical,
+ actionGroup,
+ actionPasteStyles,
+ actionSelectAll,
+ actionSendBackward,
+ actionSendToBack,
+ actionToggleGridMode,
+ actionToggleStats,
+ actionToggleZenMode,
+ actionUnbindText,
+ actionBindText,
+ actionUngroup,
+ actionLink,
+ actionToggleElementLock,
+ actionToggleLinearEditor,
+ actionToggleObjectsSnapMode,
+ actionToggleCropEditor,
+} from "../actions";
+import { createRedoAction, createUndoAction } from "../actions/actionHistory";
+import { ActionManager } from "../actions/manager";
+import { actions } from "../actions/register";
+import type { Action, ActionResult } from "../actions/types";
+import { trackEvent } from "../analytics";
+import {
+ getDefaultAppState,
+ isEraserActive,
+ isHandToolActive,
+} from "../appState";
+import type { PastedMixedContent } from "../clipboard";
+import { copyTextToSystemClipboard, parseClipboard } from "../clipboard";
+import {
+ APP_NAME,
+ CURSOR_TYPE,
+ DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT,
+ DEFAULT_VERTICAL_ALIGN,
+ DRAGGING_THRESHOLD,
+ ELEMENT_SHIFT_TRANSLATE_AMOUNT,
+ ELEMENT_TRANSLATE_AMOUNT,
+ ENV,
+ EVENT,
+ FRAME_STYLE,
+ IMAGE_MIME_TYPES,
+ IMAGE_RENDER_TIMEOUT,
+ isBrave,
+ LINE_CONFIRM_THRESHOLD,
+ MAX_ALLOWED_FILE_BYTES,
+ MIME_TYPES,
+ MQ_MAX_HEIGHT_LANDSCAPE,
+ MQ_MAX_WIDTH_LANDSCAPE,
+ MQ_MAX_WIDTH_PORTRAIT,
+ MQ_RIGHT_SIDEBAR_MIN_WIDTH,
+ POINTER_BUTTON,
+ ROUNDNESS,
+ SCROLL_TIMEOUT,
+ TAP_TWICE_TIMEOUT,
+ TEXT_TO_CENTER_SNAP_THRESHOLD,
+ THEME,
+ THEME_FILTER,
+ TOUCH_CTX_MENU_TIMEOUT,
+ VERTICAL_ALIGN,
+ YOUTUBE_STATES,
+ ZOOM_STEP,
+ POINTER_EVENTS,
+ TOOL_TYPE,
+ isIOS,
+ supportsResizeObserver,
+ DEFAULT_COLLISION_THRESHOLD,
+ DEFAULT_TEXT_ALIGN,
+ ARROW_TYPE,
+ DEFAULT_REDUCED_GLOBAL_ALPHA,
+ isSafari,
+ type EXPORT_IMAGE_TYPES,
+} from "../constants";
+import type { ExportedElements } from "../data";
+import { exportCanvas, loadFromBlob } from "../data";
+import Library, { distributeLibraryItemsOnSquareGrid } from "../data/library";
+import { restore, restoreElements } from "../data/restore";
+import {
+ dragNewElement,
+ dragSelectedElements,
+ duplicateElement,
+ getCommonBounds,
+ getCursorForResizingElement,
+ getDragOffsetXY,
+ getElementWithTransformHandleType,
+ getNormalizedDimensions,
+ getResizeArrowDirection,
+ getResizeOffsetXY,
+ getLockedLinearCursorAlignSize,
+ getTransformHandleTypeFromCoords,
+ isInvisiblySmallElement,
+ isNonDeletedElement,
+ isTextElement,
+ newElement,
+ newLinearElement,
+ newTextElement,
+ newImageElement,
+ transformElements,
+ refreshTextDimensions,
+ redrawTextBoundingBox,
+ getElementAbsoluteCoords,
+} from "../element";
+import {
+ bindOrUnbindLinearElement,
+ bindOrUnbindLinearElements,
+ fixBindingsAfterDeletion,
+ fixBindingsAfterDuplication,
+ getHoveredElementForBinding,
+ isBindingEnabled,
+ isLinearElementSimpleAndAlreadyBound,
+ maybeBindLinearElement,
+ shouldEnableBindingForPointerEvent,
+ updateBoundElements,
+ getSuggestedBindingsForArrows,
+} from "../element/binding";
+import { LinearElementEditor } from "../element/linearElementEditor";
+import { mutateElement, newElementWith } from "../element/mutateElement";
+import {
+ deepCopyElement,
+ duplicateElements,
+ newFrameElement,
+ newFreeDrawElement,
+ newEmbeddableElement,
+ newMagicFrameElement,
+ newIframeElement,
+ newArrowElement,
+} from "../element/newElement";
+import {
+ hasBoundTextElement,
+ isArrowElement,
+ isBindingElement,
+ isBindingElementType,
+ isBoundToContainer,
+ isFrameLikeElement,
+ isImageElement,
+ isEmbeddableElement,
+ isInitializedImageElement,
+ isLinearElement,
+ isLinearElementType,
+ isUsingAdaptiveRadius,
+ isIframeElement,
+ isIframeLikeElement,
+ isMagicFrameElement,
+ isTextBindableContainer,
+ isElbowArrow,
+ isFlowchartNodeElement,
+ isBindableElement,
+} from "../element/typeChecks";
+import type {
+ ExcalidrawBindableElement,
+ ExcalidrawElement,
+ ExcalidrawFreeDrawElement,
+ ExcalidrawGenericElement,
+ ExcalidrawLinearElement,
+ ExcalidrawTextElement,
+ NonDeleted,
+ InitializedExcalidrawImageElement,
+ ExcalidrawImageElement,
+ FileId,
+ NonDeletedExcalidrawElement,
+ ExcalidrawTextContainer,
+ ExcalidrawFrameLikeElement,
+ ExcalidrawMagicFrameElement,
+ ExcalidrawIframeLikeElement,
+ IframeData,
+ ExcalidrawIframeElement,
+ ExcalidrawEmbeddableElement,
+ Ordered,
+ MagicGenerationData,
+ ExcalidrawNonSelectionElement,
+ ExcalidrawArrowElement,
+} from "../element/types";
+import { getCenter, getDistance } from "../gesture";
+import {
+ editGroupForSelectedElement,
+ getElementsInGroup,
+ getSelectedGroupIdForElement,
+ getSelectedGroupIds,
+ isElementInGroup,
+ isSelectedViaGroup,
+ selectGroupsForSelectedElements,
+} from "../groups";
+import { History } from "../history";
+import { defaultLang, getLanguage, languages, setLanguage, t } from "../i18n";
+import {
+ CODES,
+ shouldResizeFromCenter,
+ shouldMaintainAspectRatio,
+ shouldRotateWithDiscreteAngle,
+ isArrowKey,
+ KEYS,
+} from "../keys";
+import {
+ isElementCompletelyInViewport,
+ isElementInViewport,
+} from "../element/sizeHelpers";
+import {
+ calculateScrollCenter,
+ getElementsWithinSelection,
+ getNormalizedZoom,
+ getSelectedElements,
+ hasBackground,
+ isSomeElementSelected,
+} from "../scene";
+import Scene from "../scene/Scene";
+import type {
+ RenderInteractiveSceneCallback,
+ ScrollBars,
+} from "../scene/types";
+import { getStateForZoom } from "../scene/zoom";
+import {
+ findShapeByKey,
+ getBoundTextShape,
+ getCornerRadius,
+ getElementShape,
+ isPathALoop,
+} from "../shapes";
+import { getSelectionBoxShape } from "@excalidraw/utils/geometry/shape";
+import { isPointInShape } from "@excalidraw/utils/collision";
+import type {
+ AppClassProperties,
+ AppProps,
+ AppState,
+ BinaryFileData,
+ DataURL,
+ ExcalidrawImperativeAPI,
+ BinaryFiles,
+ Gesture,
+ GestureEvent,
+ LibraryItems,
+ PointerDownState,
+ SceneData,
+ Device,
+ FrameNameBoundsCache,
+ SidebarName,
+ SidebarTabName,
+ KeyboardModifiersObject,
+ CollaboratorPointer,
+ ToolType,
+ OnUserFollowedPayload,
+ UnsubscribeCallback,
+ EmbedsValidationStatus,
+ ElementsPendingErasure,
+ GenerateDiagramToCode,
+ NullableGridSize,
+ Offsets,
+} from "../types";
+import {
+ debounce,
+ distance,
+ getFontString,
+ getNearestScrollableContainer,
+ isInputLike,
+ isToolIcon,
+ isWritableElement,
+ sceneCoordsToViewportCoords,
+ tupleToCoors,
+ viewportCoordsToSceneCoords,
+ wrapEvent,
+ updateObject,
+ updateActiveTool,
+ getShortcutKey,
+ isTransparent,
+ easeToValuesRAF,
+ muteFSAbortError,
+ isTestEnv,
+ easeOut,
+ updateStable,
+ addEventListener,
+ normalizeEOL,
+ getDateTime,
+ isShallowEqual,
+ arrayToMap,
+} from "../utils";
+import {
+ createSrcDoc,
+ embeddableURLValidator,
+ maybeParseEmbedSrc,
+ getEmbedLink,
+} from "../element/embeddable";
+import type { ContextMenuItems } from "./ContextMenu";
+import { ContextMenu, CONTEXT_MENU_SEPARATOR } from "./ContextMenu";
+import LayerUI from "./LayerUI";
+import { Toast } from "./Toast";
+import { actionToggleViewMode } from "../actions/actionToggleViewMode";
+import {
+ dataURLToFile,
+ dataURLToString,
+ generateIdFromFile,
+ getDataURL,
+ getDataURL_sync,
+ getFileFromEvent,
+ ImageURLToFile,
+ isImageFileHandle,
+ isSupportedImageFile,
+ loadSceneOrLibraryFromBlob,
+ normalizeFile,
+ parseLibraryJSON,
+ resizeImageFile,
+ SVGStringToFile,
+} from "../data/blob";
+import {
+ getInitializedImageElements,
+ loadHTMLImageElement,
+ normalizeSVG,
+ updateImageCache as _updateImageCache,
+} from "../element/image";
+import throttle from "lodash.throttle";
+import type { FileSystemHandle } from "../data/filesystem";
+import { fileOpen } from "../data/filesystem";
+import {
+ bindTextToShapeAfterDuplication,
+ getBoundTextElement,
+ getContainerCenter,
+ getContainerElement,
+ isValidTextContainer,
+} from "../element/textElement";
+import {
+ showHyperlinkTooltip,
+ hideHyperlinkToolip,
+ Hyperlink,
+} from "../components/hyperlink/Hyperlink";
+import { isLocalLink, normalizeLink, toValidURL } from "../data/url";
+import { shouldShowBoundingBox } from "../element/transformHandles";
+import { actionUnlockAllElements } from "../actions/actionElementLock";
+import { Fonts, getLineHeight } from "../fonts";
+import {
+ getFrameChildren,
+ isCursorInFrame,
+ bindElementsToFramesAfterDuplication,
+ addElementsToFrame,
+ replaceAllElementsInFrame,
+ removeElementsFromFrame,
+ getElementsInResizingFrame,
+ getElementsInNewFrame,
+ getContainingFrame,
+ elementOverlapsWithFrame,
+ updateFrameMembershipOfSelectedElements,
+ isElementInFrame,
+ getFrameLikeTitle,
+ getElementsOverlappingFrame,
+ filterElementsEligibleAsFrameChildren,
+} from "../frame";
+import {
+ excludeElementsInFramesFromSelection,
+ makeNextSelectedElementIds,
+} from "../scene/selection";
+import { actionPaste } from "../actions/actionClipboard";
+import {
+ actionRemoveAllElementsFromFrame,
+ actionSelectAllElementsInFrame,
+ actionWrapSelectionInFrame,
+} from "../actions/actionFrame";
+import { actionToggleHandTool, zoomToFit } from "../actions/actionCanvas";
+import { editorJotaiStore } from "../editor-jotai";
+import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
+import { ImageSceneDataError } from "../errors";
+import {
+ getSnapLinesAtPointer,
+ snapDraggedElements,
+ isActiveToolNonLinearSnappable,
+ snapNewElement,
+ snapResizingElements,
+ isSnappingEnabled,
+ getVisibleGaps,
+ getReferenceSnapPoints,
+ SnapCache,
+ isGridModeEnabled,
+ getGridPoint,
+} from "../snapping";
+import { actionWrapTextInContainer } from "../actions/actionBoundText";
+import BraveMeasureTextError from "./BraveMeasureTextError";
+import { activeEyeDropperAtom } from "./EyeDropper";
+import type { ExcalidrawElementSkeleton } from "../data/transform";
+import { convertToExcalidrawElements } from "../data/transform";
+import type { ValueOf } from "../utility-types";
+import { isSidebarDockedAtom } from "./Sidebar/Sidebar";
+import { StaticCanvas, InteractiveCanvas } from "./canvases";
+import { Renderer } from "../scene/Renderer";
+import { ShapeCache } from "../scene/ShapeCache";
+import { SVGLayer } from "./SVGLayer";
+import {
+ setEraserCursor,
+ setCursor,
+ resetCursor,
+ setCursorForShape,
+} from "../cursor";
+import { Emitter } from "../emitter";
+import { ElementCanvasButtons } from "../element/ElementCanvasButtons";
+import { COLOR_PALETTE } from "../colors";
+import { ElementCanvasButton } from "./MagicButton";
+import { MagicIcon, copyIcon, fullscreenIcon } from "./icons";
+import FollowMode from "./FollowMode/FollowMode";
+import { Store, CaptureUpdateAction } from "../store";
+import { AnimationFrameHandler } from "../animation-frame-handler";
+import { AnimatedTrail } from "../animated-trail";
+import { LaserTrails } from "../laser-trails";
+import { withBatchedUpdates, withBatchedUpdatesThrottled } from "../reactUtils";
+import { getRenderOpacity } from "../renderer/renderElement";
+import {
+ hitElementBoundText,
+ hitElementBoundingBoxOnly,
+ hitElementItself,
+} from "../element/collision";
+import { textWysiwyg } from "../element/textWysiwyg";
+import { isOverScrollBars } from "../scene/scrollbars";
+import { syncInvalidIndices, syncMovedIndices } from "../fractionalIndex";
+import {
+ isPointHittingLink,
+ isPointHittingLinkIcon,
+} from "./hyperlink/helpers";
+import { getShortcutFromShortcutName } from "../actions/shortcuts";
+import { actionTextAutoResize } from "../actions/actionTextAutoResize";
+import { getVisibleSceneBounds } from "../element/bounds";
+import { isMaybeMermaidDefinition } from "../mermaid";
+import NewElementCanvas from "./canvases/NewElementCanvas";
+import {
+ FlowChartCreator,
+ FlowChartNavigator,
+ getLinkDirectionFromKey,
+} from "../element/flowchart";
+import { searchItemInFocusAtom } from "./SearchMenu";
+import type { LocalPoint, Radians } from "@excalidraw/math";
+import {
+ clamp,
+ pointFrom,
+ pointDistance,
+ vector,
+ pointRotateRads,
+ vectorScale,
+ vectorFromPoint,
+ vectorSubtract,
+ vectorDot,
+ vectorNormalize,
+} from "@excalidraw/math";
+import { cropElement } from "../element/cropElement";
+import { wrapText } from "../element/textWrapping";
+import { actionCopyElementLink } from "../actions/actionElementLink";
+import { isElementLink, parseElementLinkFromURL } from "../element/elementLink";
+import {
+ isMeasureTextSupported,
+ normalizeText,
+ measureText,
+ getLineHeightInPx,
+ getApproxMinLineWidth,
+ getApproxMinLineHeight,
+ getMinTextElementWidth,
+} from "../element/textMeasurements";
+
+const AppContext = React.createContext(null!);
+const AppPropsContext = React.createContext(null!);
+
+const deviceContextInitialValue = {
+ viewport: {
+ isMobile: false,
+ isLandscape: false,
+ },
+ editor: {
+ isMobile: false,
+ canFitSidebar: false,
+ },
+ isTouchScreen: false,
+};
+const DeviceContext = React.createContext(deviceContextInitialValue);
+DeviceContext.displayName = "DeviceContext";
+
+export const ExcalidrawContainerContext = React.createContext<{
+ container: HTMLDivElement | null;
+ id: string | null;
+}>({ container: null, id: null });
+ExcalidrawContainerContext.displayName = "ExcalidrawContainerContext";
+
+const ExcalidrawElementsContext = React.createContext<
+ readonly NonDeletedExcalidrawElement[]
+>([]);
+ExcalidrawElementsContext.displayName = "ExcalidrawElementsContext";
+
+const ExcalidrawAppStateContext = React.createContext({
+ ...getDefaultAppState(),
+ width: 0,
+ height: 0,
+ offsetLeft: 0,
+ offsetTop: 0,
+});
+ExcalidrawAppStateContext.displayName = "ExcalidrawAppStateContext";
+
+const ExcalidrawSetAppStateContext = React.createContext<
+ React.Component["setState"]
+>(() => {
+ console.warn("Uninitialized ExcalidrawSetAppStateContext context!");
+});
+ExcalidrawSetAppStateContext.displayName = "ExcalidrawSetAppStateContext";
+
+const ExcalidrawActionManagerContext = React.createContext(
+ null!,
+);
+ExcalidrawActionManagerContext.displayName = "ExcalidrawActionManagerContext";
+
+export const useApp = () => useContext(AppContext);
+export const useAppProps = () => useContext(AppPropsContext);
+export const useDevice = () => useContext(DeviceContext);
+export const useExcalidrawContainer = () =>
+ useContext(ExcalidrawContainerContext);
+export const useExcalidrawElements = () =>
+ useContext(ExcalidrawElementsContext);
+export const useExcalidrawAppState = () =>
+ useContext(ExcalidrawAppStateContext);
+export const useExcalidrawSetAppState = () =>
+ useContext(ExcalidrawSetAppStateContext);
+export const useExcalidrawActionManager = () =>
+ useContext(ExcalidrawActionManagerContext);
+
+let didTapTwice: boolean = false;
+let tappedTwiceTimer = 0;
+let isHoldingSpace: boolean = false;
+let isPanning: boolean = false;
+let isDraggingScrollBar: boolean = false;
+let currentScrollBars: ScrollBars = { horizontal: null, vertical: null };
+let touchTimeout = 0;
+let invalidateContextMenu = false;
+
+/**
+ * Map of youtube embed video states
+ */
+const YOUTUBE_VIDEO_STATES = new Map<
+ ExcalidrawElement["id"],
+ ValueOf
+>();
+
+let IS_PLAIN_PASTE = false;
+let IS_PLAIN_PASTE_TIMER = 0;
+let PLAIN_PASTE_TOAST_SHOWN = false;
+
+let lastPointerUp: (() => void) | null = null;
+const gesture: Gesture = {
+ pointers: new Map(),
+ lastCenter: null,
+ initialDistance: null,
+ initialScale: null,
+};
+
+class App extends React.Component {
+ canvas: AppClassProperties["canvas"];
+ interactiveCanvas: AppClassProperties["interactiveCanvas"] = null;
+ rc: RoughCanvas;
+ unmounted: boolean = false;
+ actionManager: ActionManager;
+ device: Device = deviceContextInitialValue;
+
+ private excalidrawContainerRef = React.createRef();
+
+ public scene: Scene;
+ public fonts: Fonts;
+ public renderer: Renderer;
+ public visibleElements: readonly NonDeletedExcalidrawElement[];
+ private resizeObserver: ResizeObserver | undefined;
+ private nearestScrollableContainer: HTMLElement | Document | undefined;
+ public library: AppClassProperties["library"];
+ public libraryItemsFromStorage: LibraryItems | undefined;
+ public id: string;
+ private store: Store;
+ private history: History;
+ public excalidrawContainerValue: {
+ container: HTMLDivElement | null;
+ id: string;
+ };
+
+ public files: BinaryFiles = {};
+ public imageCache: AppClassProperties["imageCache"] = new Map();
+ private iFrameRefs = new Map();
+ /**
+ * Indicates whether the embeddable's url has been validated for rendering.
+ * If value not set, indicates that the validation is pending.
+ * Initially or on url change the flag is not reset so that we can guarantee
+ * the validation came from a trusted source (the editor).
+ **/
+ private embedsValidationStatus: EmbedsValidationStatus = new Map();
+ /** embeds that have been inserted to DOM (as a perf optim, we don't want to
+ * insert to DOM before user initially scrolls to them) */
+ private initializedEmbeds = new Set();
+
+ private elementsPendingErasure: ElementsPendingErasure = new Set();
+
+ public flowChartCreator: FlowChartCreator = new FlowChartCreator();
+ private flowChartNavigator: FlowChartNavigator = new FlowChartNavigator();
+
+ hitLinkElement?: NonDeletedExcalidrawElement;
+ lastPointerDownEvent: React.PointerEvent | null = null;
+ lastPointerUpEvent: React.PointerEvent | PointerEvent | null =
+ null;
+ lastPointerMoveEvent: PointerEvent | null = null;
+ lastPointerMoveCoords: { x: number; y: number } | null = null;
+ lastViewportPosition = { x: 0, y: 0 };
+
+ animationFrameHandler = new AnimationFrameHandler();
+
+ laserTrails = new LaserTrails(this.animationFrameHandler, this);
+ eraserTrail = new AnimatedTrail(this.animationFrameHandler, this, {
+ streamline: 0.2,
+ size: 5,
+ keepHead: true,
+ sizeMapping: (c) => {
+ const DECAY_TIME = 200;
+ const DECAY_LENGTH = 10;
+ const t = Math.max(0, 1 - (performance.now() - c.pressure) / DECAY_TIME);
+ const l =
+ (DECAY_LENGTH -
+ Math.min(DECAY_LENGTH, c.totalLength - c.currentIndex)) /
+ DECAY_LENGTH;
+
+ return Math.min(easeOut(l), easeOut(t));
+ },
+ fill: () =>
+ this.state.theme === THEME.LIGHT
+ ? "rgba(0, 0, 0, 0.2)"
+ : "rgba(255, 255, 255, 0.2)",
+ });
+
+ onChangeEmitter = new Emitter<
+ [
+ elements: readonly ExcalidrawElement[],
+ appState: AppState,
+ files: BinaryFiles,
+ ]
+ >();
+
+ onPointerDownEmitter = new Emitter<
+ [
+ activeTool: AppState["activeTool"],
+ pointerDownState: PointerDownState,
+ event: React.PointerEvent,
+ ]
+ >();
+
+ onPointerUpEmitter = new Emitter<
+ [
+ activeTool: AppState["activeTool"],
+ pointerDownState: PointerDownState,
+ event: PointerEvent,
+ ]
+ >();
+ onUserFollowEmitter = new Emitter<[payload: OnUserFollowedPayload]>();
+ onScrollChangeEmitter = new Emitter<
+ [scrollX: number, scrollY: number, zoom: AppState["zoom"]]
+ >();
+
+ missingPointerEventCleanupEmitter = new Emitter<
+ [event: PointerEvent | null]
+ >();
+ onRemoveEventListenersEmitter = new Emitter<[]>();
+
+ constructor(props: AppProps) {
+ super(props);
+ const defaultAppState = getDefaultAppState();
+ const {
+ excalidrawAPI,
+ viewModeEnabled = false,
+ zenModeEnabled = false,
+ gridModeEnabled = false,
+ objectsSnapModeEnabled = false,
+ theme = defaultAppState.theme,
+ name = `${t("labels.untitled")}-${getDateTime()}`,
+ } = props;
+ this.state = {
+ ...defaultAppState,
+ theme,
+ isLoading: true,
+ ...this.getCanvasOffsets(),
+ viewModeEnabled,
+ zenModeEnabled,
+ objectsSnapModeEnabled,
+ gridModeEnabled: gridModeEnabled ?? defaultAppState.gridModeEnabled,
+ name,
+ width: window.innerWidth,
+ height: window.innerHeight,
+ };
+
+ this.id = nanoid();
+ this.library = new Library(this);
+ this.actionManager = new ActionManager(
+ this.syncActionResult,
+ () => this.state,
+ () => this.scene.getElementsIncludingDeleted(),
+ this,
+ );
+ this.scene = new Scene();
+
+ this.canvas = document.createElement("canvas");
+ this.rc = rough.canvas(this.canvas);
+ this.renderer = new Renderer(this.scene);
+ this.visibleElements = [];
+
+ this.store = new Store();
+ this.history = new History();
+
+ if (excalidrawAPI) {
+ const api: ExcalidrawImperativeAPI = {
+ updateScene: this.updateScene,
+ updateLibrary: this.library.updateLibrary,
+ addFiles: this.addFiles,
+ resetScene: this.resetScene,
+ getSceneElementsIncludingDeleted: this.getSceneElementsIncludingDeleted,
+ history: {
+ clear: this.resetHistory,
+ },
+ scrollToContent: this.scrollToContent,
+ getSceneElements: this.getSceneElements,
+ getAppState: () => this.state,
+ getFiles: () => this.files,
+ getName: this.getName,
+ registerAction: (action: Action) => {
+ this.actionManager.registerAction(action);
+ },
+ refresh: this.refresh,
+ setToast: this.setToast,
+ id: this.id,
+ setActiveTool: this.setActiveTool,
+ setCursor: this.setCursor,
+ resetCursor: this.resetCursor,
+ updateFrameRendering: this.updateFrameRendering,
+ toggleSidebar: this.toggleSidebar,
+ onChange: (cb) => this.onChangeEmitter.on(cb),
+ onPointerDown: (cb) => this.onPointerDownEmitter.on(cb),
+ onPointerUp: (cb) => this.onPointerUpEmitter.on(cb),
+ onScrollChange: (cb) => this.onScrollChangeEmitter.on(cb),
+ onUserFollow: (cb) => this.onUserFollowEmitter.on(cb),
+ } as const;
+ if (typeof excalidrawAPI === "function") {
+ excalidrawAPI(api);
+ } else {
+ console.error("excalidrawAPI should be a function!");
+ }
+ }
+
+ this.excalidrawContainerValue = {
+ container: this.excalidrawContainerRef.current,
+ id: this.id,
+ };
+
+ this.fonts = new Fonts(this.scene);
+ this.history = new History();
+
+ this.actionManager.registerAll(actions);
+ this.actionManager.registerAction(
+ createUndoAction(this.history, this.store),
+ );
+ this.actionManager.registerAction(
+ createRedoAction(this.history, this.store),
+ );
+ }
+
+ private onWindowMessage(event: MessageEvent) {
+ if (
+ event.origin !== "https://player.vimeo.com" &&
+ event.origin !== "https://www.youtube.com"
+ ) {
+ return;
+ }
+
+ let data = null;
+ try {
+ data = JSON.parse(event.data);
+ } catch (e) {}
+ if (!data) {
+ return;
+ }
+
+ switch (event.origin) {
+ case "https://player.vimeo.com":
+ //Allowing for multiple instances of Excalidraw running in the window
+ if (data.method === "paused") {
+ let source: Window | null = null;
+ const iframes = document.body.querySelectorAll(
+ "iframe.excalidraw__embeddable",
+ );
+ if (!iframes) {
+ break;
+ }
+ for (const iframe of iframes as NodeListOf) {
+ if (iframe.contentWindow === event.source) {
+ source = iframe.contentWindow;
+ }
+ }
+ source?.postMessage(
+ JSON.stringify({
+ method: data.value ? "play" : "pause",
+ value: true,
+ }),
+ "*",
+ );
+ }
+ break;
+ case "https://www.youtube.com":
+ if (
+ data.event === "infoDelivery" &&
+ data.info &&
+ data.id &&
+ typeof data.info.playerState === "number"
+ ) {
+ const id = data.id;
+ const playerState = data.info.playerState as number;
+ if (
+ (Object.values(YOUTUBE_STATES) as number[]).includes(playerState)
+ ) {
+ YOUTUBE_VIDEO_STATES.set(
+ id,
+ playerState as ValueOf,
+ );
+ }
+ }
+ break;
+ }
+ }
+
+ private cacheEmbeddableRef(
+ element: ExcalidrawIframeLikeElement,
+ ref: HTMLIFrameElement | null,
+ ) {
+ if (ref) {
+ this.iFrameRefs.set(element.id, ref);
+ }
+ }
+
+ /**
+ * Returns gridSize taking into account `gridModeEnabled`.
+ * If disabled, returns null.
+ */
+ public getEffectiveGridSize = () => {
+ return (
+ isGridModeEnabled(this) ? this.state.gridSize : null
+ ) as NullableGridSize;
+ };
+
+ private getHTMLIFrameElement(
+ element: ExcalidrawIframeLikeElement,
+ ): HTMLIFrameElement | undefined {
+ return this.iFrameRefs.get(element.id);
+ }
+
+ private handleEmbeddableCenterClick(element: ExcalidrawIframeLikeElement) {
+ if (
+ this.state.activeEmbeddable?.element === element &&
+ this.state.activeEmbeddable?.state === "active"
+ ) {
+ return;
+ }
+
+ // The delay serves two purposes
+ // 1. To prevent first click propagating to iframe on mobile,
+ // else the click will immediately start and stop the video
+ // 2. If the user double clicks the frame center to activate it
+ // without the delay youtube will immediately open the video
+ // in fullscreen mode
+ setTimeout(() => {
+ this.setState({
+ activeEmbeddable: { element, state: "active" },
+ selectedElementIds: { [element.id]: true },
+ newElement: null,
+ selectionElement: null,
+ });
+ }, 100);
+
+ if (isIframeElement(element)) {
+ return;
+ }
+
+ const iframe = this.getHTMLIFrameElement(element);
+
+ if (!iframe?.contentWindow) {
+ return;
+ }
+
+ if (iframe.src.includes("youtube")) {
+ const state = YOUTUBE_VIDEO_STATES.get(element.id);
+ if (!state) {
+ YOUTUBE_VIDEO_STATES.set(element.id, YOUTUBE_STATES.UNSTARTED);
+ iframe.contentWindow.postMessage(
+ JSON.stringify({
+ event: "listening",
+ id: element.id,
+ }),
+ "*",
+ );
+ }
+ switch (state) {
+ case YOUTUBE_STATES.PLAYING:
+ case YOUTUBE_STATES.BUFFERING:
+ iframe.contentWindow?.postMessage(
+ JSON.stringify({
+ event: "command",
+ func: "pauseVideo",
+ args: "",
+ }),
+ "*",
+ );
+ break;
+ default:
+ iframe.contentWindow?.postMessage(
+ JSON.stringify({
+ event: "command",
+ func: "playVideo",
+ args: "",
+ }),
+ "*",
+ );
+ }
+ }
+
+ if (iframe.src.includes("player.vimeo.com")) {
+ iframe.contentWindow.postMessage(
+ JSON.stringify({
+ method: "paused", //video play/pause in onWindowMessage handler
+ }),
+ "*",
+ );
+ }
+ }
+
+ private isIframeLikeElementCenter(
+ el: ExcalidrawIframeLikeElement | null,
+ event: React.PointerEvent | PointerEvent,
+ sceneX: number,
+ sceneY: number,
+ ) {
+ return (
+ el &&
+ !event.altKey &&
+ !event.shiftKey &&
+ !event.metaKey &&
+ !event.ctrlKey &&
+ (this.state.activeEmbeddable?.element !== el ||
+ this.state.activeEmbeddable?.state === "hover" ||
+ !this.state.activeEmbeddable) &&
+ sceneX >= el.x + el.width / 3 &&
+ sceneX <= el.x + (2 * el.width) / 3 &&
+ sceneY >= el.y + el.height / 3 &&
+ sceneY <= el.y + (2 * el.height) / 3
+ );
+ }
+
+ private updateEmbedValidationStatus = (
+ element: ExcalidrawEmbeddableElement,
+ status: boolean,
+ ) => {
+ this.embedsValidationStatus.set(element.id, status);
+ ShapeCache.delete(element);
+ };
+
+ private updateEmbeddables = () => {
+ const iframeLikes = new Set();
+
+ let updated = false;
+ this.scene.getNonDeletedElements().filter((element) => {
+ if (isEmbeddableElement(element)) {
+ iframeLikes.add(element.id);
+ if (!this.embedsValidationStatus.has(element.id)) {
+ updated = true;
+
+ const validated = embeddableURLValidator(
+ element.link,
+ this.props.validateEmbeddable,
+ );
+
+ this.updateEmbedValidationStatus(element, validated);
+ }
+ } else if (isIframeElement(element)) {
+ iframeLikes.add(element.id);
+ }
+ return false;
+ });
+
+ if (updated) {
+ this.scene.triggerUpdate();
+ }
+
+ // GC
+ this.iFrameRefs.forEach((ref, id) => {
+ if (!iframeLikes.has(id)) {
+ this.iFrameRefs.delete(id);
+ }
+ });
+ };
+
+ private renderEmbeddables() {
+ const scale = this.state.zoom.value;
+ const normalizedWidth = this.state.width;
+ const normalizedHeight = this.state.height;
+
+ const embeddableElements = this.scene
+ .getNonDeletedElements()
+ .filter(
+ (el): el is Ordered> =>
+ (isEmbeddableElement(el) &&
+ this.embedsValidationStatus.get(el.id) === true) ||
+ isIframeElement(el),
+ );
+
+ return (
+ <>
+ {embeddableElements.map((el) => {
+ const { x, y } = sceneCoordsToViewportCoords(
+ { sceneX: el.x, sceneY: el.y },
+ this.state,
+ );
+
+ const isVisible = isElementInViewport(
+ el,
+ normalizedWidth,
+ normalizedHeight,
+ this.state,
+ this.scene.getNonDeletedElementsMap(),
+ );
+ const hasBeenInitialized = this.initializedEmbeds.has(el.id);
+
+ if (isVisible && !hasBeenInitialized) {
+ this.initializedEmbeds.add(el.id);
+ }
+ const shouldRender = isVisible || hasBeenInitialized;
+
+ if (!shouldRender) {
+ return null;
+ }
+
+ let src: IframeData | null;
+
+ if (isIframeElement(el)) {
+ src = null;
+
+ const data: MagicGenerationData = (el.customData?.generationData ??
+ this.magicGenerations.get(el.id)) || {
+ status: "error",
+ message: "No generation data",
+ code: "ERR_NO_GENERATION_DATA",
+ };
+
+ if (data.status === "done") {
+ const html = data.html;
+ src = {
+ intrinsicSize: { w: el.width, h: el.height },
+ type: "document",
+ srcdoc: () => {
+ return html;
+ },
+ } as const;
+ } else if (data.status === "pending") {
+ src = {
+ intrinsicSize: { w: el.width, h: el.height },
+ type: "document",
+ srcdoc: () => {
+ return createSrcDoc(`
+
+
+
+
+ Generating...
+ `);
+ },
+ } as const;
+ } else {
+ let message: string;
+ if (data.code === "ERR_GENERATION_INTERRUPTED") {
+ message = "Generation was interrupted...";
+ } else {
+ message = data.message || "Generation failed";
+ }
+ src = {
+ intrinsicSize: { w: el.width, h: el.height },
+ type: "document",
+ srcdoc: () => {
+ return createSrcDoc(`
+
+ Error!
+ ${message}
+ `);
+ },
+ } as const;
+ }
+ } else {
+ src = getEmbedLink(toValidURL(el.link || ""));
+ }
+
+ const isActive =
+ this.state.activeEmbeddable?.element === el &&
+ this.state.activeEmbeddable?.state === "active";
+ const isHovered =
+ this.state.activeEmbeddable?.element === el &&
+ this.state.activeEmbeddable?.state === "hover";
+
+ return (
+
+
{
+ if (!this.excalidrawContainerRef.current) {
+ return;
+ }
+ const container = this.excalidrawContainerRef.current;
+ const sh = container.scrollHeight;
+ const ch = container.clientHeight;
+ if (sh !== ch) {
+ container.style.height = `${sh}px`;
+ setTimeout(() => {
+ container.style.height = `100%`;
+ });
+ }
+ }}*/
+ className="excalidraw__embeddable-container__inner"
+ style={{
+ width: isVisible ? `${el.width}px` : 0,
+ height: isVisible ? `${el.height}px` : 0,
+ transform: isVisible ? `rotate(${el.angle}rad)` : "none",
+ pointerEvents: isActive
+ ? POINTER_EVENTS.enabled
+ : POINTER_EVENTS.disabled,
+ }}
+ >
+ {isHovered && (
+
+ {t("buttons.embeddableInteractionButton")}
+
+ )}
+
+ {(isEmbeddableElement(el)
+ ? this.props.renderEmbeddable?.(el, this.state)
+ : null) ?? (
+
+
+
+ );
+ })}
+ >
+ );
+ }
+
+ private getFrameNameDOMId = (frameElement: ExcalidrawElement) => {
+ return `${this.id}-frame-name-${frameElement.id}`;
+ };
+
+ frameNameBoundsCache: FrameNameBoundsCache = {
+ get: (frameElement) => {
+ let bounds = this.frameNameBoundsCache._cache.get(frameElement.id);
+ if (
+ !bounds ||
+ bounds.zoom !== this.state.zoom.value ||
+ bounds.versionNonce !== frameElement.versionNonce
+ ) {
+ const frameNameDiv = document.getElementById(
+ this.getFrameNameDOMId(frameElement),
+ );
+
+ if (frameNameDiv) {
+ const box = frameNameDiv.getBoundingClientRect();
+ const boxSceneTopLeft = viewportCoordsToSceneCoords(
+ { clientX: box.x, clientY: box.y },
+ this.state,
+ );
+ const boxSceneBottomRight = viewportCoordsToSceneCoords(
+ { clientX: box.right, clientY: box.bottom },
+ this.state,
+ );
+
+ bounds = {
+ x: boxSceneTopLeft.x,
+ y: boxSceneTopLeft.y,
+ width: boxSceneBottomRight.x - boxSceneTopLeft.x,
+ height: boxSceneBottomRight.y - boxSceneTopLeft.y,
+ angle: 0,
+ zoom: this.state.zoom.value,
+ versionNonce: frameElement.versionNonce,
+ };
+
+ this.frameNameBoundsCache._cache.set(frameElement.id, bounds);
+
+ return bounds;
+ }
+ return null;
+ }
+
+ return bounds;
+ },
+ /**
+ * @private
+ */
+ _cache: new Map(),
+ };
+
+ private resetEditingFrame = (frame: ExcalidrawFrameLikeElement | null) => {
+ if (frame) {
+ mutateElement(frame, { name: frame.name?.trim() || null });
+ }
+ this.setState({ editingFrame: null });
+ };
+
+ private renderFrameNames = () => {
+ if (!this.state.frameRendering.enabled || !this.state.frameRendering.name) {
+ if (this.state.editingFrame) {
+ this.resetEditingFrame(null);
+ }
+ return null;
+ }
+
+ const isDarkTheme = this.state.theme === THEME.DARK;
+
+ return this.scene.getNonDeletedFramesLikes().map((f) => {
+ if (
+ !isElementInViewport(
+ f,
+ this.canvas.width / window.devicePixelRatio,
+ this.canvas.height / window.devicePixelRatio,
+ {
+ offsetLeft: this.state.offsetLeft,
+ offsetTop: this.state.offsetTop,
+ scrollX: this.state.scrollX,
+ scrollY: this.state.scrollY,
+ zoom: this.state.zoom,
+ },
+ this.scene.getNonDeletedElementsMap(),
+ )
+ ) {
+ if (this.state.editingFrame === f.id) {
+ this.resetEditingFrame(f);
+ }
+ // if frame not visible, don't render its name
+ return null;
+ }
+
+ const { x: x1, y: y1 } = sceneCoordsToViewportCoords(
+ { sceneX: f.x, sceneY: f.y },
+ this.state,
+ );
+
+ const FRAME_NAME_EDIT_PADDING = 6;
+
+ let frameNameJSX;
+
+ const frameName = getFrameLikeTitle(f);
+
+ if (f.id === this.state.editingFrame) {
+ const frameNameInEdit = frameName;
+
+ frameNameJSX = (
+ {
+ mutateElement(f, {
+ name: e.target.value,
+ });
+ }}
+ onFocus={(e) => e.target.select()}
+ onBlur={() => this.resetEditingFrame(f)}
+ onKeyDown={(event) => {
+ // for some inexplicable reason, `onBlur` triggered on ESC
+ // does not reset `state.editingFrame` despite being called,
+ // and we need to reset it here as well
+ if (event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) {
+ this.resetEditingFrame(f);
+ }
+ }}
+ style={{
+ background: this.state.viewBackgroundColor,
+ filter: isDarkTheme ? THEME_FILTER : "none",
+ zIndex: 2,
+ border: "none",
+ display: "block",
+ padding: `${FRAME_NAME_EDIT_PADDING}px`,
+ borderRadius: 4,
+ boxShadow: "inset 0 0 0 1px var(--color-primary)",
+ fontFamily: "Assistant",
+ fontSize: "14px",
+ transform: `translate(-${FRAME_NAME_EDIT_PADDING}px, ${FRAME_NAME_EDIT_PADDING}px)`,
+ color: "var(--color-gray-80)",
+ overflow: "hidden",
+ maxWidth: `${
+ document.body.clientWidth - x1 - FRAME_NAME_EDIT_PADDING
+ }px`,
+ }}
+ size={frameNameInEdit.length + 1 || 1}
+ dir="auto"
+ autoComplete="off"
+ autoCapitalize="off"
+ autoCorrect="off"
+ />
+ );
+ } else {
+ frameNameJSX = frameName;
+ }
+
+ return (
+ this.handleCanvasPointerDown(event)}
+ onWheel={(event) => this.handleWheel(event)}
+ onContextMenu={this.handleCanvasContextMenu}
+ onDoubleClick={() => {
+ this.setState({
+ editingFrame: f.id,
+ });
+ }}
+ >
+ {frameNameJSX}
+
+ );
+ });
+ };
+
+ private toggleOverscrollBehavior(event: React.PointerEvent) {
+ // when pointer inside editor, disable overscroll behavior to prevent
+ // panning to trigger history back/forward on MacOS Chrome
+ document.documentElement.style.overscrollBehaviorX =
+ event.type === "pointerenter" ? "none" : "auto";
+ }
+
+ public render() {
+ const selectedElements = this.scene.getSelectedElements(this.state);
+ const { renderTopRightUI, renderCustomStats } = this.props;
+
+ const sceneNonce = this.scene.getSceneNonce();
+ const { elementsMap, visibleElements } =
+ this.renderer.getRenderableElements({
+ sceneNonce,
+ zoom: this.state.zoom,
+ offsetLeft: this.state.offsetLeft,
+ offsetTop: this.state.offsetTop,
+ scrollX: this.state.scrollX,
+ scrollY: this.state.scrollY,
+ height: this.state.height,
+ width: this.state.width,
+ editingTextElement: this.state.editingTextElement,
+ newElementId: this.state.newElement?.id,
+ pendingImageElementId: this.state.pendingImageElementId,
+ });
+ this.visibleElements = visibleElements;
+
+ const allElementsMap = this.scene.getNonDeletedElementsMap();
+
+ const shouldBlockPointerEvents =
+ // default back to `--ui-pointerEvents` flow if setPointerCapture
+ // not supported
+ "setPointerCapture" in HTMLElement.prototype
+ ? false
+ : this.state.selectionElement ||
+ this.state.newElement ||
+ this.state.selectedElementsAreBeingDragged ||
+ this.state.resizingElement ||
+ (this.state.activeTool.type === "laser" &&
+ // technically we can just test on this once we make it more safe
+ this.state.cursorButton === "down");
+
+ const firstSelectedElement = selectedElements[0];
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {this.props.children}
+
+
+
+
+
+
+ {selectedElements.length === 1 &&
+ this.state.openDialog?.name !==
+ "elementLinkSelector" &&
+ this.state.showHyperlinkPopup && (
+
+ )}
+ {this.props.aiEnabled !== false &&
+ selectedElements.length === 1 &&
+ isMagicFrameElement(firstSelectedElement) && (
+
+
+ this.onMagicFrameGenerate(
+ firstSelectedElement,
+ "button",
+ )
+ }
+ />
+
+ )}
+ {selectedElements.length === 1 &&
+ isIframeElement(firstSelectedElement) &&
+ firstSelectedElement.customData?.generationData
+ ?.status === "done" && (
+
+
+ this.onIframeSrcCopy(firstSelectedElement)
+ }
+ />
+ {
+ const iframe =
+ this.getHTMLIFrameElement(
+ firstSelectedElement,
+ );
+ if (iframe) {
+ try {
+ iframe.requestFullscreen();
+ this.setState({
+ activeEmbeddable: {
+ element: firstSelectedElement,
+ state: "active",
+ },
+ selectedElementIds: {
+ [firstSelectedElement.id]: true,
+ },
+ newElement: null,
+ selectionElement: null,
+ });
+ } catch (err: any) {
+ console.warn(err);
+ this.setState({
+ errorMessage:
+ "Couldn't enter fullscreen",
+ });
+ }
+ }
+ }}
+ />
+
+ )}
+ {this.state.toast !== null && (
+ this.setToast(null)}
+ duration={this.state.toast.duration}
+ closable={this.state.toast.closable}
+ />
+ )}
+ {this.state.contextMenu && (
+ {
+ this.setState({ contextMenu: null }, () => {
+ this.focusContainer();
+ callback?.();
+ });
+ }}
+ />
+ )}
+
+ {this.state.newElement && (
+
+ )}
+
+ {this.state.userToFollow && (
+
+ )}
+ {this.renderFrameNames()}
+
+ {this.renderEmbeddables()}
+
+
+
+
+
+
+
+
+ );
+ }
+
+ public focusContainer: AppClassProperties["focusContainer"] = () => {
+ this.excalidrawContainerRef.current?.focus();
+ };
+
+ public getSceneElementsIncludingDeleted = () => {
+ return this.scene.getElementsIncludingDeleted();
+ };
+
+ public getSceneElements = () => {
+ return this.scene.getNonDeletedElements();
+ };
+
+ public onInsertElements = (elements: readonly ExcalidrawElement[]) => {
+ this.addElementsFromPasteOrLibrary({
+ elements,
+ position: "center",
+ files: null,
+ });
+ };
+
+ public onExportImage = async (
+ type: keyof typeof EXPORT_IMAGE_TYPES,
+ elements: ExportedElements,
+ opts: { exportingFrame: ExcalidrawFrameLikeElement | null },
+ ) => {
+ trackEvent("export", type, "ui");
+ const fileHandle = await exportCanvas(
+ type,
+ elements,
+ this.state,
+ this.files,
+ {
+ exportBackground: this.state.exportBackground,
+ name: this.getName(),
+ viewBackgroundColor: this.state.viewBackgroundColor,
+ exportingFrame: opts.exportingFrame,
+ },
+ )
+ .catch(muteFSAbortError)
+ .catch((error) => {
+ console.error(error);
+ this.setState({ errorMessage: error.message });
+ });
+
+ if (
+ this.state.exportEmbedScene &&
+ fileHandle &&
+ isImageFileHandle(fileHandle)
+ ) {
+ this.setState({ fileHandle });
+ }
+ };
+
+ private magicGenerations = new Map<
+ ExcalidrawIframeElement["id"],
+ MagicGenerationData
+ >();
+
+ private updateMagicGeneration = ({
+ frameElement,
+ data,
+ }: {
+ frameElement: ExcalidrawIframeElement;
+ data: MagicGenerationData;
+ }) => {
+ if (data.status === "pending") {
+ // We don't wanna persist pending state to storage. It should be in-app
+ // state only.
+ // Thus reset so that we prefer local cache (if there was some
+ // generationData set previously)
+ mutateElement(
+ frameElement,
+ { customData: { generationData: undefined } },
+ false,
+ );
+ } else {
+ mutateElement(
+ frameElement,
+ { customData: { generationData: data } },
+ false,
+ );
+ }
+ this.magicGenerations.set(frameElement.id, data);
+ this.triggerRender();
+ };
+
+ public plugins: {
+ diagramToCode?: {
+ generate: GenerateDiagramToCode;
+ };
+ } = {};
+
+ public setPlugins(plugins: Partial) {
+ Object.assign(this.plugins, plugins);
+ }
+
+ private async onMagicFrameGenerate(
+ magicFrame: ExcalidrawMagicFrameElement,
+ source: "button" | "upstream",
+ ) {
+ const generateDiagramToCode = this.plugins.diagramToCode?.generate;
+
+ if (!generateDiagramToCode) {
+ this.setState({
+ errorMessage: "No diagram to code plugin found",
+ });
+ return;
+ }
+
+ const magicFrameChildren = getElementsOverlappingFrame(
+ this.scene.getNonDeletedElements(),
+ magicFrame,
+ ).filter((el) => !isMagicFrameElement(el));
+
+ if (!magicFrameChildren.length) {
+ if (source === "button") {
+ this.setState({ errorMessage: "Cannot generate from an empty frame" });
+ trackEvent("ai", "generate (no-children)", "d2c");
+ } else {
+ this.setActiveTool({ type: "magicframe" });
+ }
+ return;
+ }
+
+ const frameElement = this.insertIframeElement({
+ sceneX: magicFrame.x + magicFrame.width + 30,
+ sceneY: magicFrame.y,
+ width: magicFrame.width,
+ height: magicFrame.height,
+ });
+
+ if (!frameElement) {
+ return;
+ }
+
+ this.updateMagicGeneration({
+ frameElement,
+ data: { status: "pending" },
+ });
+
+ this.setState({
+ selectedElementIds: { [frameElement.id]: true },
+ });
+
+ trackEvent("ai", "generate (start)", "d2c");
+ try {
+ const { html } = await generateDiagramToCode({
+ frame: magicFrame,
+ children: magicFrameChildren,
+ });
+
+ trackEvent("ai", "generate (success)", "d2c");
+
+ if (!html.trim()) {
+ this.updateMagicGeneration({
+ frameElement,
+ data: {
+ status: "error",
+ code: "ERR_OAI",
+ message: "Nothing genereated :(",
+ },
+ });
+ return;
+ }
+
+ const parsedHtml =
+ html.includes("") && html.includes("