@excalidraw/excalidraw 0.17.1-c0b80a0 → 0.17.1-c329470

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/dist/browser/dev/excalidraw-assets-dev/{chunk-JGDL4H2X.js → chunk-3DLVY5XU.js} +8272 -6864
  2. package/dist/browser/dev/excalidraw-assets-dev/chunk-3DLVY5XU.js.map +7 -0
  3. package/dist/browser/dev/excalidraw-assets-dev/{chunk-V7NFEZA6.js → chunk-NOAEU4NM.js} +9 -2
  4. package/dist/browser/dev/excalidraw-assets-dev/chunk-NOAEU4NM.js.map +7 -0
  5. package/dist/browser/dev/excalidraw-assets-dev/{en-ZSVWGT55.js → en-7IBTMWBG.js} +2 -2
  6. package/dist/browser/dev/excalidraw-assets-dev/{image-RJG3J34Y.js → image-N5AC7SEK.js} +2 -6
  7. package/dist/browser/dev/index.css +85 -50
  8. package/dist/browser/dev/index.css.map +3 -3
  9. package/dist/browser/dev/index.js +4375 -3766
  10. package/dist/browser/dev/index.js.map +4 -4
  11. package/dist/browser/prod/excalidraw-assets/{chunk-LDVEIXGO.js → chunk-7CSIPVOW.js} +2 -2
  12. package/dist/browser/prod/excalidraw-assets/chunk-TX3BU7T2.js +47 -0
  13. package/dist/browser/prod/excalidraw-assets/{en-UPNEHLDS.js → en-LOGQBETY.js} +1 -1
  14. package/dist/browser/prod/excalidraw-assets/image-3V4U7GZE.js +1 -0
  15. package/dist/browser/prod/index.css +1 -1
  16. package/dist/browser/prod/index.js +40 -40
  17. package/dist/dev/index.css +85 -50
  18. package/dist/dev/index.css.map +3 -3
  19. package/dist/dev/index.js +8688 -6706
  20. package/dist/dev/index.js.map +4 -4
  21. package/dist/{prod/locales/en-ZXYG7GCR.json → dev/locales/en-V6KXFSCK.json} +8 -1
  22. package/dist/excalidraw/actions/actionAlign.d.ts +7 -6
  23. package/dist/excalidraw/actions/actionAlign.js +14 -14
  24. package/dist/excalidraw/actions/actionClipboard.d.ts +7 -3
  25. package/dist/excalidraw/actions/actionDeleteSelected.d.ts +7 -3
  26. package/dist/excalidraw/actions/actionDeleteSelected.js +103 -34
  27. package/dist/excalidraw/actions/actionDuplicateSelection.js +105 -95
  28. package/dist/excalidraw/actions/actionFlip.js +16 -7
  29. package/dist/excalidraw/actions/actionFrame.d.ts +493 -0
  30. package/dist/excalidraw/actions/actionFrame.js +45 -2
  31. package/dist/excalidraw/actions/actionGroup.js +6 -4
  32. package/dist/excalidraw/actions/actionProperties.js +145 -116
  33. package/dist/excalidraw/actions/actionSelectAll.js +4 -3
  34. package/dist/excalidraw/actions/shortcuts.d.ts +1 -1
  35. package/dist/excalidraw/actions/shortcuts.js +1 -0
  36. package/dist/excalidraw/actions/types.d.ts +1 -1
  37. package/dist/excalidraw/align.d.ts +2 -1
  38. package/dist/excalidraw/align.js +15 -6
  39. package/dist/excalidraw/clipboard.d.ts +27 -5
  40. package/dist/excalidraw/clipboard.js +55 -28
  41. package/dist/excalidraw/components/Actions.d.ts +2 -1
  42. package/dist/excalidraw/components/Actions.js +4 -2
  43. package/dist/excalidraw/components/ActiveConfirmDialog.d.ts +1 -1
  44. package/dist/excalidraw/components/ActiveConfirmDialog.js +2 -3
  45. package/dist/excalidraw/components/App.d.ts +1 -0
  46. package/dist/excalidraw/components/App.js +216 -111
  47. package/dist/excalidraw/components/ColorPicker/ColorInput.js +2 -3
  48. package/dist/excalidraw/components/ColorPicker/ColorPicker.js +2 -3
  49. package/dist/excalidraw/components/ColorPicker/CustomColorList.js +1 -1
  50. package/dist/excalidraw/components/ColorPicker/Picker.js +1 -1
  51. package/dist/excalidraw/components/ColorPicker/PickerColorList.js +1 -1
  52. package/dist/excalidraw/components/ColorPicker/ShadeList.js +1 -1
  53. package/dist/excalidraw/components/ColorPicker/colorPickerUtils.d.ts +1 -1
  54. package/dist/excalidraw/components/ColorPicker/colorPickerUtils.js +1 -1
  55. package/dist/excalidraw/components/CommandPalette/CommandPalette.js +3 -3
  56. package/dist/excalidraw/components/ConfirmDialog.js +17 -5
  57. package/dist/excalidraw/components/Dialog.js +2 -3
  58. package/dist/excalidraw/components/EyeDropper.d.ts +1 -1
  59. package/dist/excalidraw/components/EyeDropper.js +1 -1
  60. package/dist/excalidraw/components/IconPicker.d.ts +2 -2
  61. package/dist/excalidraw/components/IconPicker.js +56 -53
  62. package/dist/excalidraw/components/LayerUI.js +6 -6
  63. package/dist/excalidraw/components/LibraryMenu.d.ts +2 -16
  64. package/dist/excalidraw/components/LibraryMenu.js +70 -28
  65. package/dist/excalidraw/components/LibraryMenuHeaderContent.js +4 -5
  66. package/dist/excalidraw/components/MobileMenu.js +1 -1
  67. package/dist/excalidraw/components/OverwriteConfirm/OverwriteConfirm.js +2 -3
  68. package/dist/excalidraw/components/OverwriteConfirm/OverwriteConfirmState.d.ts +1 -1
  69. package/dist/excalidraw/components/OverwriteConfirm/OverwriteConfirmState.js +2 -3
  70. package/dist/excalidraw/components/Range.d.ts +9 -0
  71. package/dist/excalidraw/components/Range.js +24 -0
  72. package/dist/excalidraw/components/SearchMenu.d.ts +1 -1
  73. package/dist/excalidraw/components/SearchMenu.js +3 -4
  74. package/dist/excalidraw/components/Sidebar/Sidebar.d.ts +1 -1
  75. package/dist/excalidraw/components/Sidebar/Sidebar.js +2 -3
  76. package/dist/excalidraw/components/Stats/Collapsible.d.ts +2 -1
  77. package/dist/excalidraw/components/Stats/Collapsible.js +2 -2
  78. package/dist/excalidraw/components/Stats/Dimension.js +94 -8
  79. package/dist/excalidraw/components/Stats/MultiDimension.js +8 -5
  80. package/dist/excalidraw/components/Stats/Position.js +63 -3
  81. package/dist/excalidraw/components/Stats/index.js +21 -4
  82. package/dist/excalidraw/components/Stats/utils.d.ts +1 -1
  83. package/dist/excalidraw/components/Stats/utils.js +2 -55
  84. package/dist/excalidraw/components/TTDDialog/TTDDialog.js +1 -1
  85. package/dist/excalidraw/components/ToolButton.js +4 -9
  86. package/dist/excalidraw/components/hoc/withInternalFallback.js +3 -3
  87. package/dist/excalidraw/components/hyperlink/Hyperlink.js +6 -12
  88. package/dist/excalidraw/components/icons.d.ts +9 -0
  89. package/dist/excalidraw/components/icons.js +4 -4
  90. package/dist/excalidraw/components/main-menu/DefaultItems.js +2 -3
  91. package/dist/excalidraw/constants.d.ts +5 -1
  92. package/dist/excalidraw/constants.js +9 -1
  93. package/dist/excalidraw/context/tunnels.d.ts +2 -1
  94. package/dist/excalidraw/context/tunnels.js +3 -1
  95. package/dist/excalidraw/data/blob.d.ts +1 -0
  96. package/dist/excalidraw/data/blob.js +7 -3
  97. package/dist/excalidraw/data/filesystem.d.ts +2 -1
  98. package/dist/excalidraw/data/filesystem.js +1 -0
  99. package/dist/excalidraw/data/image.d.ts +0 -6
  100. package/dist/excalidraw/data/image.js +1 -43
  101. package/dist/excalidraw/data/index.js +6 -6
  102. package/dist/excalidraw/data/library.d.ts +9 -3
  103. package/dist/excalidraw/data/library.js +43 -6
  104. package/dist/excalidraw/data/restore.js +26 -8
  105. package/dist/excalidraw/data/url.d.ts +0 -1
  106. package/dist/excalidraw/data/url.js +2 -4
  107. package/dist/excalidraw/editor-jotai.d.ts +56 -0
  108. package/dist/excalidraw/editor-jotai.js +8 -0
  109. package/dist/excalidraw/element/binding.d.ts +9 -6
  110. package/dist/excalidraw/element/binding.js +124 -44
  111. package/dist/excalidraw/element/bounds.js +10 -0
  112. package/dist/excalidraw/element/cropElement.d.ts +5 -0
  113. package/dist/excalidraw/element/cropElement.js +28 -1
  114. package/dist/excalidraw/element/dragElements.js +13 -7
  115. package/dist/excalidraw/element/elbowArrow.d.ts +16 -0
  116. package/dist/excalidraw/element/elbowArrow.js +1268 -0
  117. package/dist/excalidraw/element/embeddable.js +4 -5
  118. package/dist/excalidraw/element/flowchart.d.ts +1 -1
  119. package/dist/excalidraw/element/flowchart.js +25 -9
  120. package/dist/excalidraw/element/heading.d.ts +5 -1
  121. package/dist/excalidraw/element/heading.js +5 -1
  122. package/dist/excalidraw/element/image.js +19 -5
  123. package/dist/excalidraw/element/linearElementEditor.d.ts +9 -10
  124. package/dist/excalidraw/element/linearElementEditor.js +97 -38
  125. package/dist/excalidraw/element/mutateElement.d.ts +3 -1
  126. package/dist/excalidraw/element/mutateElement.js +31 -4
  127. package/dist/excalidraw/element/newElement.d.ts +8 -12
  128. package/dist/excalidraw/element/newElement.js +36 -21
  129. package/dist/excalidraw/element/resizeElements.d.ts +20 -5
  130. package/dist/excalidraw/element/resizeElements.js +593 -361
  131. package/dist/excalidraw/element/sortElements.js +1 -4
  132. package/dist/excalidraw/element/types.d.ts +23 -1
  133. package/dist/excalidraw/fonts/Fonts.d.ts +0 -16
  134. package/dist/excalidraw/fonts/Fonts.js +6 -31
  135. package/dist/excalidraw/frame.d.ts +11 -5
  136. package/dist/excalidraw/frame.js +146 -35
  137. package/dist/excalidraw/groups.js +3 -0
  138. package/dist/excalidraw/hooks/useLibraryItemSvg.d.ts +1 -1
  139. package/dist/excalidraw/hooks/useLibraryItemSvg.js +2 -3
  140. package/dist/excalidraw/hooks/useScrollPosition.js +1 -1
  141. package/dist/excalidraw/i18n.js +3 -4
  142. package/dist/excalidraw/index.js +3 -4
  143. package/dist/excalidraw/locales/en.json +8 -1
  144. package/dist/excalidraw/renderer/interactiveScene.js +43 -32
  145. package/dist/excalidraw/renderer/staticScene.js +6 -4
  146. package/dist/excalidraw/renderer/staticSvgScene.js +1 -1
  147. package/dist/excalidraw/scene/Shape.js +40 -17
  148. package/dist/excalidraw/scene/comparisons.d.ts +0 -477
  149. package/dist/excalidraw/scene/comparisons.js +0 -37
  150. package/dist/excalidraw/scene/export.d.ts +7 -0
  151. package/dist/excalidraw/scene/export.js +107 -43
  152. package/dist/excalidraw/scene/index.d.ts +1 -1
  153. package/dist/excalidraw/scene/index.js +1 -1
  154. package/dist/excalidraw/scene/selection.js +4 -1
  155. package/dist/excalidraw/types.d.ts +15 -0
  156. package/dist/excalidraw/utility-types.d.ts +1 -0
  157. package/dist/excalidraw/utils.d.ts +8 -1
  158. package/dist/excalidraw/utils.js +9 -0
  159. package/dist/excalidraw/visualdebug.d.ts +8 -1
  160. package/dist/excalidraw/visualdebug.js +3 -0
  161. package/dist/math/line.d.ts +19 -0
  162. package/dist/math/line.js +32 -3
  163. package/dist/math/point.d.ts +10 -0
  164. package/dist/math/point.js +12 -1
  165. package/dist/prod/index.css +1 -1
  166. package/dist/prod/index.js +29 -44
  167. package/dist/{dev/locales/en-ZXYG7GCR.json → prod/locales/en-V6KXFSCK.json} +8 -1
  168. package/package.json +5 -2
  169. package/dist/browser/dev/excalidraw-assets-dev/chunk-JGDL4H2X.js.map +0 -7
  170. package/dist/browser/dev/excalidraw-assets-dev/chunk-V7NFEZA6.js.map +0 -7
  171. package/dist/browser/prod/excalidraw-assets/chunk-S2XKB3DE.js +0 -62
  172. package/dist/browser/prod/excalidraw-assets/image-OFI2YYMP.js +0 -1
  173. package/dist/excalidraw/element/routing.d.ts +0 -12
  174. package/dist/excalidraw/element/routing.js +0 -642
  175. package/dist/excalidraw/jotai.d.ts +0 -34
  176. package/dist/excalidraw/jotai.js +0 -18
  177. /package/dist/browser/dev/excalidraw-assets-dev/{en-ZSVWGT55.js.map → en-7IBTMWBG.js.map} +0 -0
  178. /package/dist/browser/dev/excalidraw-assets-dev/{image-RJG3J34Y.js.map → image-N5AC7SEK.js.map} +0 -0
@@ -1,10 +1,9 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useRef, useState } from "react";
3
3
  import { getColor } from "./ColorPicker";
4
- import { useAtom } from "jotai";
5
4
  import { activeColorPickerSectionAtom } from "./colorPickerUtils";
6
5
  import { eyeDropperIcon } from "../icons";
7
- import { jotaiScope } from "../../jotai";
6
+ import { useAtom } from "../../editor-jotai";
8
7
  import { KEYS } from "../../keys";
9
8
  import { activeEyeDropperAtom } from "../EyeDropper";
10
9
  import clsx from "clsx";
@@ -33,7 +32,7 @@ export const ColorInput = ({ color, onChange, label, colorPickerType, }) => {
33
32
  inputRef.current.focus();
34
33
  }
35
34
  }, [activeSection]);
36
- const [eyeDropperState, setEyeDropperState] = useAtom(activeEyeDropperAtom, jotaiScope);
35
+ const [eyeDropperState, setEyeDropperState] = useAtom(activeEyeDropperAtom);
37
36
  useEffect(() => {
38
37
  return () => {
39
38
  setEyeDropperState(null);
@@ -4,7 +4,6 @@ import { TopPicks } from "./TopPicks";
4
4
  import { ButtonSeparator } from "../ButtonSeparator";
5
5
  import { Picker } from "./Picker";
6
6
  import * as Popover from "@radix-ui/react-popover";
7
- import { useAtom } from "jotai";
8
7
  import { activeColorPickerSectionAtom } from "./colorPickerUtils";
9
8
  import { useExcalidrawContainer } from "../App";
10
9
  import { COLOR_PALETTE } from "../../colors";
@@ -12,7 +11,7 @@ import PickerHeading from "./PickerHeading";
12
11
  import { t } from "../../i18n";
13
12
  import clsx from "clsx";
14
13
  import { useRef } from "react";
15
- import { jotaiScope } from "../../jotai";
14
+ import { useAtom } from "../../editor-jotai";
16
15
  import { ColorInput } from "./ColorInput";
17
16
  import { activeEyeDropperAtom } from "../EyeDropper";
18
17
  import { PropertiesPopover } from "../PropertiesPopover";
@@ -38,7 +37,7 @@ export const getColor = (color) => {
38
37
  const ColorPickerPopupContent = ({ type, color, onChange, label, elements, palette = COLOR_PALETTE, updateData, }) => {
39
38
  const { container } = useExcalidrawContainer();
40
39
  const [, setActiveColorPickerSection] = useAtom(activeColorPickerSectionAtom);
41
- const [eyeDropperState, setEyeDropperState] = useAtom(activeEyeDropperAtom, jotaiScope);
40
+ const [eyeDropperState, setEyeDropperState] = useAtom(activeEyeDropperAtom);
42
41
  const colorInputJSX = (_jsxs("div", { children: [_jsx(PickerHeading, { children: t("colorPicker.hexCode") }), _jsx(ColorInput, { color: color, label: label, onChange: (color) => {
43
42
  onChange(color);
44
43
  }, colorPickerType: type })] }));
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import clsx from "clsx";
3
- import { useAtom } from "jotai";
3
+ import { useAtom } from "../../editor-jotai";
4
4
  import { useEffect, useRef } from "react";
5
5
  import { activeColorPickerSectionAtom } from "./colorPickerUtils";
6
6
  import HotkeyLabel from "./HotkeyLabel";
@@ -3,7 +3,7 @@ import React, { useEffect, useState } from "react";
3
3
  import { t } from "../../i18n";
4
4
  import { ShadeList } from "./ShadeList";
5
5
  import PickerColorList from "./PickerColorList";
6
- import { useAtom } from "jotai";
6
+ import { useAtom } from "../../editor-jotai";
7
7
  import { CustomColorList } from "./CustomColorList";
8
8
  import { colorPickerKeyNavHandler } from "./keyboardNavHandlers";
9
9
  import PickerHeading from "./PickerHeading";
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import clsx from "clsx";
3
- import { useAtom } from "jotai";
3
+ import { useAtom } from "../../editor-jotai";
4
4
  import { useEffect, useRef } from "react";
5
5
  import { activeColorPickerSectionAtom, colorPickerHotkeyBindings, getColorNameAndShadeFromColor, } from "./colorPickerUtils";
6
6
  import HotkeyLabel from "./HotkeyLabel";
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import clsx from "clsx";
3
- import { useAtom } from "jotai";
3
+ import { useAtom } from "../../editor-jotai";
4
4
  import { useEffect, useRef } from "react";
5
5
  import { activeColorPickerSectionAtom, getColorNameAndShadeFromColor, } from "./colorPickerUtils";
6
6
  import HotkeyLabel from "./HotkeyLabel";
@@ -14,7 +14,7 @@ export declare const isCustomColor: ({ color, palette, }: {
14
14
  }) => boolean;
15
15
  export declare const getMostUsedCustomColors: (elements: readonly ExcalidrawElement[], type: "elementBackground" | "elementStroke", palette: ColorPaletteCustom) => string[];
16
16
  export type ActiveColorPickerSectionAtomType = "custom" | "baseColors" | "shades" | "hex" | null;
17
- export declare const activeColorPickerSectionAtom: import("jotai").PrimitiveAtom<ActiveColorPickerSectionAtomType> & {
17
+ export declare const activeColorPickerSectionAtom: import("jotai/vanilla/atom").PrimitiveAtom<ActiveColorPickerSectionAtomType> & {
18
18
  init: ActiveColorPickerSectionAtomType;
19
19
  };
20
20
  export declare const getContrastYIQ: (bgHex: string, isCustomColor: boolean) => "white" | "black";
@@ -1,5 +1,5 @@
1
- import { atom } from "jotai";
2
1
  import { MAX_CUSTOM_COLORS_USED_IN_CANVAS } from "../../colors";
2
+ import { atom } from "../../editor-jotai";
3
3
  export const getColorNameAndShadeFromColor = ({ palette, color, }) => {
4
4
  for (const [colorName, colorVal] of Object.entries(palette)) {
5
5
  if (Array.isArray(colorVal)) {
@@ -13,14 +13,13 @@ import { LockedIcon, UnlockedIcon, clockIcon, searchIcon, boltIcon, bucketFillIc
13
13
  import fuzzy from "fuzzy";
14
14
  import { useUIAppState } from "../../context/ui-appState";
15
15
  import { capitalizeString, getShortcutKey, isWritableElement, } from "../../utils";
16
- import { atom, useAtom } from "jotai";
16
+ import { atom, useAtom, editorJotaiStore } from "../../editor-jotai";
17
17
  import { deburr } from "../../deburr";
18
18
  import { InlineIcon } from "../InlineIcon";
19
19
  import { SHAPES } from "../../shapes";
20
20
  import { canChangeBackgroundColor, canChangeStrokeColor } from "../Actions";
21
21
  import { useStableCallback } from "../../hooks/useStableCallback";
22
22
  import { actionClearCanvas, actionLink, actionToggleSearchMenu, } from "../../actions";
23
- import { jotaiStore } from "../../jotai";
24
23
  import { activeConfirmDialogAtom } from "../ActiveConfirmDialog";
25
24
  import * as defaultItems from "./defaultCommandPaletteItems";
26
25
  import { trackEvent } from "../../analytics";
@@ -163,6 +162,7 @@ function CommandPaletteInner({ customCommandPaletteItems, }) {
163
162
  actionManager.actions.cut,
164
163
  actionManager.actions.copy,
165
164
  actionManager.actions.deleteSelectedElements,
165
+ actionManager.actions.wrapSelectionInFrame,
166
166
  actionManager.actions.copyStyles,
167
167
  actionManager.actions.pasteStyles,
168
168
  actionManager.actions.bringToFront,
@@ -234,7 +234,7 @@ function CommandPaletteInner({ customCommandPaletteItems, }) {
234
234
  keywords: ["delete", "destroy"],
235
235
  viewMode: false,
236
236
  perform: () => {
237
- jotaiStore.set(activeConfirmDialogAtom, "clearCanvas");
237
+ editorJotaiStore.set(activeConfirmDialogAtom, "clearCanvas");
238
238
  },
239
239
  },
240
240
  {
@@ -1,26 +1,38 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { flushSync } from "react-dom";
2
3
  import { t } from "../i18n";
3
4
  import { Dialog } from "./Dialog";
4
5
  import "./ConfirmDialog.scss";
5
6
  import DialogActionButton from "./DialogActionButton";
6
- import { useSetAtom } from "jotai";
7
7
  import { isLibraryMenuOpenAtom } from "./LibraryMenu";
8
8
  import { useExcalidrawContainer, useExcalidrawSetAppState } from "./App";
9
- import { jotaiScope } from "../jotai";
9
+ import { useSetAtom } from "../editor-jotai";
10
10
  const ConfirmDialog = (props) => {
11
11
  const { onConfirm, onCancel, children, confirmText = t("buttons.confirm"), cancelText = t("buttons.cancel"), className = "", ...rest } = props;
12
12
  const setAppState = useExcalidrawSetAppState();
13
- const setIsLibraryMenuOpen = useSetAtom(isLibraryMenuOpenAtom, jotaiScope);
13
+ const setIsLibraryMenuOpen = useSetAtom(isLibraryMenuOpenAtom);
14
14
  const { container } = useExcalidrawContainer();
15
15
  return (_jsxs(Dialog, { onCloseRequest: onCancel, size: "small", ...rest, className: `confirm-dialog ${className}`, children: [children, _jsxs("div", { className: "confirm-dialog-buttons", children: [_jsx(DialogActionButton, { label: cancelText, onClick: () => {
16
16
  setAppState({ openMenu: null });
17
17
  setIsLibraryMenuOpen(false);
18
- onCancel();
18
+ // flush any pending updates synchronously,
19
+ // otherwise it could lead to crash in some chromium versions (131.0.6778.86),
20
+ // when `.focus` is invoked with container in some intermediate state
21
+ // (container seems mounted in DOM, but focus still causes a crash)
22
+ flushSync(() => {
23
+ onCancel();
24
+ });
19
25
  container?.focus();
20
26
  } }), _jsx(DialogActionButton, { label: confirmText, onClick: () => {
21
27
  setAppState({ openMenu: null });
22
28
  setIsLibraryMenuOpen(false);
23
- onConfirm();
29
+ // flush any pending updates synchronously,
30
+ // otherwise it leads to crash in some chromium versions (131.0.6778.86),
31
+ // when `.focus` is invoked with container in some intermediate state
32
+ // (container seems mounted in DOM, but focus still causes a crash)
33
+ flushSync(() => {
34
+ onConfirm();
35
+ });
24
36
  container?.focus();
25
37
  }, actionType: "danger" })] })] }));
26
38
  };
@@ -8,9 +8,8 @@ import "./Dialog.scss";
8
8
  import { Island } from "./Island";
9
9
  import { Modal } from "./Modal";
10
10
  import { queryFocusableElements } from "../utils";
11
- import { useSetAtom } from "jotai";
12
11
  import { isLibraryMenuOpenAtom } from "./LibraryMenu";
13
- import { jotaiScope } from "../jotai";
12
+ import { useSetAtom } from "../editor-jotai";
14
13
  import { t } from "../i18n";
15
14
  import { CloseIcon } from "./icons";
16
15
  function getDialogSize(size) {
@@ -63,7 +62,7 @@ export const Dialog = (props) => {
63
62
  return () => islandNode.removeEventListener("keydown", handleKeyDown);
64
63
  }, [islandNode, props.autofocus]);
65
64
  const setAppState = useExcalidrawSetAppState();
66
- const setIsLibraryMenuOpen = useSetAtom(isLibraryMenuOpenAtom, jotaiScope);
65
+ const setIsLibraryMenuOpen = useSetAtom(isLibraryMenuOpenAtom);
67
66
  const onClose = () => {
68
67
  setAppState({ openMenu: null });
69
68
  setIsLibraryMenuOpen(false);
@@ -14,7 +14,7 @@ export type EyeDropperProperties = {
14
14
  **/
15
15
  colorPickerType: ColorPickerType;
16
16
  };
17
- export declare const activeEyeDropperAtom: import("jotai").PrimitiveAtom<EyeDropperProperties | null> & {
17
+ export declare const activeEyeDropperAtom: import("jotai/vanilla/atom").PrimitiveAtom<EyeDropperProperties | null> & {
18
18
  init: EyeDropperProperties | null;
19
19
  };
20
20
  export declare const EyeDropper: React.FC<{
@@ -1,5 +1,4 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { atom } from "jotai";
3
2
  import { useEffect, useRef } from "react";
4
3
  import { createPortal } from "react-dom";
5
4
  import { rgbToHex } from "../colors";
@@ -12,6 +11,7 @@ import { getSelectedElements } from "../scene";
12
11
  import { useApp, useExcalidrawContainer, useExcalidrawElements } from "./App";
13
12
  import { useStable } from "../hooks/useStable";
14
13
  import "./EyeDropper.scss";
14
+ import { atom } from "../editor-jotai";
15
15
  export const activeEyeDropperAtom = atom(null);
16
16
  export const EyeDropper = ({ onCancel, onChange, onSelect, colorPickerType }) => {
17
17
  const eyeDropperContainer = useCreatePortalContainer({
@@ -1,6 +1,6 @@
1
1
  /// <reference types="react" />
2
2
  import "./IconPicker.scss";
3
- export declare function IconPicker<T>({ value, label, options, onChange, group, }: {
3
+ export declare function IconPicker<T>({ value, label, options, onChange, group, numberOfOptionsToAlwaysShow, }: {
4
4
  label: string;
5
5
  value: T;
6
6
  options: readonly {
@@ -8,8 +8,8 @@ export declare function IconPicker<T>({ value, label, options, onChange, group,
8
8
  text: string;
9
9
  icon: JSX.Element;
10
10
  keyBinding: string | null;
11
- showInPicker?: boolean;
12
11
  }[];
13
12
  onChange: (value: T) => void;
13
+ numberOfOptionsToAlwaysShow?: number;
14
14
  group?: string;
15
15
  }): JSX.Element;
@@ -1,66 +1,59 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import React from "react";
3
- import { Popover } from "./Popover";
4
- import "./IconPicker.scss";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React, { useEffect } from "react";
3
+ import * as Popover from "@radix-ui/react-popover";
5
4
  import { isArrowKey, KEYS } from "../keys";
6
- import { getLanguage } from "../i18n";
5
+ import { getLanguage, t } from "../i18n";
7
6
  import clsx from "clsx";
8
- function Picker({ options, value, label, onChange, onClose, }) {
9
- const rFirstItem = React.useRef();
10
- const rActiveItem = React.useRef();
11
- const rGallery = React.useRef(null);
12
- React.useEffect(() => {
13
- // After the component is first mounted focus on first input
14
- if (rActiveItem.current) {
15
- rActiveItem.current.focus();
16
- }
17
- else if (rGallery.current) {
18
- rGallery.current.focus();
19
- }
20
- }, []);
7
+ import Collapsible from "./Stats/Collapsible";
8
+ import { atom, useAtom } from "../editor-jotai";
9
+ import { useDevice } from "./App";
10
+ import "./IconPicker.scss";
11
+ const moreOptionsAtom = atom(false);
12
+ function Picker({ options, value, label, onChange, onClose, numberOfOptionsToAlwaysShow = options.length, }) {
13
+ const device = useDevice();
21
14
  const handleKeyDown = (event) => {
22
15
  const pressedOption = options.find((option) => option.keyBinding === event.key.toLowerCase());
23
16
  if (!(event.metaKey || event.altKey || event.ctrlKey) && pressedOption) {
24
17
  // Keybinding navigation
25
- const index = options.indexOf(pressedOption);
26
- rGallery.current.children[index].focus();
18
+ onChange(pressedOption.value);
27
19
  event.preventDefault();
28
20
  }
29
21
  else if (event.key === KEYS.TAB) {
30
- // Tab navigation cycle through options. If the user tabs
31
- // away from the picker, close the picker. We need to use
32
- // a timeout here to let the stack clear before checking.
33
- setTimeout(() => {
34
- const active = rActiveItem.current;
35
- const docActive = document.activeElement;
36
- if (active !== docActive) {
37
- onClose();
38
- }
39
- }, 0);
22
+ const index = options.findIndex((option) => option.value === value);
23
+ const nextIndex = event.shiftKey
24
+ ? (options.length + index - 1) % options.length
25
+ : (index + 1) % options.length;
26
+ onChange(options[nextIndex].value);
40
27
  }
41
28
  else if (isArrowKey(event.key)) {
42
29
  // Arrow navigation
43
- const { activeElement } = document;
44
30
  const isRTL = getLanguage().rtl;
45
- const index = Array.prototype.indexOf.call(rGallery.current.children, activeElement);
31
+ const index = options.findIndex((option) => option.value === value);
46
32
  if (index !== -1) {
47
33
  const length = options.length;
48
34
  let nextIndex = index;
49
35
  switch (event.key) {
50
36
  // Select the next option
51
37
  case isRTL ? KEYS.ARROW_LEFT : KEYS.ARROW_RIGHT:
52
- case KEYS.ARROW_DOWN: {
53
38
  nextIndex = (index + 1) % length;
54
39
  break;
55
- }
56
40
  // Select the previous option
57
41
  case isRTL ? KEYS.ARROW_RIGHT : KEYS.ARROW_LEFT:
58
- case KEYS.ARROW_UP: {
59
42
  nextIndex = (length + index - 1) % length;
60
43
  break;
44
+ // Go the next row
45
+ case KEYS.ARROW_DOWN: {
46
+ nextIndex = (index + (numberOfOptionsToAlwaysShow ?? 1)) % length;
47
+ break;
48
+ }
49
+ // Go the previous row
50
+ case KEYS.ARROW_UP: {
51
+ nextIndex =
52
+ (length + index - (numberOfOptionsToAlwaysShow ?? 1)) % length;
53
+ break;
61
54
  }
62
55
  }
63
- rGallery.current.children[nextIndex].focus();
56
+ onChange(options[nextIndex].value);
64
57
  }
65
58
  event.preventDefault();
66
59
  }
@@ -72,28 +65,38 @@ function Picker({ options, value, label, onChange, onClose, }) {
72
65
  event.nativeEvent.stopImmediatePropagation();
73
66
  event.stopPropagation();
74
67
  };
75
- return (_jsx("div", { className: `picker`, role: "dialog", "aria-modal": "true", "aria-label": label, onKeyDown: handleKeyDown, children: _jsx("div", { className: "picker-content", ref: rGallery, children: options.map((option, i) => (_jsxs("button", { type: "button", className: clsx("picker-option", {
68
+ const [showMoreOptions, setShowMoreOptions] = useAtom(moreOptionsAtom);
69
+ const alwaysVisibleOptions = React.useMemo(() => options.slice(0, numberOfOptionsToAlwaysShow), [options, numberOfOptionsToAlwaysShow]);
70
+ const moreOptions = React.useMemo(() => options.slice(numberOfOptionsToAlwaysShow), [options, numberOfOptionsToAlwaysShow]);
71
+ useEffect(() => {
72
+ if (!alwaysVisibleOptions.some((option) => option.value === value)) {
73
+ setShowMoreOptions(true);
74
+ }
75
+ }, [value, alwaysVisibleOptions, setShowMoreOptions]);
76
+ const renderOptions = (options) => {
77
+ return (_jsx("div", { className: "picker-content", children: options.map((option, i) => (_jsxs("button", { type: "button", className: clsx("picker-option", {
76
78
  active: value === option.value,
77
79
  }), onClick: (event) => {
78
- event.currentTarget.focus();
79
80
  onChange(option.value);
80
- }, title: `${option.text} ${option.keyBinding && `— ${option.keyBinding.toUpperCase()}`}`, "aria-label": option.text || "none", "aria-keyshortcuts": option.keyBinding || undefined, ref: (el) => {
81
- if (el && i === 0) {
82
- rFirstItem.current = el;
83
- }
84
- if (el && option.value === value) {
85
- rActiveItem.current = el;
81
+ }, title: `${option.text} ${option.keyBinding && `— ${option.keyBinding.toUpperCase()}`}`, "aria-label": option.text || "none", "aria-keyshortcuts": option.keyBinding || undefined, ref: (ref) => {
82
+ if (value === option.value) {
83
+ // Use a timeout here to render focus properly
84
+ setTimeout(() => {
85
+ ref?.focus();
86
+ }, 0);
86
87
  }
87
- }, onFocus: () => {
88
- onChange(option.value);
89
- }, children: [option.icon, option.keyBinding && (_jsx("span", { className: "picker-keybinding", children: option.keyBinding }))] }, option.text))) }) }));
88
+ }, children: [option.icon, option.keyBinding && (_jsx("span", { className: "picker-keybinding", children: option.keyBinding }))] }, option.text))) }));
89
+ };
90
+ return (_jsx(Popover.Content, { side: device.editor.isMobile && !device.viewport.isLandscape
91
+ ? "top"
92
+ : "bottom", align: "start", sideOffset: 12, style: { zIndex: "var(--zIndex-popup)" }, onKeyDown: handleKeyDown, children: _jsxs("div", { className: `picker`, role: "dialog", "aria-modal": "true", "aria-label": label, children: [renderOptions(alwaysVisibleOptions), moreOptions.length > 0 && (_jsx(Collapsible, { label: t("labels.more_options"), open: showMoreOptions, openTrigger: () => {
93
+ setShowMoreOptions((value) => !value);
94
+ }, className: "picker-collapsible", children: renderOptions(moreOptions) }))] }) }));
90
95
  }
91
- export function IconPicker({ value, label, options, onChange, group = "", }) {
96
+ export function IconPicker({ value, label, options, onChange, group = "", numberOfOptionsToAlwaysShow, }) {
92
97
  const [isActive, setActive] = React.useState(false);
93
98
  const rPickerButton = React.useRef(null);
94
- const isRTL = getLanguage().rtl;
95
- return (_jsxs("div", { children: [_jsx("button", { name: group, type: "button", className: isActive ? "active" : "", "aria-label": label, onClick: () => setActive(!isActive), ref: rPickerButton, children: options.find((option) => option.value === value)?.icon }), _jsx(React.Suspense, { fallback: "", children: isActive ? (_jsxs(_Fragment, { children: [_jsx(Popover, { onCloseRequest: (event) => event.target !== rPickerButton.current && setActive(false), ...(isRTL ? { right: 5.5 } : { left: -5.5 }), children: _jsx(Picker, { options: options.filter((opt) => opt.showInPicker !== false), value: value, label: label, onChange: onChange, onClose: () => {
96
- setActive(false);
97
- rPickerButton.current?.focus();
98
- } }) }), _jsx("div", { className: "picker-triangle" })] })) : null })] }));
99
+ return (_jsx("div", { children: _jsxs(Popover.Root, { open: isActive, onOpenChange: (open) => setActive(open), children: [_jsx(Popover.Trigger, { name: group, type: "button", "aria-label": label, onClick: () => setActive(!isActive), ref: rPickerButton, className: isActive ? "active" : "", children: options.find((option) => option.value === value)?.icon }), isActive && (_jsx(Picker, { options: options, value: value, label: label, onChange: onChange, onClose: () => {
100
+ setActive(false);
101
+ }, numberOfOptionsToAlwaysShow: numberOfOptionsToAlwaysShow }))] }) }));
99
102
  }
@@ -26,8 +26,7 @@ import { trackEvent } from "../analytics";
26
26
  import { useDevice } from "./App";
27
27
  import Footer from "./footer/Footer";
28
28
  import { isSidebarDockedAtom } from "./Sidebar/Sidebar";
29
- import { jotaiScope } from "../jotai";
30
- import { Provider, useAtom, useAtomValue } from "jotai";
29
+ import { useAtom, useAtomValue } from "../editor-jotai";
31
30
  import MainMenu from "./main-menu/MainMenu";
32
31
  import { ActiveConfirmDialog } from "./ActiveConfirmDialog";
33
32
  import { OverwriteConfirmDialog } from "./OverwriteConfirm/OverwriteConfirm";
@@ -57,7 +56,8 @@ const DefaultOverwriteConfirmDialog = () => {
57
56
  const LayerUI = ({ actionManager, appState, files, setAppState, elements, canvas, onLockToggle, onHandToolToggle, onPenModeToggle, showExitZenModeBtn, renderTopRightUI, renderCustomStats, UIOptions, onExportImage, renderWelcomeScreen, children, app, isCollaborating, generateLinkForSelection, }) => {
58
57
  const device = useDevice();
59
58
  const tunnels = useInitializeTunnels();
60
- const [eyeDropperState, setEyeDropperState] = useAtom(activeEyeDropperAtom, jotaiScope);
59
+ const TunnelsJotaiProvider = tunnels.tunnelsJotai.Provider;
60
+ const [eyeDropperState, setEyeDropperState] = useAtom(activeEyeDropperAtom);
61
61
  const renderJSONExportDialog = () => {
62
62
  if (!UIOptions.canvasActions.export) {
63
63
  return null;
@@ -78,7 +78,7 @@ const LayerUI = ({ actionManager, appState, files, setAppState, elements, canvas
78
78
  // we want to make sure this doesn't overflow so subtracting the
79
79
  // approximate height of hamburgerMenu + footer
80
80
  maxHeight: `${appState.height - 166}px`,
81
- }, children: _jsx(SelectedShapeActions, { appState: appState, elementsMap: app.scene.getNonDeletedElementsMap(), renderAction: actionManager.renderAction }) }) }));
81
+ }, children: _jsx(SelectedShapeActions, { appState: appState, elementsMap: app.scene.getNonDeletedElementsMap(), renderAction: actionManager.renderAction, app: app }) }) }));
82
82
  const renderFixedSideContainer = () => {
83
83
  const shouldRenderSelectedShapeActions = showSelectedShapeActions(appState, elements);
84
84
  const shouldShowStats = appState.stats.open &&
@@ -109,7 +109,7 @@ const LayerUI = ({ actionManager, appState, files, setAppState, elements, canvas
109
109
  trackEvent("sidebar", `toggleDock (${docked ? "dock" : "undock"})`, `(${device.editor.isMobile ? "mobile" : "desktop"})`);
110
110
  } }));
111
111
  };
112
- const isSidebarDocked = useAtomValue(isSidebarDockedAtom, jotaiScope);
112
+ const isSidebarDocked = useAtomValue(isSidebarDockedAtom);
113
113
  const layerUIJSX = (_jsxs(_Fragment, { children: [children, _jsx(DefaultMainMenu, { UIOptions: UIOptions }), _jsx(DefaultSidebar.Trigger, { __fallback: true, icon: LibraryIcon, title: capitalizeString(t("toolBar.library")), onToggle: (open) => {
114
114
  if (open) {
115
115
  trackEvent("sidebar", `${DEFAULT_SIDEBAR.name} (open)`, `button (${device.editor.isMobile ? "mobile" : "desktop"})`);
@@ -166,7 +166,7 @@ const LayerUI = ({ actionManager, appState, files, setAppState, elements, canvas
166
166
  ...calculateScrollCenter(elements, appState),
167
167
  }));
168
168
  }, children: t("buttons.scrollBackToContent") }))] }), renderSidebars()] }))] }));
169
- return (_jsx(UIAppStateContext.Provider, { value: appState, children: _jsx(Provider, { scope: tunnels.jotaiScope, children: _jsx(TunnelsContext.Provider, { value: tunnels, children: layerUIJSX }) }) }));
169
+ return (_jsx(UIAppStateContext.Provider, { value: appState, children: _jsx(TunnelsJotaiProvider, { children: _jsx(TunnelsContext.Provider, { value: tunnels, children: layerUIJSX }) }) }));
170
170
  };
171
171
  const stripIrrelevantAppStateProps = (appState) => {
172
172
  const { suggestedBindings, startBoundElement, cursorButton, scrollX, scrollY, ...ret } = appState;
@@ -1,24 +1,10 @@
1
1
  import React from "react";
2
- import type Library from "../data/library";
3
- import type { LibraryItems, LibraryItem, ExcalidrawProps, UIAppState } from "../types";
4
2
  import "./LibraryMenu.scss";
5
- export declare const isLibraryMenuOpenAtom: import("jotai").PrimitiveAtom<boolean> & {
3
+ export declare const isLibraryMenuOpenAtom: import("jotai/vanilla/atom").PrimitiveAtom<boolean> & {
6
4
  init: boolean;
7
5
  };
8
- export declare const LibraryMenuContent: ({ onInsertLibraryItems, pendingElements, onAddToLibrary, setAppState, libraryReturnUrl, library, id, theme, selectedItems, onSelectItems, }: {
9
- pendingElements: LibraryItem["elements"];
10
- onInsertLibraryItems: (libraryItems: LibraryItems) => void;
11
- onAddToLibrary: () => void;
12
- setAppState: React.Component<any, UIAppState>["setState"];
13
- libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
14
- library: Library;
15
- id: string;
16
- theme: UIAppState["theme"];
17
- selectedItems: LibraryItem["id"][];
18
- onSelectItems: (id: LibraryItem["id"][]) => void;
19
- }) => JSX.Element;
20
6
  /**
21
7
  * This component is meant to be rendered inside <Sidebar.Tab/> inside our
22
8
  * <DefaultSidebar/> or host apps Sidebar components.
23
9
  */
24
- export declare const LibraryMenu: () => JSX.Element;
10
+ export declare const LibraryMenu: React.MemoExoticComponent<() => JSX.Element>;
@@ -1,26 +1,25 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState, useCallback, useMemo, useRef } from "react";
2
+ import { useState, useCallback, useMemo, useEffect, memo, useRef, } from "react";
3
3
  import { distributeLibraryItemsOnSquareGrid, libraryItemsAtom, } from "../data/library";
4
4
  import { t } from "../i18n";
5
5
  import { randomId } from "../random";
6
6
  import LibraryMenuItems from "./LibraryMenuItems";
7
7
  import { trackEvent } from "../analytics";
8
- import { atom, useAtom } from "jotai";
9
- import { jotaiScope } from "../jotai";
8
+ import { atom, useAtom } from "../editor-jotai";
10
9
  import Spinner from "./Spinner";
11
10
  import { useApp, useAppProps, useExcalidrawElements, useExcalidrawSetAppState, } from "./App";
12
11
  import { getSelectedElements } from "../scene";
13
12
  import { useUIAppState } from "../context/ui-appState";
14
13
  import "./LibraryMenu.scss";
15
14
  import { LibraryMenuControlButtons } from "./LibraryMenuControlButtons";
16
- import { isShallowEqual } from "../utils";
17
15
  import { LIBRARY_DISABLED_TYPES } from "../constants";
16
+ import { isShallowEqual } from "../utils";
18
17
  export const isLibraryMenuOpenAtom = atom(false);
19
18
  const LibraryMenuWrapper = ({ children }) => {
20
19
  return _jsx("div", { className: "layer-ui__library", children: children });
21
20
  };
22
- export const LibraryMenuContent = ({ onInsertLibraryItems, pendingElements, onAddToLibrary, setAppState, libraryReturnUrl, library, id, theme, selectedItems, onSelectItems, }) => {
23
- const [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope);
21
+ const LibraryMenuContent = memo(({ onInsertLibraryItems, pendingElements, onAddToLibrary, setAppState, libraryReturnUrl, library, id, theme, selectedItems, onSelectItems, }) => {
22
+ const [libraryItemsData] = useAtom(libraryItemsAtom);
24
23
  const _onAddToLibrary = useCallback((elements) => {
25
24
  const addToLibrary = async (processedElements, libraryItems) => {
26
25
  trackEvent("element", "addToLibrary", "ui");
@@ -54,37 +53,80 @@ export const LibraryMenuContent = ({ onInsertLibraryItems, pendingElements, onAd
54
53
  }
55
54
  const showBtn = libraryItemsData.libraryItems.length > 0 || pendingElements.length > 0;
56
55
  return (_jsxs(LibraryMenuWrapper, { children: [_jsx(LibraryMenuItems, { isLoading: libraryItemsData.status === "loading", libraryItems: libraryItems, onAddToLibrary: _onAddToLibrary, onInsertLibraryItems: onInsertLibraryItems, pendingElements: pendingElements, id: id, libraryReturnUrl: libraryReturnUrl, theme: theme, onSelectItems: onSelectItems, selectedItems: selectedItems }), showBtn && (_jsx(LibraryMenuControlButtons, { className: "library-menu-control-buttons--at-bottom", style: { padding: "16px 12px 0 12px" }, id: id, libraryReturnUrl: libraryReturnUrl, theme: theme }))] }));
57
- };
58
- const usePendingElementsMemo = (appState, elements) => {
59
- const create = () => getSelectedElements(elements, appState, {
56
+ });
57
+ const getPendingElements = (elements, selectedElementIds) => ({
58
+ elements,
59
+ pending: getSelectedElements(elements, { selectedElementIds }, {
60
60
  includeBoundTextElement: true,
61
61
  includeElementsInFrames: true,
62
- });
63
- const val = useRef(create());
64
- const prevAppState = useRef(appState);
65
- const prevElements = useRef(elements);
66
- if (!isShallowEqual(appState.selectedElementIds, prevAppState.current.selectedElementIds) ||
67
- !isShallowEqual(elements, prevElements.current)) {
68
- val.current = create();
69
- prevAppState.current = appState;
70
- prevElements.current = elements;
71
- }
72
- return val.current;
62
+ }),
63
+ selectedElementIds,
64
+ });
65
+ const usePendingElementsMemo = (appState, app) => {
66
+ const elements = useExcalidrawElements();
67
+ const [state, setState] = useState(() => getPendingElements(elements, appState.selectedElementIds));
68
+ const selectedElementVersions = useRef(new Map());
69
+ useEffect(() => {
70
+ for (const element of state.pending) {
71
+ selectedElementVersions.current.set(element.id, element.version);
72
+ }
73
+ }, [state.pending]);
74
+ useEffect(() => {
75
+ if (
76
+ // Only update once pointer is released.
77
+ // Reading directly from app.state to make it clear it's not reactive
78
+ // (hence, there's potential for stale state)
79
+ app.state.cursorButton === "up" &&
80
+ app.state.activeTool.type === "selection") {
81
+ setState((prev) => {
82
+ // if selectedElementIds changed, we don't have to compare versions
83
+ // ---------------------------------------------------------------------
84
+ if (!isShallowEqual(prev.selectedElementIds, appState.selectedElementIds)) {
85
+ selectedElementVersions.current.clear();
86
+ return getPendingElements(elements, appState.selectedElementIds);
87
+ }
88
+ // otherwise we need to check whether selected elements changed
89
+ // ---------------------------------------------------------------------
90
+ const elementsMap = app.scene.getNonDeletedElementsMap();
91
+ for (const id of Object.keys(appState.selectedElementIds)) {
92
+ const currVersion = elementsMap.get(id)?.version;
93
+ if (currVersion &&
94
+ currVersion !== selectedElementVersions.current.get(id)) {
95
+ // we can't update the selectedElementVersions in here
96
+ // because of double render in StrictMode which would overwrite
97
+ // the state in the second pass with the old `prev` state.
98
+ // Thus, we update versions in a separate effect. May create
99
+ // a race condition since current effect is not fully reactive.
100
+ return getPendingElements(elements, appState.selectedElementIds);
101
+ }
102
+ }
103
+ // nothing changed
104
+ // ---------------------------------------------------------------------
105
+ return prev;
106
+ });
107
+ }
108
+ }, [
109
+ app,
110
+ app.state.cursorButton,
111
+ app.state.activeTool.type,
112
+ appState.selectedElementIds,
113
+ elements,
114
+ ]);
115
+ return state.pending;
73
116
  };
74
117
  /**
75
118
  * This component is meant to be rendered inside <Sidebar.Tab/> inside our
76
119
  * <DefaultSidebar/> or host apps Sidebar components.
77
120
  */
78
- export const LibraryMenu = () => {
79
- const { library, id, onInsertElements } = useApp();
121
+ export const LibraryMenu = memo(() => {
122
+ const app = useApp();
123
+ const { onInsertElements } = app;
80
124
  const appProps = useAppProps();
81
125
  const appState = useUIAppState();
82
126
  const setAppState = useExcalidrawSetAppState();
83
- const elements = useExcalidrawElements();
84
127
  const [selectedItems, setSelectedItems] = useState([]);
85
- const memoizedLibrary = useMemo(() => library, [library]);
86
- // BUG: pendingElements are still causing some unnecessary rerenders because clicking into canvas returns some ids even when no element is selected.
87
- const pendingElements = usePendingElementsMemo(appState, elements);
128
+ const memoizedLibrary = useMemo(() => app.library, [app.library]);
129
+ const pendingElements = usePendingElementsMemo(appState, app);
88
130
  const onInsertLibraryItems = useCallback((libraryItems) => {
89
131
  onInsertElements(distributeLibraryItemsOnSquareGrid(libraryItems));
90
132
  }, [onInsertElements]);
@@ -95,5 +137,5 @@ export const LibraryMenu = () => {
95
137
  activeEmbeddable: null,
96
138
  });
97
139
  }, [setAppState]);
98
- return (_jsx(LibraryMenuContent, { pendingElements: pendingElements, onInsertLibraryItems: onInsertLibraryItems, onAddToLibrary: deselectItems, setAppState: setAppState, libraryReturnUrl: appProps.libraryReturnUrl, library: memoizedLibrary, id: id, theme: appState.theme, selectedItems: selectedItems, onSelectItems: setSelectedItems }));
99
- };
140
+ return (_jsx(LibraryMenuContent, { pendingElements: pendingElements, onInsertLibraryItems: onInsertLibraryItems, onAddToLibrary: deselectItems, setAppState: setAppState, libraryReturnUrl: appProps.libraryReturnUrl, library: memoizedLibrary, id: app.id, theme: appState.theme, selectedItems: selectedItems, onSelectItems: setSelectedItems }));
141
+ });