feat: overwrite confirmation dialogs (#6658)
Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
parent
6d56634289
commit
7558a4e2be
@ -17,16 +17,34 @@ import { useSetAtom } from "jotai";
|
|||||||
import { isLibraryMenuOpenAtom } from "./LibraryMenu";
|
import { isLibraryMenuOpenAtom } from "./LibraryMenu";
|
||||||
import { jotaiScope } from "../jotai";
|
import { jotaiScope } from "../jotai";
|
||||||
|
|
||||||
|
export type DialogSize = number | "small" | "regular" | "wide" | undefined;
|
||||||
|
|
||||||
export interface DialogProps {
|
export interface DialogProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
size?: "small" | "regular" | "wide";
|
size?: DialogSize;
|
||||||
onCloseRequest(): void;
|
onCloseRequest(): void;
|
||||||
title: React.ReactNode | false;
|
title: React.ReactNode | false;
|
||||||
autofocus?: boolean;
|
autofocus?: boolean;
|
||||||
closeOnClickOutside?: boolean;
|
closeOnClickOutside?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDialogSize(size: DialogSize): number {
|
||||||
|
if (size && typeof size === "number") {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (size) {
|
||||||
|
case "small":
|
||||||
|
return 550;
|
||||||
|
case "wide":
|
||||||
|
return 1024;
|
||||||
|
case "regular":
|
||||||
|
default:
|
||||||
|
return 800;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const Dialog = (props: DialogProps) => {
|
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);
|
||||||
@ -85,9 +103,7 @@ export const Dialog = (props: DialogProps) => {
|
|||||||
<Modal
|
<Modal
|
||||||
className={clsx("Dialog", props.className)}
|
className={clsx("Dialog", props.className)}
|
||||||
labelledBy="dialog-title"
|
labelledBy="dialog-title"
|
||||||
maxWidth={
|
maxWidth={getDialogSize(props.size)}
|
||||||
props.size === "wide" ? 1024 : props.size === "small" ? 550 : 800
|
|
||||||
}
|
|
||||||
onCloseRequest={onClose}
|
onCloseRequest={onClose}
|
||||||
closeOnClickOutside={props.closeOnClickOutside}
|
closeOnClickOutside={props.closeOnClickOutside}
|
||||||
>
|
>
|
||||||
|
@ -2,20 +2,140 @@
|
|||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
.ExcButton {
|
.ExcButton {
|
||||||
&--color-primary {
|
--text-color: transparent;
|
||||||
color: var(--input-bg-color);
|
--border-color: transparent;
|
||||||
|
--back-color: transparent;
|
||||||
|
|
||||||
--accent-color: var(--color-primary);
|
color: var(--text-color);
|
||||||
--accent-color-hover: var(--color-primary-darker);
|
background-color: var(--back-color);
|
||||||
--accent-color-active: var(--color-primary-darkest);
|
border-color: var(--border-color);
|
||||||
|
|
||||||
|
&--color-primary {
|
||||||
|
&.ExcButton--variant-filled {
|
||||||
|
--text-color: var(--input-bg-color);
|
||||||
|
--back-color: var(--color-primary);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
--back-color: var(--color-primary-darker);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
--back-color: var(--color-primary-darkest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ExcButton--variant-outlined,
|
||||||
|
&.ExcButton--variant-icon {
|
||||||
|
--text-color: var(--color-primary);
|
||||||
|
--border-color: var(--color-primary);
|
||||||
|
--back-color: var(--input-bg-color);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
--text-color: var(--color-primary-darker);
|
||||||
|
--border-color: var(--color-primary-darker);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
--text-color: var(--color-primary-darkest);
|
||||||
|
--border-color: var(--color-primary-darkest);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&--color-danger {
|
&--color-danger {
|
||||||
color: var(--input-bg-color);
|
&.ExcButton--variant-filled {
|
||||||
|
--text-color: var(--color-danger-text);
|
||||||
|
--back-color: var(--color-danger-dark);
|
||||||
|
|
||||||
--accent-color: var(--color-danger);
|
&:hover {
|
||||||
--accent-color-hover: #d65550;
|
--back-color: var(--color-danger-darker);
|
||||||
--accent-color-active: #d1413c;
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
--back-color: var(--color-danger-darkest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ExcButton--variant-outlined,
|
||||||
|
&.ExcButton--variant-icon {
|
||||||
|
--text-color: var(--color-danger);
|
||||||
|
--border-color: var(--color-danger);
|
||||||
|
--back-color: transparent;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
--text-color: var(--color-danger-darkest);
|
||||||
|
--border-color: var(--color-danger-darkest);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
--text-color: var(--color-danger-darker);
|
||||||
|
--border-color: var(--color-danger-darker);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--color-muted {
|
||||||
|
&.ExcButton--variant-filled {
|
||||||
|
--text-color: var(--island-bg-color);
|
||||||
|
--back-color: var(--color-gray-50);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
--back-color: var(--color-gray-60);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
--back-color: var(--color-gray-80);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ExcButton--variant-outlined,
|
||||||
|
&.ExcButton--variant-icon {
|
||||||
|
--text-color: var(--color-muted-background);
|
||||||
|
--border-color: var(--color-muted);
|
||||||
|
--back-color: var(--island-bg-color);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
--text-color: var(--color-muted-background-darker);
|
||||||
|
--border-color: var(--color-muted-darker);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
--text-color: var(--color-muted-background-darker);
|
||||||
|
--border-color: var(--color-muted-darkest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--color-warning {
|
||||||
|
&.ExcButton--variant-filled {
|
||||||
|
--text-color: black;
|
||||||
|
--back-color: var(--color-warning-dark);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
--back-color: var(--color-warning-darker);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
--back-color: var(--color-warning-darkest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ExcButton--variant-outlined,
|
||||||
|
&.ExcButton--variant-icon {
|
||||||
|
--text-color: var(--color-warning-dark);
|
||||||
|
--border-color: var(--color-warning-dark);
|
||||||
|
--back-color: var(--input-bg-color);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
--text-color: var(--color-warning-darker);
|
||||||
|
--border-color: var(--color-warning-darker);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
--text-color: var(--color-warning-darkest);
|
||||||
|
--border-color: var(--color-warning-darkest);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -25,6 +145,8 @@
|
|||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
|
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
|
border-width: 1px;
|
||||||
|
border-style: solid;
|
||||||
|
|
||||||
font-family: "Assistant";
|
font-family: "Assistant";
|
||||||
|
|
||||||
@ -33,9 +155,9 @@
|
|||||||
transition: all 150ms ease-out;
|
transition: all 150ms ease-out;
|
||||||
|
|
||||||
&--size-large {
|
&--size-large {
|
||||||
font-weight: 400;
|
font-weight: 600;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
height: 3rem;
|
min-height: 3rem;
|
||||||
padding: 0.5rem 1.5rem;
|
padding: 0.5rem 1.5rem;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
|
|
||||||
@ -45,48 +167,22 @@
|
|||||||
&--size-medium {
|
&--size-medium {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
height: 2.5rem;
|
min-height: 2.5rem;
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
|
||||||
letter-spacing: normal;
|
letter-spacing: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--variant-filled {
|
|
||||||
background: var(--accent-color);
|
|
||||||
border: 1px solid transparent;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--accent-color-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background: var(--accent-color-active);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--variant-outlined,
|
|
||||||
&--variant-icon {
|
|
||||||
border: 1px solid var(--accent-color);
|
|
||||||
color: var(--accent-color);
|
|
||||||
background: transparent;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border: 1px solid var(--accent-color-hover);
|
|
||||||
color: var(--accent-color-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
border: 1px solid var(--accent-color-active);
|
|
||||||
color: var(--accent-color-active);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--variant-icon {
|
&--variant-icon {
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.5rem 0.75rem;
|
||||||
width: 3rem;
|
width: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--fullWidth {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
&__icon {
|
&__icon {
|
||||||
width: 1.25rem;
|
width: 1.25rem;
|
||||||
height: 1.25rem;
|
height: 1.25rem;
|
||||||
|
@ -4,7 +4,7 @@ import clsx from "clsx";
|
|||||||
import "./FilledButton.scss";
|
import "./FilledButton.scss";
|
||||||
|
|
||||||
export type ButtonVariant = "filled" | "outlined" | "icon";
|
export type ButtonVariant = "filled" | "outlined" | "icon";
|
||||||
export type ButtonColor = "primary" | "danger";
|
export type ButtonColor = "primary" | "danger" | "warning" | "muted";
|
||||||
export type ButtonSize = "medium" | "large";
|
export type ButtonSize = "medium" | "large";
|
||||||
|
|
||||||
export type FilledButtonProps = {
|
export type FilledButtonProps = {
|
||||||
@ -17,6 +17,7 @@ export type FilledButtonProps = {
|
|||||||
color?: ButtonColor;
|
color?: ButtonColor;
|
||||||
size?: ButtonSize;
|
size?: ButtonSize;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
fullWidth?: boolean;
|
||||||
|
|
||||||
startIcon?: React.ReactNode;
|
startIcon?: React.ReactNode;
|
||||||
};
|
};
|
||||||
@ -31,6 +32,7 @@ export const FilledButton = forwardRef<HTMLButtonElement, FilledButtonProps>(
|
|||||||
variant = "filled",
|
variant = "filled",
|
||||||
color = "primary",
|
color = "primary",
|
||||||
size = "medium",
|
size = "medium",
|
||||||
|
fullWidth,
|
||||||
className,
|
className,
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
@ -42,6 +44,7 @@ export const FilledButton = forwardRef<HTMLButtonElement, FilledButtonProps>(
|
|||||||
`ExcButton--color-${color}`,
|
`ExcButton--color-${color}`,
|
||||||
`ExcButton--variant-${variant}`,
|
`ExcButton--variant-${variant}`,
|
||||||
`ExcButton--size-${size}`,
|
`ExcButton--size-${size}`,
|
||||||
|
{ "ExcButton--fullWidth": fullWidth },
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
@ -41,6 +41,7 @@ import { jotaiScope } from "../jotai";
|
|||||||
import { Provider, useAtom, useAtomValue } from "jotai";
|
import { Provider, useAtom, useAtomValue } from "jotai";
|
||||||
import MainMenu from "./main-menu/MainMenu";
|
import MainMenu from "./main-menu/MainMenu";
|
||||||
import { ActiveConfirmDialog } from "./ActiveConfirmDialog";
|
import { ActiveConfirmDialog } from "./ActiveConfirmDialog";
|
||||||
|
import { OverwriteConfirmDialog } from "./OverwriteConfirm/OverwriteConfirm";
|
||||||
import { HandButton } from "./HandButton";
|
import { HandButton } from "./HandButton";
|
||||||
import { isHandToolActive } from "../appState";
|
import { isHandToolActive } from "../appState";
|
||||||
import { TunnelsContext, useInitializeTunnels } from "../context/tunnels";
|
import { TunnelsContext, useInitializeTunnels } from "../context/tunnels";
|
||||||
@ -99,6 +100,15 @@ const DefaultMainMenu: React.FC<{
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DefaultOverwriteConfirmDialog = () => {
|
||||||
|
return (
|
||||||
|
<OverwriteConfirmDialog __fallback>
|
||||||
|
<OverwriteConfirmDialog.Actions.SaveToDisk />
|
||||||
|
<OverwriteConfirmDialog.Actions.ExportToImage />
|
||||||
|
</OverwriteConfirmDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const LayerUI = ({
|
const LayerUI = ({
|
||||||
actionManager,
|
actionManager,
|
||||||
appState,
|
appState,
|
||||||
@ -343,6 +353,7 @@ const LayerUI = ({
|
|||||||
>
|
>
|
||||||
{t("toolBar.library")}
|
{t("toolBar.library")}
|
||||||
</DefaultSidebar.Trigger>
|
</DefaultSidebar.Trigger>
|
||||||
|
<DefaultOverwriteConfirmDialog />
|
||||||
{/* ------------------------------------------------------------------ */}
|
{/* ------------------------------------------------------------------ */}
|
||||||
|
|
||||||
{appState.isLoading && <LoadingMessage delay={250} />}
|
{appState.isLoading && <LoadingMessage delay={250} />}
|
||||||
@ -374,6 +385,7 @@ const LayerUI = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<ActiveConfirmDialog />
|
<ActiveConfirmDialog />
|
||||||
|
<tunnels.OverwriteConfirmDialogTunnel.Out />
|
||||||
{renderImageExportDialog()}
|
{renderImageExportDialog()}
|
||||||
{renderJSONExportDialog()}
|
{renderJSONExportDialog()}
|
||||||
{appState.pasteDialog.shown && (
|
{appState.pasteDialog.shown && (
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
.excalidraw {
|
.excalidraw {
|
||||||
&.excalidraw-modal-container {
|
&.excalidraw-modal-container {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 10;
|
z-index: var(--zIndex-modal);
|
||||||
}
|
}
|
||||||
|
|
||||||
.Modal {
|
.Modal {
|
||||||
|
126
src/components/OverwriteConfirm/OverwriteConfirm.scss
Normal file
126
src/components/OverwriteConfirm/OverwriteConfirm.scss
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
@import "../../css/variables.module";
|
||||||
|
|
||||||
|
.excalidraw {
|
||||||
|
.OverwriteConfirm {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
isolation: isolate;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.3125rem;
|
||||||
|
line-height: 130%;
|
||||||
|
align-self: flex-start;
|
||||||
|
|
||||||
|
color: var(--text-primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__Description {
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
@include isMobile {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
padding: 2.5rem;
|
||||||
|
|
||||||
|
background: var(--color-danger-background);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
|
||||||
|
font-family: "Assistant";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 150%;
|
||||||
|
|
||||||
|
color: var(--color-danger-color);
|
||||||
|
|
||||||
|
&__spacer {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 2.5rem;
|
||||||
|
background: var(--color-danger-icon-background);
|
||||||
|
width: 3.5rem;
|
||||||
|
height: 3.5rem;
|
||||||
|
|
||||||
|
padding: 0.75rem;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
color: var(--color-danger-icon-color);
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.OverwriteConfirm__Description--color-warning {
|
||||||
|
background: var(--color-warning-background);
|
||||||
|
color: var(--color-warning-color);
|
||||||
|
|
||||||
|
.OverwriteConfirm__Description__icon {
|
||||||
|
background: var(--color-warning-icon-background);
|
||||||
|
flex: 0 0 auto;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
color: var(--color-warning-icon-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__Actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: stretch;
|
||||||
|
justify-items: stretch;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1.5rem;
|
||||||
|
|
||||||
|
@include isMobile {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__Action {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1.5rem;
|
||||||
|
gap: 0.75rem;
|
||||||
|
flex-basis: 50%;
|
||||||
|
flex-grow: 0;
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
height: 100%;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
line-height: 130%;
|
||||||
|
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
color: var(--text-primary-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
76
src/components/OverwriteConfirm/OverwriteConfirm.tsx
Normal file
76
src/components/OverwriteConfirm/OverwriteConfirm.tsx
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useAtom } from "jotai";
|
||||||
|
|
||||||
|
import { useTunnels } from "../../context/tunnels";
|
||||||
|
import { jotaiScope } from "../../jotai";
|
||||||
|
import { Dialog } from "../Dialog";
|
||||||
|
import { withInternalFallback } from "../hoc/withInternalFallback";
|
||||||
|
import { overwriteConfirmStateAtom } from "./OverwriteConfirmState";
|
||||||
|
|
||||||
|
import { FilledButton } from "../FilledButton";
|
||||||
|
import { alertTriangleIcon } from "../icons";
|
||||||
|
import { Actions, Action } from "./OverwriteConfirmActions";
|
||||||
|
import "./OverwriteConfirm.scss";
|
||||||
|
|
||||||
|
export type OverwriteConfirmDialogProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const OverwriteConfirmDialog = Object.assign(
|
||||||
|
withInternalFallback(
|
||||||
|
"OverwriteConfirmDialog",
|
||||||
|
({ children }: OverwriteConfirmDialogProps) => {
|
||||||
|
const { OverwriteConfirmDialogTunnel } = useTunnels();
|
||||||
|
const [overwriteConfirmState, setState] = useAtom(
|
||||||
|
overwriteConfirmStateAtom,
|
||||||
|
jotaiScope,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!overwriteConfirmState.active) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
overwriteConfirmState.onClose();
|
||||||
|
setState((state) => ({ ...state, active: false }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
overwriteConfirmState.onConfirm();
|
||||||
|
setState((state) => ({ ...state, active: false }));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OverwriteConfirmDialogTunnel.In>
|
||||||
|
<Dialog onCloseRequest={handleClose} title={false} size={916}>
|
||||||
|
<div className="OverwriteConfirm">
|
||||||
|
<h3>{overwriteConfirmState.title}</h3>
|
||||||
|
<div
|
||||||
|
className={`OverwriteConfirm__Description OverwriteConfirm__Description--color-${overwriteConfirmState.color}`}
|
||||||
|
>
|
||||||
|
<div className="OverwriteConfirm__Description__icon">
|
||||||
|
{alertTriangleIcon}
|
||||||
|
</div>
|
||||||
|
<div>{overwriteConfirmState.description}</div>
|
||||||
|
<div className="OverwriteConfirm__Description__spacer"></div>
|
||||||
|
<FilledButton
|
||||||
|
color={overwriteConfirmState.color}
|
||||||
|
size="large"
|
||||||
|
label={overwriteConfirmState.actionLabel}
|
||||||
|
onClick={handleConfirm}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Actions>{children}</Actions>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</OverwriteConfirmDialogTunnel.In>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
{
|
||||||
|
Actions,
|
||||||
|
Action,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export { OverwriteConfirmDialog };
|
85
src/components/OverwriteConfirm/OverwriteConfirmActions.tsx
Normal file
85
src/components/OverwriteConfirm/OverwriteConfirmActions.tsx
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { FilledButton } from "../FilledButton";
|
||||||
|
import { useExcalidrawActionManager, useExcalidrawSetAppState } from "../App";
|
||||||
|
import { actionSaveFileToDisk } from "../../actions";
|
||||||
|
import { useI18n } from "../../i18n";
|
||||||
|
import { actionChangeExportEmbedScene } from "../../actions/actionExport";
|
||||||
|
|
||||||
|
export type ActionProps = {
|
||||||
|
title: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
actionLabel: string;
|
||||||
|
onClick: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Action = ({
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
actionLabel,
|
||||||
|
onClick,
|
||||||
|
}: ActionProps) => {
|
||||||
|
return (
|
||||||
|
<div className="OverwriteConfirm__Actions__Action">
|
||||||
|
<h4>{title}</h4>
|
||||||
|
<div className="OverwriteConfirm__Actions__Action__content">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
<FilledButton
|
||||||
|
variant="outlined"
|
||||||
|
color="muted"
|
||||||
|
label={actionLabel}
|
||||||
|
size="large"
|
||||||
|
fullWidth
|
||||||
|
onClick={onClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ExportToImage = () => {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const actionManager = useExcalidrawActionManager();
|
||||||
|
const setAppState = useExcalidrawSetAppState();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Action
|
||||||
|
title={t("overwriteConfirm.action.exportToImage.title")}
|
||||||
|
actionLabel={t("overwriteConfirm.action.exportToImage.button")}
|
||||||
|
onClick={() => {
|
||||||
|
actionManager.executeAction(actionChangeExportEmbedScene, "ui", true);
|
||||||
|
setAppState({ openDialog: "imageExport" });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("overwriteConfirm.action.exportToImage.description")}
|
||||||
|
</Action>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SaveToDisk = () => {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const actionManager = useExcalidrawActionManager();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Action
|
||||||
|
title={t("overwriteConfirm.action.saveToDisk.title")}
|
||||||
|
actionLabel={t("overwriteConfirm.action.saveToDisk.button")}
|
||||||
|
onClick={() => {
|
||||||
|
actionManager.executeAction(actionSaveFileToDisk, "ui");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("overwriteConfirm.action.saveToDisk.description")}
|
||||||
|
</Action>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Actions = Object.assign(
|
||||||
|
({ children }: { children: React.ReactNode }) => {
|
||||||
|
return <div className="OverwriteConfirm__Actions">{children}</div>;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ExportToImage,
|
||||||
|
SaveToDisk,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export { Actions };
|
46
src/components/OverwriteConfirm/OverwriteConfirmState.ts
Normal file
46
src/components/OverwriteConfirm/OverwriteConfirmState.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { atom } from "jotai";
|
||||||
|
import { jotaiStore } from "../../jotai";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export type OverwriteConfirmState =
|
||||||
|
| {
|
||||||
|
active: true;
|
||||||
|
title: string;
|
||||||
|
description: React.ReactNode;
|
||||||
|
actionLabel: string;
|
||||||
|
color: "danger" | "warning";
|
||||||
|
|
||||||
|
onClose: () => void;
|
||||||
|
onConfirm: () => void;
|
||||||
|
onReject: () => void;
|
||||||
|
}
|
||||||
|
| { active: false };
|
||||||
|
|
||||||
|
export const overwriteConfirmStateAtom = atom<OverwriteConfirmState>({
|
||||||
|
active: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function openConfirmModal({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
actionLabel,
|
||||||
|
color,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
description: React.ReactNode;
|
||||||
|
actionLabel: string;
|
||||||
|
color: "danger" | "warning";
|
||||||
|
}) {
|
||||||
|
return new Promise<boolean>((resolve) => {
|
||||||
|
jotaiStore.set(overwriteConfirmStateAtom, {
|
||||||
|
active: true,
|
||||||
|
onConfirm: () => resolve(true),
|
||||||
|
onClose: () => resolve(false),
|
||||||
|
onReject: () => resolve(false),
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
actionLabel,
|
||||||
|
color,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
91
src/components/ShareableLinkDialog.scss
Normal file
91
src/components/ShareableLinkDialog.scss
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
@import "../css/variables.module";
|
||||||
|
|
||||||
|
.excalidraw {
|
||||||
|
.ShareableLinkDialog {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
|
|
||||||
|
color: var(--text-primary-color);
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background: var(--color-primary-light-darker);
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-family: "Assistant";
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.313rem;
|
||||||
|
line-height: 130%;
|
||||||
|
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__popover {
|
||||||
|
@keyframes RoomDialog__popover__scaleIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
box-sizing: border-box;
|
||||||
|
z-index: 100;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 0.125rem 0.5rem;
|
||||||
|
gap: 0.125rem;
|
||||||
|
|
||||||
|
height: 1.125rem;
|
||||||
|
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.6875rem;
|
||||||
|
|
||||||
|
font-family: "Assistant";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 110%;
|
||||||
|
|
||||||
|
background: var(--color-success-lighter);
|
||||||
|
color: var(--color-success);
|
||||||
|
|
||||||
|
& > svg {
|
||||||
|
width: 0.875rem;
|
||||||
|
height: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
transform-origin: var(--radix-popover-content-transform-origin);
|
||||||
|
animation: RoomDialog__popover__scaleIn 150ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__linkRow {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__description {
|
||||||
|
border-top: 1px solid var(--color-gray-20);
|
||||||
|
|
||||||
|
padding: 0.5rem 0.5rem 0;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 150%;
|
||||||
|
|
||||||
|
& p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
& p + p {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
91
src/components/ShareableLinkDialog.tsx
Normal file
91
src/components/ShareableLinkDialog.tsx
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import { useRef, useState } from "react";
|
||||||
|
import * as Popover from "@radix-ui/react-popover";
|
||||||
|
|
||||||
|
import { copyTextToSystemClipboard } from "../clipboard";
|
||||||
|
import { useI18n } from "../i18n";
|
||||||
|
|
||||||
|
import { Dialog } from "./Dialog";
|
||||||
|
import { TextField } from "./TextField";
|
||||||
|
import { FilledButton } from "./FilledButton";
|
||||||
|
import { copyIcon, tablerCheckIcon } from "./icons";
|
||||||
|
|
||||||
|
import "./ShareableLinkDialog.scss";
|
||||||
|
|
||||||
|
export type ShareableLinkDialogProps = {
|
||||||
|
link: string;
|
||||||
|
|
||||||
|
onCloseRequest: () => void;
|
||||||
|
setErrorMessage: (error: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ShareableLinkDialog = ({
|
||||||
|
link,
|
||||||
|
onCloseRequest,
|
||||||
|
setErrorMessage,
|
||||||
|
}: ShareableLinkDialogProps) => {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const [justCopied, setJustCopied] = useState(false);
|
||||||
|
const timerRef = useRef<number>(0);
|
||||||
|
const ref = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const copyRoomLink = async () => {
|
||||||
|
try {
|
||||||
|
await copyTextToSystemClipboard(link);
|
||||||
|
|
||||||
|
setJustCopied(true);
|
||||||
|
|
||||||
|
if (timerRef.current) {
|
||||||
|
window.clearTimeout(timerRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
timerRef.current = window.setTimeout(() => {
|
||||||
|
setJustCopied(false);
|
||||||
|
}, 3000);
|
||||||
|
} catch (error: any) {
|
||||||
|
setErrorMessage(error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
ref.current?.select();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog onCloseRequest={onCloseRequest} title={false} size="small">
|
||||||
|
<div className="ShareableLinkDialog">
|
||||||
|
<h3>Shareable link</h3>
|
||||||
|
<div className="ShareableLinkDialog__linkRow">
|
||||||
|
<TextField
|
||||||
|
ref={ref}
|
||||||
|
label="Link"
|
||||||
|
readonly
|
||||||
|
fullWidth
|
||||||
|
value={link}
|
||||||
|
selectOnRender
|
||||||
|
/>
|
||||||
|
<Popover.Root open={justCopied}>
|
||||||
|
<Popover.Trigger asChild>
|
||||||
|
<FilledButton
|
||||||
|
size="large"
|
||||||
|
label="Copy link"
|
||||||
|
startIcon={copyIcon}
|
||||||
|
onClick={copyRoomLink}
|
||||||
|
/>
|
||||||
|
</Popover.Trigger>
|
||||||
|
<Popover.Content
|
||||||
|
onOpenAutoFocus={(event) => event.preventDefault()}
|
||||||
|
onCloseAutoFocus={(event) => event.preventDefault()}
|
||||||
|
className="ShareableLinkDialog__popover"
|
||||||
|
side="top"
|
||||||
|
align="end"
|
||||||
|
sideOffset={5.5}
|
||||||
|
>
|
||||||
|
{tablerCheckIcon} copied
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover.Root>
|
||||||
|
</div>
|
||||||
|
<div className="ShareableLinkDialog__description">
|
||||||
|
🔒 {t("alerts.uploadedSecurly")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
@ -1,4 +1,10 @@
|
|||||||
import { forwardRef, useRef, useImperativeHandle, KeyboardEvent } from "react";
|
import {
|
||||||
|
forwardRef,
|
||||||
|
useRef,
|
||||||
|
useImperativeHandle,
|
||||||
|
KeyboardEvent,
|
||||||
|
useLayoutEffect,
|
||||||
|
} from "react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
import "./TextField.scss";
|
import "./TextField.scss";
|
||||||
@ -12,6 +18,7 @@ export type TextFieldProps = {
|
|||||||
|
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
|
selectOnRender?: boolean;
|
||||||
|
|
||||||
label?: string;
|
label?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
@ -19,13 +26,28 @@ export type TextFieldProps = {
|
|||||||
|
|
||||||
export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
||||||
(
|
(
|
||||||
{ value, onChange, label, fullWidth, placeholder, readonly, onKeyDown },
|
{
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
label,
|
||||||
|
fullWidth,
|
||||||
|
placeholder,
|
||||||
|
readonly,
|
||||||
|
selectOnRender,
|
||||||
|
onKeyDown,
|
||||||
|
},
|
||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
const innerRef = useRef<HTMLInputElement | null>(null);
|
const innerRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
useImperativeHandle(ref, () => innerRef.current!);
|
useImperativeHandle(ref, () => innerRef.current!);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (selectOnRender) {
|
||||||
|
innerRef.current?.select();
|
||||||
|
}
|
||||||
|
}, [selectOnRender]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx("ExcTextField", {
|
className={clsx("ExcTextField", {
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
Roboto, Helvetica, Arial, sans-serif;
|
Roboto, Helvetica, Arial, sans-serif;
|
||||||
font-family: var(--ui-font);
|
font-family: var(--ui-font);
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 1000;
|
z-index: var(--zIndex-popup);
|
||||||
|
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
@ -1608,6 +1608,16 @@ export const tablerCheckIcon = createIcon(
|
|||||||
tablerIconProps,
|
tablerIconProps,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const alertTriangleIcon = createIcon(
|
||||||
|
<>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M10.24 3.957l-8.422 14.06a1.989 1.989 0 0 0 1.7 2.983h16.845a1.989 1.989 0 0 0 1.7 -2.983l-8.423 -14.06a1.989 1.989 0 0 0 -3.4 0z" />
|
||||||
|
<path d="M12 9v4" />
|
||||||
|
<path d="M12 17h.01" />
|
||||||
|
</>,
|
||||||
|
tablerIconProps,
|
||||||
|
);
|
||||||
|
|
||||||
export const eyeDropperIcon = createIcon(
|
export const eyeDropperIcon = createIcon(
|
||||||
<g strokeWidth={1.25}>
|
<g strokeWidth={1.25}>
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
import { getShortcutFromShortcutName } from "../../actions/shortcuts";
|
import { getShortcutFromShortcutName } from "../../actions/shortcuts";
|
||||||
import { useI18n } from "../../i18n";
|
import { useI18n } from "../../i18n";
|
||||||
import { useExcalidrawSetAppState, useExcalidrawActionManager } from "../App";
|
import {
|
||||||
|
useExcalidrawSetAppState,
|
||||||
|
useExcalidrawActionManager,
|
||||||
|
useExcalidrawElements,
|
||||||
|
} from "../App";
|
||||||
import {
|
import {
|
||||||
ExportIcon,
|
ExportIcon,
|
||||||
ExportImageIcon,
|
ExportImageIcon,
|
||||||
@ -29,19 +33,42 @@ import { useSetAtom } from "jotai";
|
|||||||
import { activeConfirmDialogAtom } from "../ActiveConfirmDialog";
|
import { activeConfirmDialogAtom } from "../ActiveConfirmDialog";
|
||||||
import { jotaiScope } from "../../jotai";
|
import { jotaiScope } from "../../jotai";
|
||||||
import { useUIAppState } from "../../context/ui-appState";
|
import { useUIAppState } from "../../context/ui-appState";
|
||||||
|
import { openConfirmModal } from "../OverwriteConfirm/OverwriteConfirmState";
|
||||||
|
import Trans from "../Trans";
|
||||||
|
|
||||||
export const LoadScene = () => {
|
export const LoadScene = () => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const actionManager = useExcalidrawActionManager();
|
const actionManager = useExcalidrawActionManager();
|
||||||
|
const elements = useExcalidrawElements();
|
||||||
|
|
||||||
if (!actionManager.isActionEnabled(actionLoadScene)) {
|
if (!actionManager.isActionEnabled(actionLoadScene)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleSelect = async () => {
|
||||||
|
if (
|
||||||
|
!elements.length ||
|
||||||
|
(await openConfirmModal({
|
||||||
|
title: t("overwriteConfirm.modal.loadFromFile.title"),
|
||||||
|
actionLabel: t("overwriteConfirm.modal.loadFromFile.button"),
|
||||||
|
color: "warning",
|
||||||
|
description: (
|
||||||
|
<Trans
|
||||||
|
i18nKey="overwriteConfirm.modal.loadFromFile.description"
|
||||||
|
bold={(text) => <strong>{text}</strong>}
|
||||||
|
br={() => <br />}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
) {
|
||||||
|
actionManager.executeAction(actionLoadScene);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
icon={LoadIcon}
|
icon={LoadIcon}
|
||||||
onSelect={() => actionManager.executeAction(actionLoadScene)}
|
onSelect={handleSelect}
|
||||||
data-testid="load-button"
|
data-testid="load-button"
|
||||||
shortcut={getShortcutFromShortcutName("loadScene")}
|
shortcut={getShortcutFromShortcutName("loadScene")}
|
||||||
aria-label={t("buttons.load")}
|
aria-label={t("buttons.load")}
|
||||||
|
@ -12,6 +12,7 @@ type TunnelsContextValue = {
|
|||||||
FooterCenterTunnel: Tunnel;
|
FooterCenterTunnel: Tunnel;
|
||||||
DefaultSidebarTriggerTunnel: Tunnel;
|
DefaultSidebarTriggerTunnel: Tunnel;
|
||||||
DefaultSidebarTabTriggersTunnel: Tunnel;
|
DefaultSidebarTabTriggersTunnel: Tunnel;
|
||||||
|
OverwriteConfirmDialogTunnel: Tunnel;
|
||||||
jotaiScope: symbol;
|
jotaiScope: symbol;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -30,6 +31,7 @@ export const useInitializeTunnels = () => {
|
|||||||
FooterCenterTunnel: tunnel(),
|
FooterCenterTunnel: tunnel(),
|
||||||
DefaultSidebarTriggerTunnel: tunnel(),
|
DefaultSidebarTriggerTunnel: tunnel(),
|
||||||
DefaultSidebarTabTriggersTunnel: tunnel(),
|
DefaultSidebarTabTriggersTunnel: tunnel(),
|
||||||
|
OverwriteConfirmDialogTunnel: tunnel(),
|
||||||
jotaiScope: Symbol(),
|
jotaiScope: Symbol(),
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
@ -5,6 +5,10 @@
|
|||||||
--zIndex-canvas: 1;
|
--zIndex-canvas: 1;
|
||||||
--zIndex-wysiwyg: 2;
|
--zIndex-wysiwyg: 2;
|
||||||
--zIndex-layerUI: 3;
|
--zIndex-layerUI: 3;
|
||||||
|
|
||||||
|
--zIndex-modal: 1000;
|
||||||
|
--zIndex-popup: 1001;
|
||||||
|
--zIndex-toast: 999999;
|
||||||
}
|
}
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
|
@ -99,9 +99,33 @@
|
|||||||
--color-gray-100: #121212;
|
--color-gray-100: #121212;
|
||||||
|
|
||||||
--color-warning: #fceeca;
|
--color-warning: #fceeca;
|
||||||
|
--color-warning-dark: #f5c354;
|
||||||
|
--color-warning-darker: #f3ab2c;
|
||||||
|
--color-warning-darkest: #ec8b14;
|
||||||
--color-text-warning: var(--text-primary-color);
|
--color-text-warning: var(--text-primary-color);
|
||||||
|
|
||||||
--color-danger: #db6965;
|
--color-danger: #db6965;
|
||||||
|
--color-danger-dark: #db6965;
|
||||||
|
--color-danger-darker: #d65550;
|
||||||
|
--color-danger-darkest: #d1413c;
|
||||||
|
--color-danger-text: black;
|
||||||
|
|
||||||
|
--color-danger-background: #fff0f0;
|
||||||
|
--color-danger-icon-background: #ffdad6;
|
||||||
|
--color-danger-color: #700000;
|
||||||
|
--color-danger-icon-color: #700000;
|
||||||
|
|
||||||
|
--color-warning-background: var(--color-warning);
|
||||||
|
--color-warning-icon-background: var(--color-warning-dark);
|
||||||
|
--color-warning-color: var(--text-primary-color);
|
||||||
|
--color-warning-icon-color: var(--text-primary-color);
|
||||||
|
|
||||||
|
--color-muted: var(--color-gray-30);
|
||||||
|
--color-muted-darker: var(--color-gray-60);
|
||||||
|
--color-muted-darkest: var(--color-gray-100);
|
||||||
|
--color-muted-background: var(--color-gray-80);
|
||||||
|
--color-muted-background-darker: var(--color-gray-100);
|
||||||
|
|
||||||
--color-promo: #e70078;
|
--color-promo: #e70078;
|
||||||
--color-success: #268029;
|
--color-success: #268029;
|
||||||
--color-success-lighter: #cafccc;
|
--color-success-lighter: #cafccc;
|
||||||
@ -177,6 +201,27 @@
|
|||||||
--color-text-warning: var(--color-gray-80);
|
--color-text-warning: var(--color-gray-80);
|
||||||
|
|
||||||
--color-danger: #ffa8a5;
|
--color-danger: #ffa8a5;
|
||||||
|
--color-danger-dark: #672120;
|
||||||
|
--color-danger-darker: #8f2625;
|
||||||
|
--color-danger-darkest: #ac2b29;
|
||||||
|
--color-danger-text: #fbcbcc;
|
||||||
|
|
||||||
|
--color-danger-background: #fbcbcc;
|
||||||
|
--color-danger-icon-background: #672120;
|
||||||
|
--color-danger-color: #261919;
|
||||||
|
--color-danger-icon-color: #fbcbcc;
|
||||||
|
|
||||||
|
--color-warning-background: var(--color-warning);
|
||||||
|
--color-warning-icon-background: var(--color-warning-dark);
|
||||||
|
--color-warning-color: var(--color-gray-80);
|
||||||
|
--color-warning-icon-color: var(--color-gray-80);
|
||||||
|
|
||||||
|
--color-muted: var(--color-gray-80);
|
||||||
|
--color-muted-darker: var(--color-gray-60);
|
||||||
|
--color-muted-darkest: var(--color-gray-20);
|
||||||
|
--color-muted-background: var(--color-gray-40);
|
||||||
|
--color-muted-background-darker: var(--color-gray-20);
|
||||||
|
|
||||||
--color-promo: #d297ff;
|
--color-promo: #d297ff;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ import { MIME_TYPES } from "../../constants";
|
|||||||
import { trackEvent } from "../../analytics";
|
import { trackEvent } from "../../analytics";
|
||||||
import { getFrame } from "../../utils";
|
import { getFrame } from "../../utils";
|
||||||
|
|
||||||
const exportToExcalidrawPlus = async (
|
export const exportToExcalidrawPlus = async (
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
appState: Partial<AppState>,
|
appState: Partial<AppState>,
|
||||||
files: BinaryFiles,
|
files: BinaryFiles,
|
||||||
|
@ -282,11 +282,15 @@ export const loadScene = async (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ExportToBackendResult =
|
||||||
|
| { url: null; errorMessage: string }
|
||||||
|
| { url: string; errorMessage: null };
|
||||||
|
|
||||||
export const exportToBackend = async (
|
export const exportToBackend = async (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: Partial<AppState>,
|
appState: Partial<AppState>,
|
||||||
files: BinaryFiles,
|
files: BinaryFiles,
|
||||||
) => {
|
): Promise<ExportToBackendResult> => {
|
||||||
const encryptionKey = await generateEncryptionKey("string");
|
const encryptionKey = await generateEncryptionKey("string");
|
||||||
|
|
||||||
const payload = await compressData(
|
const payload = await compressData(
|
||||||
@ -327,14 +331,18 @@ export const exportToBackend = async (
|
|||||||
files: filesToUpload,
|
files: filesToUpload,
|
||||||
});
|
});
|
||||||
|
|
||||||
window.prompt(`🔒${t("alerts.uploadedSecurly")}`, urlString);
|
return { url: urlString, errorMessage: null };
|
||||||
} else if (json.error_class === "RequestTooLargeError") {
|
} else if (json.error_class === "RequestTooLargeError") {
|
||||||
window.alert(t("alerts.couldNotCreateShareableLinkTooBig"));
|
return {
|
||||||
} else {
|
url: null,
|
||||||
window.alert(t("alerts.couldNotCreateShareableLink"));
|
errorMessage: t("alerts.couldNotCreateShareableLinkTooBig"),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return { url: null, errorMessage: t("alerts.couldNotCreateShareableLink") };
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
window.alert(t("alerts.couldNotCreateShareableLink"));
|
|
||||||
|
return { url: null, errorMessage: t("alerts.couldNotCreateShareableLink") };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -69,7 +69,10 @@ import {
|
|||||||
} from "./data/localStorage";
|
} from "./data/localStorage";
|
||||||
import CustomStats from "./CustomStats";
|
import CustomStats from "./CustomStats";
|
||||||
import { restore, restoreAppState, RestoredDataState } from "../data/restore";
|
import { restore, restoreAppState, RestoredDataState } from "../data/restore";
|
||||||
import { ExportToExcalidrawPlus } from "./components/ExportToExcalidrawPlus";
|
import {
|
||||||
|
ExportToExcalidrawPlus,
|
||||||
|
exportToExcalidrawPlus,
|
||||||
|
} from "./components/ExportToExcalidrawPlus";
|
||||||
import { updateStaleImageStatuses } from "./data/FileManager";
|
import { updateStaleImageStatuses } from "./data/FileManager";
|
||||||
import { newElementWith } from "../element/mutateElement";
|
import { newElementWith } from "../element/mutateElement";
|
||||||
import { isInitializedImageElement } from "../element/typeChecks";
|
import { isInitializedImageElement } from "../element/typeChecks";
|
||||||
@ -88,6 +91,10 @@ import { appJotaiStore } from "./app-jotai";
|
|||||||
|
|
||||||
import "./index.scss";
|
import "./index.scss";
|
||||||
import { ResolutionType } from "../utility-types";
|
import { ResolutionType } from "../utility-types";
|
||||||
|
import { ShareableLinkDialog } from "../components/ShareableLinkDialog";
|
||||||
|
import { openConfirmModal } from "../components/OverwriteConfirm/OverwriteConfirmState";
|
||||||
|
import { OverwriteConfirmDialog } from "../components/OverwriteConfirm/OverwriteConfirm";
|
||||||
|
import Trans from "../components/Trans";
|
||||||
|
|
||||||
polyfill();
|
polyfill();
|
||||||
|
|
||||||
@ -98,6 +105,19 @@ languageDetector.init({
|
|||||||
languageUtils: {},
|
languageUtils: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const shareableLinkConfirmDialog = {
|
||||||
|
title: t("overwriteConfirm.modal.shareableLink.title"),
|
||||||
|
description: (
|
||||||
|
<Trans
|
||||||
|
i18nKey="overwriteConfirm.modal.shareableLink.description"
|
||||||
|
bold={(text) => <strong>{text}</strong>}
|
||||||
|
br={() => <br />}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
actionLabel: t("overwriteConfirm.modal.shareableLink.button"),
|
||||||
|
color: "danger",
|
||||||
|
} as const;
|
||||||
|
|
||||||
const initializeScene = async (opts: {
|
const initializeScene = async (opts: {
|
||||||
collabAPI: CollabAPI | null;
|
collabAPI: CollabAPI | null;
|
||||||
excalidrawAPI: ExcalidrawImperativeAPI;
|
excalidrawAPI: ExcalidrawImperativeAPI;
|
||||||
@ -129,7 +149,7 @@ const initializeScene = async (opts: {
|
|||||||
// don't prompt for collab scenes because we don't override local storage
|
// don't prompt for collab scenes because we don't override local storage
|
||||||
roomLinkData ||
|
roomLinkData ||
|
||||||
// otherwise, prompt whether user wants to override current scene
|
// otherwise, prompt whether user wants to override current scene
|
||||||
window.confirm(t("alerts.loadSceneOverridePrompt"))
|
(await openConfirmModal(shareableLinkConfirmDialog))
|
||||||
) {
|
) {
|
||||||
if (jsonBackendMatch) {
|
if (jsonBackendMatch) {
|
||||||
scene = await loadScene(
|
scene = await loadScene(
|
||||||
@ -168,7 +188,7 @@ const initializeScene = async (opts: {
|
|||||||
const data = await loadFromBlob(await request.blob(), null, null);
|
const data = await loadFromBlob(await request.blob(), null, null);
|
||||||
if (
|
if (
|
||||||
!scene.elements.length ||
|
!scene.elements.length ||
|
||||||
window.confirm(t("alerts.loadSceneOverridePrompt"))
|
(await openConfirmModal(shareableLinkConfirmDialog))
|
||||||
) {
|
) {
|
||||||
return { scene: data, isExternalScene };
|
return { scene: data, isExternalScene };
|
||||||
}
|
}
|
||||||
@ -554,6 +574,10 @@ const ExcalidrawWrapper = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [latestShareableLink, setLatestShareableLink] = useState<string | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
const onExportToBackend = async (
|
const onExportToBackend = async (
|
||||||
exportedElements: readonly NonDeletedExcalidrawElement[],
|
exportedElements: readonly NonDeletedExcalidrawElement[],
|
||||||
appState: Partial<AppState>,
|
appState: Partial<AppState>,
|
||||||
@ -565,7 +589,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
}
|
}
|
||||||
if (canvas) {
|
if (canvas) {
|
||||||
try {
|
try {
|
||||||
await exportToBackend(
|
const { url, errorMessage } = await exportToBackend(
|
||||||
exportedElements,
|
exportedElements,
|
||||||
{
|
{
|
||||||
...appState,
|
...appState,
|
||||||
@ -575,6 +599,14 @@ const ExcalidrawWrapper = () => {
|
|||||||
},
|
},
|
||||||
files,
|
files,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (errorMessage) {
|
||||||
|
setErrorMessage(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url) {
|
||||||
|
setLatestShareableLink(url);
|
||||||
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error.name !== "AbortError") {
|
if (error.name !== "AbortError") {
|
||||||
const { width, height } = canvas;
|
const { width, height } = canvas;
|
||||||
@ -674,21 +706,47 @@ const ExcalidrawWrapper = () => {
|
|||||||
setCollabDialogShown={setCollabDialogShown}
|
setCollabDialogShown={setCollabDialogShown}
|
||||||
isCollabEnabled={!isCollabDisabled}
|
isCollabEnabled={!isCollabDisabled}
|
||||||
/>
|
/>
|
||||||
|
<OverwriteConfirmDialog>
|
||||||
|
<OverwriteConfirmDialog.Actions.ExportToImage />
|
||||||
|
<OverwriteConfirmDialog.Actions.SaveToDisk />
|
||||||
|
{excalidrawAPI && (
|
||||||
|
<OverwriteConfirmDialog.Action
|
||||||
|
title={t("overwriteConfirm.action.excalidrawPlus.title")}
|
||||||
|
actionLabel={t("overwriteConfirm.action.excalidrawPlus.button")}
|
||||||
|
onClick={() => {
|
||||||
|
exportToExcalidrawPlus(
|
||||||
|
excalidrawAPI.getSceneElements(),
|
||||||
|
excalidrawAPI.getAppState(),
|
||||||
|
excalidrawAPI.getFiles(),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("overwriteConfirm.action.excalidrawPlus.description")}
|
||||||
|
</OverwriteConfirmDialog.Action>
|
||||||
|
)}
|
||||||
|
</OverwriteConfirmDialog>
|
||||||
<AppFooter />
|
<AppFooter />
|
||||||
{isCollaborating && isOffline && (
|
{isCollaborating && isOffline && (
|
||||||
<div className="collab-offline-warning">
|
<div className="collab-offline-warning">
|
||||||
{t("alerts.collabOfflineWarning")}
|
{t("alerts.collabOfflineWarning")}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{latestShareableLink && (
|
||||||
|
<ShareableLinkDialog
|
||||||
|
link={latestShareableLink}
|
||||||
|
onCloseRequest={() => setLatestShareableLink(null)}
|
||||||
|
setErrorMessage={setErrorMessage}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{excalidrawAPI && !isCollabDisabled && (
|
{excalidrawAPI && !isCollabDisabled && (
|
||||||
<Collab excalidrawAPI={excalidrawAPI} />
|
<Collab excalidrawAPI={excalidrawAPI} />
|
||||||
)}
|
)}
|
||||||
|
{errorMessage && (
|
||||||
|
<ErrorDialog onClose={() => setErrorMessage("")}>
|
||||||
|
{errorMessage}
|
||||||
|
</ErrorDialog>
|
||||||
|
)}
|
||||||
</Excalidraw>
|
</Excalidraw>
|
||||||
{errorMessage && (
|
|
||||||
<ErrorDialog onClose={() => setErrorMessage("")}>
|
|
||||||
{errorMessage}
|
|
||||||
</ErrorDialog>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -449,5 +449,36 @@
|
|||||||
"shades": "Shades",
|
"shades": "Shades",
|
||||||
"hexCode": "Hex code",
|
"hexCode": "Hex code",
|
||||||
"noShades": "No shades available for this color"
|
"noShades": "No shades available for this color"
|
||||||
|
},
|
||||||
|
"overwriteConfirm": {
|
||||||
|
"action": {
|
||||||
|
"exportToImage": {
|
||||||
|
"title": "Export as image",
|
||||||
|
"button": "Export as image",
|
||||||
|
"description": "Export the scene data as an image from which you can import later."
|
||||||
|
},
|
||||||
|
"saveToDisk": {
|
||||||
|
"title": "Save to disk",
|
||||||
|
"button": "Save to disk",
|
||||||
|
"description": "Export the scene data to a file from which you can import later."
|
||||||
|
},
|
||||||
|
"excalidrawPlus": {
|
||||||
|
"title": "Excalidraw+",
|
||||||
|
"button": "Export to Excalidraw+",
|
||||||
|
"description": "Save the scene to your Excalidraw+ workspace."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"modal": {
|
||||||
|
"loadFromFile": {
|
||||||
|
"title": "Load from file",
|
||||||
|
"button": "Load from file",
|
||||||
|
"description": "Loading from a file will <bold>replace your existing content</bold>.<br></br>You can back up your drawing first using one of the options below."
|
||||||
|
},
|
||||||
|
"shareableLink": {
|
||||||
|
"title": "Load from link",
|
||||||
|
"button": "Replace my content",
|
||||||
|
"description": "Loading external drawing will <bold>replace your existing content</bold>.<br></br>You can back up your drawing first by using one of the options below."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user