feat: update design of ImageExportDialog (#6614)
Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
parent
6459ccda6a
commit
08563e7d7b
@ -26,7 +26,7 @@ export const actionChangeProjectName = register({
|
|||||||
perform: (_elements, appState, value) => {
|
perform: (_elements, appState, value) => {
|
||||||
return { appState: { ...appState, name: value }, commitToHistory: false };
|
return { appState: { ...appState, name: value }, commitToHistory: false };
|
||||||
},
|
},
|
||||||
PanelComponent: ({ appState, updateData, appProps }) => (
|
PanelComponent: ({ appState, updateData, appProps, data }) => (
|
||||||
<ProjectName
|
<ProjectName
|
||||||
label={t("labels.fileTitle")}
|
label={t("labels.fileTitle")}
|
||||||
value={appState.name || "Unnamed"}
|
value={appState.name || "Unnamed"}
|
||||||
@ -34,6 +34,7 @@ export const actionChangeProjectName = register({
|
|||||||
isNameEditable={
|
isNameEditable={
|
||||||
typeof appProps.name === "undefined" && !appState.viewModeEnabled
|
typeof appProps.name === "undefined" && !appState.viewModeEnabled
|
||||||
}
|
}
|
||||||
|
ignoreFocus={data?.ignoreFocus ?? false}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
@ -118,10 +118,13 @@ export class ActionManager {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
executeAction(action: Action, source: ActionSource = "api") {
|
executeAction(
|
||||||
|
action: Action,
|
||||||
|
source: ActionSource = "api",
|
||||||
|
value: any = null,
|
||||||
|
) {
|
||||||
const elements = this.getElementsIncludingDeleted();
|
const elements = this.getElementsIncludingDeleted();
|
||||||
const appState = this.getAppState();
|
const appState = this.getAppState();
|
||||||
const value = null;
|
|
||||||
|
|
||||||
trackAction(action, source, appState, elements, this.app, value);
|
trackAction(action, source, appState, elements, this.app, value);
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ const ConfirmDialog = (props: Props) => {
|
|||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
onCloseRequest={onCancel}
|
onCloseRequest={onCancel}
|
||||||
small={true}
|
size="small"
|
||||||
{...rest}
|
{...rest}
|
||||||
className={`confirm-dialog ${className}`}
|
className={`confirm-dialog ${className}`}
|
||||||
>
|
>
|
||||||
|
@ -14,4 +14,33 @@
|
|||||||
padding: 0 0 0.75rem;
|
padding: 0 0 0.75rem;
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Dialog__close {
|
||||||
|
color: var(--color-gray-40);
|
||||||
|
margin: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: 0.75rem;
|
||||||
|
right: 0.5rem;
|
||||||
|
border: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
line-height: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--color-gray-60);
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
color: var(--color-gray-40);
|
||||||
|
}
|
||||||
|
|
||||||
|
@include isMobile {
|
||||||
|
top: 1.25rem;
|
||||||
|
right: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,9 +21,9 @@ import { jotaiScope } from "../jotai";
|
|||||||
export interface DialogProps {
|
export interface DialogProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
small?: boolean;
|
size?: "small" | "regular" | "wide";
|
||||||
onCloseRequest(): void;
|
onCloseRequest(): void;
|
||||||
title: React.ReactNode;
|
title: React.ReactNode | false;
|
||||||
autofocus?: boolean;
|
autofocus?: boolean;
|
||||||
theme?: AppState["theme"];
|
theme?: AppState["theme"];
|
||||||
closeOnClickOutside?: boolean;
|
closeOnClickOutside?: boolean;
|
||||||
@ -33,6 +33,7 @@ export const Dialog = (props: DialogProps) => {
|
|||||||
const [islandNode, setIslandNode] = useCallbackRefState<HTMLDivElement>();
|
const [islandNode, setIslandNode] = useCallbackRefState<HTMLDivElement>();
|
||||||
const [lastActiveElement] = useState(document.activeElement);
|
const [lastActiveElement] = useState(document.activeElement);
|
||||||
const { id } = useExcalidrawContainer();
|
const { id } = useExcalidrawContainer();
|
||||||
|
const device = useDevice();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!islandNode) {
|
if (!islandNode) {
|
||||||
@ -86,23 +87,27 @@ export const Dialog = (props: DialogProps) => {
|
|||||||
<Modal
|
<Modal
|
||||||
className={clsx("Dialog", props.className)}
|
className={clsx("Dialog", props.className)}
|
||||||
labelledBy="dialog-title"
|
labelledBy="dialog-title"
|
||||||
maxWidth={props.small ? 550 : 800}
|
maxWidth={
|
||||||
|
props.size === "wide" ? 1024 : props.size === "small" ? 550 : 800
|
||||||
|
}
|
||||||
onCloseRequest={onClose}
|
onCloseRequest={onClose}
|
||||||
theme={props.theme}
|
theme={props.theme}
|
||||||
closeOnClickOutside={props.closeOnClickOutside}
|
closeOnClickOutside={props.closeOnClickOutside}
|
||||||
>
|
>
|
||||||
<Island ref={setIslandNode}>
|
<Island ref={setIslandNode}>
|
||||||
<h2 id={`${id}-dialog-title`} className="Dialog__title">
|
{props.title && (
|
||||||
<span className="Dialog__titleContent">{props.title}</span>
|
<h2 id={`${id}-dialog-title`} className="Dialog__title">
|
||||||
<button
|
<span className="Dialog__titleContent">{props.title}</span>
|
||||||
className="Modal__close"
|
</h2>
|
||||||
onClick={onClose}
|
)}
|
||||||
title={t("buttons.close")}
|
<button
|
||||||
aria-label={t("buttons.close")}
|
className="Dialog__close"
|
||||||
>
|
onClick={onClose}
|
||||||
{useDevice().isMobile ? back : CloseIcon}
|
title={t("buttons.close")}
|
||||||
</button>
|
aria-label={t("buttons.close")}
|
||||||
</h2>
|
>
|
||||||
|
{device.isMobile ? back : CloseIcon}
|
||||||
|
</button>
|
||||||
<div className="Dialog__content">{props.children}</div>
|
<div className="Dialog__content">{props.children}</div>
|
||||||
</Island>
|
</Island>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@ -28,7 +28,7 @@ export const ErrorDialog = ({
|
|||||||
<>
|
<>
|
||||||
{modalIsShown && (
|
{modalIsShown && (
|
||||||
<Dialog
|
<Dialog
|
||||||
small
|
size="small"
|
||||||
onCloseRequest={handleClose}
|
onCloseRequest={handleClose}
|
||||||
title={t("errorDialog.title")}
|
title={t("errorDialog.title")}
|
||||||
>
|
>
|
||||||
|
215
src/components/ImageExportDialog.scss
Normal file
215
src/components/ImageExportDialog.scss
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
@import "../css/variables.module";
|
||||||
|
|
||||||
|
.excalidraw {
|
||||||
|
--ImageExportModal-preview-border: #d6d6d6;
|
||||||
|
|
||||||
|
&.theme--dark {
|
||||||
|
--ImageExportModal-preview-border: #5c5c5c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ImageExportModal {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
& h3 {
|
||||||
|
font-family: "Assistant";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.313rem;
|
||||||
|
line-height: 130%;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
@include isMobile {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > h3 {
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
@include isMobile {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include isMobile {
|
||||||
|
flex-direction: column;
|
||||||
|
height: calc(100vh - 5rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__preview {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
height: 360px;
|
||||||
|
width: 55%;
|
||||||
|
|
||||||
|
margin-right: 1.5rem;
|
||||||
|
|
||||||
|
@include isMobile {
|
||||||
|
max-width: unset;
|
||||||
|
margin-right: unset;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: unset;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__filename {
|
||||||
|
& > input {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__canvas {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMUlEQVQ4T2NkYGAQYcAP3uCTZhw1gGGYhAGBZIA/nYDCgBDAm9BGDWAAJyRCgLaBCAAgXwixzAS0pgAAAABJRU5ErkJggg==")
|
||||||
|
left center;
|
||||||
|
|
||||||
|
border: 1px solid var(--ImageExportModal-preview-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
& > canvas {
|
||||||
|
max-width: calc(100% - 2rem);
|
||||||
|
max-height: calc(100% - 2rem);
|
||||||
|
|
||||||
|
filter: none !important;
|
||||||
|
|
||||||
|
@include isMobile {
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include isMobile {
|
||||||
|
margin-top: 24px;
|
||||||
|
max-width: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__settings {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 18px;
|
||||||
|
|
||||||
|
@include isMobile {
|
||||||
|
margin-left: unset;
|
||||||
|
margin-top: 1rem;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 6px 34px;
|
||||||
|
|
||||||
|
align-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__setting {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
@include isMobile {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: start;
|
||||||
|
justify-content: unset;
|
||||||
|
height: 52px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
font-family: "Assistant";
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 150%;
|
||||||
|
|
||||||
|
& svg {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__buttons {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 11px;
|
||||||
|
|
||||||
|
align-items: flex-end;
|
||||||
|
align-content: flex-end;
|
||||||
|
|
||||||
|
@include isMobile {
|
||||||
|
padding-top: 32px;
|
||||||
|
flex-basis: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__button {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: fit-content;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
height: 40px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
user-select: none;
|
||||||
|
font-family: "Assistant";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 100%;
|
||||||
|
transition: 150ms ease-out;
|
||||||
|
transition-property: background, color;
|
||||||
|
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: var(--color-icon-white);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--color-primary-darker);
|
||||||
|
color: var(--color-icon-white);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: var(--color-primary-darkest);
|
||||||
|
}
|
||||||
|
|
||||||
|
& > svg {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,25 +1,39 @@
|
|||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
import type { ActionManager } from "../actions/manager";
|
||||||
|
import type { AppClassProperties, BinaryFiles, UIAppState } from "../types";
|
||||||
|
|
||||||
|
import {
|
||||||
|
actionExportWithDarkMode,
|
||||||
|
actionChangeExportBackground,
|
||||||
|
actionChangeExportEmbedScene,
|
||||||
|
actionChangeExportScale,
|
||||||
|
actionChangeProjectName,
|
||||||
|
} from "../actions/actionExport";
|
||||||
import { probablySupportsClipboardBlob } from "../clipboard";
|
import { probablySupportsClipboardBlob } from "../clipboard";
|
||||||
import { canvasToBlob } from "../data/blob";
|
|
||||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
|
||||||
import { t } from "../i18n";
|
|
||||||
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
|
||||||
import { AppClassProperties, BinaryFiles, UIAppState } from "../types";
|
|
||||||
import { Dialog } from "./Dialog";
|
|
||||||
import { clipboard } from "./icons";
|
|
||||||
import Stack from "./Stack";
|
|
||||||
import OpenColor from "open-color";
|
|
||||||
import { CheckboxItem } from "./CheckboxItem";
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_EXPORT_PADDING,
|
DEFAULT_EXPORT_PADDING,
|
||||||
EXPORT_IMAGE_TYPES,
|
EXPORT_IMAGE_TYPES,
|
||||||
isFirefox,
|
isFirefox,
|
||||||
|
EXPORT_SCALES,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
|
|
||||||
|
import { canvasToBlob } from "../data/blob";
|
||||||
import { nativeFileSystemSupported } from "../data/filesystem";
|
import { nativeFileSystemSupported } from "../data/filesystem";
|
||||||
import { ActionManager } from "../actions/manager";
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
|
import { t } from "../i18n";
|
||||||
|
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
||||||
import { exportToCanvas } from "../packages/utils";
|
import { exportToCanvas } from "../packages/utils";
|
||||||
|
|
||||||
import "./ExportDialog.scss";
|
import { copyIcon, downloadIcon, helpIcon } from "./icons";
|
||||||
|
import { Button } from "./Button";
|
||||||
|
import { Dialog } from "./Dialog";
|
||||||
|
import { RadioGroup } from "./RadioGroup";
|
||||||
|
import { Switch } from "./Switch";
|
||||||
|
import { Tooltip } from "./Tooltip";
|
||||||
|
|
||||||
|
import "./ImageExportDialog.scss";
|
||||||
|
import { useAppProps } from "./App";
|
||||||
|
|
||||||
const supportsContextFilters =
|
const supportsContextFilters =
|
||||||
"filter" in document.createElement("canvas").getContext("2d")!;
|
"filter" in document.createElement("canvas").getContext("2d")!;
|
||||||
@ -36,50 +50,36 @@ export const ErrorCanvasPreview = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ExportCB = (
|
type ImageExportModalProps = {
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
|
||||||
scale?: number,
|
|
||||||
) => void;
|
|
||||||
|
|
||||||
const ExportButton: React.FC<{
|
|
||||||
color: keyof OpenColor;
|
|
||||||
onClick: () => void;
|
|
||||||
title: string;
|
|
||||||
shade?: number;
|
|
||||||
children?: React.ReactNode;
|
|
||||||
}> = ({ 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,
|
|
||||||
files,
|
|
||||||
actionManager,
|
|
||||||
onExportImage,
|
|
||||||
}: {
|
|
||||||
appState: UIAppState;
|
appState: UIAppState;
|
||||||
elements: readonly NonDeletedExcalidrawElement[];
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
files: BinaryFiles;
|
files: BinaryFiles;
|
||||||
actionManager: ActionManager;
|
actionManager: ActionManager;
|
||||||
onExportImage: AppClassProperties["onExportImage"];
|
onExportImage: AppClassProperties["onExportImage"];
|
||||||
}) => {
|
};
|
||||||
|
|
||||||
|
const ImageExportModal = ({
|
||||||
|
appState,
|
||||||
|
elements,
|
||||||
|
files,
|
||||||
|
actionManager,
|
||||||
|
onExportImage,
|
||||||
|
}: ImageExportModalProps) => {
|
||||||
|
const appProps = useAppProps();
|
||||||
|
const [projectName, setProjectName] = useState(appState.name);
|
||||||
|
|
||||||
const someElementIsSelected = isSomeElementSelected(elements, appState);
|
const someElementIsSelected = isSomeElementSelected(elements, appState);
|
||||||
|
|
||||||
const [exportSelected, setExportSelected] = useState(someElementIsSelected);
|
const [exportSelected, setExportSelected] = useState(someElementIsSelected);
|
||||||
|
const [exportWithBackground, setExportWithBackground] = useState(
|
||||||
|
appState.exportBackground,
|
||||||
|
);
|
||||||
|
const [exportDarkMode, setExportDarkMode] = useState(
|
||||||
|
appState.exportWithDarkMode,
|
||||||
|
);
|
||||||
|
const [embedScene, setEmbedScene] = useState(appState.exportEmbedScene);
|
||||||
|
const [exportScale, setExportScale] = useState(appState.exportScale);
|
||||||
|
|
||||||
const previewRef = useRef<HTMLDivElement>(null);
|
const previewRef = useRef<HTMLDivElement>(null);
|
||||||
const [renderError, setRenderError] = useState<Error | null>(null);
|
const [renderError, setRenderError] = useState<Error | null>(null);
|
||||||
|
|
||||||
@ -93,6 +93,7 @@ const ImageExportModal = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const maxWidth = previewNode.offsetWidth;
|
const maxWidth = previewNode.offsetWidth;
|
||||||
|
const maxHeight = previewNode.offsetHeight;
|
||||||
if (!maxWidth) {
|
if (!maxWidth) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -101,7 +102,7 @@ const ImageExportModal = ({
|
|||||||
appState,
|
appState,
|
||||||
files,
|
files,
|
||||||
exportPadding: DEFAULT_EXPORT_PADDING,
|
exportPadding: DEFAULT_EXPORT_PADDING,
|
||||||
maxWidthOrHeight: maxWidth,
|
maxWidthOrHeight: Math.max(maxWidth, maxHeight),
|
||||||
})
|
})
|
||||||
.then((canvas) => {
|
.then((canvas) => {
|
||||||
setRenderError(null);
|
setRenderError(null);
|
||||||
@ -118,89 +119,190 @@ const ImageExportModal = ({
|
|||||||
}, [appState, files, exportedElements]);
|
}, [appState, files, exportedElements]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ExportDialog">
|
<div className="ImageExportModal">
|
||||||
<div className="ExportDialog__preview" ref={previewRef}>
|
<h3>{t("imageExportDialog.header")}</h3>
|
||||||
{renderError && <ErrorCanvasPreview />}
|
<div className="ImageExportModal__preview">
|
||||||
</div>
|
<div className="ImageExportModal__preview__canvas" ref={previewRef}>
|
||||||
{supportsContextFilters &&
|
{renderError && <ErrorCanvasPreview />}
|
||||||
actionManager.renderAction("exportWithDarkMode")}
|
</div>
|
||||||
<div style={{ display: "grid", gridTemplateColumns: "1fr" }}>
|
<div className="ImageExportModal__preview__filename">
|
||||||
<div
|
{!nativeFileSystemSupported && (
|
||||||
style={{
|
<input
|
||||||
display: "grid",
|
type="text"
|
||||||
gridTemplateColumns: "repeat(auto-fit, minmax(190px, 1fr))",
|
className="TextInput"
|
||||||
// dunno why this is needed, but when the items wrap it creates
|
value={projectName}
|
||||||
// an overflow
|
style={{ width: "30ch" }}
|
||||||
overflow: "hidden",
|
disabled={
|
||||||
}}
|
typeof appProps.name !== "undefined" || appState.viewModeEnabled
|
||||||
>
|
}
|
||||||
{actionManager.renderAction("changeExportBackground")}
|
onChange={(event) => {
|
||||||
{someElementIsSelected && (
|
setProjectName(event.target.value);
|
||||||
<CheckboxItem
|
actionManager.executeAction(
|
||||||
checked={exportSelected}
|
actionChangeProjectName,
|
||||||
onChange={(checked) => setExportSelected(checked)}
|
"ui",
|
||||||
>
|
event.target.value,
|
||||||
{t("labels.onlySelected")}
|
);
|
||||||
</CheckboxItem>
|
}}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{actionManager.renderAction("changeExportEmbedScene")}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: "flex", alignItems: "center", marginTop: ".6em" }}>
|
<div className="ImageExportModal__settings">
|
||||||
<Stack.Row gap={2}>
|
<h3>{t("imageExportDialog.header")}</h3>
|
||||||
{actionManager.renderAction("changeExportScale")}
|
{someElementIsSelected && (
|
||||||
</Stack.Row>
|
<ExportSetting
|
||||||
<p style={{ marginLeft: "1em", userSelect: "none" }}>
|
label={t("imageExportDialog.label.onlySelected")}
|
||||||
{t("buttons.scale")}
|
name="exportOnlySelected"
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
margin: ".6em 0",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{!nativeFileSystemSupported &&
|
|
||||||
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={() =>
|
|
||||||
onExportImage(EXPORT_IMAGE_TYPES.png, exportedElements)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
PNG
|
|
||||||
</ExportButton>
|
|
||||||
<ExportButton
|
|
||||||
color="red"
|
|
||||||
title={t("buttons.exportToSvg")}
|
|
||||||
aria-label={t("buttons.exportToSvg")}
|
|
||||||
onClick={() =>
|
|
||||||
onExportImage(EXPORT_IMAGE_TYPES.svg, exportedElements)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
SVG
|
|
||||||
</ExportButton>
|
|
||||||
{/* firefox supports clipboard API under a flag,
|
|
||||||
so let's throw and tell people what they can do */}
|
|
||||||
{(probablySupportsClipboardBlob || isFirefox) && (
|
|
||||||
<ExportButton
|
|
||||||
title={t("buttons.copyPngToClipboard")}
|
|
||||||
onClick={() =>
|
|
||||||
onExportImage(EXPORT_IMAGE_TYPES.clipboard, exportedElements)
|
|
||||||
}
|
|
||||||
color="gray"
|
|
||||||
shade={7}
|
|
||||||
>
|
>
|
||||||
{clipboard}
|
<Switch
|
||||||
</ExportButton>
|
name="exportOnlySelected"
|
||||||
|
checked={exportSelected}
|
||||||
|
onChange={(checked) => {
|
||||||
|
setExportSelected(checked);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ExportSetting>
|
||||||
)}
|
)}
|
||||||
</Stack.Row>
|
<ExportSetting
|
||||||
|
label={t("imageExportDialog.label.withBackground")}
|
||||||
|
name="exportBackgroundSwitch"
|
||||||
|
>
|
||||||
|
<Switch
|
||||||
|
name="exportBackgroundSwitch"
|
||||||
|
checked={exportWithBackground}
|
||||||
|
onChange={(checked) => {
|
||||||
|
setExportWithBackground(checked);
|
||||||
|
actionManager.executeAction(
|
||||||
|
actionChangeExportBackground,
|
||||||
|
"ui",
|
||||||
|
checked,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ExportSetting>
|
||||||
|
{supportsContextFilters && (
|
||||||
|
<ExportSetting
|
||||||
|
label={t("imageExportDialog.label.darkMode")}
|
||||||
|
name="exportDarkModeSwitch"
|
||||||
|
>
|
||||||
|
<Switch
|
||||||
|
name="exportDarkModeSwitch"
|
||||||
|
checked={exportDarkMode}
|
||||||
|
onChange={(checked) => {
|
||||||
|
setExportDarkMode(checked);
|
||||||
|
actionManager.executeAction(
|
||||||
|
actionExportWithDarkMode,
|
||||||
|
"ui",
|
||||||
|
checked,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ExportSetting>
|
||||||
|
)}
|
||||||
|
<ExportSetting
|
||||||
|
label={t("imageExportDialog.label.embedScene")}
|
||||||
|
tooltip={t("imageExportDialog.tooltip.embedScene")}
|
||||||
|
name="exportEmbedSwitch"
|
||||||
|
>
|
||||||
|
<Switch
|
||||||
|
name="exportEmbedSwitch"
|
||||||
|
checked={embedScene}
|
||||||
|
onChange={(checked) => {
|
||||||
|
setEmbedScene(checked);
|
||||||
|
actionManager.executeAction(
|
||||||
|
actionChangeExportEmbedScene,
|
||||||
|
"ui",
|
||||||
|
checked,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ExportSetting>
|
||||||
|
<ExportSetting
|
||||||
|
label={t("imageExportDialog.label.scale")}
|
||||||
|
name="exportScale"
|
||||||
|
>
|
||||||
|
<RadioGroup
|
||||||
|
name="exportScale"
|
||||||
|
value={exportScale}
|
||||||
|
onChange={(scale) => {
|
||||||
|
setExportScale(scale);
|
||||||
|
actionManager.executeAction(actionChangeExportScale, "ui", scale);
|
||||||
|
}}
|
||||||
|
choices={EXPORT_SCALES.map((scale) => ({
|
||||||
|
value: scale,
|
||||||
|
label: `${scale}\u00d7`,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</ExportSetting>
|
||||||
|
|
||||||
|
<div className="ImageExportModal__settings__buttons">
|
||||||
|
<Button
|
||||||
|
className="ImageExportModal__settings__buttons__button"
|
||||||
|
title={t("imageExportDialog.title.exportToPng")}
|
||||||
|
aria-label={t("imageExportDialog.title.exportToPng")}
|
||||||
|
onSelect={() =>
|
||||||
|
onExportImage(EXPORT_IMAGE_TYPES.png, exportedElements)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{downloadIcon} {t("imageExportDialog.button.exportToPng")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="ImageExportModal__settings__buttons__button"
|
||||||
|
title={t("imageExportDialog.title.exportToSvg")}
|
||||||
|
aria-label={t("imageExportDialog.title.exportToSvg")}
|
||||||
|
onSelect={() =>
|
||||||
|
onExportImage(EXPORT_IMAGE_TYPES.svg, exportedElements)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{downloadIcon} {t("imageExportDialog.button.exportToSvg")}
|
||||||
|
</Button>
|
||||||
|
{(probablySupportsClipboardBlob || isFirefox) && (
|
||||||
|
<Button
|
||||||
|
className="ImageExportModal__settings__buttons__button"
|
||||||
|
title={t("imageExportDialog.title.copyPngToClipboard")}
|
||||||
|
aria-label={t("imageExportDialog.title.copyPngToClipboard")}
|
||||||
|
onSelect={() =>
|
||||||
|
onExportImage(EXPORT_IMAGE_TYPES.clipboard, exportedElements)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{copyIcon} {t("imageExportDialog.button.copyPngToClipboard")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type ExportSettingProps = {
|
||||||
|
label: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
tooltip?: string;
|
||||||
|
name?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ExportSetting = ({
|
||||||
|
label,
|
||||||
|
children,
|
||||||
|
tooltip,
|
||||||
|
name,
|
||||||
|
}: ExportSettingProps) => {
|
||||||
|
return (
|
||||||
|
<div className="ImageExportModal__settings__setting" title={label}>
|
||||||
|
<label
|
||||||
|
htmlFor={name}
|
||||||
|
className="ImageExportModal__settings__setting__label"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
{tooltip && (
|
||||||
|
<Tooltip label={tooltip} long={true}>
|
||||||
|
{helpIcon}
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
<div className="ImageExportModal__settings__setting__content">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -225,7 +327,7 @@ export const ImageExportDialog = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog onCloseRequest={onCloseRequest} title={t("buttons.exportImage")}>
|
<Dialog onCloseRequest={onCloseRequest} size="wide" title={false}>
|
||||||
<ImageExportModal
|
<ImageExportModal
|
||||||
elements={elements}
|
elements={elements}
|
||||||
appState={appState}
|
appState={appState}
|
||||||
|
@ -106,7 +106,7 @@ export const LibraryDropdownMenuButton: React.FC<{
|
|||||||
onCloseRequest={() => setPublishLibSuccess(null)}
|
onCloseRequest={() => setPublishLibSuccess(null)}
|
||||||
title={t("publishSuccessDialog.title")}
|
title={t("publishSuccessDialog.title")}
|
||||||
className="publish-library-success"
|
className="publish-library-success"
|
||||||
small={true}
|
size="small"
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
<Trans
|
<Trans
|
||||||
|
@ -24,13 +24,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.Modal__background {
|
.Modal__background {
|
||||||
position: absolute;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
background-color: rgba(#121212, 0.2);
|
background-color: rgba(#121212, 0.2);
|
||||||
|
|
||||||
|
animation: Modal__background__fade-in 0.125s linear forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Modal__content {
|
.Modal__content {
|
||||||
@ -65,14 +67,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes Modal__content_fade-in {
|
@keyframes Modal__background__fade-in {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(10px);
|
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes Modal__content_fade-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ export const Modal: React.FC<{
|
|||||||
<div
|
<div
|
||||||
className="Modal__background"
|
className="Modal__background"
|
||||||
onClick={closeOnClickOutside ? props.onCloseRequest : undefined}
|
onClick={closeOnClickOutside ? props.onCloseRequest : undefined}
|
||||||
></div>
|
/>
|
||||||
<div
|
<div
|
||||||
className="Modal__content"
|
className="Modal__content"
|
||||||
style={{ "--max-width": `${props.maxWidth}px` }}
|
style={{ "--max-width": `${props.maxWidth}px` }}
|
||||||
|
@ -106,7 +106,7 @@ export const PasteChartDialog = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
small
|
size="small"
|
||||||
onCloseRequest={handleClose}
|
onCloseRequest={handleClose}
|
||||||
title={t("labels.pasteCharts")}
|
title={t("labels.pasteCharts")}
|
||||||
className={"PasteChartDialog"}
|
className={"PasteChartDialog"}
|
||||||
|
@ -12,6 +12,7 @@ type Props = {
|
|||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
label: string;
|
label: string;
|
||||||
isNameEditable: boolean;
|
isNameEditable: boolean;
|
||||||
|
ignoreFocus?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ProjectName = (props: Props) => {
|
export const ProjectName = (props: Props) => {
|
||||||
@ -19,7 +20,9 @@ export const ProjectName = (props: Props) => {
|
|||||||
const [fileName, setFileName] = useState<string>(props.value);
|
const [fileName, setFileName] = useState<string>(props.value);
|
||||||
|
|
||||||
const handleBlur = (event: any) => {
|
const handleBlur = (event: any) => {
|
||||||
focusNearestParent(event.target);
|
if (!props.ignoreFocus) {
|
||||||
|
focusNearestParent(event.target);
|
||||||
|
}
|
||||||
const value = event.target.value;
|
const value = event.target.value;
|
||||||
if (value !== props.value) {
|
if (value !== props.value) {
|
||||||
props.onChange(value);
|
props.onChange(value);
|
||||||
|
100
src/components/RadioGroup.scss
Normal file
100
src/components/RadioGroup.scss
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
@import "../css/variables.module";
|
||||||
|
|
||||||
|
.excalidraw {
|
||||||
|
--RadioGroup-background: #ffffff;
|
||||||
|
--RadioGroup-border: var(--color-gray-30);
|
||||||
|
|
||||||
|
--RadioGroup-choice-color-off: var(--color-primary);
|
||||||
|
--RadioGroup-choice-color-off-hover: var(--color-primary-darkest);
|
||||||
|
--RadioGroup-choice-background-off: white;
|
||||||
|
--RadioGroup-choice-background-off-active: var(--color-gray-20);
|
||||||
|
|
||||||
|
--RadioGroup-choice-color-on: white;
|
||||||
|
--RadioGroup-choice-background-on: var(--color-primary);
|
||||||
|
--RadioGroup-choice-background-on-hover: var(--color-primary-darker);
|
||||||
|
--RadioGroup-choice-background-on-active: var(--color-primary-darkest);
|
||||||
|
|
||||||
|
&.theme--dark {
|
||||||
|
--RadioGroup-background: var(--color-gray-85);
|
||||||
|
--RadioGroup-border: var(--color-gray-70);
|
||||||
|
|
||||||
|
--RadioGroup-choice-background-off: var(--color-gray-85);
|
||||||
|
--RadioGroup-choice-background-off-active: var(--color-gray-70);
|
||||||
|
--RadioGroup-choice-color-on: var(--color-gray-85);
|
||||||
|
}
|
||||||
|
|
||||||
|
.RadioGroup {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
padding: 3px;
|
||||||
|
border-radius: 10px;
|
||||||
|
|
||||||
|
background: var(--RadioGroup-background);
|
||||||
|
border: 1px solid var(--RadioGroup-border);
|
||||||
|
|
||||||
|
&__choice {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 24px;
|
||||||
|
|
||||||
|
color: var(--RadioGroup-choice-color-off);
|
||||||
|
background: var(--RadioGroup-choice-background-off);
|
||||||
|
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
font-family: "Assistant";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 100%;
|
||||||
|
user-select: none;
|
||||||
|
letter-spacing: 0.4px;
|
||||||
|
|
||||||
|
transition: all 75ms ease-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--RadioGroup-choice-color-off-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: var(--RadioGroup-choice-background-off-active);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: var(--RadioGroup-choice-color-on);
|
||||||
|
background: var(--RadioGroup-choice-background-on);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--RadioGroup-choice-background-on-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: var(--RadioGroup-choice-background-on-active);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& input {
|
||||||
|
z-index: 1;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
42
src/components/RadioGroup.tsx
Normal file
42
src/components/RadioGroup.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import "./RadioGroup.scss";
|
||||||
|
|
||||||
|
export type RadioGroupChoice<T> = {
|
||||||
|
value: T;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RadioGroupProps<T> = {
|
||||||
|
choices: RadioGroupChoice<T>[];
|
||||||
|
value: T;
|
||||||
|
onChange: (value: T) => void;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RadioGroup = function <T>({
|
||||||
|
onChange,
|
||||||
|
value,
|
||||||
|
choices,
|
||||||
|
name,
|
||||||
|
}: RadioGroupProps<T>) {
|
||||||
|
return (
|
||||||
|
<div className="RadioGroup">
|
||||||
|
{choices.map((choice) => (
|
||||||
|
<div
|
||||||
|
className={clsx("RadioGroup__choice", {
|
||||||
|
active: choice.value === value,
|
||||||
|
})}
|
||||||
|
key={choice.label}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
name={name}
|
||||||
|
type="radio"
|
||||||
|
checked={choice.value === value}
|
||||||
|
onChange={() => onChange(choice.value)}
|
||||||
|
/>
|
||||||
|
{choice.label}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
116
src/components/Switch.scss
Normal file
116
src/components/Switch.scss
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
@import "../css/variables.module";
|
||||||
|
|
||||||
|
.excalidraw {
|
||||||
|
--Switch-disabled-color: #d6d6d6;
|
||||||
|
--Switch-track-background: white;
|
||||||
|
--Switch-thumb-background: #3d3d3d;
|
||||||
|
|
||||||
|
&.theme--dark {
|
||||||
|
--Switch-disabled-color: #5c5c5c;
|
||||||
|
--Switch-track-background: #242424;
|
||||||
|
--Switch-thumb-background: #b8b8b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Switch {
|
||||||
|
position: relative;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
width: 40px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
|
||||||
|
transition-property: background, border;
|
||||||
|
transition-duration: 150ms;
|
||||||
|
transition-timing-function: ease-out;
|
||||||
|
|
||||||
|
background: var(--Switch-track-background);
|
||||||
|
border: 1px solid var(--Switch-disabled-color);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--Switch-track-background);
|
||||||
|
border: 1px solid #999999;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.toggled {
|
||||||
|
background: var(--color-primary);
|
||||||
|
border: 1px solid var(--color-primary);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--color-primary-darker);
|
||||||
|
border: 1px solid var(--color-primary-darker);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
background: var(--Switch-track-background);
|
||||||
|
border: 1px solid var(--Switch-disabled-color);
|
||||||
|
|
||||||
|
&.toggled {
|
||||||
|
background: var(--Switch-disabled-color);
|
||||||
|
border: 1px solid var(--Switch-disabled-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: block;
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
border-radius: 100%;
|
||||||
|
transition: all 150ms ease-out;
|
||||||
|
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
top: 4px;
|
||||||
|
left: 4px;
|
||||||
|
|
||||||
|
background: var(--Switch-thumb-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:before {
|
||||||
|
width: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.toggled:before {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
left: 22px;
|
||||||
|
top: 2px;
|
||||||
|
|
||||||
|
background: var(--Switch-track-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.toggled:active:before {
|
||||||
|
width: 16px;
|
||||||
|
left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled:before {
|
||||||
|
background: var(--Switch-disabled-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled.toggled:before {
|
||||||
|
background: var(--color-gray-50);
|
||||||
|
}
|
||||||
|
|
||||||
|
& input {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
border-radius: 12px;
|
||||||
|
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
cursor: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
38
src/components/Switch.tsx
Normal file
38
src/components/Switch.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
import "./Switch.scss";
|
||||||
|
|
||||||
|
export type SwitchProps = {
|
||||||
|
name: string;
|
||||||
|
checked: boolean;
|
||||||
|
title?: string;
|
||||||
|
onChange: (value: boolean) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Switch = ({
|
||||||
|
title,
|
||||||
|
name,
|
||||||
|
checked,
|
||||||
|
onChange,
|
||||||
|
disabled = false,
|
||||||
|
}: SwitchProps) => {
|
||||||
|
return (
|
||||||
|
<div className={clsx("Switch", { toggled: checked, disabled })}>
|
||||||
|
<input
|
||||||
|
name={name}
|
||||||
|
id={name}
|
||||||
|
title={title}
|
||||||
|
type="checkbox"
|
||||||
|
checked={checked}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={() => onChange(!checked)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === " ") {
|
||||||
|
onChange(!checked);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -1550,3 +1550,32 @@ export const handIcon = createIcon(
|
|||||||
</g>,
|
</g>,
|
||||||
tablerIconProps,
|
tablerIconProps,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const downloadIcon = createIcon(
|
||||||
|
<>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2v-2"></path>
|
||||||
|
<path d="M7 11l5 5l5 -5"></path>
|
||||||
|
<path d="M12 4l0 12"></path>
|
||||||
|
</>,
|
||||||
|
tablerIconProps,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const copyIcon = createIcon(
|
||||||
|
<>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path>
|
||||||
|
<path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path>
|
||||||
|
</>,
|
||||||
|
tablerIconProps,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const helpIcon = createIcon(
|
||||||
|
<>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0"></path>
|
||||||
|
<path d="M12 17l0 .01"></path>
|
||||||
|
<path d="M12 13.5a1.5 1.5 0 0 1 1 -1.5a2.6 2.6 0 1 0 -3 -4"></path>
|
||||||
|
</>,
|
||||||
|
tablerIconProps,
|
||||||
|
);
|
||||||
|
@ -180,7 +180,7 @@ const RoomDialog = ({
|
|||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
small
|
size="small"
|
||||||
onCloseRequest={handleClose}
|
onCloseRequest={handleClose}
|
||||||
title={t("labels.liveCollaboration")}
|
title={t("labels.liveCollaboration")}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
|
@ -40,10 +40,6 @@
|
|||||||
"arrowhead_triangle": "Triangle",
|
"arrowhead_triangle": "Triangle",
|
||||||
"fontSize": "Font size",
|
"fontSize": "Font size",
|
||||||
"fontFamily": "Font family",
|
"fontFamily": "Font family",
|
||||||
"onlySelected": "Only selected",
|
|
||||||
"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\"",
|
"addWatermark": "Add \"Made with Excalidraw\"",
|
||||||
"handDrawn": "Hand-drawn",
|
"handDrawn": "Hand-drawn",
|
||||||
"normal": "Normal",
|
"normal": "Normal",
|
||||||
@ -100,7 +96,6 @@
|
|||||||
"flipHorizontal": "Flip horizontal",
|
"flipHorizontal": "Flip horizontal",
|
||||||
"flipVertical": "Flip vertical",
|
"flipVertical": "Flip vertical",
|
||||||
"viewMode": "View mode",
|
"viewMode": "View mode",
|
||||||
"toggleExportColorScheme": "Toggle export color scheme",
|
|
||||||
"share": "Share",
|
"share": "Share",
|
||||||
"showStroke": "Show stroke color picker",
|
"showStroke": "Show stroke color picker",
|
||||||
"showBackground": "Show background color picker",
|
"showBackground": "Show background color picker",
|
||||||
@ -140,11 +135,7 @@
|
|||||||
"exportJSON": "Export to file",
|
"exportJSON": "Export to file",
|
||||||
"exportImage": "Export image...",
|
"exportImage": "Export image...",
|
||||||
"export": "Save to...",
|
"export": "Save to...",
|
||||||
"exportToPng": "Export to PNG",
|
|
||||||
"exportToSvg": "Export to SVG",
|
|
||||||
"copyToClipboard": "Copy to clipboard",
|
"copyToClipboard": "Copy to clipboard",
|
||||||
"copyPngToClipboard": "Copy PNG to clipboard",
|
|
||||||
"scale": "Scale",
|
|
||||||
"save": "Save to current file",
|
"save": "Save to current file",
|
||||||
"saveAs": "Save as",
|
"saveAs": "Save as",
|
||||||
"load": "Open",
|
"load": "Open",
|
||||||
@ -363,6 +354,30 @@
|
|||||||
"resetLibrary": "Reset library",
|
"resetLibrary": "Reset library",
|
||||||
"removeItemsFromLib": "Remove selected items from library"
|
"removeItemsFromLib": "Remove selected items from library"
|
||||||
},
|
},
|
||||||
|
"imageExportDialog": {
|
||||||
|
"header": "Export image",
|
||||||
|
"label": {
|
||||||
|
"withBackground": "Background",
|
||||||
|
"onlySelected": "Only selected",
|
||||||
|
"darkMode": "Dark mode",
|
||||||
|
"embedScene": "Embed scene",
|
||||||
|
"scale": "Scale",
|
||||||
|
"padding": "Padding"
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"embedScene": "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."
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"exportToPng": "Export to PNG",
|
||||||
|
"exportToSvg": "Export to SVG",
|
||||||
|
"copyPngToClipboard": "Copy PNG to clipboard"
|
||||||
|
},
|
||||||
|
"button": {
|
||||||
|
"exportToPng": "PNG",
|
||||||
|
"exportToSvg": "SVG",
|
||||||
|
"copyPngToClipboard": "Copy to clipboard"
|
||||||
|
}
|
||||||
|
},
|
||||||
"encrypted": {
|
"encrypted": {
|
||||||
"tooltip": "Your drawings are end-to-end encrypted so Excalidraw's servers will never see them.",
|
"tooltip": "Your drawings are end-to-end encrypted so Excalidraw's servers will never see them.",
|
||||||
"link": "Blog post on end-to-end encryption in Excalidraw"
|
"link": "Blog post on end-to-end encryption in Excalidraw"
|
||||||
|
@ -290,7 +290,7 @@ describe("<Excalidraw/>", () => {
|
|||||||
toggleMenu(container);
|
toggleMenu(container);
|
||||||
fireEvent.click(queryByTestId(container, "image-export-button")!);
|
fireEvent.click(queryByTestId(container, "image-export-button")!);
|
||||||
const textInput: HTMLInputElement | null = document.querySelector(
|
const textInput: HTMLInputElement | null = document.querySelector(
|
||||||
".ExportDialog .ProjectName .TextInput",
|
".ImageExportModal .ImageExportModal__preview__filename .TextInput",
|
||||||
);
|
);
|
||||||
expect(textInput?.value).toContain(`${t("labels.untitled")}`);
|
expect(textInput?.value).toContain(`${t("labels.untitled")}`);
|
||||||
expect(textInput?.nodeName).toBe("INPUT");
|
expect(textInput?.nodeName).toBe("INPUT");
|
||||||
@ -303,10 +303,11 @@ describe("<Excalidraw/>", () => {
|
|||||||
toggleMenu(container);
|
toggleMenu(container);
|
||||||
await fireEvent.click(queryByTestId(container, "image-export-button")!);
|
await fireEvent.click(queryByTestId(container, "image-export-button")!);
|
||||||
const textInput = document.querySelector(
|
const textInput = document.querySelector(
|
||||||
".ExportDialog .ProjectName .TextInput--readonly",
|
".ImageExportModal .ImageExportModal__preview__filename .TextInput",
|
||||||
);
|
) as HTMLInputElement;
|
||||||
expect(textInput?.textContent).toEqual(name);
|
expect(textInput?.value).toEqual(name);
|
||||||
expect(textInput?.nodeName).toBe("SPAN");
|
expect(textInput?.nodeName).toBe("INPUT");
|
||||||
|
expect(textInput?.disabled).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user