feat: exporting redesign (#3613)
Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
This commit is contained in:
parent
357266e9ab
commit
790c9fd02e
@ -11,7 +11,8 @@ import { t } from "../i18n";
|
||||
import { useIsMobile } from "../components/App";
|
||||
import { KEYS } from "../keys";
|
||||
import { register } from "./register";
|
||||
import { supported } from "browser-fs-access";
|
||||
import { supported as fsSupported } from "browser-fs-access";
|
||||
import { CheckboxItem } from "../components/CheckboxItem";
|
||||
|
||||
export const actionChangeProjectName = register({
|
||||
name: "changeProjectName",
|
||||
@ -40,14 +41,12 @@ export const actionChangeExportBackground = register({
|
||||
};
|
||||
},
|
||||
PanelComponent: ({ appState, updateData }) => (
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={appState.exportBackground}
|
||||
onChange={(event) => updateData(event.target.checked)}
|
||||
/>{" "}
|
||||
<CheckboxItem
|
||||
checked={appState.exportBackground}
|
||||
onChange={(checked) => updateData(checked)}
|
||||
>
|
||||
{t("labels.withBackground")}
|
||||
</label>
|
||||
</CheckboxItem>
|
||||
),
|
||||
});
|
||||
|
||||
@ -60,17 +59,15 @@ export const actionChangeExportEmbedScene = register({
|
||||
};
|
||||
},
|
||||
PanelComponent: ({ appState, updateData }) => (
|
||||
<label style={{ display: "flex" }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={appState.exportEmbedScene}
|
||||
onChange={(event) => updateData(event.target.checked)}
|
||||
/>{" "}
|
||||
<CheckboxItem
|
||||
checked={appState.exportEmbedScene}
|
||||
onChange={(checked) => updateData(checked)}
|
||||
>
|
||||
{t("labels.exportEmbedScene")}
|
||||
<Tooltip label={t("labels.exportEmbedScene_details")} long={true}>
|
||||
<div className="TooltipIcon">{questionCircle}</div>
|
||||
<div className="Tooltip-icon">{questionCircle}</div>
|
||||
</Tooltip>
|
||||
</label>
|
||||
</CheckboxItem>
|
||||
),
|
||||
});
|
||||
|
||||
@ -83,14 +80,12 @@ export const actionChangeShouldAddWatermark = register({
|
||||
};
|
||||
},
|
||||
PanelComponent: ({ appState, updateData }) => (
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={appState.shouldAddWatermark}
|
||||
onChange={(event) => updateData(event.target.checked)}
|
||||
/>{" "}
|
||||
<CheckboxItem
|
||||
checked={appState.shouldAddWatermark}
|
||||
onChange={(checked) => updateData(checked)}
|
||||
>
|
||||
{t("labels.addWatermark")}
|
||||
</label>
|
||||
</CheckboxItem>
|
||||
),
|
||||
});
|
||||
|
||||
@ -126,11 +121,10 @@ export const actionSaveScene = register({
|
||||
event.key === KEYS.S && event[KEYS.CTRL_OR_CMD] && !event.shiftKey,
|
||||
PanelComponent: ({ updateData }) => (
|
||||
<ToolButton
|
||||
type="button"
|
||||
type="icon"
|
||||
icon={save}
|
||||
title={t("buttons.save")}
|
||||
aria-label={t("buttons.save")}
|
||||
showAriaLabel={useIsMobile()}
|
||||
onClick={() => updateData(null)}
|
||||
data-testid="save-button"
|
||||
/>
|
||||
@ -162,7 +156,7 @@ export const actionSaveAsScene = register({
|
||||
title={t("buttons.saveAs")}
|
||||
aria-label={t("buttons.saveAs")}
|
||||
showAriaLabel={useIsMobile()}
|
||||
hidden={!supported}
|
||||
hidden={!fsSupported}
|
||||
onClick={() => updateData(null)}
|
||||
data-testid="save-as-button"
|
||||
/>
|
||||
|
@ -131,4 +131,5 @@ export interface ActionsManagerInterface {
|
||||
registerAction: (action: Action) => void;
|
||||
handleKeyDown: (event: React.KeyboardEvent | KeyboardEvent) => boolean;
|
||||
renderAction: (name: ActionName) => React.ReactElement | null;
|
||||
executeAction: (action: Action) => void;
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import React, { useContext } from "react";
|
||||
import { RoughCanvas } from "roughjs/bin/canvas";
|
||||
import rough from "roughjs/bin/rough";
|
||||
import clsx from "clsx";
|
||||
import { supported } from "browser-fs-access";
|
||||
import { supported as fsSupported } from "browser-fs-access";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
import {
|
||||
@ -3885,7 +3885,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
// default: assume an Excalidraw file regardless of extension/MimeType
|
||||
} else {
|
||||
this.setState({ isLoading: true });
|
||||
if (supported) {
|
||||
if (fsSupported) {
|
||||
try {
|
||||
// This will only work as of Chrome 86,
|
||||
// but can be safely ignored on older releases.
|
||||
|
@ -15,6 +15,11 @@ export const BackgroundPickerAndDarkModeToggle = ({
|
||||
}) => (
|
||||
<div style={{ display: "flex" }}>
|
||||
{actionManager.renderAction("changeViewBackgroundColor")}
|
||||
{showThemeBtn && <>{actionManager.renderAction("toggleTheme")}</>}
|
||||
{showThemeBtn && actionManager.renderAction("toggleTheme")}
|
||||
{appState.fileHandle && (
|
||||
<div style={{ marginInlineStart: "0.25rem" }}>
|
||||
{actionManager.renderAction("saveScene")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
53
src/components/Card.scss
Normal file
53
src/components/Card.scss
Normal file
@ -0,0 +1,53 @@
|
||||
@import "../css/variables.module";
|
||||
|
||||
.excalidraw {
|
||||
.Card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
max-width: 290px;
|
||||
|
||||
margin: 1em;
|
||||
|
||||
text-align: center;
|
||||
|
||||
.Card-icon {
|
||||
font-size: 2.6em;
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
padding: 1.4rem;
|
||||
border-radius: 50%;
|
||||
background: var(--card-color);
|
||||
color: $oc-white;
|
||||
|
||||
svg {
|
||||
width: 2.8rem;
|
||||
height: 2.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
.Card-details {
|
||||
font-size: 0.96em;
|
||||
min-height: 90px;
|
||||
padding: 0 1em;
|
||||
margin-bottom: auto;
|
||||
}
|
||||
|
||||
& .Card-button.ToolIcon_type_button {
|
||||
height: 2.5rem;
|
||||
margin-top: 1em;
|
||||
margin-bottom: 0.3em;
|
||||
background-color: var(--card-color);
|
||||
&:hover {
|
||||
background-color: var(--card-color-darker);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--card-color-darkest);
|
||||
}
|
||||
.ToolIcon__label {
|
||||
color: $oc-white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
20
src/components/Card.tsx
Normal file
20
src/components/Card.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import OpenColor from "open-color";
|
||||
|
||||
import "./Card.scss";
|
||||
|
||||
export const Card: React.FC<{
|
||||
color: keyof OpenColor;
|
||||
}> = ({ children, color }) => {
|
||||
return (
|
||||
<div
|
||||
className="Card"
|
||||
style={{
|
||||
["--card-color" as any]: OpenColor[color][7],
|
||||
["--card-color-darker" as any]: OpenColor[color][8],
|
||||
["--card-color-darkest" as any]: OpenColor[color][9],
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
85
src/components/CheckboxItem.scss
Normal file
85
src/components/CheckboxItem.scss
Normal file
@ -0,0 +1,85 @@
|
||||
@import "../css/variables.module";
|
||||
|
||||
.excalidraw {
|
||||
.Checkbox {
|
||||
margin: 3px 0.3em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
&:hover:not(.is-checked) .Checkbox-box {
|
||||
box-shadow: 0 0 0 2px #{$oc-blue-4};
|
||||
|
||||
svg {
|
||||
display: block;
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
.Checkbox-box {
|
||||
box-shadow: 0 0 2px 1px inset #{$oc-blue-7} !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.Checkbox-box {
|
||||
background-color: fade-out($oc-blue-1, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-checked {
|
||||
.Checkbox-box {
|
||||
background-color: #{$oc-blue-1};
|
||||
svg {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
&:hover .Checkbox-box {
|
||||
background-color: #{$oc-blue-2};
|
||||
}
|
||||
}
|
||||
|
||||
.Checkbox-box {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
padding: 0;
|
||||
flex: 0 0 auto;
|
||||
|
||||
margin: 0 1em;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
box-shadow: 0 0 0 2px #{$oc-blue-7};
|
||||
background-color: transparent;
|
||||
border-radius: 4px;
|
||||
|
||||
color: #{$oc-blue-7};
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 3px #{$oc-blue-7};
|
||||
}
|
||||
|
||||
svg {
|
||||
display: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
stroke-width: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.Checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.Tooltip-icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
}
|
||||
}
|
26
src/components/CheckboxItem.tsx
Normal file
26
src/components/CheckboxItem.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import clsx from "clsx";
|
||||
import { checkIcon } from "./icons";
|
||||
|
||||
import "./CheckboxItem.scss";
|
||||
|
||||
export const CheckboxItem: React.FC<{
|
||||
checked: boolean;
|
||||
onChange: (checked: boolean) => void;
|
||||
}> = ({ children, checked, onChange }) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx("Checkbox", { "is-checked": checked })}
|
||||
onClick={(event) => {
|
||||
onChange(!checked);
|
||||
((event.currentTarget as HTMLDivElement).querySelector(
|
||||
".Checkbox-box",
|
||||
) as HTMLButtonElement).focus();
|
||||
}}
|
||||
>
|
||||
<button className="Checkbox-box" role="checkbox" aria-checked={checked}>
|
||||
{checkIcon}
|
||||
</button>
|
||||
<div className="Checkbox-label">{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -160,7 +160,7 @@
|
||||
}
|
||||
|
||||
.color-picker-input {
|
||||
width: 12ch; /* length of `transparent` + 1 */
|
||||
width: 11ch; /* length of `transparent` */
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
background-color: var(--input-bg-color);
|
||||
|
@ -2,6 +2,7 @@ import "./ToolIcon.scss";
|
||||
|
||||
import React from "react";
|
||||
import { t } from "../i18n";
|
||||
import { ToolButton } from "./ToolButton";
|
||||
|
||||
export type Appearence = "light" | "dark";
|
||||
|
||||
@ -12,31 +13,19 @@ export const DarkModeToggle = (props: {
|
||||
onChange: (value: Appearence) => void;
|
||||
title?: string;
|
||||
}) => {
|
||||
const title = props.title
|
||||
? props.title
|
||||
: props.value === "dark"
|
||||
? t("buttons.lightMode")
|
||||
: t("buttons.darkMode");
|
||||
const title =
|
||||
props.title ||
|
||||
(props.value === "dark" ? t("buttons.lightMode") : t("buttons.darkMode"));
|
||||
|
||||
return (
|
||||
<label
|
||||
className="ToolIcon ToolIcon_type_floating ToolIcon_size_M"
|
||||
data-testid="toggle-dark-mode"
|
||||
<ToolButton
|
||||
type="icon"
|
||||
icon={props.value === "light" ? ICONS.MOON : ICONS.SUN}
|
||||
title={title}
|
||||
>
|
||||
<input
|
||||
className="ToolIcon_type_checkbox ToolIcon_toggle_opaque"
|
||||
type="checkbox"
|
||||
onChange={(event) =>
|
||||
props.onChange(event.target.checked ? "dark" : "light")
|
||||
}
|
||||
checked={props.value === "dark"}
|
||||
aria-label={title}
|
||||
/>
|
||||
<div className="ToolIcon__icon">
|
||||
{props.value === "light" ? ICONS.MOON : ICONS.SUN}
|
||||
</div>
|
||||
</label>
|
||||
aria-label={title}
|
||||
onClick={() => props.onChange(props.value === "dark" ? "light" : "dark")}
|
||||
data-testid="toggle-dark-mode"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -28,33 +28,6 @@
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.ExportDialog__name {
|
||||
grid-column: project-name;
|
||||
margin: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.TextInput {
|
||||
height: calc(1rem - 3px);
|
||||
width: 200px;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
margin-left: 8px;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&--readonly {
|
||||
background: none;
|
||||
border: none;
|
||||
&:hover {
|
||||
background: none;
|
||||
}
|
||||
width: auto;
|
||||
max-width: 200px;
|
||||
padding-left: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include isMobile {
|
||||
.ExportDialog {
|
||||
display: flex;
|
||||
@ -84,4 +57,62 @@
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.ExportDialog--json {
|
||||
.ExportDialog-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
justify-items: center;
|
||||
row-gap: 2em;
|
||||
|
||||
@media (max-width: 460px) {
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
.Card-details {
|
||||
min-height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.ProjectName {
|
||||
width: fit-content;
|
||||
margin: 1em auto;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
|
||||
.TextInput {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.ProjectName-label {
|
||||
margin: 0.625em 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button.ExportDialog-imageExportButton {
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
margin: 0 0.2em;
|
||||
|
||||
border-radius: 1rem;
|
||||
background-color: var(--button-color);
|
||||
box-shadow: 0 3px 5px -1px rgb(0 0 0 / 28%), 0 6px 10px 0 rgb(0 0 0 / 14%);
|
||||
|
||||
font-family: Cascadia;
|
||||
font-size: 1.8em;
|
||||
color: $oc-white;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--button-color-darker);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--button-color-darkest);
|
||||
box-shadow: 0 3px 5px -1px rgb(0 0 0 / 20%), 0 6px 10px 0 rgb(0 0 0 / 14%);
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 0.9em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,16 +6,20 @@ import { canvasToBlob } from "../data/blob";
|
||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||
import { CanvasError } from "../errors";
|
||||
import { t } from "../i18n";
|
||||
import { useIsMobile } from "../components/App";
|
||||
import { useIsMobile } from "./App";
|
||||
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
||||
import { exportToCanvas, getExportSize } from "../scene/export";
|
||||
import { AppState } from "../types";
|
||||
import { Dialog } from "./Dialog";
|
||||
import "./ExportDialog.scss";
|
||||
import { clipboard, exportFile, link } from "./icons";
|
||||
import { clipboard, exportImage } from "./icons";
|
||||
import Stack from "./Stack";
|
||||
import { ToolButton } from "./ToolButton";
|
||||
|
||||
import "./ExportDialog.scss";
|
||||
import { supported as fsSupported } from "browser-fs-access";
|
||||
import OpenColor from "open-color";
|
||||
import { CheckboxItem } from "./CheckboxItem";
|
||||
|
||||
const scales = [1, 2, 3];
|
||||
const defaultScale = scales.includes(devicePixelRatio) ? devicePixelRatio : 1;
|
||||
|
||||
@ -52,7 +56,30 @@ export type ExportCB = (
|
||||
scale?: number,
|
||||
) => void;
|
||||
|
||||
const ExportModal = ({
|
||||
const ExportButton: React.FC<{
|
||||
color: keyof OpenColor;
|
||||
onClick: () => void;
|
||||
title: string;
|
||||
shade?: number;
|
||||
}> = ({ children, title, onClick, color, shade = 6 }) => {
|
||||
return (
|
||||
<button
|
||||
className="ExportDialog-imageExportButton"
|
||||
style={{
|
||||
["--button-color" as any]: OpenColor[color][shade],
|
||||
["--button-color-darker" as any]: OpenColor[color][shade + 1],
|
||||
["--button-color-darkest" as any]: OpenColor[color][shade + 2],
|
||||
}}
|
||||
title={title}
|
||||
aria-label={title}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const ImageExportModal = ({
|
||||
elements,
|
||||
appState,
|
||||
exportPadding = 10,
|
||||
@ -60,7 +87,6 @@ const ExportModal = ({
|
||||
onExportToPng,
|
||||
onExportToSvg,
|
||||
onExportToClipboard,
|
||||
onExportToBackend,
|
||||
}: {
|
||||
appState: AppState;
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
@ -69,7 +95,6 @@ const ExportModal = ({
|
||||
onExportToPng: ExportCB;
|
||||
onExportToSvg: ExportCB;
|
||||
onExportToClipboard: ExportCB;
|
||||
onExportToBackend?: ExportCB;
|
||||
onCloseRequest: () => void;
|
||||
}) => {
|
||||
const someElementIsSelected = isSomeElementSelected(elements, appState);
|
||||
@ -133,98 +158,103 @@ const ExportModal = ({
|
||||
<div className="ExportDialog__preview" ref={previewRef} />
|
||||
{supportsContextFilters &&
|
||||
actionManager.renderAction("exportWithDarkMode")}
|
||||
<Stack.Col gap={2} align="center">
|
||||
<div className="ExportDialog__actions">
|
||||
<Stack.Row gap={2}>
|
||||
<ToolButton
|
||||
type="button"
|
||||
label="PNG"
|
||||
title={t("buttons.exportToPng")}
|
||||
aria-label={t("buttons.exportToPng")}
|
||||
onClick={() => onExportToPng(exportedElements, scale)}
|
||||
/>
|
||||
<ToolButton
|
||||
type="button"
|
||||
label="SVG"
|
||||
title={t("buttons.exportToSvg")}
|
||||
aria-label={t("buttons.exportToSvg")}
|
||||
onClick={() => onExportToSvg(exportedElements, scale)}
|
||||
/>
|
||||
{probablySupportsClipboardBlob && (
|
||||
<ToolButton
|
||||
type="button"
|
||||
icon={clipboard}
|
||||
title={t("buttons.copyPngToClipboard")}
|
||||
aria-label={t("buttons.copyPngToClipboard")}
|
||||
onClick={() => onExportToClipboard(exportedElements, scale)}
|
||||
/>
|
||||
)}
|
||||
{onExportToBackend && (
|
||||
<ToolButton
|
||||
type="button"
|
||||
icon={link}
|
||||
title={t("buttons.getShareableLink")}
|
||||
aria-label={t("buttons.getShareableLink")}
|
||||
onClick={() => onExportToBackend(exportedElements)}
|
||||
/>
|
||||
)}
|
||||
</Stack.Row>
|
||||
<div className="ExportDialog__name">
|
||||
{actionManager.renderAction("changeProjectName")}
|
||||
</div>
|
||||
<Stack.Row gap={2}>
|
||||
{scales.map((s) => {
|
||||
const [width, height] = getExportSize(
|
||||
exportedElements,
|
||||
exportPadding,
|
||||
shouldAddWatermark,
|
||||
s,
|
||||
);
|
||||
|
||||
const scaleButtonTitle = `${t(
|
||||
"buttons.scale",
|
||||
)} ${s}x (${width}x${height})`;
|
||||
|
||||
return (
|
||||
<ToolButton
|
||||
key={s}
|
||||
size="s"
|
||||
type="radio"
|
||||
icon={`${s}x`}
|
||||
name="export-canvas-scale"
|
||||
title={scaleButtonTitle}
|
||||
aria-label={scaleButtonTitle}
|
||||
id="export-canvas-scale"
|
||||
checked={s === scale}
|
||||
onChange={() => setScale(s)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Stack.Row>
|
||||
</div>
|
||||
{actionManager.renderAction("changeExportBackground")}
|
||||
{someElementIsSelected && (
|
||||
<div>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={exportSelected}
|
||||
onChange={(event) =>
|
||||
setExportSelected(event.currentTarget.checked)
|
||||
}
|
||||
/>{" "}
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1fr" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(auto-fit, minmax(190px, 1fr))",
|
||||
// dunno why this is needed, but when the items wrap it creates
|
||||
// an overflow
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{actionManager.renderAction("changeExportBackground")}
|
||||
{someElementIsSelected && (
|
||||
<CheckboxItem
|
||||
checked={exportSelected}
|
||||
onChange={(checked) => setExportSelected(checked)}
|
||||
>
|
||||
{t("labels.onlySelected")}
|
||||
</label>
|
||||
</div>
|
||||
</CheckboxItem>
|
||||
)}
|
||||
{actionManager.renderAction("changeExportEmbedScene")}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: "flex", alignItems: "center", marginTop: ".6em" }}>
|
||||
<Stack.Row gap={2} justifyContent={"center"}>
|
||||
{scales.map((_scale) => {
|
||||
const [width, height] = getExportSize(
|
||||
exportedElements,
|
||||
exportPadding,
|
||||
shouldAddWatermark,
|
||||
_scale,
|
||||
);
|
||||
|
||||
const scaleButtonTitle = `${t(
|
||||
"buttons.scale",
|
||||
)} ${_scale}x (${width}x${height})`;
|
||||
|
||||
return (
|
||||
<ToolButton
|
||||
key={_scale}
|
||||
size="s"
|
||||
type="radio"
|
||||
icon={`${_scale}x`}
|
||||
name="export-canvas-scale"
|
||||
title={scaleButtonTitle}
|
||||
aria-label={scaleButtonTitle}
|
||||
id="export-canvas-scale"
|
||||
checked={_scale === scale}
|
||||
onChange={() => setScale(_scale)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Stack.Row>
|
||||
<p style={{ marginLeft: "1em", userSelect: "none" }}>Scale</p>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
margin: ".6em 0",
|
||||
}}
|
||||
>
|
||||
{!fsSupported && actionManager.renderAction("changeProjectName")}
|
||||
</div>
|
||||
<Stack.Row gap={2} justifyContent="center" style={{ margin: "2em 0" }}>
|
||||
<ExportButton
|
||||
color="indigo"
|
||||
title={t("buttons.exportToPng")}
|
||||
aria-label={t("buttons.exportToPng")}
|
||||
onClick={() => onExportToPng(exportedElements, scale)}
|
||||
>
|
||||
PNG
|
||||
</ExportButton>
|
||||
<ExportButton
|
||||
color="red"
|
||||
title={t("buttons.exportToSvg")}
|
||||
aria-label={t("buttons.exportToSvg")}
|
||||
onClick={() => onExportToSvg(exportedElements, scale)}
|
||||
>
|
||||
SVG
|
||||
</ExportButton>
|
||||
{probablySupportsClipboardBlob && (
|
||||
<ExportButton
|
||||
title={t("buttons.copyPngToClipboard")}
|
||||
onClick={() => onExportToClipboard(exportedElements, scale)}
|
||||
color="gray"
|
||||
shade={7}
|
||||
>
|
||||
{clipboard}
|
||||
</ExportButton>
|
||||
)}
|
||||
{actionManager.renderAction("changeExportEmbedScene")}
|
||||
{actionManager.renderAction("changeShouldAddWatermark")}
|
||||
</Stack.Col>
|
||||
</Stack.Row>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ExportDialog = ({
|
||||
export const ImageExportDialog = ({
|
||||
elements,
|
||||
appState,
|
||||
exportPadding = 10,
|
||||
@ -232,7 +262,6 @@ export const ExportDialog = ({
|
||||
onExportToPng,
|
||||
onExportToSvg,
|
||||
onExportToClipboard,
|
||||
onExportToBackend,
|
||||
}: {
|
||||
appState: AppState;
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
@ -241,7 +270,6 @@ export const ExportDialog = ({
|
||||
onExportToPng: ExportCB;
|
||||
onExportToSvg: ExportCB;
|
||||
onExportToClipboard: ExportCB;
|
||||
onExportToBackend?: ExportCB;
|
||||
}) => {
|
||||
const [modalIsShown, setModalIsShown] = useState(false);
|
||||
|
||||
@ -255,16 +283,16 @@ export const ExportDialog = ({
|
||||
onClick={() => {
|
||||
setModalIsShown(true);
|
||||
}}
|
||||
data-testid="export-button"
|
||||
icon={exportFile}
|
||||
data-testid="image-export-button"
|
||||
icon={exportImage}
|
||||
type="button"
|
||||
aria-label={t("buttons.export")}
|
||||
aria-label={t("buttons.exportImage")}
|
||||
showAriaLabel={useIsMobile()}
|
||||
title={t("buttons.export")}
|
||||
title={t("buttons.exportImage")}
|
||||
/>
|
||||
{modalIsShown && (
|
||||
<Dialog onCloseRequest={handleClose} title={t("buttons.export")}>
|
||||
<ExportModal
|
||||
<Dialog onCloseRequest={handleClose} title={t("buttons.exportImage")}>
|
||||
<ImageExportModal
|
||||
elements={elements}
|
||||
appState={appState}
|
||||
exportPadding={exportPadding}
|
||||
@ -272,7 +300,6 @@ export const ExportDialog = ({
|
||||
onExportToPng={onExportToPng}
|
||||
onExportToSvg={onExportToSvg}
|
||||
onExportToClipboard={onExportToClipboard}
|
||||
onExportToBackend={onExportToBackend}
|
||||
onCloseRequest={handleClose}
|
||||
/>
|
||||
</Dialog>
|
117
src/components/JSONExportDialog.tsx
Normal file
117
src/components/JSONExportDialog.tsx
Normal file
@ -0,0 +1,117 @@
|
||||
import React, { useState } from "react";
|
||||
import { ActionsManagerInterface } from "../actions/types";
|
||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||
import { t } from "../i18n";
|
||||
import { useIsMobile } from "./App";
|
||||
import { AppState } from "../types";
|
||||
import { Dialog } from "./Dialog";
|
||||
import { exportFile, exportToFileIcon, link } from "./icons";
|
||||
import { ToolButton } from "./ToolButton";
|
||||
import { actionSaveAsScene } from "../actions/actionExport";
|
||||
import { Card } from "./Card";
|
||||
|
||||
import "./ExportDialog.scss";
|
||||
import { supported as fsSupported } from "browser-fs-access";
|
||||
|
||||
export type ExportCB = (
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
scale?: number,
|
||||
) => void;
|
||||
|
||||
const JSONExportModal = ({
|
||||
elements,
|
||||
appState,
|
||||
actionManager,
|
||||
onExportToBackend,
|
||||
}: {
|
||||
appState: AppState;
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
actionManager: ActionsManagerInterface;
|
||||
onExportToBackend?: ExportCB;
|
||||
onCloseRequest: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<div className="ExportDialog ExportDialog--json">
|
||||
<div className="ExportDialog-cards">
|
||||
<Card color="lime">
|
||||
<div className="Card-icon">{exportToFileIcon}</div>
|
||||
<h2>{t("exportDialog.disk_title")}</h2>
|
||||
<div className="Card-details">
|
||||
{t("exportDialog.disk_details")}
|
||||
{!fsSupported && actionManager.renderAction("changeProjectName")}
|
||||
</div>
|
||||
<ToolButton
|
||||
className="Card-button"
|
||||
type="button"
|
||||
title={t("exportDialog.disk_button")}
|
||||
aria-label={t("exportDialog.disk_button")}
|
||||
showAriaLabel={true}
|
||||
onClick={() => {
|
||||
actionManager.executeAction(actionSaveAsScene);
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
{onExportToBackend && (
|
||||
<Card color="pink">
|
||||
<div className="Card-icon">{link}</div>
|
||||
<h2>{t("exportDialog.link_title")}</h2>
|
||||
<div className="Card-details">{t("exportDialog.link_details")}</div>
|
||||
<ToolButton
|
||||
className="Card-button"
|
||||
type="button"
|
||||
title={t("exportDialog.link_button")}
|
||||
aria-label={t("exportDialog.link_button")}
|
||||
showAriaLabel={true}
|
||||
onClick={() => onExportToBackend(elements)}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const JSONExportDialog = ({
|
||||
elements,
|
||||
appState,
|
||||
actionManager,
|
||||
onExportToBackend,
|
||||
}: {
|
||||
appState: AppState;
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
actionManager: ActionsManagerInterface;
|
||||
onExportToBackend?: ExportCB;
|
||||
}) => {
|
||||
const [modalIsShown, setModalIsShown] = useState(false);
|
||||
|
||||
const handleClose = React.useCallback(() => {
|
||||
setModalIsShown(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ToolButton
|
||||
onClick={() => {
|
||||
setModalIsShown(true);
|
||||
}}
|
||||
data-testid="json-export-button"
|
||||
icon={exportFile}
|
||||
type="button"
|
||||
aria-label={t("buttons.export")}
|
||||
showAriaLabel={useIsMobile()}
|
||||
title={t("buttons.export")}
|
||||
/>
|
||||
{modalIsShown && (
|
||||
<Dialog onCloseRequest={handleClose} title={t("buttons.export")}>
|
||||
<JSONExportModal
|
||||
elements={elements}
|
||||
appState={appState}
|
||||
actionManager={actionManager}
|
||||
onExportToBackend={onExportToBackend}
|
||||
onCloseRequest={handleClose}
|
||||
/>
|
||||
</Dialog>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -28,7 +28,7 @@ import { SelectedShapeActions, ShapesSwitcher, ZoomActions } from "./Actions";
|
||||
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
|
||||
import CollabButton from "./CollabButton";
|
||||
import { ErrorDialog } from "./ErrorDialog";
|
||||
import { ExportCB, ExportDialog } from "./ExportDialog";
|
||||
import { ExportCB, ImageExportDialog } from "./ImageExportDialog";
|
||||
import { FixedSideContainer } from "./FixedSideContainer";
|
||||
import { HintViewer } from "./HintViewer";
|
||||
import { exportFile, load, trash } from "./icons";
|
||||
@ -46,6 +46,7 @@ import { ToolButton } from "./ToolButton";
|
||||
import { Tooltip } from "./Tooltip";
|
||||
import { UserList } from "./UserList";
|
||||
import Library from "../data/library";
|
||||
import { JSONExportDialog } from "./JSONExportDialog";
|
||||
|
||||
interface LayerUIProps {
|
||||
actionManager: ActionManager;
|
||||
@ -382,7 +383,29 @@ const LayerUI = ({
|
||||
}: LayerUIProps) => {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const renderExportDialog = () => {
|
||||
const renderJSONExportDialog = () => {
|
||||
if (!UIOptions.canvasActions.export) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<JSONExportDialog
|
||||
elements={elements}
|
||||
appState={appState}
|
||||
actionManager={actionManager}
|
||||
onExportToBackend={
|
||||
onExportToBackend
|
||||
? (elements) => {
|
||||
onExportToBackend &&
|
||||
onExportToBackend(elements, appState, canvas);
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderImageExportDialog = () => {
|
||||
if (!UIOptions.canvasActions.export) {
|
||||
return null;
|
||||
}
|
||||
@ -406,25 +429,21 @@ const LayerUI = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<ExportDialog
|
||||
<ImageExportDialog
|
||||
elements={elements}
|
||||
appState={appState}
|
||||
actionManager={actionManager}
|
||||
onExportToPng={createExporter("png")}
|
||||
onExportToSvg={createExporter("svg")}
|
||||
onExportToClipboard={createExporter("clipboard")}
|
||||
onExportToBackend={
|
||||
onExportToBackend
|
||||
? (elements) => {
|
||||
onExportToBackend &&
|
||||
onExportToBackend(elements, appState, canvas);
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const Separator = () => {
|
||||
return <div style={{ width: ".625em" }} />;
|
||||
};
|
||||
|
||||
const renderViewModeCanvasActions = () => {
|
||||
return (
|
||||
<Section
|
||||
@ -438,9 +457,8 @@ const LayerUI = ({
|
||||
<Island padding={2} style={{ zIndex: 1 }}>
|
||||
<Stack.Col gap={4}>
|
||||
<Stack.Row gap={1} justifyContent="space-between">
|
||||
{actionManager.renderAction("saveScene")}
|
||||
{actionManager.renderAction("saveAsScene")}
|
||||
{renderExportDialog()}
|
||||
{renderJSONExportDialog()}
|
||||
{renderImageExportDialog()}
|
||||
</Stack.Row>
|
||||
</Stack.Col>
|
||||
</Island>
|
||||
@ -459,11 +477,12 @@ const LayerUI = ({
|
||||
<Island padding={2} style={{ zIndex: 1 }}>
|
||||
<Stack.Col gap={4}>
|
||||
<Stack.Row gap={1} justifyContent="space-between">
|
||||
{actionManager.renderAction("loadScene")}
|
||||
{actionManager.renderAction("saveScene")}
|
||||
{actionManager.renderAction("saveAsScene")}
|
||||
{renderExportDialog()}
|
||||
{actionManager.renderAction("clearCanvas")}
|
||||
<Separator />
|
||||
{actionManager.renderAction("loadScene")}
|
||||
{renderJSONExportDialog()}
|
||||
{renderImageExportDialog()}
|
||||
<Separator />
|
||||
{onCollabButtonClick && (
|
||||
<CollabButton
|
||||
isCollaborating={isCollaborating}
|
||||
@ -712,7 +731,8 @@ const LayerUI = ({
|
||||
elements={elements}
|
||||
actionManager={actionManager}
|
||||
libraryMenu={libraryMenu}
|
||||
exportButton={renderExportDialog()}
|
||||
renderJSONExportDialog={renderJSONExportDialog}
|
||||
renderImageExportDialog={renderImageExportDialog}
|
||||
setAppState={setAppState}
|
||||
onCollabButtonClick={onCollabButtonClick}
|
||||
onLockToggle={onLockToggle}
|
||||
|
@ -20,7 +20,8 @@ import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkMode
|
||||
type MobileMenuProps = {
|
||||
appState: AppState;
|
||||
actionManager: ActionManager;
|
||||
exportButton: React.ReactNode;
|
||||
renderJSONExportDialog: () => React.ReactNode;
|
||||
renderImageExportDialog: () => React.ReactNode;
|
||||
setAppState: React.Component<any, AppState>["setState"];
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
libraryMenu: JSX.Element | null;
|
||||
@ -38,7 +39,8 @@ export const MobileMenu = ({
|
||||
elements,
|
||||
libraryMenu,
|
||||
actionManager,
|
||||
exportButton,
|
||||
renderJSONExportDialog,
|
||||
renderImageExportDialog,
|
||||
setAppState,
|
||||
onCollabButtonClick,
|
||||
onLockToggle,
|
||||
@ -107,19 +109,17 @@ export const MobileMenu = ({
|
||||
if (viewModeEnabled) {
|
||||
return (
|
||||
<>
|
||||
{actionManager.renderAction("saveScene")}
|
||||
{actionManager.renderAction("saveAsScene")}
|
||||
{exportButton}
|
||||
{renderJSONExportDialog()}
|
||||
{renderImageExportDialog()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{actionManager.renderAction("loadScene")}
|
||||
{actionManager.renderAction("saveScene")}
|
||||
{actionManager.renderAction("saveAsScene")}
|
||||
{exportButton}
|
||||
{actionManager.renderAction("clearCanvas")}
|
||||
{actionManager.renderAction("loadScene")}
|
||||
{renderJSONExportDialog()}
|
||||
{renderImageExportDialog()}
|
||||
{onCollabButtonClick && (
|
||||
<CollabButton
|
||||
isCollaborating={isCollaborating}
|
||||
|
25
src/components/ProjectName.scss
Normal file
25
src/components/ProjectName.scss
Normal file
@ -0,0 +1,25 @@
|
||||
.ProjectName {
|
||||
margin: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.TextInput {
|
||||
height: calc(1rem - 3px);
|
||||
width: 200px;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
margin-left: 8px;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&--readonly {
|
||||
background: none;
|
||||
border: none;
|
||||
&:hover {
|
||||
background: none;
|
||||
}
|
||||
width: auto;
|
||||
max-width: 200px;
|
||||
padding-left: 2px;
|
||||
}
|
||||
}
|
||||
}
|
@ -3,6 +3,8 @@ import "./TextInput.scss";
|
||||
import React, { Component } from "react";
|
||||
import { focusNearestParent } from "../utils";
|
||||
|
||||
import "./ProjectName.scss";
|
||||
|
||||
type Props = {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
@ -37,8 +39,8 @@ export class ProjectName extends Component<Props, State> {
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
<label htmlFor="file-name">
|
||||
<div className="ProjectName">
|
||||
<label className="ProjectName-label" htmlFor="filename">
|
||||
{`${this.props.label}${this.props.isNameEditable ? "" : ":"}`}
|
||||
</label>
|
||||
{this.props.isNameEditable ? (
|
||||
@ -46,18 +48,18 @@ export class ProjectName extends Component<Props, State> {
|
||||
className="TextInput"
|
||||
onBlur={this.handleBlur}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
id="file-name"
|
||||
id="filename"
|
||||
value={this.state.fileName}
|
||||
onChange={(event) =>
|
||||
this.setState({ fileName: event.target.value })
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<span className="TextInput TextInput--readonly" id="file-name">
|
||||
<span className="TextInput TextInput--readonly" id="filename">
|
||||
{this.props.value}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@
|
||||
top: 64px;
|
||||
right: 12px;
|
||||
font-size: 12px;
|
||||
z-index: 999;
|
||||
z-index: 10;
|
||||
|
||||
h3 {
|
||||
margin: 0 24px 8px 0;
|
||||
|
@ -29,9 +29,13 @@ type ToolButtonProps =
|
||||
children?: React.ReactNode;
|
||||
onClick?(): void;
|
||||
})
|
||||
| (ToolButtonBaseProps & {
|
||||
type: "icon";
|
||||
children?: React.ReactNode;
|
||||
onClick?(): void;
|
||||
})
|
||||
| (ToolButtonBaseProps & {
|
||||
type: "radio";
|
||||
|
||||
checked: boolean;
|
||||
onChange?(): void;
|
||||
});
|
||||
@ -43,7 +47,7 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
|
||||
React.useImperativeHandle(ref, () => innerRef.current);
|
||||
const sizeCn = `ToolIcon_size_${props.size || DEFAULT_SIZE}`;
|
||||
|
||||
if (props.type === "button") {
|
||||
if (props.type === "button" || props.type === "icon") {
|
||||
return (
|
||||
<button
|
||||
className={clsx(
|
||||
@ -56,6 +60,7 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
|
||||
{
|
||||
ToolIcon: !props.hidden,
|
||||
"ToolIcon--selected": props.selected,
|
||||
"ToolIcon--plain": props.type === "icon",
|
||||
},
|
||||
)}
|
||||
data-testid={props["data-testid"]}
|
||||
@ -66,14 +71,16 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
|
||||
onClick={props.onClick}
|
||||
ref={innerRef}
|
||||
>
|
||||
<div className="ToolIcon__icon" aria-hidden="true">
|
||||
{props.icon || props.label}
|
||||
{props.keyBindingLabel && (
|
||||
<span className="ToolIcon__keybinding">
|
||||
{props.keyBindingLabel}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{(props.icon || props.label) && (
|
||||
<div className="ToolIcon__icon" aria-hidden="true">
|
||||
{props.icon || props.label}
|
||||
{props.keyBindingLabel && (
|
||||
<span className="ToolIcon__keybinding">
|
||||
{props.keyBindingLabel}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{props.showAriaLabel && (
|
||||
<div className="ToolIcon__label">{props["aria-label"]}</div>
|
||||
)}
|
||||
|
@ -11,6 +11,15 @@
|
||||
background-color: var(--button-gray-1);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
border-radius: var(--space-factor);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.ToolIcon--plain {
|
||||
background-color: transparent;
|
||||
.ToolIcon__icon {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.ToolIcon__icon {
|
||||
@ -187,17 +196,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.TooltipIcon {
|
||||
width: 0.9em;
|
||||
height: 0.9em;
|
||||
margin-left: 5px;
|
||||
margin-top: 1px;
|
||||
|
||||
@include isMobile {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.unlocked-icon {
|
||||
:root[dir="ltr"] & {
|
||||
left: 2px;
|
||||
|
@ -23,3 +23,17 @@
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.excalidraw {
|
||||
.Tooltip-icon {
|
||||
width: 0.9em;
|
||||
height: 0.9em;
|
||||
margin-left: 5px;
|
||||
margin-top: 1px;
|
||||
display: flex;
|
||||
|
||||
@include isMobile {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -41,6 +41,14 @@ const createIcon = (d: string | React.ReactNode, opts: number | Opts = 512) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const checkIcon = createIcon(
|
||||
<polyline fill="none" stroke="currentColor" points="20 6 9 17 4 12" />,
|
||||
{
|
||||
width: 24,
|
||||
height: 24,
|
||||
},
|
||||
);
|
||||
|
||||
export const link = createIcon(
|
||||
"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",
|
||||
{ mirror: true },
|
||||
@ -80,6 +88,25 @@ export const exportFile = createIcon(
|
||||
{ width: 576, height: 512, mirror: true },
|
||||
);
|
||||
|
||||
export const exportImage = createIcon(
|
||||
<>
|
||||
<path
|
||||
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"
|
||||
fill-rule="nonzero"
|
||||
/>
|
||||
<path
|
||||
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"
|
||||
fill-rule="nonzero"
|
||||
/>
|
||||
</>,
|
||||
{ width: 576, height: 512, mirror: true },
|
||||
);
|
||||
|
||||
export const exportToFileIcon = createIcon(
|
||||
"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",
|
||||
{ width: 512, height: 512 },
|
||||
);
|
||||
|
||||
export const zoomIn = createIcon(
|
||||
"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",
|
||||
{ width: 448, height: 512 },
|
||||
@ -350,14 +377,6 @@ export const DistributeHorizontallyIcon = React.memo(
|
||||
),
|
||||
);
|
||||
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
></svg>;
|
||||
|
||||
export const DistributeVerticallyIcon = React.memo(
|
||||
({ theme }: { theme: "light" | "dark" }) =>
|
||||
createIcon(
|
||||
|
@ -42,8 +42,8 @@
|
||||
"fontSize": "Font size",
|
||||
"fontFamily": "Font family",
|
||||
"onlySelected": "Only selected",
|
||||
"withBackground": "With background",
|
||||
"exportEmbedScene": "Embed scene into exported file",
|
||||
"withBackground": "Background",
|
||||
"exportEmbedScene": "Embed scene",
|
||||
"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.",
|
||||
"addWatermark": "Add \"Made with Excalidraw\"",
|
||||
"handDrawn": "Hand-drawn",
|
||||
@ -105,13 +105,15 @@
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "Reset the canvas",
|
||||
"exportJSON": "Export to file",
|
||||
"exportImage": "Save as image",
|
||||
"export": "Export",
|
||||
"exportToPng": "Export to PNG",
|
||||
"exportToSvg": "Export to SVG",
|
||||
"copyToClipboard": "Copy to clipboard",
|
||||
"copyPngToClipboard": "Copy PNG to clipboard",
|
||||
"scale": "Scale",
|
||||
"save": "Save",
|
||||
"save": "Save to current file",
|
||||
"saveAs": "Save as",
|
||||
"load": "Load",
|
||||
"getShareableLink": "Get shareable link",
|
||||
@ -215,6 +217,14 @@
|
||||
"errorDialog": {
|
||||
"title": "Error"
|
||||
},
|
||||
"exportDialog": {
|
||||
"disk_title": "Save to disk",
|
||||
"disk_details": "Export the scene data to a file from which you can import later.",
|
||||
"disk_button": "Save to file",
|
||||
"link_title": "Shareable link",
|
||||
"link_details": "Export as a read-only link.",
|
||||
"link_button": "Export to Link"
|
||||
},
|
||||
"helpDialog": {
|
||||
"blog": "Read our blog",
|
||||
"click": "click",
|
||||
|
@ -23,6 +23,34 @@ exports[`<Excalidraw/> Test UIOptions prop Test canvasActions should not hide an
|
||||
class="Stack Stack_horizontal"
|
||||
style="--gap: 1; justify-content: space-between;"
|
||||
>
|
||||
<button
|
||||
aria-label="Reset the canvas"
|
||||
class="ToolIcon_type_button ToolIcon_size_m ToolIcon_type_button--show ToolIcon"
|
||||
data-testid="clear-canvas-button"
|
||||
title="Reset the canvas"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="ToolIcon__icon"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class=""
|
||||
focusable="false"
|
||||
role="img"
|
||||
viewBox="0 0 448 512"
|
||||
>
|
||||
<path
|
||||
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"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
style="width: .625em;"
|
||||
/>
|
||||
<button
|
||||
aria-label="Load"
|
||||
class="ToolIcon_type_button ToolIcon_size_m ToolIcon_type_button--show ToolIcon"
|
||||
@ -48,61 +76,10 @@ exports[`<Excalidraw/> Test UIOptions prop Test canvasActions should not hide an
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Save"
|
||||
class="ToolIcon_type_button ToolIcon_size_m ToolIcon_type_button--show ToolIcon"
|
||||
data-testid="save-button"
|
||||
title="Save"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="ToolIcon__icon"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class=""
|
||||
focusable="false"
|
||||
role="img"
|
||||
viewBox="0 0 448 512"
|
||||
>
|
||||
<path
|
||||
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"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Save as"
|
||||
class="ToolIcon_type_button ToolIcon_size_m ToolIcon_type_button--hide"
|
||||
data-testid="save-as-button"
|
||||
hidden=""
|
||||
title="Save as"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="ToolIcon__icon"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class=""
|
||||
focusable="false"
|
||||
role="img"
|
||||
viewBox="0 0 448 512"
|
||||
>
|
||||
<path
|
||||
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"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Export"
|
||||
class="ToolIcon_type_button ToolIcon_size_m ToolIcon_type_button--show ToolIcon"
|
||||
data-testid="export-button"
|
||||
data-testid="json-export-button"
|
||||
title="Export"
|
||||
type="button"
|
||||
>
|
||||
@ -125,10 +102,10 @@ exports[`<Excalidraw/> Test UIOptions prop Test canvasActions should not hide an
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Reset the canvas"
|
||||
aria-label="Save as image"
|
||||
class="ToolIcon_type_button ToolIcon_size_m ToolIcon_type_button--show ToolIcon"
|
||||
data-testid="clear-canvas-button"
|
||||
title="Reset the canvas"
|
||||
data-testid="image-export-button"
|
||||
title="Save as image"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
@ -137,18 +114,25 @@ exports[`<Excalidraw/> Test UIOptions prop Test canvasActions should not hide an
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class=""
|
||||
class="rtl-mirror"
|
||||
focusable="false"
|
||||
role="img"
|
||||
viewBox="0 0 448 512"
|
||||
viewBox="0 0 576 512"
|
||||
>
|
||||
<path
|
||||
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"
|
||||
fill="currentColor"
|
||||
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"
|
||||
fill-rule="nonzero"
|
||||
/>
|
||||
<path
|
||||
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"
|
||||
fill-rule="nonzero"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
style="width: .625em;"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style="display: flex;"
|
||||
@ -186,17 +170,15 @@ exports[`<Excalidraw/> Test UIOptions prop Test canvasActions should not hide an
|
||||
<div
|
||||
style="margin-inline-start: 0.25rem;"
|
||||
>
|
||||
<label
|
||||
class="ToolIcon ToolIcon_type_floating ToolIcon_size_M"
|
||||
<button
|
||||
aria-label="Dark mode"
|
||||
class="ToolIcon_type_button ToolIcon_size_m ToolIcon_type_button--show ToolIcon ToolIcon--plain"
|
||||
data-testid="toggle-dark-mode"
|
||||
title="Dark mode"
|
||||
type="button"
|
||||
>
|
||||
<input
|
||||
aria-label="Dark mode"
|
||||
class="ToolIcon_type_checkbox ToolIcon_toggle_opaque"
|
||||
type="checkbox"
|
||||
/>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="ToolIcon__icon"
|
||||
>
|
||||
<svg
|
||||
@ -211,7 +193,7 @@ exports[`<Excalidraw/> Test UIOptions prop Test canvasActions should not hide an
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</label>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -242,6 +224,34 @@ exports[`<Excalidraw/> Test UIOptions prop should not hide any UI element when t
|
||||
class="Stack Stack_horizontal"
|
||||
style="--gap: 1; justify-content: space-between;"
|
||||
>
|
||||
<button
|
||||
aria-label="Reset the canvas"
|
||||
class="ToolIcon_type_button ToolIcon_size_m ToolIcon_type_button--show ToolIcon"
|
||||
data-testid="clear-canvas-button"
|
||||
title="Reset the canvas"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="ToolIcon__icon"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class=""
|
||||
focusable="false"
|
||||
role="img"
|
||||
viewBox="0 0 448 512"
|
||||
>
|
||||
<path
|
||||
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"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
style="width: .625em;"
|
||||
/>
|
||||
<button
|
||||
aria-label="Load"
|
||||
class="ToolIcon_type_button ToolIcon_size_m ToolIcon_type_button--show ToolIcon"
|
||||
@ -267,61 +277,10 @@ exports[`<Excalidraw/> Test UIOptions prop should not hide any UI element when t
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Save"
|
||||
class="ToolIcon_type_button ToolIcon_size_m ToolIcon_type_button--show ToolIcon"
|
||||
data-testid="save-button"
|
||||
title="Save"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="ToolIcon__icon"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class=""
|
||||
focusable="false"
|
||||
role="img"
|
||||
viewBox="0 0 448 512"
|
||||
>
|
||||
<path
|
||||
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"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Save as"
|
||||
class="ToolIcon_type_button ToolIcon_size_m ToolIcon_type_button--hide"
|
||||
data-testid="save-as-button"
|
||||
hidden=""
|
||||
title="Save as"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="ToolIcon__icon"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class=""
|
||||
focusable="false"
|
||||
role="img"
|
||||
viewBox="0 0 448 512"
|
||||
>
|
||||
<path
|
||||
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"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Export"
|
||||
class="ToolIcon_type_button ToolIcon_size_m ToolIcon_type_button--show ToolIcon"
|
||||
data-testid="export-button"
|
||||
data-testid="json-export-button"
|
||||
title="Export"
|
||||
type="button"
|
||||
>
|
||||
@ -344,10 +303,10 @@ exports[`<Excalidraw/> Test UIOptions prop should not hide any UI element when t
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Reset the canvas"
|
||||
aria-label="Save as image"
|
||||
class="ToolIcon_type_button ToolIcon_size_m ToolIcon_type_button--show ToolIcon"
|
||||
data-testid="clear-canvas-button"
|
||||
title="Reset the canvas"
|
||||
data-testid="image-export-button"
|
||||
title="Save as image"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
@ -356,18 +315,25 @@ exports[`<Excalidraw/> Test UIOptions prop should not hide any UI element when t
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class=""
|
||||
class="rtl-mirror"
|
||||
focusable="false"
|
||||
role="img"
|
||||
viewBox="0 0 448 512"
|
||||
viewBox="0 0 576 512"
|
||||
>
|
||||
<path
|
||||
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"
|
||||
fill="currentColor"
|
||||
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"
|
||||
fill-rule="nonzero"
|
||||
/>
|
||||
<path
|
||||
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"
|
||||
fill-rule="nonzero"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
style="width: .625em;"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style="display: flex;"
|
||||
@ -405,17 +371,15 @@ exports[`<Excalidraw/> Test UIOptions prop should not hide any UI element when t
|
||||
<div
|
||||
style="margin-inline-start: 0.25rem;"
|
||||
>
|
||||
<label
|
||||
class="ToolIcon ToolIcon_type_floating ToolIcon_size_M"
|
||||
<button
|
||||
aria-label="Dark mode"
|
||||
class="ToolIcon_type_button ToolIcon_size_m ToolIcon_type_button--show ToolIcon ToolIcon--plain"
|
||||
data-testid="toggle-dark-mode"
|
||||
title="Dark mode"
|
||||
type="button"
|
||||
>
|
||||
<input
|
||||
aria-label="Dark mode"
|
||||
class="ToolIcon_type_checkbox ToolIcon_toggle_opaque"
|
||||
type="checkbox"
|
||||
/>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="ToolIcon__icon"
|
||||
>
|
||||
<svg
|
||||
@ -430,7 +394,7 @@ exports[`<Excalidraw/> Test UIOptions prop should not hide any UI element when t
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</label>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -110,9 +110,9 @@ describe("<Excalidraw/>", () => {
|
||||
it('should allow editing name when the name prop is "undefined"', async () => {
|
||||
const { container } = await render(<Excalidraw />);
|
||||
|
||||
fireEvent.click(queryByTestId(container, "export-button")!);
|
||||
fireEvent.click(queryByTestId(container, "image-export-button")!);
|
||||
const textInput: HTMLInputElement | null = document.querySelector(
|
||||
".ExportDialog__name .TextInput",
|
||||
".ExportDialog .ProjectName .TextInput",
|
||||
);
|
||||
expect(textInput?.value).toContain(`${t("labels.untitled")}`);
|
||||
expect(textInput?.nodeName).toBe("INPUT");
|
||||
@ -122,9 +122,9 @@ describe("<Excalidraw/>", () => {
|
||||
const name = "test";
|
||||
const { container } = await render(<Excalidraw name={name} />);
|
||||
|
||||
await fireEvent.click(queryByTestId(container, "export-button")!);
|
||||
await fireEvent.click(queryByTestId(container, "image-export-button")!);
|
||||
const textInput = document.querySelector(
|
||||
".ExportDialog__name .TextInput--readonly",
|
||||
".ExportDialog .ProjectName .TextInput--readonly",
|
||||
);
|
||||
expect(textInput?.textContent).toEqual(name);
|
||||
expect(textInput?.nodeName).toBe("SPAN");
|
||||
@ -166,7 +166,8 @@ describe("<Excalidraw/>", () => {
|
||||
<Excalidraw UIOptions={{ canvasActions: { export: false } }} />,
|
||||
);
|
||||
|
||||
expect(queryByTestId(container, "export-button")).toBeNull();
|
||||
expect(queryByTestId(container, "json-export-button")).toBeNull();
|
||||
expect(queryByTestId(container, "image-export-button")).toBeNull();
|
||||
});
|
||||
|
||||
it("should hide load button when loadScene is false", async () => {
|
||||
|
Loading…
x
Reference in New Issue
Block a user