Quellcode durchsuchen

feat: exporting redesign (#3613)

Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
vanilla_orig
David Luzar vor 3 Jahren
Ursprung
Commit
790c9fd02e
Es ist kein Account mit der E-Mail-Adresse des Committers verbunden

+ 20
- 26
src/actions/actionExport.tsx Datei anzeigen

@@ -11,7 +11,8 @@ import { t } from "../i18n";
11 11
 import { useIsMobile } from "../components/App";
12 12
 import { KEYS } from "../keys";
13 13
 import { register } from "./register";
14
-import { supported } from "browser-fs-access";
14
+import { supported as fsSupported } from "browser-fs-access";
15
+import { CheckboxItem } from "../components/CheckboxItem";
15 16
 
16 17
 export const actionChangeProjectName = register({
17 18
   name: "changeProjectName",
@@ -40,14 +41,12 @@ export const actionChangeExportBackground = register({
40 41
     };
41 42
   },
42 43
   PanelComponent: ({ appState, updateData }) => (
43
-    <label>
44
-      <input
45
-        type="checkbox"
46
-        checked={appState.exportBackground}
47
-        onChange={(event) => updateData(event.target.checked)}
48
-      />{" "}
44
+    <CheckboxItem
45
+      checked={appState.exportBackground}
46
+      onChange={(checked) => updateData(checked)}
47
+    >
49 48
       {t("labels.withBackground")}
50
-    </label>
49
+    </CheckboxItem>
51 50
   ),
52 51
 });
53 52
 
@@ -60,17 +59,15 @@ export const actionChangeExportEmbedScene = register({
60 59
     };
61 60
   },
62 61
   PanelComponent: ({ appState, updateData }) => (
63
-    <label style={{ display: "flex" }}>
64
-      <input
65
-        type="checkbox"
66
-        checked={appState.exportEmbedScene}
67
-        onChange={(event) => updateData(event.target.checked)}
68
-      />{" "}
62
+    <CheckboxItem
63
+      checked={appState.exportEmbedScene}
64
+      onChange={(checked) => updateData(checked)}
65
+    >
69 66
       {t("labels.exportEmbedScene")}
70 67
       <Tooltip label={t("labels.exportEmbedScene_details")} long={true}>
71
-        <div className="TooltipIcon">{questionCircle}</div>
68
+        <div className="Tooltip-icon">{questionCircle}</div>
72 69
       </Tooltip>
73
-    </label>
70
+    </CheckboxItem>
74 71
   ),
75 72
 });
76 73
 
@@ -83,14 +80,12 @@ export const actionChangeShouldAddWatermark = register({
83 80
     };
84 81
   },
85 82
   PanelComponent: ({ appState, updateData }) => (
86
-    <label>
87
-      <input
88
-        type="checkbox"
89
-        checked={appState.shouldAddWatermark}
90
-        onChange={(event) => updateData(event.target.checked)}
91
-      />{" "}
83
+    <CheckboxItem
84
+      checked={appState.shouldAddWatermark}
85
+      onChange={(checked) => updateData(checked)}
86
+    >
92 87
       {t("labels.addWatermark")}
93
-    </label>
88
+    </CheckboxItem>
94 89
   ),
95 90
 });
96 91
 
@@ -126,11 +121,10 @@ export const actionSaveScene = register({
126 121
     event.key === KEYS.S && event[KEYS.CTRL_OR_CMD] && !event.shiftKey,
127 122
   PanelComponent: ({ updateData }) => (
128 123
     <ToolButton
129
-      type="button"
124
+      type="icon"
130 125
       icon={save}
131 126
       title={t("buttons.save")}
132 127
       aria-label={t("buttons.save")}
133
-      showAriaLabel={useIsMobile()}
134 128
       onClick={() => updateData(null)}
135 129
       data-testid="save-button"
136 130
     />
@@ -162,7 +156,7 @@ export const actionSaveAsScene = register({
162 156
       title={t("buttons.saveAs")}
163 157
       aria-label={t("buttons.saveAs")}
164 158
       showAriaLabel={useIsMobile()}
165
-      hidden={!supported}
159
+      hidden={!fsSupported}
166 160
       onClick={() => updateData(null)}
167 161
       data-testid="save-as-button"
168 162
     />

+ 1
- 0
src/actions/types.ts Datei anzeigen

@@ -131,4 +131,5 @@ export interface ActionsManagerInterface {
131 131
   registerAction: (action: Action) => void;
132 132
   handleKeyDown: (event: React.KeyboardEvent | KeyboardEvent) => boolean;
133 133
   renderAction: (name: ActionName) => React.ReactElement | null;
134
+  executeAction: (action: Action) => void;
134 135
 }

+ 2
- 2
src/components/App.tsx Datei anzeigen

@@ -2,7 +2,7 @@ import React, { useContext } from "react";
2 2
 import { RoughCanvas } from "roughjs/bin/canvas";
3 3
 import rough from "roughjs/bin/rough";
4 4
 import clsx from "clsx";
5
-import { supported } from "browser-fs-access";
5
+import { supported as fsSupported } from "browser-fs-access";
6 6
 import { nanoid } from "nanoid";
7 7
 
8 8
 import {
@@ -3885,7 +3885,7 @@ class App extends React.Component<AppProps, AppState> {
3885 3885
       // default: assume an Excalidraw file regardless of extension/MimeType
3886 3886
     } else {
3887 3887
       this.setState({ isLoading: true });
3888
-      if (supported) {
3888
+      if (fsSupported) {
3889 3889
         try {
3890 3890
           // This will only work as of Chrome 86,
3891 3891
           // but can be safely ignored on older releases.

+ 6
- 1
src/components/BackgroundPickerAndDarkModeToggle.tsx Datei anzeigen

@@ -15,6 +15,11 @@ export const BackgroundPickerAndDarkModeToggle = ({
15 15
 }) => (
16 16
   <div style={{ display: "flex" }}>
17 17
     {actionManager.renderAction("changeViewBackgroundColor")}
18
-    {showThemeBtn && <>{actionManager.renderAction("toggleTheme")}</>}
18
+    {showThemeBtn && actionManager.renderAction("toggleTheme")}
19
+    {appState.fileHandle && (
20
+      <div style={{ marginInlineStart: "0.25rem" }}>
21
+        {actionManager.renderAction("saveScene")}
22
+      </div>
23
+    )}
19 24
   </div>
20 25
 );

+ 53
- 0
src/components/Card.scss Datei anzeigen

@@ -0,0 +1,53 @@
1
+@import "../css/variables.module";
2
+
3
+.excalidraw {
4
+  .Card {
5
+    display: flex;
6
+    flex-direction: column;
7
+    align-items: center;
8
+
9
+    max-width: 290px;
10
+
11
+    margin: 1em;
12
+
13
+    text-align: center;
14
+
15
+    .Card-icon {
16
+      font-size: 2.6em;
17
+      display: flex;
18
+      flex: 0 0 auto;
19
+      padding: 1.4rem;
20
+      border-radius: 50%;
21
+      background: var(--card-color);
22
+      color: $oc-white;
23
+
24
+      svg {
25
+        width: 2.8rem;
26
+        height: 2.8rem;
27
+      }
28
+    }
29
+
30
+    .Card-details {
31
+      font-size: 0.96em;
32
+      min-height: 90px;
33
+      padding: 0 1em;
34
+      margin-bottom: auto;
35
+    }
36
+
37
+    & .Card-button.ToolIcon_type_button {
38
+      height: 2.5rem;
39
+      margin-top: 1em;
40
+      margin-bottom: 0.3em;
41
+      background-color: var(--card-color);
42
+      &:hover {
43
+        background-color: var(--card-color-darker);
44
+      }
45
+      &:active {
46
+        background-color: var(--card-color-darkest);
47
+      }
48
+      .ToolIcon__label {
49
+        color: $oc-white;
50
+      }
51
+    }
52
+  }
53
+}

+ 20
- 0
src/components/Card.tsx Datei anzeigen

@@ -0,0 +1,20 @@
1
+import OpenColor from "open-color";
2
+
3
+import "./Card.scss";
4
+
5
+export const Card: React.FC<{
6
+  color: keyof OpenColor;
7
+}> = ({ children, color }) => {
8
+  return (
9
+    <div
10
+      className="Card"
11
+      style={{
12
+        ["--card-color" as any]: OpenColor[color][7],
13
+        ["--card-color-darker" as any]: OpenColor[color][8],
14
+        ["--card-color-darkest" as any]: OpenColor[color][9],
15
+      }}
16
+    >
17
+      {children}
18
+    </div>
19
+  );
20
+};

+ 85
- 0
src/components/CheckboxItem.scss Datei anzeigen

@@ -0,0 +1,85 @@
1
+@import "../css/variables.module";
2
+
3
+.excalidraw {
4
+  .Checkbox {
5
+    margin: 3px 0.3em;
6
+    display: flex;
7
+    align-items: center;
8
+
9
+    cursor: pointer;
10
+    user-select: none;
11
+
12
+    &:hover:not(.is-checked) .Checkbox-box {
13
+      box-shadow: 0 0 0 2px #{$oc-blue-4};
14
+
15
+      svg {
16
+        display: block;
17
+        opacity: 0.3;
18
+      }
19
+    }
20
+
21
+    &:active {
22
+      .Checkbox-box {
23
+        box-shadow: 0 0 2px 1px inset #{$oc-blue-7} !important;
24
+      }
25
+    }
26
+
27
+    &:hover {
28
+      .Checkbox-box {
29
+        background-color: fade-out($oc-blue-1, 0.8);
30
+      }
31
+    }
32
+
33
+    &.is-checked {
34
+      .Checkbox-box {
35
+        background-color: #{$oc-blue-1};
36
+        svg {
37
+          display: block;
38
+        }
39
+      }
40
+      &:hover .Checkbox-box {
41
+        background-color: #{$oc-blue-2};
42
+      }
43
+    }
44
+
45
+    .Checkbox-box {
46
+      width: 22px;
47
+      height: 22px;
48
+      padding: 0;
49
+      flex: 0 0 auto;
50
+
51
+      margin: 0 1em;
52
+
53
+      display: flex;
54
+      align-items: center;
55
+      justify-content: center;
56
+
57
+      box-shadow: 0 0 0 2px #{$oc-blue-7};
58
+      background-color: transparent;
59
+      border-radius: 4px;
60
+
61
+      color: #{$oc-blue-7};
62
+
63
+      &:focus {
64
+        box-shadow: 0 0 0 3px #{$oc-blue-7};
65
+      }
66
+
67
+      svg {
68
+        display: none;
69
+        width: 16px;
70
+        height: 16px;
71
+        stroke-width: 3px;
72
+      }
73
+    }
74
+
75
+    .Checkbox-label {
76
+      display: flex;
77
+      align-items: center;
78
+    }
79
+
80
+    .Tooltip-icon {
81
+      width: 1em;
82
+      height: 1em;
83
+    }
84
+  }
85
+}

+ 26
- 0
src/components/CheckboxItem.tsx Datei anzeigen

@@ -0,0 +1,26 @@
1
+import clsx from "clsx";
2
+import { checkIcon } from "./icons";
3
+
4
+import "./CheckboxItem.scss";
5
+
6
+export const CheckboxItem: React.FC<{
7
+  checked: boolean;
8
+  onChange: (checked: boolean) => void;
9
+}> = ({ children, checked, onChange }) => {
10
+  return (
11
+    <div
12
+      className={clsx("Checkbox", { "is-checked": checked })}
13
+      onClick={(event) => {
14
+        onChange(!checked);
15
+        ((event.currentTarget as HTMLDivElement).querySelector(
16
+          ".Checkbox-box",
17
+        ) as HTMLButtonElement).focus();
18
+      }}
19
+    >
20
+      <button className="Checkbox-box" role="checkbox" aria-checked={checked}>
21
+        {checkIcon}
22
+      </button>
23
+      <div className="Checkbox-label">{children}</div>
24
+    </div>
25
+  );
26
+};

+ 1
- 1
src/components/ColorPicker.scss Datei anzeigen

@@ -160,7 +160,7 @@
160 160
   }
161 161
 
162 162
   .color-picker-input {
163
-    width: 12ch; /* length of `transparent` + 1 */
163
+    width: 11ch; /* length of `transparent` */
164 164
     margin: 0;
165 165
     font-size: 1rem;
166 166
     background-color: var(--input-bg-color);

+ 11
- 22
src/components/DarkModeToggle.tsx Datei anzeigen

@@ -2,6 +2,7 @@ import "./ToolIcon.scss";
2 2
 
3 3
 import React from "react";
4 4
 import { t } from "../i18n";
5
+import { ToolButton } from "./ToolButton";
5 6
 
6 7
 export type Appearence = "light" | "dark";
7 8
 
@@ -12,31 +13,19 @@ export const DarkModeToggle = (props: {
12 13
   onChange: (value: Appearence) => void;
13 14
   title?: string;
14 15
 }) => {
15
-  const title = props.title
16
-    ? props.title
17
-    : props.value === "dark"
18
-    ? t("buttons.lightMode")
19
-    : t("buttons.darkMode");
16
+  const title =
17
+    props.title ||
18
+    (props.value === "dark" ? t("buttons.lightMode") : t("buttons.darkMode"));
20 19
 
21 20
   return (
22
-    <label
23
-      className="ToolIcon ToolIcon_type_floating ToolIcon_size_M"
24
-      data-testid="toggle-dark-mode"
21
+    <ToolButton
22
+      type="icon"
23
+      icon={props.value === "light" ? ICONS.MOON : ICONS.SUN}
25 24
       title={title}
26
-    >
27
-      <input
28
-        className="ToolIcon_type_checkbox ToolIcon_toggle_opaque"
29
-        type="checkbox"
30
-        onChange={(event) =>
31
-          props.onChange(event.target.checked ? "dark" : "light")
32
-        }
33
-        checked={props.value === "dark"}
34
-        aria-label={title}
35
-      />
36
-      <div className="ToolIcon__icon">
37
-        {props.value === "light" ? ICONS.MOON : ICONS.SUN}
38
-      </div>
39
-    </label>
25
+      aria-label={title}
26
+      onClick={() => props.onChange(props.value === "dark" ? "light" : "dark")}
27
+      data-testid="toggle-dark-mode"
28
+    />
40 29
   );
41 30
 };
42 31
 

+ 58
- 27
src/components/ExportDialog.scss Datei anzeigen

@@ -28,33 +28,6 @@
28 28
     justify-content: space-between;
29 29
   }
30 30
 
31
-  .ExportDialog__name {
32
-    grid-column: project-name;
33
-    margin: auto;
34
-    display: flex;
35
-    align-items: center;
36
-
37
-    .TextInput {
38
-      height: calc(1rem - 3px);
39
-      width: 200px;
40
-      overflow: hidden;
41
-      text-align: center;
42
-      margin-left: 8px;
43
-      text-overflow: ellipsis;
44
-
45
-      &--readonly {
46
-        background: none;
47
-        border: none;
48
-        &:hover {
49
-          background: none;
50
-        }
51
-        width: auto;
52
-        max-width: 200px;
53
-        padding-left: 2px;
54
-      }
55
-    }
56
-  }
57
-
58 31
   @include isMobile {
59 32
     .ExportDialog {
60 33
       display: flex;
@@ -84,4 +57,62 @@
84 57
       overflow-y: auto;
85 58
     }
86 59
   }
60
+
61
+  .ExportDialog--json {
62
+    .ExportDialog-cards {
63
+      display: grid;
64
+      grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
65
+      justify-items: center;
66
+      row-gap: 2em;
67
+
68
+      @media (max-width: 460px) {
69
+        grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
70
+        .Card-details {
71
+          min-height: 40px;
72
+        }
73
+      }
74
+
75
+      .ProjectName {
76
+        width: fit-content;
77
+        margin: 1em auto;
78
+        align-items: flex-start;
79
+        flex-direction: column;
80
+
81
+        .TextInput {
82
+          width: auto;
83
+        }
84
+      }
85
+
86
+      .ProjectName-label {
87
+        margin: 0.625em 0;
88
+        font-weight: bold;
89
+      }
90
+    }
91
+  }
92
+
93
+  button.ExportDialog-imageExportButton {
94
+    width: 5rem;
95
+    height: 5rem;
96
+    margin: 0 0.2em;
97
+
98
+    border-radius: 1rem;
99
+    background-color: var(--button-color);
100
+    box-shadow: 0 3px 5px -1px rgb(0 0 0 / 28%), 0 6px 10px 0 rgb(0 0 0 / 14%);
101
+
102
+    font-family: Cascadia;
103
+    font-size: 1.8em;
104
+    color: $oc-white;
105
+
106
+    &:hover {
107
+      background-color: var(--button-color-darker);
108
+    }
109
+    &:active {
110
+      background-color: var(--button-color-darkest);
111
+      box-shadow: 0 3px 5px -1px rgb(0 0 0 / 20%), 0 6px 10px 0 rgb(0 0 0 / 14%);
112
+    }
113
+
114
+    svg {
115
+      width: 0.9em;
116
+    }
117
+  }
87 118
 }

src/components/ExportDialog.tsx → src/components/ImageExportDialog.tsx Datei anzeigen

@@ -6,16 +6,20 @@ import { canvasToBlob } from "../data/blob";
6 6
 import { NonDeletedExcalidrawElement } from "../element/types";
7 7
 import { CanvasError } from "../errors";
8 8
 import { t } from "../i18n";
9
-import { useIsMobile } from "../components/App";
9
+import { useIsMobile } from "./App";
10 10
 import { getSelectedElements, isSomeElementSelected } from "../scene";
11 11
 import { exportToCanvas, getExportSize } from "../scene/export";
12 12
 import { AppState } from "../types";
13 13
 import { Dialog } from "./Dialog";
14
-import "./ExportDialog.scss";
15
-import { clipboard, exportFile, link } from "./icons";
14
+import { clipboard, exportImage } from "./icons";
16 15
 import Stack from "./Stack";
17 16
 import { ToolButton } from "./ToolButton";
18 17
 
18
+import "./ExportDialog.scss";
19
+import { supported as fsSupported } from "browser-fs-access";
20
+import OpenColor from "open-color";
21
+import { CheckboxItem } from "./CheckboxItem";
22
+
19 23
 const scales = [1, 2, 3];
20 24
 const defaultScale = scales.includes(devicePixelRatio) ? devicePixelRatio : 1;
21 25
 
@@ -52,7 +56,30 @@ export type ExportCB = (
52 56
   scale?: number,
53 57
 ) => void;
54 58
 
55
-const ExportModal = ({
59
+const ExportButton: React.FC<{
60
+  color: keyof OpenColor;
61
+  onClick: () => void;
62
+  title: string;
63
+  shade?: number;
64
+}> = ({ children, title, onClick, color, shade = 6 }) => {
65
+  return (
66
+    <button
67
+      className="ExportDialog-imageExportButton"
68
+      style={{
69
+        ["--button-color" as any]: OpenColor[color][shade],
70
+        ["--button-color-darker" as any]: OpenColor[color][shade + 1],
71
+        ["--button-color-darkest" as any]: OpenColor[color][shade + 2],
72
+      }}
73
+      title={title}
74
+      aria-label={title}
75
+      onClick={onClick}
76
+    >
77
+      {children}
78
+    </button>
79
+  );
80
+};
81
+
82
+const ImageExportModal = ({
56 83
   elements,
57 84
   appState,
58 85
   exportPadding = 10,
@@ -60,7 +87,6 @@ const ExportModal = ({
60 87
   onExportToPng,
61 88
   onExportToSvg,
62 89
   onExportToClipboard,
63
-  onExportToBackend,
64 90
 }: {
65 91
   appState: AppState;
66 92
   elements: readonly NonDeletedExcalidrawElement[];
@@ -69,7 +95,6 @@ const ExportModal = ({
69 95
   onExportToPng: ExportCB;
70 96
   onExportToSvg: ExportCB;
71 97
   onExportToClipboard: ExportCB;
72
-  onExportToBackend?: ExportCB;
73 98
   onCloseRequest: () => void;
74 99
 }) => {
75 100
   const someElementIsSelected = isSomeElementSelected(elements, appState);
@@ -133,98 +158,103 @@ const ExportModal = ({
133 158
       <div className="ExportDialog__preview" ref={previewRef} />
134 159
       {supportsContextFilters &&
135 160
         actionManager.renderAction("exportWithDarkMode")}
136
-      <Stack.Col gap={2} align="center">
137
-        <div className="ExportDialog__actions">
138
-          <Stack.Row gap={2}>
139
-            <ToolButton
140
-              type="button"
141
-              label="PNG"
142
-              title={t("buttons.exportToPng")}
143
-              aria-label={t("buttons.exportToPng")}
144
-              onClick={() => onExportToPng(exportedElements, scale)}
145
-            />
146
-            <ToolButton
147
-              type="button"
148
-              label="SVG"
149
-              title={t("buttons.exportToSvg")}
150
-              aria-label={t("buttons.exportToSvg")}
151
-              onClick={() => onExportToSvg(exportedElements, scale)}
152
-            />
153
-            {probablySupportsClipboardBlob && (
154
-              <ToolButton
155
-                type="button"
156
-                icon={clipboard}
157
-                title={t("buttons.copyPngToClipboard")}
158
-                aria-label={t("buttons.copyPngToClipboard")}
159
-                onClick={() => onExportToClipboard(exportedElements, scale)}
160
-              />
161
-            )}
162
-            {onExportToBackend && (
163
-              <ToolButton
164
-                type="button"
165
-                icon={link}
166
-                title={t("buttons.getShareableLink")}
167
-                aria-label={t("buttons.getShareableLink")}
168
-                onClick={() => onExportToBackend(exportedElements)}
169
-              />
170
-            )}
171
-          </Stack.Row>
172
-          <div className="ExportDialog__name">
173
-            {actionManager.renderAction("changeProjectName")}
174
-          </div>
175
-          <Stack.Row gap={2}>
176
-            {scales.map((s) => {
177
-              const [width, height] = getExportSize(
178
-                exportedElements,
179
-                exportPadding,
180
-                shouldAddWatermark,
181
-                s,
182
-              );
161
+      <div style={{ display: "grid", gridTemplateColumns: "1fr" }}>
162
+        <div
163
+          style={{
164
+            display: "grid",
165
+            gridTemplateColumns: "repeat(auto-fit, minmax(190px, 1fr))",
166
+            // dunno why this is needed, but when the items wrap it creates
167
+            // an overflow
168
+            overflow: "hidden",
169
+          }}
170
+        >
171
+          {actionManager.renderAction("changeExportBackground")}
172
+          {someElementIsSelected && (
173
+            <CheckboxItem
174
+              checked={exportSelected}
175
+              onChange={(checked) => setExportSelected(checked)}
176
+            >
177
+              {t("labels.onlySelected")}
178
+            </CheckboxItem>
179
+          )}
180
+          {actionManager.renderAction("changeExportEmbedScene")}
181
+        </div>
182
+      </div>
183
+      <div style={{ display: "flex", alignItems: "center", marginTop: ".6em" }}>
184
+        <Stack.Row gap={2} justifyContent={"center"}>
185
+          {scales.map((_scale) => {
186
+            const [width, height] = getExportSize(
187
+              exportedElements,
188
+              exportPadding,
189
+              shouldAddWatermark,
190
+              _scale,
191
+            );
183 192
 
184
-              const scaleButtonTitle = `${t(
185
-                "buttons.scale",
186
-              )} ${s}x (${width}x${height})`;
193
+            const scaleButtonTitle = `${t(
194
+              "buttons.scale",
195
+            )} ${_scale}x (${width}x${height})`;
187 196
 
188
-              return (
189
-                <ToolButton
190
-                  key={s}
191
-                  size="s"
192
-                  type="radio"
193
-                  icon={`${s}x`}
194
-                  name="export-canvas-scale"
195
-                  title={scaleButtonTitle}
196
-                  aria-label={scaleButtonTitle}
197
-                  id="export-canvas-scale"
198
-                  checked={s === scale}
199
-                  onChange={() => setScale(s)}
200
-                />
201
-              );
202
-            })}
203
-          </Stack.Row>
204
-        </div>
205
-        {actionManager.renderAction("changeExportBackground")}
206
-        {someElementIsSelected && (
207
-          <div>
208
-            <label>
209
-              <input
210
-                type="checkbox"
211
-                checked={exportSelected}
212
-                onChange={(event) =>
213
-                  setExportSelected(event.currentTarget.checked)
214
-                }
215
-              />{" "}
216
-              {t("labels.onlySelected")}
217
-            </label>
218
-          </div>
197
+            return (
198
+              <ToolButton
199
+                key={_scale}
200
+                size="s"
201
+                type="radio"
202
+                icon={`${_scale}x`}
203
+                name="export-canvas-scale"
204
+                title={scaleButtonTitle}
205
+                aria-label={scaleButtonTitle}
206
+                id="export-canvas-scale"
207
+                checked={_scale === scale}
208
+                onChange={() => setScale(_scale)}
209
+              />
210
+            );
211
+          })}
212
+        </Stack.Row>
213
+        <p style={{ marginLeft: "1em", userSelect: "none" }}>Scale</p>
214
+      </div>
215
+      <div
216
+        style={{
217
+          display: "flex",
218
+          alignItems: "center",
219
+          justifyContent: "center",
220
+          margin: ".6em 0",
221
+        }}
222
+      >
223
+        {!fsSupported && actionManager.renderAction("changeProjectName")}
224
+      </div>
225
+      <Stack.Row gap={2} justifyContent="center" style={{ margin: "2em 0" }}>
226
+        <ExportButton
227
+          color="indigo"
228
+          title={t("buttons.exportToPng")}
229
+          aria-label={t("buttons.exportToPng")}
230
+          onClick={() => onExportToPng(exportedElements, scale)}
231
+        >
232
+          PNG
233
+        </ExportButton>
234
+        <ExportButton
235
+          color="red"
236
+          title={t("buttons.exportToSvg")}
237
+          aria-label={t("buttons.exportToSvg")}
238
+          onClick={() => onExportToSvg(exportedElements, scale)}
239
+        >
240
+          SVG
241
+        </ExportButton>
242
+        {probablySupportsClipboardBlob && (
243
+          <ExportButton
244
+            title={t("buttons.copyPngToClipboard")}
245
+            onClick={() => onExportToClipboard(exportedElements, scale)}
246
+            color="gray"
247
+            shade={7}
248
+          >
249
+            {clipboard}
250
+          </ExportButton>
219 251
         )}
220
-        {actionManager.renderAction("changeExportEmbedScene")}
221
-        {actionManager.renderAction("changeShouldAddWatermark")}
222
-      </Stack.Col>
252
+      </Stack.Row>
223 253
     </div>
224 254
   );
225 255
 };
226 256
 
227
-export const ExportDialog = ({
257
+export const ImageExportDialog = ({
228 258
   elements,
229 259
   appState,
230 260
   exportPadding = 10,
@@ -232,7 +262,6 @@ export const ExportDialog = ({
232 262
   onExportToPng,
233 263
   onExportToSvg,
234 264
   onExportToClipboard,
235
-  onExportToBackend,
236 265
 }: {
237 266
   appState: AppState;
238 267
   elements: readonly NonDeletedExcalidrawElement[];
@@ -241,7 +270,6 @@ export const ExportDialog = ({
241 270
   onExportToPng: ExportCB;
242 271
   onExportToSvg: ExportCB;
243 272
   onExportToClipboard: ExportCB;
244
-  onExportToBackend?: ExportCB;
245 273
 }) => {
246 274
   const [modalIsShown, setModalIsShown] = useState(false);
247 275
 
@@ -255,16 +283,16 @@ export const ExportDialog = ({
255 283
         onClick={() => {
256 284
           setModalIsShown(true);
257 285
         }}
258
-        data-testid="export-button"
259
-        icon={exportFile}
286
+        data-testid="image-export-button"
287
+        icon={exportImage}
260 288
         type="button"
261
-        aria-label={t("buttons.export")}
289
+        aria-label={t("buttons.exportImage")}
262 290
         showAriaLabel={useIsMobile()}
263
-        title={t("buttons.export")}
291
+        title={t("buttons.exportImage")}
264 292
       />
265 293
       {modalIsShown && (
266
-        <Dialog onCloseRequest={handleClose} title={t("buttons.export")}>
267
-          <ExportModal
294
+        <Dialog onCloseRequest={handleClose} title={t("buttons.exportImage")}>
295
+          <ImageExportModal
268 296
             elements={elements}
269 297
             appState={appState}
270 298
             exportPadding={exportPadding}
@@ -272,7 +300,6 @@ export const ExportDialog = ({
272 300
             onExportToPng={onExportToPng}
273 301
             onExportToSvg={onExportToSvg}
274 302
             onExportToClipboard={onExportToClipboard}
275
-            onExportToBackend={onExportToBackend}
276 303
             onCloseRequest={handleClose}
277 304
           />
278 305
         </Dialog>

+ 117
- 0
src/components/JSONExportDialog.tsx Datei anzeigen

@@ -0,0 +1,117 @@
1
+import React, { useState } from "react";
2
+import { ActionsManagerInterface } from "../actions/types";
3
+import { NonDeletedExcalidrawElement } from "../element/types";
4
+import { t } from "../i18n";
5
+import { useIsMobile } from "./App";
6
+import { AppState } from "../types";
7
+import { Dialog } from "./Dialog";
8
+import { exportFile, exportToFileIcon, link } from "./icons";
9
+import { ToolButton } from "./ToolButton";
10
+import { actionSaveAsScene } from "../actions/actionExport";
11
+import { Card } from "./Card";
12
+
13
+import "./ExportDialog.scss";
14
+import { supported as fsSupported } from "browser-fs-access";
15
+
16
+export type ExportCB = (
17
+  elements: readonly NonDeletedExcalidrawElement[],
18
+  scale?: number,
19
+) => void;
20
+
21
+const JSONExportModal = ({
22
+  elements,
23
+  appState,
24
+  actionManager,
25
+  onExportToBackend,
26
+}: {
27
+  appState: AppState;
28
+  elements: readonly NonDeletedExcalidrawElement[];
29
+  actionManager: ActionsManagerInterface;
30
+  onExportToBackend?: ExportCB;
31
+  onCloseRequest: () => void;
32
+}) => {
33
+  return (
34
+    <div className="ExportDialog ExportDialog--json">
35
+      <div className="ExportDialog-cards">
36
+        <Card color="lime">
37
+          <div className="Card-icon">{exportToFileIcon}</div>
38
+          <h2>{t("exportDialog.disk_title")}</h2>
39
+          <div className="Card-details">
40
+            {t("exportDialog.disk_details")}
41
+            {!fsSupported && actionManager.renderAction("changeProjectName")}
42
+          </div>
43
+          <ToolButton
44
+            className="Card-button"
45
+            type="button"
46
+            title={t("exportDialog.disk_button")}
47
+            aria-label={t("exportDialog.disk_button")}
48
+            showAriaLabel={true}
49
+            onClick={() => {
50
+              actionManager.executeAction(actionSaveAsScene);
51
+            }}
52
+          />
53
+        </Card>
54
+        {onExportToBackend && (
55
+          <Card color="pink">
56
+            <div className="Card-icon">{link}</div>
57
+            <h2>{t("exportDialog.link_title")}</h2>
58
+            <div className="Card-details">{t("exportDialog.link_details")}</div>
59
+            <ToolButton
60
+              className="Card-button"
61
+              type="button"
62
+              title={t("exportDialog.link_button")}
63
+              aria-label={t("exportDialog.link_button")}
64
+              showAriaLabel={true}
65
+              onClick={() => onExportToBackend(elements)}
66
+            />
67
+          </Card>
68
+        )}
69
+      </div>
70
+    </div>
71
+  );
72
+};
73
+
74
+export const JSONExportDialog = ({
75
+  elements,
76
+  appState,
77
+  actionManager,
78
+  onExportToBackend,
79
+}: {
80
+  appState: AppState;
81
+  elements: readonly NonDeletedExcalidrawElement[];
82
+  actionManager: ActionsManagerInterface;
83
+  onExportToBackend?: ExportCB;
84
+}) => {
85
+  const [modalIsShown, setModalIsShown] = useState(false);
86
+
87
+  const handleClose = React.useCallback(() => {
88
+    setModalIsShown(false);
89
+  }, []);
90
+
91
+  return (
92
+    <>
93
+      <ToolButton
94
+        onClick={() => {
95
+          setModalIsShown(true);
96
+        }}
97
+        data-testid="json-export-button"
98
+        icon={exportFile}
99
+        type="button"
100
+        aria-label={t("buttons.export")}
101
+        showAriaLabel={useIsMobile()}
102
+        title={t("buttons.export")}
103
+      />
104
+      {modalIsShown && (
105
+        <Dialog onCloseRequest={handleClose} title={t("buttons.export")}>
106
+          <JSONExportModal
107
+            elements={elements}
108
+            appState={appState}
109
+            actionManager={actionManager}
110
+            onExportToBackend={onExportToBackend}
111
+            onCloseRequest={handleClose}
112
+          />
113
+        </Dialog>
114
+      )}
115
+    </>
116
+  );
117
+};

+ 39
- 19
src/components/LayerUI.tsx Datei anzeigen

@@ -28,7 +28,7 @@ import { SelectedShapeActions, ShapesSwitcher, ZoomActions } from "./Actions";
28 28
 import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
29 29
 import CollabButton from "./CollabButton";
30 30
 import { ErrorDialog } from "./ErrorDialog";
31
-import { ExportCB, ExportDialog } from "./ExportDialog";
31
+import { ExportCB, ImageExportDialog } from "./ImageExportDialog";
32 32
 import { FixedSideContainer } from "./FixedSideContainer";
33 33
 import { HintViewer } from "./HintViewer";
34 34
 import { exportFile, load, trash } from "./icons";
@@ -46,6 +46,7 @@ import { ToolButton } from "./ToolButton";
46 46
 import { Tooltip } from "./Tooltip";
47 47
 import { UserList } from "./UserList";
48 48
 import Library from "../data/library";
49
+import { JSONExportDialog } from "./JSONExportDialog";
49 50
 
50 51
 interface LayerUIProps {
51 52
   actionManager: ActionManager;
@@ -382,7 +383,29 @@ const LayerUI = ({
382 383
 }: LayerUIProps) => {
383 384
   const isMobile = useIsMobile();
384 385
 
385
-  const renderExportDialog = () => {
386
+  const renderJSONExportDialog = () => {
387
+    if (!UIOptions.canvasActions.export) {
388
+      return null;
389
+    }
390
+
391
+    return (
392
+      <JSONExportDialog
393
+        elements={elements}
394
+        appState={appState}
395
+        actionManager={actionManager}
396
+        onExportToBackend={
397
+          onExportToBackend
398
+            ? (elements) => {
399
+                onExportToBackend &&
400
+                  onExportToBackend(elements, appState, canvas);
401
+              }
402
+            : undefined
403
+        }
404
+      />
405
+    );
406
+  };
407
+
408
+  const renderImageExportDialog = () => {
386 409
     if (!UIOptions.canvasActions.export) {
387 410
       return null;
388 411
     }
@@ -406,25 +429,21 @@ const LayerUI = ({
406 429
     };
407 430
 
408 431
     return (
409
-      <ExportDialog
432
+      <ImageExportDialog
410 433
         elements={elements}
411 434
         appState={appState}
412 435
         actionManager={actionManager}
413 436
         onExportToPng={createExporter("png")}
414 437
         onExportToSvg={createExporter("svg")}
415 438
         onExportToClipboard={createExporter("clipboard")}
416
-        onExportToBackend={
417
-          onExportToBackend
418
-            ? (elements) => {
419
-                onExportToBackend &&
420
-                  onExportToBackend(elements, appState, canvas);
421
-              }
422
-            : undefined
423
-        }
424 439
       />
425 440
     );
426 441
   };
427 442
 
443
+  const Separator = () => {
444
+    return <div style={{ width: ".625em" }} />;
445
+  };
446
+
428 447
   const renderViewModeCanvasActions = () => {
429 448
     return (
430 449
       <Section
@@ -438,9 +457,8 @@ const LayerUI = ({
438 457
         <Island padding={2} style={{ zIndex: 1 }}>
439 458
           <Stack.Col gap={4}>
440 459
             <Stack.Row gap={1} justifyContent="space-between">
441
-              {actionManager.renderAction("saveScene")}
442
-              {actionManager.renderAction("saveAsScene")}
443
-              {renderExportDialog()}
460
+              {renderJSONExportDialog()}
461
+              {renderImageExportDialog()}
444 462
             </Stack.Row>
445 463
           </Stack.Col>
446 464
         </Island>
@@ -459,11 +477,12 @@ const LayerUI = ({
459 477
       <Island padding={2} style={{ zIndex: 1 }}>
460 478
         <Stack.Col gap={4}>
461 479
           <Stack.Row gap={1} justifyContent="space-between">
462
-            {actionManager.renderAction("loadScene")}
463
-            {actionManager.renderAction("saveScene")}
464
-            {actionManager.renderAction("saveAsScene")}
465
-            {renderExportDialog()}
466 480
             {actionManager.renderAction("clearCanvas")}
481
+            <Separator />
482
+            {actionManager.renderAction("loadScene")}
483
+            {renderJSONExportDialog()}
484
+            {renderImageExportDialog()}
485
+            <Separator />
467 486
             {onCollabButtonClick && (
468 487
               <CollabButton
469 488
                 isCollaborating={isCollaborating}
@@ -712,7 +731,8 @@ const LayerUI = ({
712 731
         elements={elements}
713 732
         actionManager={actionManager}
714 733
         libraryMenu={libraryMenu}
715
-        exportButton={renderExportDialog()}
734
+        renderJSONExportDialog={renderJSONExportDialog}
735
+        renderImageExportDialog={renderImageExportDialog}
716 736
         setAppState={setAppState}
717 737
         onCollabButtonClick={onCollabButtonClick}
718 738
         onLockToggle={onLockToggle}

+ 9
- 9
src/components/MobileMenu.tsx Datei anzeigen

@@ -20,7 +20,8 @@ import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkMode
20 20
 type MobileMenuProps = {
21 21
   appState: AppState;
22 22
   actionManager: ActionManager;
23
-  exportButton: React.ReactNode;
23
+  renderJSONExportDialog: () => React.ReactNode;
24
+  renderImageExportDialog: () => React.ReactNode;
24 25
   setAppState: React.Component<any, AppState>["setState"];
25 26
   elements: readonly NonDeletedExcalidrawElement[];
26 27
   libraryMenu: JSX.Element | null;
@@ -38,7 +39,8 @@ export const MobileMenu = ({
38 39
   elements,
39 40
   libraryMenu,
40 41
   actionManager,
41
-  exportButton,
42
+  renderJSONExportDialog,
43
+  renderImageExportDialog,
42 44
   setAppState,
43 45
   onCollabButtonClick,
44 46
   onLockToggle,
@@ -107,19 +109,17 @@ export const MobileMenu = ({
107 109
     if (viewModeEnabled) {
108 110
       return (
109 111
         <>
110
-          {actionManager.renderAction("saveScene")}
111
-          {actionManager.renderAction("saveAsScene")}
112
-          {exportButton}
112
+          {renderJSONExportDialog()}
113
+          {renderImageExportDialog()}
113 114
         </>
114 115
       );
115 116
     }
116 117
     return (
117 118
       <>
118
-        {actionManager.renderAction("loadScene")}
119
-        {actionManager.renderAction("saveScene")}
120
-        {actionManager.renderAction("saveAsScene")}
121
-        {exportButton}
122 119
         {actionManager.renderAction("clearCanvas")}
120
+        {actionManager.renderAction("loadScene")}
121
+        {renderJSONExportDialog()}
122
+        {renderImageExportDialog()}
123 123
         {onCollabButtonClick && (
124 124
           <CollabButton
125 125
             isCollaborating={isCollaborating}

+ 25
- 0
src/components/ProjectName.scss Datei anzeigen

@@ -0,0 +1,25 @@
1
+.ProjectName {
2
+  margin: auto;
3
+  display: flex;
4
+  align-items: center;
5
+
6
+  .TextInput {
7
+    height: calc(1rem - 3px);
8
+    width: 200px;
9
+    overflow: hidden;
10
+    text-align: center;
11
+    margin-left: 8px;
12
+    text-overflow: ellipsis;
13
+
14
+    &--readonly {
15
+      background: none;
16
+      border: none;
17
+      &:hover {
18
+        background: none;
19
+      }
20
+      width: auto;
21
+      max-width: 200px;
22
+      padding-left: 2px;
23
+    }
24
+  }
25
+}

+ 7
- 5
src/components/ProjectName.tsx Datei anzeigen

@@ -3,6 +3,8 @@ import "./TextInput.scss";
3 3
 import React, { Component } from "react";
4 4
 import { focusNearestParent } from "../utils";
5 5
 
6
+import "./ProjectName.scss";
7
+
6 8
 type Props = {
7 9
   value: string;
8 10
   onChange: (value: string) => void;
@@ -37,8 +39,8 @@ export class ProjectName extends Component<Props, State> {
37 39
 
38 40
   public render() {
39 41
     return (
40
-      <>
41
-        <label htmlFor="file-name">
42
+      <div className="ProjectName">
43
+        <label className="ProjectName-label" htmlFor="filename">
42 44
           {`${this.props.label}${this.props.isNameEditable ? "" : ":"}`}
43 45
         </label>
44 46
         {this.props.isNameEditable ? (
@@ -46,18 +48,18 @@ export class ProjectName extends Component<Props, State> {
46 48
             className="TextInput"
47 49
             onBlur={this.handleBlur}
48 50
             onKeyDown={this.handleKeyDown}
49
-            id="file-name"
51
+            id="filename"
50 52
             value={this.state.fileName}
51 53
             onChange={(event) =>
52 54
               this.setState({ fileName: event.target.value })
53 55
             }
54 56
           />
55 57
         ) : (
56
-          <span className="TextInput TextInput--readonly" id="file-name">
58
+          <span className="TextInput TextInput--readonly" id="filename">
57 59
             {this.props.value}
58 60
           </span>
59 61
         )}
60
-      </>
62
+      </div>
61 63
     );
62 64
   }
63 65
 }

+ 1
- 1
src/components/Stats.scss Datei anzeigen

@@ -6,7 +6,7 @@
6 6
     top: 64px;
7 7
     right: 12px;
8 8
     font-size: 12px;
9
-    z-index: 999;
9
+    z-index: 10;
10 10
 
11 11
     h3 {
12 12
       margin: 0 24px 8px 0;

+ 17
- 10
src/components/ToolButton.tsx Datei anzeigen

@@ -29,9 +29,13 @@ type ToolButtonProps =
29 29
       children?: React.ReactNode;
30 30
       onClick?(): void;
31 31
     })
32
+  | (ToolButtonBaseProps & {
33
+      type: "icon";
34
+      children?: React.ReactNode;
35
+      onClick?(): void;
36
+    })
32 37
   | (ToolButtonBaseProps & {
33 38
       type: "radio";
34
-
35 39
       checked: boolean;
36 40
       onChange?(): void;
37 41
     });
@@ -43,7 +47,7 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
43 47
   React.useImperativeHandle(ref, () => innerRef.current);
44 48
   const sizeCn = `ToolIcon_size_${props.size || DEFAULT_SIZE}`;
45 49
 
46
-  if (props.type === "button") {
50
+  if (props.type === "button" || props.type === "icon") {
47 51
     return (
48 52
       <button
49 53
         className={clsx(
@@ -56,6 +60,7 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
56 60
           {
57 61
             ToolIcon: !props.hidden,
58 62
             "ToolIcon--selected": props.selected,
63
+            "ToolIcon--plain": props.type === "icon",
59 64
           },
60 65
         )}
61 66
         data-testid={props["data-testid"]}
@@ -66,14 +71,16 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
66 71
         onClick={props.onClick}
67 72
         ref={innerRef}
68 73
       >
69
-        <div className="ToolIcon__icon" aria-hidden="true">
70
-          {props.icon || props.label}
71
-          {props.keyBindingLabel && (
72
-            <span className="ToolIcon__keybinding">
73
-              {props.keyBindingLabel}
74
-            </span>
75
-          )}
76
-        </div>
74
+        {(props.icon || props.label) && (
75
+          <div className="ToolIcon__icon" aria-hidden="true">
76
+            {props.icon || props.label}
77
+            {props.keyBindingLabel && (
78
+              <span className="ToolIcon__keybinding">
79
+                {props.keyBindingLabel}
80
+              </span>
81
+            )}
82
+          </div>
83
+        )}
77 84
         {props.showAriaLabel && (
78 85
           <div className="ToolIcon__label">{props["aria-label"]}</div>
79 86
         )}

+ 9
- 11
src/components/ToolIcon.scss Datei anzeigen

@@ -11,6 +11,15 @@
11 11
     background-color: var(--button-gray-1);
12 12
     -webkit-tap-highlight-color: transparent;
13 13
     border-radius: var(--space-factor);
14
+    user-select: none;
15
+  }
16
+
17
+  .ToolIcon--plain {
18
+    background-color: transparent;
19
+    .ToolIcon__icon {
20
+      width: 2rem;
21
+      height: 2rem;
22
+    }
14 23
   }
15 24
 
16 25
   .ToolIcon__icon {
@@ -187,17 +196,6 @@
187 196
     }
188 197
   }
189 198
 
190
-  .TooltipIcon {
191
-    width: 0.9em;
192
-    height: 0.9em;
193
-    margin-left: 5px;
194
-    margin-top: 1px;
195
-
196
-    @include isMobile {
197
-      display: none;
198
-    }
199
-  }
200
-
201 199
   .unlocked-icon {
202 200
     :root[dir="ltr"] & {
203 201
       left: 2px;

+ 14
- 0
src/components/Tooltip.scss Datei anzeigen

@@ -23,3 +23,17 @@
23 23
     display: block;
24 24
   }
25 25
 }
26
+
27
+.excalidraw {
28
+  .Tooltip-icon {
29
+    width: 0.9em;
30
+    height: 0.9em;
31
+    margin-left: 5px;
32
+    margin-top: 1px;
33
+    display: flex;
34
+
35
+    @include isMobile {
36
+      display: none;
37
+    }
38
+  }
39
+}

+ 27
- 8
src/components/icons.tsx Datei anzeigen

@@ -41,6 +41,14 @@ const createIcon = (d: string | React.ReactNode, opts: number | Opts = 512) => {
41 41
   );
42 42
 };
43 43
 
44
+export const checkIcon = createIcon(
45
+  <polyline fill="none" stroke="currentColor" points="20 6 9 17 4 12" />,
46
+  {
47
+    width: 24,
48
+    height: 24,
49
+  },
50
+);
51
+
44 52
 export const link = createIcon(
45 53
   "M326.612 185.391c59.747 59.809 58.927 155.698.36 214.59-.11.12-.24.25-.36.37l-67.2 67.2c-59.27 59.27-155.699 59.262-214.96 0-59.27-59.26-59.27-155.7 0-214.96l37.106-37.106c9.84-9.84 26.786-3.3 27.294 10.606.648 17.722 3.826 35.527 9.69 52.721 1.986 5.822.567 12.262-3.783 16.612l-13.087 13.087c-28.026 28.026-28.905 73.66-1.155 101.96 28.024 28.579 74.086 28.749 102.325.51l67.2-67.19c28.191-28.191 28.073-73.757 0-101.83-3.701-3.694-7.429-6.564-10.341-8.569a16.037 16.037 0 0 1-6.947-12.606c-.396-10.567 3.348-21.456 11.698-29.806l21.054-21.055c5.521-5.521 14.182-6.199 20.584-1.731a152.482 152.482 0 0 1 20.522 17.197zM467.547 44.449c-59.261-59.262-155.69-59.27-214.96 0l-67.2 67.2c-.12.12-.25.25-.36.37-58.566 58.892-59.387 154.781.36 214.59a152.454 152.454 0 0 0 20.521 17.196c6.402 4.468 15.064 3.789 20.584-1.731l21.054-21.055c8.35-8.35 12.094-19.239 11.698-29.806a16.037 16.037 0 0 0-6.947-12.606c-2.912-2.005-6.64-4.875-10.341-8.569-28.073-28.073-28.191-73.639 0-101.83l67.2-67.19c28.239-28.239 74.3-28.069 102.325.51 27.75 28.3 26.872 73.934-1.155 101.96l-13.087 13.087c-4.35 4.35-5.769 10.79-3.783 16.612 5.864 17.194 9.042 34.999 9.69 52.721.509 13.906 17.454 20.446 27.294 10.606l37.106-37.106c59.271-59.259 59.271-155.699.001-214.959z",
46 54
   { mirror: true },
@@ -80,6 +88,25 @@ export const exportFile = createIcon(
80 88
   { width: 576, height: 512, mirror: true },
81 89
 );
82 90
 
91
+export const exportImage = createIcon(
92
+  <>
93
+    <path
94
+      d="M571 308l-95.7-96.4c-10.1-10.1-27.4-3-27.4 11.3V288h-64v64h64v65.2c0 14.3 17.3 21.4 27.4 11.3L571 332c6.6-6.6 6.6-17.4 0-24zm-187 44v-64 64z"
95
+      fill-rule="nonzero"
96
+    />
97
+    <path
98
+      d="M384 121.941V128H256V0h6.059c6.362 0 12.471 2.53 16.97 7.029l97.941 97.941a24.01 24.01 0 017.03 16.971zM248 160c-13.2 0-24-10.8-24-24V0H24C10.745 0 0 10.745 0 24v464c0 13.255 10.745 24 24 24h336c13.255 0 24-10.745 24-24V160H248zm-135.455 16c26.51 0 48 21.49 48 48s-21.49 48-48 48-48-21.49-48-48 21.491-48 48-48zm208 240h-256l.485-48.485L104.545 328c4.686-4.686 11.799-4.201 16.485.485L160.545 368 264.06 264.485c4.686-4.686 12.284-4.686 16.971 0L320.545 304v112z"
99
+      fill-rule="nonzero"
100
+    />
101
+  </>,
102
+  { width: 576, height: 512, mirror: true },
103
+);
104
+
105
+export const exportToFileIcon = createIcon(
106
+  "M216 0h80c13.3 0 24 10.7 24 24v168h87.7c17.8 0 26.7 21.5 14.1 34.1L269.7 378.3c-7.5 7.5-19.8 7.5-27.3 0L90.1 226.1c-12.6-12.6-3.7-34.1 14.1-34.1H192V24c0-13.3 10.7-24 24-24zm296 376v112c0 13.3-10.7 24-24 24H24c-13.3 0-24-10.7-24-24V376c0-13.3 10.7-24 24-24h146.7l49 49c20.1 20.1 52.5 20.1 72.6 0l49-49H488c13.3 0 24 10.7 24 24zm-124 88c0-11-9-20-20-20s-20 9-20 20 9 20 20 20 20-9 20-20zm64 0c0-11-9-20-20-20s-20 9-20 20 9 20 20 20 20-9 20-20z",
107
+  { width: 512, height: 512 },
108
+);
109
+
83 110
 export const zoomIn = createIcon(
84 111
   "M416 208H272V64c0-17.67-14.33-32-32-32h-32c-17.67 0-32 14.33-32 32v144H32c-17.67 0-32 14.33-32 32v32c0 17.67 14.33 32 32 32h144v144c0 17.67 14.33 32 32 32h32c17.67 0 32-14.33 32-32V304h144c17.67 0 32-14.33 32-32v-32c0-17.67-14.33-32-32-32z",
85 112
   { width: 448, height: 512 },
@@ -350,14 +377,6 @@ export const DistributeHorizontallyIcon = React.memo(
350 377
     ),
351 378
 );
352 379
 
353
-<svg
354
-  width="24"
355
-  height="24"
356
-  viewBox="0 0 24 24"
357
-  fill="none"
358
-  xmlns="http://www.w3.org/2000/svg"
359
-></svg>;
360
-
361 380
 export const DistributeVerticallyIcon = React.memo(
362 381
   ({ theme }: { theme: "light" | "dark" }) =>
363 382
     createIcon(

+ 13
- 3
src/locales/en.json Datei anzeigen

@@ -42,8 +42,8 @@
42 42
     "fontSize": "Font size",
43 43
     "fontFamily": "Font family",
44 44
     "onlySelected": "Only selected",
45
-    "withBackground": "With background",
46
-    "exportEmbedScene": "Embed scene into exported file",
45
+    "withBackground": "Background",
46
+    "exportEmbedScene": "Embed scene",
47 47
     "exportEmbedScene_details": "Scene data will be saved into the exported PNG/SVG file so that the scene can be restored from it.\nWill increase exported file size.",
48 48
     "addWatermark": "Add \"Made with Excalidraw\"",
49 49
     "handDrawn": "Hand-drawn",
@@ -105,13 +105,15 @@
105 105
   },
106 106
   "buttons": {
107 107
     "clearReset": "Reset the canvas",
108
+    "exportJSON": "Export to file",
109
+    "exportImage": "Save as image",
108 110
     "export": "Export",
109 111
     "exportToPng": "Export to PNG",
110 112
     "exportToSvg": "Export to SVG",
111 113
     "copyToClipboard": "Copy to clipboard",
112 114
     "copyPngToClipboard": "Copy PNG to clipboard",
113 115
     "scale": "Scale",
114
-    "save": "Save",
116
+    "save": "Save to current file",
115 117
     "saveAs": "Save as",
116 118
     "load": "Load",
117 119
     "getShareableLink": "Get shareable link",
@@ -215,6 +217,14 @@
215 217
   "errorDialog": {
216 218
     "title": "Error"
217 219
   },
220
+  "exportDialog": {
221
+    "disk_title": "Save to disk",
222
+    "disk_details": "Export the scene data to a file from which you can import later.",
223
+    "disk_button": "Save to file",
224
+    "link_title": "Shareable link",
225
+    "link_details": "Export as a read-only link.",
226
+    "link_button": "Export to Link"
227
+  },
218 228
   "helpDialog": {
219 229
     "blog": "Read our blog",
220 230
     "click": "click",

+ 70
- 106
src/tests/__snapshots__/excalidrawPackage.test.tsx.snap Datei anzeigen

@@ -24,35 +24,10 @@ exports[`<Excalidraw/> Test UIOptions prop Test canvasActions should not hide an
24 24
         style="--gap: 1; justify-content: space-between;"
25 25
       >
26 26
         <button
27
-          aria-label="Load"
28
-          class="ToolIcon_type_button ToolIcon_size_m ToolIcon_type_button--show ToolIcon"
29
-          data-testid="load-button"
30
-          title="Load"
31
-          type="button"
32
-        >
33
-          <div
34
-            aria-hidden="true"
35
-            class="ToolIcon__icon"
36
-          >
37
-            <svg
38
-              aria-hidden="true"
39
-              class="rtl-mirror"
40
-              focusable="false"
41
-              role="img"
42
-              viewBox="0 0 576 512"
43
-            >
44
-              <path
45
-                d="M572.694 292.093L500.27 416.248A63.997 63.997 0 0 1 444.989 448H45.025c-18.523 0-30.064-20.093-20.731-36.093l72.424-124.155A64 64 0 0 1 152 256h399.964c18.523 0 30.064 20.093 20.73 36.093zM152 224h328v-48c0-26.51-21.49-48-48-48H272l-64-64H48C21.49 64 0 85.49 0 112v278.046l69.077-118.418C86.214 242.25 117.989 224 152 224z"
46
-                fill="currentColor"
47
-              />
48
-            </svg>
49
-          </div>
50
-        </button>
51
-        <button
52
-          aria-label="Save"
27
+          aria-label="Reset the canvas"
53 28
           class="ToolIcon_type_button ToolIcon_size_m ToolIcon_type_button--show ToolIcon"
54
-          data-testid="save-button"
55
-          title="Save"
29
+          data-testid="clear-canvas-button"
30
+          title="Reset the canvas"
56 31
           type="button"
57 32
         >
58 33
           <div
@@ -67,18 +42,20 @@ exports[`<Excalidraw/> Test UIOptions prop Test canvasActions should not hide an
67 42
               viewBox="0 0 448 512"
68 43
             >
69 44
               <path
70
-                d="M433.941 129.941l-83.882-83.882A48 48 0 0 0 316.118 32H48C21.49 32 0 53.49 0 80v352c0 26.51 21.49 48 48 48h352c26.51 0 48-21.49 48-48V163.882a48 48 0 0 0-14.059-33.941zM224 416c-35.346 0-64-28.654-64-64 0-35.346 28.654-64 64-64s64 28.654 64 64c0 35.346-28.654 64-64 64zm96-304.52V212c0 6.627-5.373 12-12 12H76c-6.627 0-12-5.373-12-12V108c0-6.627 5.373-12 12-12h228.52c3.183 0 6.235 1.264 8.485 3.515l3.48 3.48A11.996 11.996 0 0 1 320 111.48z"
45
+                d="M32 464a48 48 0 0 0 48 48h288a48 48 0 0 0 48-48V128H32zm272-256a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zm-96 0a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zm-96 0a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zM432 32H312l-9.4-18.7A24 24 0 0 0 281.1 0H166.8a23.72 23.72 0 0 0-21.4 13.3L136 32H16A16 16 0 0 0 0 48v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16z"
71 46
                 fill="currentColor"
72 47
               />
73 48
             </svg>
74 49
           </div>
75 50
         </button>
51
+        <div
52
+          style="width: .625em;"
53
+        />
76 54
         <button
77
-          aria-label="Save as"
78
-          class="ToolIcon_type_button ToolIcon_size_m ToolIcon_type_button--hide"
79
-          data-testid="save-as-button"
80
-          hidden=""
81
-          title="Save as"
55
+          aria-label="Load"
56
+          class="ToolIcon_type_button ToolIcon_size_m ToolIcon_type_button--show ToolIcon"
57
+          data-testid="load-button"
58
+          title="Load"
82 59
           type="button"
83 60
         >
84 61
           <div
@@ -87,13 +64,13 @@ exports[`<Excalidraw/> Test UIOptions prop Test canvasActions should not hide an
87 64
           >
88 65
             <svg
89 66
               aria-hidden="true"
90
-              class=""
67
+              class="rtl-mirror"
91 68
               focusable="false"
92 69
               role="img"
93
-              viewBox="0 0 448 512"
70
+              viewBox="0 0 576 512"
94 71
             >
95 72
               <path
96
-                d="M252 54L203 8a28 27 0 00-20-8H28C12 0 0 12 0 27v195c0 15 12 26 28 26h204c15 0 28-11 28-26V73a28 27 0 00-8-19zM130 213c-21 0-37-16-37-36 0-19 16-35 37-35 20 0 37 16 37 35 0 20-17 36-37 36zm56-169v56c0 4-4 6-7 6H44c-4 0-7-2-7-6V42c0-4 3-7 7-7h133l4 2 3 2a7 7 0 012 5z M296 201l87 95-188 205-78 9c-10 1-19-8-18-20l9-84zm141-14l-41-44a31 31 0 00-46 0l-38 41 87 95 38-42c13-14 13-36 0-50z"
73
+                d="M572.694 292.093L500.27 416.248A63.997 63.997 0 0 1 444.989 448H45.025c-18.523 0-30.064-20.093-20.731-36.093l72.424-124.155A64 64 0 0 1 152 256h399.964c18.523 0 30.064 20.093 20.73 36.093zM152 224h328v-48c0-26.51-21.49-48-48-48H272l-64-64H48C21.49 64 0 85.49 0 112v278.046l69.077-118.418C86.214 242.25 117.989 224 152 224z"
97 74
                 fill="currentColor"
98 75
               />
99 76
             </svg>
@@ -102,7 +79,7 @@ exports[`<Excalidraw/> Test UIOptions prop Test canvasActions should not hide an
102 79
         <button
103 80
           aria-label="Export"
104 81
           class="ToolIcon_type_button ToolIcon_size_m ToolIcon_type_button--show ToolIcon"
105
-          data-testid="export-button"
82
+          data-testid="json-export-button"
106 83
           title="Export"
107 84
           type="button"
108 85
         >
@@ -125,10 +102,10 @@ exports[`<Excalidraw/> Test UIOptions prop Test canvasActions should not hide an
125 102
           </div>
126 103
         </button>
127 104
         <button
128
-          aria-label="Reset the canvas"
105
+          aria-label="Save as image"
129 106
           class="ToolIcon_type_button ToolIcon_size_m ToolIcon_type_button--show ToolIcon"
130
-          data-testid="clear-canvas-button"
131
-          title="Reset the canvas"
107
+          data-testid="image-export-button"
108
+          title="Save as image"
132 109
           type="button"
133 110
         >
134 111
           <div
@@ -137,18 +114,25 @@ exports[`<Excalidraw/> Test UIOptions prop Test canvasActions should not hide an
137 114
           >
138 115
             <svg
139 116
               aria-hidden="true"
140
-              class=""
117
+              class="rtl-mirror"
141 118
               focusable="false"
142 119
               role="img"
143
-              viewBox="0 0 448 512"
120
+              viewBox="0 0 576 512"
144 121
             >
145 122
               <path
146
-                d="M32 464a48 48 0 0 0 48 48h288a48 48 0 0 0 48-48V128H32zm272-256a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zm-96 0a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zm-96 0a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zM432 32H312l-9.4-18.7A24 24 0 0 0 281.1 0H166.8a23.72 23.72 0 0 0-21.4 13.3L136 32H16A16 16 0 0 0 0 48v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16z"
147
-                fill="currentColor"
123
+                d="M571 308l-95.7-96.4c-10.1-10.1-27.4-3-27.4 11.3V288h-64v64h64v65.2c0 14.3 17.3 21.4 27.4 11.3L571 332c6.6-6.6 6.6-17.4 0-24zm-187 44v-64 64z"
124
+                fill-rule="nonzero"
125
+              />
126
+              <path
127
+                d="M384 121.941V128H256V0h6.059c6.362 0 12.471 2.53 16.97 7.029l97.941 97.941a24.01 24.01 0 017.03 16.971zM248 160c-13.2 0-24-10.8-24-24V0H24C10.745 0 0 10.745 0 24v464c0 13.255 10.745 24 24 24h336c13.255 0 24-10.745 24-24V160H248zm-135.455 16c26.51 0 48 21.49 48 48s-21.49 48-48 48-48-21.49-48-48 21.491-48 48-48zm208 240h-256l.485-48.485L104.545 328c4.686-4.686 11.799-4.201 16.485.485L160.545 368 264.06 264.485c4.686-4.686 12.284-4.686 16.971 0L320.545 304v112z"
128
+                fill-rule="nonzero"
148 129
               />
149 130
             </svg>
150 131
           </div>
151 132
         </button>
133
+        <div
134
+          style="width: .625em;"
135
+        />
152 136
       </div>
153 137
       <div
154 138
         style="display: flex;"
@@ -186,17 +170,15 @@ exports[`<Excalidraw/> Test UIOptions prop Test canvasActions should not hide an
186 170
         <div
187 171
           style="margin-inline-start: 0.25rem;"
188 172
         >
189
-          <label
190
-            class="ToolIcon ToolIcon_type_floating ToolIcon_size_M"
173
+          <button
174
+            aria-label="Dark mode"
175
+            class="ToolIcon_type_button ToolIcon_size_m ToolIcon_type_button--show ToolIcon ToolIcon--plain"
191 176
             data-testid="toggle-dark-mode"
192 177
             title="Dark mode"
178
+            type="button"
193 179
           >
194
-            <input
195
-              aria-label="Dark mode"
196
-              class="ToolIcon_type_checkbox ToolIcon_toggle_opaque"
197
-              type="checkbox"
198
-            />
199 180
             <div
181
+              aria-hidden="true"
200 182
               class="ToolIcon__icon"
201 183
             >
202 184
               <svg
@@ -211,7 +193,7 @@ exports[`<Excalidraw/> Test UIOptions prop Test canvasActions should not hide an
211 193
                 />
212 194
               </svg>
213 195
             </div>
214
-          </label>
196
+          </button>
215 197
         </div>
216 198
       </div>
217 199
     </div>
@@ -243,35 +225,10 @@ exports[`<Excalidraw/> Test UIOptions prop should not hide any UI element when t
243 225
         style="--gap: 1; justify-content: space-between;"
244 226
       >
245 227
         <button
246
-          aria-label="Load"
247
-          class="ToolIcon_type_button ToolIcon_size_m ToolIcon_type_button--show ToolIcon"
248
-          data-testid="load-button"
249
-          title="Load"
250
-          type="button"
251
-        >
252
-          <div
253
-            aria-hidden="true"
254
-            class="ToolIcon__icon"
255
-          >
256
-            <svg
257
-              aria-hidden="true"
258
-              class="rtl-mirror"
259
-              focusable="false"
260
-              role="img"
261
-              viewBox="0 0 576 512"
262
-            >
263
-              <path
264
-                d="M572.694 292.093L500.27 416.248A63.997 63.997 0 0 1 444.989 448H45.025c-18.523 0-30.064-20.093-20.731-36.093l72.424-124.155A64 64 0 0 1 152 256h399.964c18.523 0 30.064 20.093 20.73 36.093zM152 224h328v-48c0-26.51-21.49-48-48-48H272l-64-64H48C21.49 64 0 85.49 0 112v278.046l69.077-118.418C86.214 242.25 117.989 224 152 224z"
265
-                fill="currentColor"
266
-              />
267
-            </svg>
268
-          </div>
269
-        </button>
270
-        <button
271
-          aria-label="Save"
228
+          aria-label="Reset the canvas"
272 229
           class="ToolIcon_type_button ToolIcon_size_m ToolIcon_type_button--show ToolIcon"
273
-          data-testid="save-button"
274
-          title="Save"
230
+          data-testid="clear-canvas-button"
231
+          title="Reset the canvas"
275 232
           type="button"
276 233
         >
277 234
           <div
@@ -286,18 +243,20 @@ exports[`<Excalidraw/> Test UIOptions prop should not hide any UI element when t
286 243
               viewBox="0 0 448 512"
287 244
             >
288 245
               <path
289
-                d="M433.941 129.941l-83.882-83.882A48 48 0 0 0 316.118 32H48C21.49 32 0 53.49 0 80v352c0 26.51 21.49 48 48 48h352c26.51 0 48-21.49 48-48V163.882a48 48 0 0 0-14.059-33.941zM224 416c-35.346 0-64-28.654-64-64 0-35.346 28.654-64 64-64s64 28.654 64 64c0 35.346-28.654 64-64 64zm96-304.52V212c0 6.627-5.373 12-12 12H76c-6.627 0-12-5.373-12-12V108c0-6.627 5.373-12 12-12h228.52c3.183 0 6.235 1.264 8.485 3.515l3.48 3.48A11.996 11.996 0 0 1 320 111.48z"
246
+                d="M32 464a48 48 0 0 0 48 48h288a48 48 0 0 0 48-48V128H32zm272-256a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zm-96 0a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zm-96 0a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zM432 32H312l-9.4-18.7A24 24 0 0 0 281.1 0H166.8a23.72 23.72 0 0 0-21.4 13.3L136 32H16A16 16 0 0 0 0 48v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16z"
290 247
                 fill="currentColor"
291 248
               />
292 249
             </svg>
293 250
           </div>
294 251
         </button>
252
+        <div
253
+          style="width: .625em;"
254
+        />
295 255
         <button
296
-          aria-label="Save as"
297
-          class="ToolIcon_type_button ToolIcon_size_m ToolIcon_type_button--hide"
298
-          data-testid="save-as-button"
299
-          hidden=""
300
-          title="Save as"
256
+          aria-label="Load"
257
+          class="ToolIcon_type_button ToolIcon_size_m ToolIcon_type_button--show ToolIcon"
258
+          data-testid="load-button"
259
+          title="Load"
301 260
           type="button"
302 261
         >
303 262
           <div
@@ -306,13 +265,13 @@ exports[`<Excalidraw/> Test UIOptions prop should not hide any UI element when t
306 265
           >
307 266
             <svg
308 267
               aria-hidden="true"
309
-              class=""
268
+              class="rtl-mirror"
310 269
               focusable="false"
311 270
               role="img"
312
-              viewBox="0 0 448 512"
271
+              viewBox="0 0 576 512"
313 272
             >
314 273
               <path
315
-                d="M252 54L203 8a28 27 0 00-20-8H28C12 0 0 12 0 27v195c0 15 12 26 28 26h204c15 0 28-11 28-26V73a28 27 0 00-8-19zM130 213c-21 0-37-16-37-36 0-19 16-35 37-35 20 0 37 16 37 35 0 20-17 36-37 36zm56-169v56c0 4-4 6-7 6H44c-4 0-7-2-7-6V42c0-4 3-7 7-7h133l4 2 3 2a7 7 0 012 5z M296 201l87 95-188 205-78 9c-10 1-19-8-18-20l9-84zm141-14l-41-44a31 31 0 00-46 0l-38 41 87 95 38-42c13-14 13-36 0-50z"
274
+                d="M572.694 292.093L500.27 416.248A63.997 63.997 0 0 1 444.989 448H45.025c-18.523 0-30.064-20.093-20.731-36.093l72.424-124.155A64 64 0 0 1 152 256h399.964c18.523 0 30.064 20.093 20.73 36.093zM152 224h328v-48c0-26.51-21.49-48-48-48H272l-64-64H48C21.49 64 0 85.49 0 112v278.046l69.077-118.418C86.214 242.25 117.989 224 152 224z"
316 275
                 fill="currentColor"
317 276
               />
318 277
             </svg>
@@ -321,7 +280,7 @@ exports[`<Excalidraw/> Test UIOptions prop should not hide any UI element when t
321 280
         <button
322 281
           aria-label="Export"
323 282
           class="ToolIcon_type_button ToolIcon_size_m ToolIcon_type_button--show ToolIcon"
324
-          data-testid="export-button"
283
+          data-testid="json-export-button"
325 284
           title="Export"
326 285
           type="button"
327 286
         >
@@ -344,10 +303,10 @@ exports[`<Excalidraw/> Test UIOptions prop should not hide any UI element when t
344 303
           </div>
345 304
         </button>
346 305
         <button
347
-          aria-label="Reset the canvas"
306
+          aria-label="Save as image"
348 307
           class="ToolIcon_type_button ToolIcon_size_m ToolIcon_type_button--show ToolIcon"
349
-          data-testid="clear-canvas-button"
350
-          title="Reset the canvas"
308
+          data-testid="image-export-button"
309
+          title="Save as image"
351 310
           type="button"
352 311
         >
353 312
           <div
@@ -356,18 +315,25 @@ exports[`<Excalidraw/> Test UIOptions prop should not hide any UI element when t
356 315
           >
357 316
             <svg
358 317
               aria-hidden="true"
359
-              class=""
318
+              class="rtl-mirror"
360 319
               focusable="false"
361 320
               role="img"
362
-              viewBox="0 0 448 512"
321
+              viewBox="0 0 576 512"
363 322
             >
364 323
               <path
365
-                d="M32 464a48 48 0 0 0 48 48h288a48 48 0 0 0 48-48V128H32zm272-256a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zm-96 0a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zm-96 0a16 16 0 0 1 32 0v224a16 16 0 0 1-32 0zM432 32H312l-9.4-18.7A24 24 0 0 0 281.1 0H166.8a23.72 23.72 0 0 0-21.4 13.3L136 32H16A16 16 0 0 0 0 48v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16z"
366
-                fill="currentColor"
324
+                d="M571 308l-95.7-96.4c-10.1-10.1-27.4-3-27.4 11.3V288h-64v64h64v65.2c0 14.3 17.3 21.4 27.4 11.3L571 332c6.6-6.6 6.6-17.4 0-24zm-187 44v-64 64z"
325
+                fill-rule="nonzero"
326
+              />
327
+              <path
328
+                d="M384 121.941V128H256V0h6.059c6.362 0 12.471 2.53 16.97 7.029l97.941 97.941a24.01 24.01 0 017.03 16.971zM248 160c-13.2 0-24-10.8-24-24V0H24C10.745 0 0 10.745 0 24v464c0 13.255 10.745 24 24 24h336c13.255 0 24-10.745 24-24V160H248zm-135.455 16c26.51 0 48 21.49 48 48s-21.49 48-48 48-48-21.49-48-48 21.491-48 48-48zm208 240h-256l.485-48.485L104.545 328c4.686-4.686 11.799-4.201 16.485.485L160.545 368 264.06 264.485c4.686-4.686 12.284-4.686 16.971 0L320.545 304v112z"
329
+                fill-rule="nonzero"
367 330
               />
368 331
             </svg>
369 332
           </div>
370 333
         </button>
334
+        <div
335
+          style="width: .625em;"
336
+        />
371 337
       </div>
372 338
       <div
373 339
         style="display: flex;"
@@ -405,17 +371,15 @@ exports[`<Excalidraw/> Test UIOptions prop should not hide any UI element when t
405 371
         <div
406 372
           style="margin-inline-start: 0.25rem;"
407 373
         >
408
-          <label
409
-            class="ToolIcon ToolIcon_type_floating ToolIcon_size_M"
374
+          <button
375
+            aria-label="Dark mode"
376
+            class="ToolIcon_type_button ToolIcon_size_m ToolIcon_type_button--show ToolIcon ToolIcon--plain"
410 377
             data-testid="toggle-dark-mode"
411 378
             title="Dark mode"
379
+            type="button"
412 380
           >
413
-            <input
414
-              aria-label="Dark mode"
415
-              class="ToolIcon_type_checkbox ToolIcon_toggle_opaque"
416
-              type="checkbox"
417
-            />
418 381
             <div
382
+              aria-hidden="true"
419 383
               class="ToolIcon__icon"
420 384
             >
421 385
               <svg
@@ -430,7 +394,7 @@ exports[`<Excalidraw/> Test UIOptions prop should not hide any UI element when t
430 394
                 />
431 395
               </svg>
432 396
             </div>
433
-          </label>
397
+          </button>
434 398
         </div>
435 399
       </div>
436 400
     </div>

+ 6
- 5
src/tests/excalidrawPackage.test.tsx Datei anzeigen

@@ -110,9 +110,9 @@ describe("<Excalidraw/>", () => {
110 110
     it('should allow editing name when the name prop is "undefined"', async () => {
111 111
       const { container } = await render(<Excalidraw />);
112 112
 
113
-      fireEvent.click(queryByTestId(container, "export-button")!);
113
+      fireEvent.click(queryByTestId(container, "image-export-button")!);
114 114
       const textInput: HTMLInputElement | null = document.querySelector(
115
-        ".ExportDialog__name .TextInput",
115
+        ".ExportDialog .ProjectName .TextInput",
116 116
       );
117 117
       expect(textInput?.value).toContain(`${t("labels.untitled")}`);
118 118
       expect(textInput?.nodeName).toBe("INPUT");
@@ -122,9 +122,9 @@ describe("<Excalidraw/>", () => {
122 122
       const name = "test";
123 123
       const { container } = await render(<Excalidraw name={name} />);
124 124
 
125
-      await fireEvent.click(queryByTestId(container, "export-button")!);
125
+      await fireEvent.click(queryByTestId(container, "image-export-button")!);
126 126
       const textInput = document.querySelector(
127
-        ".ExportDialog__name .TextInput--readonly",
127
+        ".ExportDialog .ProjectName .TextInput--readonly",
128 128
       );
129 129
       expect(textInput?.textContent).toEqual(name);
130 130
       expect(textInput?.nodeName).toBe("SPAN");
@@ -166,7 +166,8 @@ describe("<Excalidraw/>", () => {
166 166
           <Excalidraw UIOptions={{ canvasActions: { export: false } }} />,
167 167
         );
168 168
 
169
-        expect(queryByTestId(container, "export-button")).toBeNull();
169
+        expect(queryByTestId(container, "json-export-button")).toBeNull();
170
+        expect(queryByTestId(container, "image-export-button")).toBeNull();
170 171
       });
171 172
 
172 173
       it("should hide load button when loadScene is false", async () => {

Laden…
Abbrechen
Speichern