@excalidraw/excalidraw 0.17.1-1d71f84 → 0.17.1-4689a6b

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 (111) hide show
  1. package/CHANGELOG.md +1 -0
  2. package/dist/browser/dev/excalidraw-assets-dev/{chunk-AK7SWNLN.js → chunk-23CKV3WP.js} +4 -2
  3. package/dist/browser/dev/excalidraw-assets-dev/chunk-23CKV3WP.js.map +7 -0
  4. package/dist/browser/dev/excalidraw-assets-dev/{chunk-RWZVJAQU.js → chunk-7D5BMEAB.js} +2227 -1976
  5. package/dist/browser/dev/excalidraw-assets-dev/chunk-7D5BMEAB.js.map +7 -0
  6. package/dist/browser/dev/excalidraw-assets-dev/{en-5TCZHGGJ.js → en-W7TECCRB.js} +2 -2
  7. package/dist/browser/dev/excalidraw-assets-dev/{image-EDKQZH7Z.js → image-JKT6GXZD.js} +2 -2
  8. package/dist/browser/dev/index.css +20 -0
  9. package/dist/browser/dev/index.css.map +2 -2
  10. package/dist/browser/dev/index.js +770 -585
  11. package/dist/browser/dev/index.js.map +4 -4
  12. package/dist/browser/prod/excalidraw-assets/chunk-DWOM5R6H.js +55 -0
  13. package/dist/browser/prod/excalidraw-assets/{chunk-CTYINSWT.js → chunk-SK23VHAR.js} +2 -2
  14. package/dist/browser/prod/excalidraw-assets/{en-LROPV2RN.js → en-SMMH575S.js} +1 -1
  15. package/dist/browser/prod/excalidraw-assets/image-WDEQS5RL.js +1 -0
  16. package/dist/browser/prod/index.css +1 -1
  17. package/dist/browser/prod/index.js +22 -22
  18. package/dist/{prod/en-II4GK66F.json → dev/en-CVBEBUBY.json} +3 -1
  19. package/dist/dev/index.css +20 -0
  20. package/dist/dev/index.css.map +2 -2
  21. package/dist/dev/index.js +2383 -2074
  22. package/dist/dev/index.js.map +4 -4
  23. package/dist/excalidraw/actions/actionBoundText.js +4 -1
  24. package/dist/excalidraw/actions/actionCanvas.js +3 -1
  25. package/dist/excalidraw/actions/actionDuplicateSelection.js +4 -0
  26. package/dist/excalidraw/actions/actionExport.d.ts +1 -1
  27. package/dist/excalidraw/actions/actionFinalize.d.ts +1 -1
  28. package/dist/excalidraw/actions/actionFinalize.js +3 -3
  29. package/dist/excalidraw/actions/actionFlip.d.ts +3 -3
  30. package/dist/excalidraw/actions/actionFlip.js +6 -6
  31. package/dist/excalidraw/actions/actionGroup.js +4 -2
  32. package/dist/excalidraw/actions/actionHistory.js +3 -0
  33. package/dist/excalidraw/actions/actionZindex.d.ts +11 -11
  34. package/dist/excalidraw/actions/shortcuts.js +1 -1
  35. package/dist/excalidraw/analytics.js +1 -1
  36. package/dist/excalidraw/components/App.d.ts +13 -3
  37. package/dist/excalidraw/components/App.js +212 -83
  38. package/dist/excalidraw/components/CommandPalette/CommandPalette.js +24 -10
  39. package/dist/excalidraw/components/DarkModeToggle.js +3 -1
  40. package/dist/excalidraw/components/HelpDialog.js +8 -6
  41. package/dist/excalidraw/components/RadioGroup.d.ts +2 -1
  42. package/dist/excalidraw/components/RadioGroup.js +1 -1
  43. package/dist/excalidraw/components/TTDDialog/MermaidToExcalidraw.js +6 -2
  44. package/dist/excalidraw/components/dropdownMenu/DropdownMenuItemContentRadio.d.ts +18 -0
  45. package/dist/excalidraw/components/dropdownMenu/DropdownMenuItemContentRadio.js +9 -0
  46. package/dist/excalidraw/components/hyperlink/Hyperlink.js +3 -3
  47. package/dist/excalidraw/components/hyperlink/helpers.js +2 -3
  48. package/dist/excalidraw/components/icons.d.ts +3 -0
  49. package/dist/excalidraw/components/icons.js +5 -1
  50. package/dist/excalidraw/components/main-menu/DefaultItems.d.ts +12 -2
  51. package/dist/excalidraw/components/main-menu/DefaultItems.js +38 -7
  52. package/dist/excalidraw/constants.d.ts +0 -3
  53. package/dist/excalidraw/constants.js +0 -3
  54. package/dist/excalidraw/data/magic.js +2 -1
  55. package/dist/excalidraw/data/reconcile.d.ts +6 -0
  56. package/dist/excalidraw/data/reconcile.js +49 -0
  57. package/dist/excalidraw/data/restore.d.ts +3 -3
  58. package/dist/excalidraw/data/restore.js +5 -6
  59. package/dist/excalidraw/data/transform.d.ts +1 -1
  60. package/dist/excalidraw/data/transform.js +12 -3
  61. package/dist/excalidraw/element/binding.d.ts +22 -9
  62. package/dist/excalidraw/element/binding.js +403 -26
  63. package/dist/excalidraw/element/bounds.d.ts +0 -1
  64. package/dist/excalidraw/element/bounds.js +0 -3
  65. package/dist/excalidraw/element/collision.d.ts +14 -19
  66. package/dist/excalidraw/element/collision.js +36 -713
  67. package/dist/excalidraw/element/embeddable.js +18 -43
  68. package/dist/excalidraw/element/index.d.ts +0 -1
  69. package/dist/excalidraw/element/index.js +0 -1
  70. package/dist/excalidraw/element/linearElementEditor.d.ts +10 -10
  71. package/dist/excalidraw/element/linearElementEditor.js +6 -4
  72. package/dist/excalidraw/element/newElement.d.ts +1 -1
  73. package/dist/excalidraw/element/newElement.js +2 -1
  74. package/dist/excalidraw/element/textElement.d.ts +0 -1
  75. package/dist/excalidraw/element/textElement.js +0 -30
  76. package/dist/excalidraw/element/types.d.ts +17 -2
  77. package/dist/excalidraw/errors.d.ts +3 -0
  78. package/dist/excalidraw/errors.js +3 -0
  79. package/dist/excalidraw/fractionalIndex.d.ts +40 -0
  80. package/dist/excalidraw/fractionalIndex.js +241 -0
  81. package/dist/excalidraw/frame.d.ts +1 -1
  82. package/dist/excalidraw/hooks/useCreatePortalContainer.js +2 -1
  83. package/dist/excalidraw/locales/en.json +3 -1
  84. package/dist/excalidraw/renderer/helpers.js +2 -2
  85. package/dist/excalidraw/renderer/interactiveScene.js +1 -1
  86. package/dist/excalidraw/renderer/renderElement.js +3 -3
  87. package/dist/excalidraw/renderer/renderSnaps.js +2 -1
  88. package/dist/excalidraw/scene/Scene.d.ts +7 -6
  89. package/dist/excalidraw/scene/Scene.js +28 -13
  90. package/dist/excalidraw/scene/export.js +4 -3
  91. package/dist/excalidraw/types.d.ts +4 -3
  92. package/dist/excalidraw/utils.d.ts +1 -0
  93. package/dist/excalidraw/utils.js +1 -0
  94. package/dist/excalidraw/zindex.d.ts +2 -2
  95. package/dist/excalidraw/zindex.js +9 -13
  96. package/dist/{dev/en-II4GK66F.json → prod/en-CVBEBUBY.json} +3 -1
  97. package/dist/prod/index.css +1 -1
  98. package/dist/prod/index.js +36 -36
  99. package/dist/utils/collision.d.ts +4 -0
  100. package/dist/utils/collision.js +48 -0
  101. package/dist/utils/geometry/geometry.d.ts +71 -0
  102. package/dist/utils/geometry/geometry.js +674 -0
  103. package/dist/utils/geometry/shape.d.ts +55 -0
  104. package/dist/utils/geometry/shape.js +149 -0
  105. package/package.json +2 -1
  106. package/dist/browser/dev/excalidraw-assets-dev/chunk-AK7SWNLN.js.map +0 -7
  107. package/dist/browser/dev/excalidraw-assets-dev/chunk-RWZVJAQU.js.map +0 -7
  108. package/dist/browser/prod/excalidraw-assets/chunk-LL4GORAM.js +0 -55
  109. package/dist/browser/prod/excalidraw-assets/image-EFCJDJH3.js +0 -1
  110. /package/dist/browser/dev/excalidraw-assets-dev/{en-5TCZHGGJ.js.map → en-W7TECCRB.js.map} +0 -0
  111. /package/dist/browser/dev/excalidraw-assets-dev/{image-EDKQZH7Z.js.map → image-JKT6GXZD.js.map} +0 -0
@@ -23,6 +23,8 @@ import { actionClearCanvas, actionLink } from "../../actions";
23
23
  import { jotaiStore } from "../../jotai";
24
24
  import { activeConfirmDialogAtom } from "../ActiveConfirmDialog";
25
25
  import * as defaultItems from "./defaultCommandPaletteItems";
26
+ import { trackEvent } from "../../analytics";
27
+ import { useStable } from "../../hooks/useStable";
26
28
  import "./CommandPalette.scss";
27
29
  const lastUsedPaletteItem = atom(null);
28
30
  export const DEFAULT_CATEGORIES = {
@@ -71,11 +73,17 @@ export const CommandPalette = Object.assign((props) => {
71
73
  if (isCommandPaletteToggleShortcut(event)) {
72
74
  event.preventDefault();
73
75
  event.stopPropagation();
74
- setAppState((appState) => ({
75
- openDialog: appState.openDialog?.name === "commandPalette"
76
+ setAppState((appState) => {
77
+ const nextState = appState.openDialog?.name === "commandPalette"
76
78
  ? null
77
- : { name: "commandPalette" },
78
- }));
79
+ : { name: "commandPalette" };
80
+ if (nextState) {
81
+ trackEvent("command_palette", "open", "shortcut");
82
+ }
83
+ return {
84
+ openDialog: nextState,
85
+ };
86
+ });
79
87
  }
80
88
  };
81
89
  window.addEventListener(EVENT.KEYDOWN, commandPaletteShortcut, {
@@ -101,10 +109,18 @@ function CommandPaletteInner({ customCommandPaletteItems, }) {
101
109
  const [lastUsed, setLastUsed] = useAtom(lastUsedPaletteItem);
102
110
  const [allCommands, setAllCommands] = useState(null);
103
111
  const inputRef = useRef(null);
112
+ const stableDeps = useStable({
113
+ uiAppState,
114
+ customCommandPaletteItems,
115
+ appProps,
116
+ });
104
117
  useEffect(() => {
105
- if (!uiAppState || !app.scene || !actionManager) {
106
- return;
107
- }
118
+ // these props change often and we don't want them to re-run the effect
119
+ // which would renew `allCommands`, cascading down and resetting state.
120
+ //
121
+ // This means that the commands won't update on appState/appProps changes
122
+ // while the command palette is open
123
+ const { uiAppState, customCommandPaletteItems, appProps } = stableDeps;
108
124
  const getActionLabel = (action) => {
109
125
  let label = "";
110
126
  if (action.label) {
@@ -407,15 +423,13 @@ function CommandPaletteInner({ customCommandPaletteItems, }) {
407
423
  null);
408
424
  }
409
425
  }, [
426
+ stableDeps,
410
427
  app,
411
- appProps,
412
- uiAppState,
413
428
  actionManager,
414
429
  setAllCommands,
415
430
  lastUsed?.label,
416
431
  setLastUsed,
417
432
  setAppState,
418
- customCommandPaletteItems,
419
433
  ]);
420
434
  const [commandSearch, setCommandSearch] = useState("");
421
435
  const [currentCommand, setCurrentCommand] = useState(null);
@@ -7,7 +7,9 @@ import { THEME } from "../constants";
7
7
  // but this could be added in the future.
8
8
  export const DarkModeToggle = (props) => {
9
9
  const title = props.title ||
10
- (props.value === "dark" ? t("buttons.lightMode") : t("buttons.darkMode"));
10
+ (props.value === THEME.DARK
11
+ ? t("buttons.lightMode")
12
+ : t("buttons.darkMode"));
11
13
  return (_jsx(ToolButton, { type: "icon", icon: props.value === THEME.LIGHT ? ICONS.MOON : ICONS.SUN, title: title, "aria-label": title, onClick: () => props.onChange(props.value === THEME.DARK ? THEME.LIGHT : THEME.DARK), "data-testid": "toggle-dark-mode" }));
12
14
  };
13
15
  const ICONS = {
@@ -5,11 +5,11 @@ import { KEYS } from "../keys";
5
5
  import { Dialog } from "./Dialog";
6
6
  import { getShortcutKey } from "../utils";
7
7
  import "./HelpDialog.scss";
8
- import { ExternalLinkIcon } from "./icons";
8
+ import { ExternalLinkIcon, GithubIcon, youtubeIcon } from "./icons";
9
9
  import { probablySupportsClipboardBlob } from "../clipboard";
10
10
  import { isDarwin, isFirefox, isWindows } from "../constants";
11
11
  import { getShortcutFromShortcutName } from "../actions/shortcuts";
12
- const Header = () => (_jsxs("div", { className: "HelpDialog__header", children: [_jsxs("a", { className: "HelpDialog__btn", href: "https://docs.excalidraw.com", target: "_blank", rel: "noopener noreferrer", children: [t("helpDialog.documentation"), _jsx("div", { className: "HelpDialog__link-icon", children: ExternalLinkIcon })] }), _jsxs("a", { className: "HelpDialog__btn", href: "https://blog.excalidraw.com", target: "_blank", rel: "noopener noreferrer", children: [t("helpDialog.blog"), _jsx("div", { className: "HelpDialog__link-icon", children: ExternalLinkIcon })] }), _jsxs("a", { className: "HelpDialog__btn", href: "https://github.com/excalidraw/excalidraw/issues", target: "_blank", rel: "noopener noreferrer", children: [t("helpDialog.github"), _jsx("div", { className: "HelpDialog__link-icon", children: ExternalLinkIcon })] })] }));
12
+ const Header = () => (_jsxs("div", { className: "HelpDialog__header", children: [_jsxs("a", { className: "HelpDialog__btn", href: "https://docs.excalidraw.com", target: "_blank", rel: "noopener noreferrer", children: [_jsx("div", { className: "HelpDialog__link-icon", children: ExternalLinkIcon }), t("helpDialog.documentation")] }), _jsxs("a", { className: "HelpDialog__btn", href: "https://blog.excalidraw.com", target: "_blank", rel: "noopener noreferrer", children: [_jsx("div", { className: "HelpDialog__link-icon", children: ExternalLinkIcon }), t("helpDialog.blog")] }), _jsxs("a", { className: "HelpDialog__btn", href: "https://github.com/excalidraw/excalidraw/issues", target: "_blank", rel: "noopener noreferrer", children: [_jsx("div", { className: "HelpDialog__link-icon", children: GithubIcon }), t("helpDialog.github")] }), _jsxs("a", { className: "HelpDialog__btn", href: "https://youtube.com/@excalidraw", target: "_blank", rel: "noopener noreferrer", children: [_jsx("div", { className: "HelpDialog__link-icon", children: youtubeIcon }), "YouTube"] })] }));
13
13
  const Section = (props) => (_jsxs(_Fragment, { children: [_jsx("h3", { children: props.title }), _jsx("div", { className: "HelpDialog__islands-container", children: props.children })] }));
14
14
  const ShortcutIsland = (props) => (_jsxs("div", { className: `HelpDialog__island ${props.className}`, children: [_jsx("h4", { className: "HelpDialog__island-title", children: props.caption }), _jsx("div", { className: "HelpDialog__island-content", children: props.children })] }));
15
15
  function* intersperse(as, delim) {
@@ -57,10 +57,12 @@ export const HelpDialog = ({ onClose }) => {
57
57
  t("helpDialog.click"),
58
58
  t("helpDialog.click"),
59
59
  t("helpDialog.click"),
60
- ], isOr: false }), _jsx(Shortcut, { label: t("toolBar.lock"), shortcuts: [KEYS.Q] }), _jsx(Shortcut, { label: t("helpDialog.preventBinding"), shortcuts: [getShortcutKey("CtrlOrCmd")] }), _jsx(Shortcut, { label: t("toolBar.link"), shortcuts: [getShortcutKey("CtrlOrCmd+K")] })] }), _jsxs(ShortcutIsland, { className: "HelpDialog__island--view", caption: t("helpDialog.view"), children: [_jsx(Shortcut, { label: t("buttons.zoomIn"), shortcuts: [getShortcutKey("CtrlOrCmd++")] }), _jsx(Shortcut, { label: t("buttons.zoomOut"), shortcuts: [getShortcutKey("CtrlOrCmd+-")] }), _jsx(Shortcut, { label: t("buttons.resetZoom"), shortcuts: [getShortcutKey("CtrlOrCmd+0")] }), _jsx(Shortcut, { label: t("helpDialog.zoomToFit"), shortcuts: ["Shift+1"] }), _jsx(Shortcut, { label: t("helpDialog.zoomToSelection"), shortcuts: ["Shift+2"] }), _jsx(Shortcut, { label: t("helpDialog.movePageUpDown"), shortcuts: ["PgUp/PgDn"] }), _jsx(Shortcut, { label: t("helpDialog.movePageLeftRight"), shortcuts: ["Shift+PgUp/PgDn"] }), _jsx(Shortcut, { label: t("buttons.zenMode"), shortcuts: [getShortcutKey("Alt+Z")] }), _jsx(Shortcut, { label: t("buttons.objectsSnapMode"), shortcuts: [getShortcutKey("Alt+S")] }), _jsx(Shortcut, { label: t("labels.showGrid"), shortcuts: [getShortcutKey("CtrlOrCmd+'")] }), _jsx(Shortcut, { label: t("labels.viewMode"), shortcuts: [getShortcutKey("Alt+R")] }), _jsx(Shortcut, { label: t("labels.toggleTheme"), shortcuts: [getShortcutKey("Alt+Shift+D")] }), _jsx(Shortcut, { label: t("stats.title"), shortcuts: [getShortcutKey("Alt+/")] }), _jsx(Shortcut, { label: t("commandPalette.title"), shortcuts: [
61
- getShortcutFromShortcutName("commandPalette"),
62
- getShortcutFromShortcutName("commandPalette", 1),
63
- ] })] }), _jsxs(ShortcutIsland, { className: "HelpDialog__island--editor", caption: t("helpDialog.editor"), children: [_jsx(Shortcut, { label: t("labels.moveCanvas"), shortcuts: [
60
+ ], isOr: false }), _jsx(Shortcut, { label: t("toolBar.lock"), shortcuts: [KEYS.Q] }), _jsx(Shortcut, { label: t("helpDialog.preventBinding"), shortcuts: [getShortcutKey("CtrlOrCmd")] }), _jsx(Shortcut, { label: t("toolBar.link"), shortcuts: [getShortcutKey("CtrlOrCmd+K")] })] }), _jsxs(ShortcutIsland, { className: "HelpDialog__island--view", caption: t("helpDialog.view"), children: [_jsx(Shortcut, { label: t("buttons.zoomIn"), shortcuts: [getShortcutKey("CtrlOrCmd++")] }), _jsx(Shortcut, { label: t("buttons.zoomOut"), shortcuts: [getShortcutKey("CtrlOrCmd+-")] }), _jsx(Shortcut, { label: t("buttons.resetZoom"), shortcuts: [getShortcutKey("CtrlOrCmd+0")] }), _jsx(Shortcut, { label: t("helpDialog.zoomToFit"), shortcuts: ["Shift+1"] }), _jsx(Shortcut, { label: t("helpDialog.zoomToSelection"), shortcuts: ["Shift+2"] }), _jsx(Shortcut, { label: t("helpDialog.movePageUpDown"), shortcuts: ["PgUp/PgDn"] }), _jsx(Shortcut, { label: t("helpDialog.movePageLeftRight"), shortcuts: ["Shift+PgUp/PgDn"] }), _jsx(Shortcut, { label: t("buttons.zenMode"), shortcuts: [getShortcutKey("Alt+Z")] }), _jsx(Shortcut, { label: t("buttons.objectsSnapMode"), shortcuts: [getShortcutKey("Alt+S")] }), _jsx(Shortcut, { label: t("labels.showGrid"), shortcuts: [getShortcutKey("CtrlOrCmd+'")] }), _jsx(Shortcut, { label: t("labels.viewMode"), shortcuts: [getShortcutKey("Alt+R")] }), _jsx(Shortcut, { label: t("labels.toggleTheme"), shortcuts: [getShortcutKey("Alt+Shift+D")] }), _jsx(Shortcut, { label: t("stats.title"), shortcuts: [getShortcutKey("Alt+/")] }), _jsx(Shortcut, { label: t("commandPalette.title"), shortcuts: isFirefox
61
+ ? [getShortcutFromShortcutName("commandPalette")]
62
+ : [
63
+ getShortcutFromShortcutName("commandPalette"),
64
+ getShortcutFromShortcutName("commandPalette", 1),
65
+ ] })] }), _jsxs(ShortcutIsland, { className: "HelpDialog__island--editor", caption: t("helpDialog.editor"), children: [_jsx(Shortcut, { label: t("labels.moveCanvas"), shortcuts: [
64
66
  getShortcutKey(`Space+${t("helpDialog.drag")}`),
65
67
  getShortcutKey(`Wheel+${t("helpDialog.drag")}`),
66
68
  ], isOr: true }), _jsx(Shortcut, { label: t("buttons.clearReset"), shortcuts: [getShortcutKey("CtrlOrCmd+Delete")] }), _jsx(Shortcut, { label: t("labels.delete"), shortcuts: [getShortcutKey("Delete")] }), _jsx(Shortcut, { label: t("labels.cut"), shortcuts: [getShortcutKey("CtrlOrCmd+X")] }), _jsx(Shortcut, { label: t("labels.copy"), shortcuts: [getShortcutKey("CtrlOrCmd+C")] }), _jsx(Shortcut, { label: t("labels.paste"), shortcuts: [getShortcutKey("CtrlOrCmd+V")] }), _jsx(Shortcut, { label: t("labels.pasteAsPlaintext"), shortcuts: [getShortcutKey("CtrlOrCmd+Shift+V")] }), _jsx(Shortcut, { label: t("labels.selectAll"), shortcuts: [getShortcutKey("CtrlOrCmd+A")] }), _jsx(Shortcut, { label: t("labels.multiSelect"), shortcuts: [getShortcutKey(`Shift+${t("helpDialog.click")}`)] }), _jsx(Shortcut, { label: t("helpDialog.deepSelect"), shortcuts: [getShortcutKey(`CtrlOrCmd+${t("helpDialog.click")}`)] }), _jsx(Shortcut, { label: t("helpDialog.deepBoxSelect"), shortcuts: [getShortcutKey(`CtrlOrCmd+${t("helpDialog.drag")}`)] }), (probablySupportsClipboardBlob || isFirefox) && (_jsx(Shortcut, { label: t("labels.copyAsPng"), shortcuts: [getShortcutKey("Shift+Alt+C")] })), _jsx(Shortcut, { label: t("labels.copyStyles"), shortcuts: [getShortcutKey("CtrlOrCmd+Alt+C")] }), _jsx(Shortcut, { label: t("labels.pasteStyles"), shortcuts: [getShortcutKey("CtrlOrCmd+Alt+V")] }), _jsx(Shortcut, { label: t("labels.sendToBack"), shortcuts: [
@@ -2,7 +2,8 @@
2
2
  import "./RadioGroup.scss";
3
3
  export type RadioGroupChoice<T> = {
4
4
  value: T;
5
- label: string;
5
+ label: React.ReactNode;
6
+ ariaLabel?: string;
6
7
  };
7
8
  export type RadioGroupProps<T> = {
8
9
  choices: RadioGroupChoice<T>[];
@@ -4,5 +4,5 @@ import "./RadioGroup.scss";
4
4
  export const RadioGroup = function ({ onChange, value, choices, name, }) {
5
5
  return (_jsx("div", { className: "RadioGroup", children: choices.map((choice) => (_jsxs("div", { className: clsx("RadioGroup__choice", {
6
6
  active: choice.value === value,
7
- }), children: [_jsx("input", { name: name, type: "radio", checked: choice.value === value, onChange: () => onChange(choice.value) }), choice.label] }, choice.label))) }));
7
+ }), title: choice.ariaLabel, children: [_jsx("input", { name: name, type: "radio", checked: choice.value === value, onChange: () => onChange(choice.value), "aria-label": choice.ariaLabel }), choice.label] }, String(choice.value)))) }));
8
8
  };
@@ -12,7 +12,7 @@ import { TTDDialogInput } from "./TTDDialogInput";
12
12
  import { TTDDialogOutput } from "./TTDDialogOutput";
13
13
  import { EditorLocalStorage } from "../../data/EditorLocalStorage";
14
14
  import { EDITOR_LS_KEYS } from "../../constants";
15
- import { debounce } from "../../utils";
15
+ import { debounce, isDevEnv } from "../../utils";
16
16
  import { TTDDialogSubmitShortcut } from "./TTDDialogSubmitShortcut";
17
17
  const MERMAID_EXAMPLE = "flowchart TD\n A[Christmas] -->|Get money| B(Go shopping)\n B --> C{Let me think}\n C -->|One| D[Laptop]\n C -->|Two| E[iPhone]\n C -->|Three| F[Car]";
18
18
  const debouncedSaveMermaidDefinition = debounce(saveMermaidDataToStorage, 300);
@@ -31,7 +31,11 @@ const MermaidToExcalidraw = ({ mermaidToExcalidrawLib, }) => {
31
31
  mermaidToExcalidrawLib,
32
32
  setError,
33
33
  mermaidDefinition: deferredText,
34
- }).catch(() => { });
34
+ }).catch((err) => {
35
+ if (isDevEnv()) {
36
+ console.error("Failed to parse mermaid definition", err);
37
+ }
38
+ });
35
39
  debouncedSaveMermaidDefinition(deferredText);
36
40
  }, [deferredText, mermaidToExcalidrawLib]);
37
41
  useEffect(() => () => {
@@ -0,0 +1,18 @@
1
+ /// <reference types="react" />
2
+ type Props<T> = {
3
+ value: T;
4
+ shortcut?: string;
5
+ choices: {
6
+ value: T;
7
+ label: React.ReactNode;
8
+ ariaLabel?: string;
9
+ }[];
10
+ onChange: (value: T) => void;
11
+ children: React.ReactNode;
12
+ name: string;
13
+ };
14
+ declare const DropdownMenuItemContentRadio: {
15
+ <T>({ value, shortcut, onChange, choices, children, name, }: Props<T>): JSX.Element;
16
+ displayName: string;
17
+ };
18
+ export default DropdownMenuItemContentRadio;
@@ -0,0 +1,9 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useDevice } from "../App";
3
+ import { RadioGroup } from "../RadioGroup";
4
+ const DropdownMenuItemContentRadio = ({ value, shortcut, onChange, choices, children, name, }) => {
5
+ const device = useDevice();
6
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "dropdown-menu-item-base dropdown-menu-item-bare", children: [_jsx("label", { className: "dropdown-menu-item__text", htmlFor: name, children: children }), _jsx(RadioGroup, { name: name, value: value, onChange: onChange, choices: choices })] }), shortcut && !device.editor.isMobile && (_jsx("div", { className: "dropdown-menu-item__shortcut dropdown-menu-item__shortcut--orphaned", children: shortcut }))] }));
7
+ };
8
+ DropdownMenuItemContentRadio.displayName = "DropdownMenuItemContentRadio";
9
+ export default DropdownMenuItemContentRadio;
@@ -10,9 +10,9 @@ import clsx from "clsx";
10
10
  import { KEYS } from "../../keys";
11
11
  import { EVENT, HYPERLINK_TOOLTIP_DELAY } from "../../constants";
12
12
  import { getElementAbsoluteCoords } from "../../element/bounds";
13
- import { getTooltipDiv, updateTooltipPosition } from "../Tooltip";
13
+ import { getTooltipDiv, updateTooltipPosition } from "../../components/Tooltip";
14
14
  import { getSelectedElements } from "../../scene";
15
- import { isPointHittingElementBoundingBox } from "../../element/collision";
15
+ import { hitElementBoundingBox } from "../../element/collision";
16
16
  import { isLocalLink, normalizeLink } from "../../data/url";
17
17
  import "./Hyperlink.scss";
18
18
  import { trackEvent } from "../../analytics";
@@ -252,7 +252,7 @@ const shouldHideLinkPopup = (element, elementsMap, appState, [clientX, clientY])
252
252
  const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords({ clientX, clientY }, appState);
253
253
  const threshold = 15 / appState.zoom.value;
254
254
  // hitbox to prevent hiding when hovered in element bounding box
255
- if (isPointHittingElementBoundingBox(element, elementsMap, [sceneX, sceneY], threshold, null)) {
255
+ if (hitElementBoundingBox(sceneX, sceneY, element, elementsMap)) {
256
256
  return false;
257
257
  }
258
258
  const [x1, y1, x2] = getElementAbsoluteCoords(element, elementsMap);
@@ -1,6 +1,6 @@
1
1
  import { MIME_TYPES } from "../../constants";
2
2
  import { getElementAbsoluteCoords } from "../../element/bounds";
3
- import { isPointHittingElementBoundingBox } from "../../element/collision";
3
+ import { hitElementBoundingBox } from "../../element/collision";
4
4
  import { rotate } from "../../math";
5
5
  import { DEFAULT_LINK_SIZE } from "../../renderer/renderElement";
6
6
  export const EXTERNAL_LINK_IMG = document.createElement("img");
@@ -39,10 +39,9 @@ export const isPointHittingLink = (element, elementsMap, appState, [x, y], isMob
39
39
  if (!element.link || appState.selectedElementIds[element.id]) {
40
40
  return false;
41
41
  }
42
- const threshold = 4 / appState.zoom.value;
43
42
  if (!isMobile &&
44
43
  appState.viewModeEnabled &&
45
- isPointHittingElementBoundingBox(element, elementsMap, [x, y], threshold, null)) {
44
+ hitElementBoundingBox(x, y, element, elementsMap)) {
46
45
  return true;
47
46
  }
48
47
  return isPointHittingLinkIcon(element, elementsMap, appState, [x, y]);
@@ -194,4 +194,7 @@ export declare const svgIcon: JSX.Element;
194
194
  export declare const pngIcon: JSX.Element;
195
195
  export declare const magnetIcon: JSX.Element;
196
196
  export declare const coffeeIcon: JSX.Element;
197
+ export declare const DeviceDesktopIcon: JSX.Element;
198
+ export declare const arrowBarToLeftIcon: JSX.Element;
199
+ export declare const youtubeIcon: JSX.Element;
197
200
  export {};
@@ -83,7 +83,7 @@ export const TrashIcon = createIcon(_jsx("path", { strokeWidth: "1.25", d: "M3.3
83
83
  export const EmbedIcon = createIcon(_jsxs("g", { strokeWidth: "1.25", children: [_jsx("polyline", { points: "12 16 18 10 12 4" }), _jsx("polyline", { points: "8 4 2 10 8 16" })] }), modifiedTablerIconProps);
84
84
  export const DuplicateIcon = createIcon(_jsxs("g", { strokeWidth: "1.25", children: [_jsx("path", { d: "M14.375 6.458H8.958a2.5 2.5 0 0 0-2.5 2.5v5.417a2.5 2.5 0 0 0 2.5 2.5h5.417a2.5 2.5 0 0 0 2.5-2.5V8.958a2.5 2.5 0 0 0-2.5-2.5Z" }), _jsx("path", { clipRule: "evenodd", d: "M11.667 3.125c.517 0 .986.21 1.325.55.34.338.55.807.55 1.325v1.458H8.333c-.485 0-.927.185-1.26.487-.343.312-.57.75-.609 1.24l-.005 5.357H5a1.87 1.87 0 0 1-1.326-.55 1.87 1.87 0 0 1-.549-1.325V5c0-.518.21-.987.55-1.326.338-.34.807-.549 1.325-.549h6.667Z" })] }), modifiedTablerIconProps);
85
85
  export const MoonIcon = createIcon(_jsx("path", { clipRule: "evenodd", d: "M10 2.5h.328a6.25 6.25 0 0 0 6.6 10.372A7.5 7.5 0 1 1 10 2.493V2.5Z", stroke: "currentColor" }), modifiedTablerIconProps);
86
- export const SunIcon = createIcon(_jsx("g", { stroke: "currentColor", strokeWidth: "1.25", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "M10 12.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5ZM10 4.167V2.5M14.167 5.833l1.166-1.166M15.833 10H17.5M14.167 14.167l1.166 1.166M10 15.833V17.5M5.833 14.167l-1.166 1.166M5 10H3.333M5.833 5.833 4.667 4.667" }) }), modifiedTablerIconProps);
86
+ export const SunIcon = createIcon(_jsx("g", { stroke: "currentColor", strokeLinejoin: "round", children: _jsx("path", { d: "M10 12.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5ZM10 4.167V2.5M14.167 5.833l1.166-1.166M15.833 10H17.5M14.167 14.167l1.166 1.166M10 15.833V17.5M5.833 14.167l-1.166 1.166M5 10H3.333M5.833 5.833 4.667 4.667" }) }), { ...modifiedTablerIconProps, strokeWidth: 1.5 });
87
87
  export const HamburgerMenuIcon = createIcon(_jsxs("g", { strokeWidth: "1.5", children: [_jsx("path", { stroke: "none", d: "M0 0h24v24H0z", fill: "none" }), _jsx("line", { x1: "4", y1: "6", x2: "20", y2: "6" }), _jsx("line", { x1: "4", y1: "12", x2: "20", y2: "12" }), _jsx("line", { x1: "4", y1: "18", x2: "20", y2: "18" })] }), tablerIconProps);
88
88
  export const ExportIcon = createIcon(_jsx("path", { strokeWidth: "1.25", d: "M3.333 14.167v1.666c0 .92.747 1.667 1.667 1.667h10c.92 0 1.667-.746 1.667-1.667v-1.666M5.833 9.167 10 13.333l4.167-4.166M10 3.333v10" }), modifiedTablerIconProps);
89
89
  export const HelpIcon = createIcon(_jsxs("g", { strokeWidth: "1.5", children: [_jsx("path", { stroke: "none", d: "M0 0h24v24H0z", fill: "none" }), _jsx("circle", { cx: "12", cy: "12", r: "9" }), _jsx("line", { x1: "12", y1: "17", x2: "12", y2: "17.01" }), _jsx("path", { d: "M12 13.5a1.5 1.5 0 0 1 1 -1.5a2.6 2.6 0 1 0 -3 -4" })] }), tablerIconProps);
@@ -240,3 +240,7 @@ export const svgIcon = createIcon(_jsxs("g", { strokeWidth: 1.25, children: [_js
240
240
  export const pngIcon = createIcon(_jsxs("g", { strokeWidth: 1.25, children: [_jsx("path", { stroke: "none", d: "M0 0h24v24H0z", fill: "none" }), _jsx("path", { d: "M14 3v4a1 1 0 0 0 1 1h4" }), _jsx("path", { d: "M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4" }), _jsx("path", { d: "M20 15h-1a2 2 0 0 0 -2 2v2a2 2 0 0 0 2 2h1v-3" }), _jsx("path", { d: "M5 18h1.5a1.5 1.5 0 0 0 0 -3h-1.5v6" }), _jsx("path", { d: "M11 21v-6l3 6v-6" })] }), tablerIconProps);
241
241
  export const magnetIcon = createIcon(_jsxs("g", { strokeWidth: 1.25, children: [_jsx("path", { stroke: "none", d: "M0 0h24v24H0z", fill: "none" }), _jsx("path", { d: "M4 13v-8a2 2 0 0 1 2 -2h1a2 2 0 0 1 2 2v8a2 2 0 0 0 6 0v-8a2 2 0 0 1 2 -2h1a2 2 0 0 1 2 2v8a8 8 0 0 1 -16 0" }), _jsx("path", { d: "M4 8l5 0" }), _jsx("path", { d: "M15 8l4 0" })] }), tablerIconProps);
242
242
  export const coffeeIcon = createIcon(_jsxs("g", { strokeWidth: 1.25, children: [_jsx("path", { stroke: "none", d: "M0 0h24v24H0z", fill: "none" }), _jsx("path", { d: "M3 14c.83 .642 2.077 1.017 3.5 1c1.423 .017 2.67 -.358 3.5 -1c.83 -.642 2.077 -1.017 3.5 -1c1.423 -.017 2.67 .358 3.5 1" }), _jsx("path", { d: "M8 3a2.4 2.4 0 0 0 -1 2a2.4 2.4 0 0 0 1 2" }), _jsx("path", { d: "M12 3a2.4 2.4 0 0 0 -1 2a2.4 2.4 0 0 0 1 2" }), _jsx("path", { d: "M3 10h14v5a6 6 0 0 1 -6 6h-2a6 6 0 0 1 -6 -6v-5z" }), _jsx("path", { d: "M16.746 16.726a3 3 0 1 0 .252 -5.555" })] }), tablerIconProps);
243
+ export const DeviceDesktopIcon = createIcon(_jsxs("g", { stroke: "currentColor", children: [_jsx("path", { stroke: "none", d: "M0 0h24v24H0z", fill: "none" }), _jsx("path", { d: "M3 5a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1h-16a1 1 0 0 1-1-1v-10zM7 20h10M9 16v4M15 16v4" })] }), { ...tablerIconProps, strokeWidth: 1.5 });
244
+ // arrow-bar-to-left
245
+ export const arrowBarToLeftIcon = createIcon(_jsxs("g", { children: [_jsx("path", { stroke: "none", d: "M0 0h24v24H0z", fill: "none" }), _jsx("path", { d: "M10 12l10 0" }), _jsx("path", { d: "M10 12l4 4" }), _jsx("path", { d: "M10 12l4 -4" }), _jsx("path", { d: "M4 4l0 16" })] }), tablerIconProps);
246
+ export const youtubeIcon = createIcon(_jsxs("g", { children: [_jsx("path", { stroke: "none", d: "M0 0h24v24H0z", fill: "none" }), _jsx("path", { d: "M2 8a4 4 0 0 1 4 -4h12a4 4 0 0 1 4 4v8a4 4 0 0 1 -4 4h-12a4 4 0 0 1 -4 -4v-8z" }), _jsx("path", { d: "M10 9l5 3l-5 3z" })] }), tablerIconProps);
@@ -1,4 +1,5 @@
1
1
  /// <reference types="react" />
2
+ import type { Theme } from "../../element/types";
2
3
  import "./DefaultItems.scss";
3
4
  export declare const LoadScene: {
4
5
  (): JSX.Element | null;
@@ -13,7 +14,9 @@ export declare const SaveAsImage: {
13
14
  displayName: string;
14
15
  };
15
16
  export declare const CommandPalette: {
16
- (): JSX.Element;
17
+ (opts?: {
18
+ className?: string;
19
+ }): JSX.Element;
17
20
  displayName: string;
18
21
  };
19
22
  export declare const Help: {
@@ -25,7 +28,14 @@ export declare const ClearCanvas: {
25
28
  displayName: string;
26
29
  };
27
30
  export declare const ToggleTheme: {
28
- (): JSX.Element | null;
31
+ (props: {
32
+ allowSystemTheme: true;
33
+ theme: Theme | "system";
34
+ onSelect: (theme: Theme | "system") => void;
35
+ } | {
36
+ allowSystemTheme?: false | undefined;
37
+ onSelect?: ((theme: Theme) => void) | undefined;
38
+ }): JSX.Element | null;
29
39
  displayName: string;
30
40
  };
31
41
  export declare const ChangeCanvasBackground: {
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { getShortcutFromShortcutName } from "../../actions/shortcuts";
3
3
  import { useI18n } from "../../i18n";
4
4
  import { useExcalidrawSetAppState, useExcalidrawActionManager, useExcalidrawElements, useAppProps, } from "../App";
5
- import { boltIcon, ExportIcon, ExportImageIcon, HelpIcon, LoadIcon, MoonIcon, save, SunIcon, TrashIcon, usersIcon, } from "../icons";
5
+ import { boltIcon, DeviceDesktopIcon, ExportIcon, ExportImageIcon, HelpIcon, LoadIcon, MoonIcon, save, SunIcon, TrashIcon, usersIcon, } from "../icons";
6
6
  import { GithubIcon, DiscordIcon, XBrandIcon } from "../icons";
7
7
  import DropdownMenuItem from "../dropdownMenu/DropdownMenuItem";
8
8
  import DropdownMenuItemLink from "../dropdownMenu/DropdownMenuItemLink";
@@ -14,6 +14,9 @@ import { jotaiScope } from "../../jotai";
14
14
  import { useUIAppState } from "../../context/ui-appState";
15
15
  import { openConfirmModal } from "../OverwriteConfirm/OverwriteConfirmState";
16
16
  import Trans from "../Trans";
17
+ import DropdownMenuItemContentRadio from "../dropdownMenu/DropdownMenuItemContentRadio";
18
+ import { THEME } from "../../constants";
19
+ import { trackEvent } from "../../analytics";
17
20
  import "./DefaultItems.scss";
18
21
  export const LoadScene = () => {
19
22
  const { t } = useI18n();
@@ -51,10 +54,13 @@ export const SaveAsImage = () => {
51
54
  return (_jsx(DropdownMenuItem, { icon: ExportImageIcon, "data-testid": "image-export-button", onSelect: () => setAppState({ openDialog: { name: "imageExport" } }), shortcut: getShortcutFromShortcutName("imageExport"), "aria-label": t("buttons.exportImage"), children: t("buttons.exportImage") }));
52
55
  };
53
56
  SaveAsImage.displayName = "SaveAsImage";
54
- export const CommandPalette = () => {
57
+ export const CommandPalette = (opts) => {
55
58
  const setAppState = useExcalidrawSetAppState();
56
59
  const { t } = useI18n();
57
- return (_jsx(DropdownMenuItem, { icon: boltIcon, "data-testid": "command-palette-button", onSelect: () => setAppState({ openDialog: { name: "commandPalette" } }), shortcut: getShortcutFromShortcutName("commandPalette"), "aria-label": t("commandPalette.title"), children: t("commandPalette.title") }));
60
+ return (_jsx(DropdownMenuItem, { icon: boltIcon, "data-testid": "command-palette-button", onSelect: () => {
61
+ trackEvent("command_palette", "open", "menu");
62
+ setAppState({ openDialog: { name: "commandPalette" } });
63
+ }, shortcut: getShortcutFromShortcutName("commandPalette"), "aria-label": t("commandPalette.title"), className: opts?.className, children: t("commandPalette.title") }));
58
64
  };
59
65
  CommandPalette.displayName = "CommandPalette";
60
66
  export const Help = () => {
@@ -73,20 +79,45 @@ export const ClearCanvas = () => {
73
79
  return (_jsx(DropdownMenuItem, { icon: TrashIcon, onSelect: () => setActiveConfirmDialog("clearCanvas"), "data-testid": "clear-canvas-button", "aria-label": t("buttons.clearReset"), children: t("buttons.clearReset") }));
74
80
  };
75
81
  ClearCanvas.displayName = "ClearCanvas";
76
- export const ToggleTheme = () => {
82
+ export const ToggleTheme = (props) => {
77
83
  const { t } = useI18n();
78
84
  const appState = useUIAppState();
79
85
  const actionManager = useExcalidrawActionManager();
86
+ const shortcut = getShortcutFromShortcutName("toggleTheme");
80
87
  if (!actionManager.isActionEnabled(actionToggleTheme)) {
81
88
  return null;
82
89
  }
90
+ if (props?.allowSystemTheme) {
91
+ return (_jsx(DropdownMenuItemContentRadio, { name: "theme", value: props.theme, onChange: (value) => props.onSelect(value), choices: [
92
+ {
93
+ value: THEME.LIGHT,
94
+ label: SunIcon,
95
+ ariaLabel: `${t("buttons.lightMode")} - ${shortcut}`,
96
+ },
97
+ {
98
+ value: THEME.DARK,
99
+ label: MoonIcon,
100
+ ariaLabel: `${t("buttons.darkMode")} - ${shortcut}`,
101
+ },
102
+ {
103
+ value: "system",
104
+ label: DeviceDesktopIcon,
105
+ ariaLabel: t("buttons.systemMode"),
106
+ },
107
+ ], children: t("labels.theme") }));
108
+ }
83
109
  return (_jsx(DropdownMenuItem, { onSelect: (event) => {
84
110
  // do not close the menu when changing theme
85
111
  event.preventDefault();
86
- return actionManager.executeAction(actionToggleTheme);
87
- }, icon: appState.theme === "dark" ? SunIcon : MoonIcon, "data-testid": "toggle-dark-mode", shortcut: getShortcutFromShortcutName("toggleTheme"), "aria-label": appState.theme === "dark"
112
+ if (props?.onSelect) {
113
+ props.onSelect(appState.theme === THEME.DARK ? THEME.LIGHT : THEME.DARK);
114
+ }
115
+ else {
116
+ return actionManager.executeAction(actionToggleTheme);
117
+ }
118
+ }, icon: appState.theme === THEME.DARK ? SunIcon : MoonIcon, "data-testid": "toggle-dark-mode", shortcut: shortcut, "aria-label": appState.theme === THEME.DARK
88
119
  ? t("buttons.lightMode")
89
- : t("buttons.darkMode"), children: appState.theme === "dark"
120
+ : t("buttons.darkMode"), children: appState.theme === THEME.DARK
90
121
  ? t("buttons.lightMode")
91
122
  : t("buttons.darkMode") }));
92
123
  };
@@ -223,9 +223,6 @@ export declare const ROUNDNESS: {
223
223
  readonly PROPORTIONAL_RADIUS: 2;
224
224
  readonly ADAPTIVE_RADIUS: 3;
225
225
  };
226
- /** key containt id of precedeing elemnt id we use in reconciliation during
227
- * collaboration */
228
- export declare const PRECEDING_ELEMENT_KEY = "__precedingElement__";
229
226
  export declare const ROUGHNESS: {
230
227
  readonly architect: 0;
231
228
  readonly artist: 1;
@@ -264,9 +264,6 @@ export const ROUNDNESS = {
264
264
  // (see DEFAULT_ADAPTIVE_RADIUS constant)
265
265
  ADAPTIVE_RADIUS: 3,
266
266
  };
267
- /** key containt id of precedeing elemnt id we use in reconciliation during
268
- * collaboration */
269
- export const PRECEDING_ELEMENT_KEY = "__precedingElement__";
270
267
  export const ROUGHNESS = {
271
268
  architect: 0,
272
269
  artist: 1,
@@ -1,3 +1,4 @@
1
+ import { THEME } from "../constants";
1
2
  const SYSTEM_PROMPT = `You are a skilled front-end developer who builds interactive prototypes from wireframes, and is an expert at CSS Grid and Flex design.
2
3
  Your role is to transform low-fidelity wireframes into working front-end HTML code.
3
4
 
@@ -19,7 +20,7 @@ If the wireframes, diagrams, or text is unclear or unreadable, refer to provided
19
20
  Your goal is a production-ready prototype that brings the wireframes to life.
20
21
 
21
22
  Please output JUST THE HTML file containing your best attempt at implementing the provided wireframes.`;
22
- export async function diagramToHTML({ image, apiKey, text, theme = "light", }) {
23
+ export async function diagramToHTML({ image, apiKey, text, theme = THEME.LIGHT, }) {
23
24
  const body = {
24
25
  model: "gpt-4-vision-preview",
25
26
  // 4096 are max output tokens allowed for `gpt-4-vision-preview` currently
@@ -0,0 +1,6 @@
1
+ import { OrderedExcalidrawElement } from "../element/types";
2
+ import { AppState } from "../types";
3
+ import { MakeBrand } from "../utility-types";
4
+ export type ReconciledExcalidrawElement = OrderedExcalidrawElement & MakeBrand<"ReconciledElement">;
5
+ export type RemoteExcalidrawElement = OrderedExcalidrawElement & MakeBrand<"RemoteExcalidrawElement">;
6
+ export declare const reconcileElements: (localElements: readonly OrderedExcalidrawElement[], remoteElements: readonly RemoteExcalidrawElement[], localAppState: AppState) => ReconciledExcalidrawElement[];
@@ -0,0 +1,49 @@
1
+ import { orderByFractionalIndex, syncInvalidIndices } from "../fractionalIndex";
2
+ import { arrayToMap } from "../utils";
3
+ const shouldDiscardRemoteElement = (localAppState, local, remote) => {
4
+ if (local &&
5
+ // local element is being edited
6
+ (local.id === localAppState.editingElement?.id ||
7
+ local.id === localAppState.resizingElement?.id ||
8
+ local.id === localAppState.draggingElement?.id ||
9
+ // local element is newer
10
+ local.version > remote.version ||
11
+ // resolve conflicting edits deterministically by taking the one with
12
+ // the lowest versionNonce
13
+ (local.version === remote.version &&
14
+ local.versionNonce < remote.versionNonce))) {
15
+ return true;
16
+ }
17
+ return false;
18
+ };
19
+ export const reconcileElements = (localElements, remoteElements, localAppState) => {
20
+ const localElementsMap = arrayToMap(localElements);
21
+ const reconciledElements = [];
22
+ const added = new Set();
23
+ // process remote elements
24
+ for (const remoteElement of remoteElements) {
25
+ if (!added.has(remoteElement.id)) {
26
+ const localElement = localElementsMap.get(remoteElement.id);
27
+ const discardRemoteElement = shouldDiscardRemoteElement(localAppState, localElement, remoteElement);
28
+ if (localElement && discardRemoteElement) {
29
+ reconciledElements.push(localElement);
30
+ added.add(localElement.id);
31
+ }
32
+ else {
33
+ reconciledElements.push(remoteElement);
34
+ added.add(remoteElement.id);
35
+ }
36
+ }
37
+ }
38
+ // process remaining local elements
39
+ for (const localElement of localElements) {
40
+ if (!added.has(localElement.id)) {
41
+ reconciledElements.push(localElement);
42
+ added.add(localElement.id);
43
+ }
44
+ }
45
+ const orderedElements = orderByFractionalIndex(reconciledElements);
46
+ // de-duplicate indices
47
+ syncInvalidIndices(orderedElements);
48
+ return orderedElements;
49
+ };
@@ -1,17 +1,17 @@
1
- import { ExcalidrawElement } from "../element/types";
1
+ import { ExcalidrawElement, OrderedExcalidrawElement } from "../element/types";
2
2
  import { AppState, BinaryFiles, LibraryItem } from "../types";
3
3
  import { ImportedDataState } from "./types";
4
4
  type RestoredAppState = Omit<AppState, "offsetTop" | "offsetLeft" | "width" | "height">;
5
5
  export declare const AllowedExcalidrawActiveTools: Record<AppState["activeTool"]["type"], boolean>;
6
6
  export type RestoredDataState = {
7
- elements: ExcalidrawElement[];
7
+ elements: OrderedExcalidrawElement[];
8
8
  appState: RestoredAppState;
9
9
  files: BinaryFiles;
10
10
  };
11
11
  export declare const restoreElements: (elements: ImportedDataState["elements"], localElements: readonly ExcalidrawElement[] | null | undefined, opts?: {
12
12
  refreshDimensions?: boolean;
13
13
  repairBindings?: boolean;
14
- } | undefined) => ExcalidrawElement[];
14
+ } | undefined) => OrderedExcalidrawElement[];
15
15
  export declare const restoreAppState: (appState: ImportedDataState["appState"], localAppState: Partial<AppState> | null | undefined) => RestoredAppState;
16
16
  export declare const restore: (data: Pick<ImportedDataState, "appState" | "elements" | "files"> | null, localAppState: Partial<AppState> | null | undefined, localElements: readonly ExcalidrawElement[] | null | undefined, elementsConfig?: {
17
17
  refreshDimensions?: boolean;
@@ -1,7 +1,7 @@
1
1
  import { getNonDeletedElements, getNormalizedDimensions, isInvisiblySmallElement, refreshTextDimensions, } from "../element";
2
2
  import { isTextElement, isUsingAdaptiveRadius } from "../element/typeChecks";
3
3
  import { randomId } from "../random";
4
- import { DEFAULT_FONT_FAMILY, DEFAULT_TEXT_ALIGN, DEFAULT_VERTICAL_ALIGN, PRECEDING_ELEMENT_KEY, FONT_FAMILY, ROUNDNESS, DEFAULT_SIDEBAR, DEFAULT_ELEMENT_PROPS, } from "../constants";
4
+ import { DEFAULT_FONT_FAMILY, DEFAULT_TEXT_ALIGN, DEFAULT_VERTICAL_ALIGN, FONT_FAMILY, ROUNDNESS, DEFAULT_SIDEBAR, DEFAULT_ELEMENT_PROPS, } from "../constants";
5
5
  import { getDefaultAppState } from "../appState";
6
6
  import { LinearElementEditor } from "../element/linearElementEditor";
7
7
  import { bumpVersion } from "../element/mutateElement";
@@ -9,6 +9,7 @@ import { getUpdatedTimestamp, updateActiveTool } from "../utils";
9
9
  import { arrayToMap } from "../utils";
10
10
  import { detectLineHeight, getContainerElement, getDefaultLineHeight, } from "../element/textElement";
11
11
  import { normalizeLink } from "./url";
12
+ import { syncInvalidIndices } from "../fractionalIndex";
12
13
  export const AllowedExcalidrawActiveTools = {
13
14
  selection: true,
14
15
  text: true,
@@ -46,6 +47,7 @@ const restoreElementWithProperties = (element, extra) => {
46
47
  // newly added elements
47
48
  version: element.version || 1,
48
49
  versionNonce: element.versionNonce ?? 0,
50
+ index: element.index ?? null,
49
51
  isDeleted: element.isDeleted ?? false,
50
52
  id: element.id || randomId(),
51
53
  fillStyle: element.fillStyle || DEFAULT_ELEMENT_PROPS.fillStyle,
@@ -85,9 +87,6 @@ const restoreElementWithProperties = (element, extra) => {
85
87
  base.customData =
86
88
  "customData" in extra ? extra.customData : element.customData;
87
89
  }
88
- if (PRECEDING_ELEMENT_KEY in element) {
89
- base[PRECEDING_ELEMENT_KEY] = element[PRECEDING_ELEMENT_KEY];
90
- }
91
90
  return {
92
91
  ...base,
93
92
  ...getNormalizedDimensions(base),
@@ -275,7 +274,7 @@ localElements, opts) => {
275
274
  // used to detect duplicate top-level element ids
276
275
  const existingIds = new Set();
277
276
  const localElementsMap = localElements ? arrayToMap(localElements) : null;
278
- const restoredElements = (elements || []).reduce((elements, element) => {
277
+ const restoredElements = syncInvalidIndices((elements || []).reduce((elements, element) => {
279
278
  // filtering out selection, which is legacy, no longer kept in elements,
280
279
  // and causing issues if retained
281
280
  if (element.type !== "selection" && !isInvisiblySmallElement(element)) {
@@ -293,7 +292,7 @@ localElements, opts) => {
293
292
  }
294
293
  }
295
294
  return elements;
296
- }, []);
295
+ }, []));
297
296
  if (!opts?.repairBindings) {
298
297
  return restoredElements;
299
298
  }
@@ -78,4 +78,4 @@ export type ExcalidrawElementSkeleton = Extract<Exclude<ExcalidrawElement, Excal
78
78
  } & Partial<ExcalidrawMagicFrameElement>);
79
79
  export declare const convertToExcalidrawElements: (elementsSkeleton: ExcalidrawElementSkeleton[] | null, opts?: {
80
80
  regenerateIds: boolean;
81
- }) => ExcalidrawElement[];
81
+ }) => import("../element/types").OrderedExcalidrawElement[];
@@ -3,9 +3,10 @@ import { getCommonBounds, newElement, newLinearElement, redrawTextBoundingBox, }
3
3
  import { bindLinearElement } from "../element/binding";
4
4
  import { newFrameElement, newImageElement, newMagicFrameElement, newTextElement, } from "../element/newElement";
5
5
  import { getDefaultLineHeight, measureText, normalizeText, } from "../element/textElement";
6
- import { assertNever, cloneJSON, getFontString, toBrandedType } from "../utils";
6
+ import { arrayToMap, assertNever, cloneJSON, getFontString, toBrandedType, } from "../utils";
7
7
  import { getSizeFromPoints } from "../points";
8
8
  import { randomId } from "../random";
9
+ import { syncInvalidIndices } from "../fractionalIndex";
9
10
  const DEFAULT_LINEAR_ELEMENT_PROPS = {
10
11
  width: 100,
11
12
  height: 0,
@@ -162,6 +163,14 @@ const bindLinearElementToElement = (linearElement, start, end, elementStore, ele
162
163
  bindLinearElement(linearElement, endBoundElement, "end", elementsMap);
163
164
  }
164
165
  }
166
+ // Safe check to early return for single point
167
+ if (linearElement.points.length < 2) {
168
+ return {
169
+ linearElement,
170
+ startBoundElement,
171
+ endBoundElement,
172
+ };
173
+ }
165
174
  // Update start/end points by 0.5 so bindings don't overlap with start/end bound element coordinates.
166
175
  const endPointIndex = linearElement.points.length - 1;
167
176
  const delta = 0.5;
@@ -206,10 +215,10 @@ class ElementStore {
206
215
  this.excalidrawElements.set(ele.id, ele);
207
216
  };
208
217
  getElements = () => {
209
- return Array.from(this.excalidrawElements.values());
218
+ return syncInvalidIndices(Array.from(this.excalidrawElements.values()));
210
219
  };
211
220
  getElementsMap = () => {
212
- return toBrandedType(this.excalidrawElements);
221
+ return toBrandedType(arrayToMap(this.getElements()));
213
222
  };
214
223
  getElement = (id) => {
215
224
  return this.excalidrawElements.get(id);