- {actionManager.renderAction("changeExportBackground")}
- {someElementIsSelected && (
-
setExportSelected(checked)}
- >
- {t("labels.onlySelected")}
-
+
+
{t("imageExportDialog.header")}
+
+
+ {renderError && }
+
+
+ {!nativeFileSystemSupported && (
+ {
+ setProjectName(event.target.value);
+ actionManager.executeAction(
+ actionChangeProjectName,
+ "ui",
+ event.target.value,
+ );
+ }}
+ />
)}
- {actionManager.renderAction("changeExportEmbedScene")}
-
-
- {actionManager.renderAction("changeExportScale")}
-
-
- {t("buttons.scale")}
-
-
-
- {!nativeFileSystemSupported &&
- actionManager.renderAction("changeProjectName")}
-
-
-
- onExportImage(EXPORT_IMAGE_TYPES.png, exportedElements)
- }
- >
- PNG
-
-
- onExportImage(EXPORT_IMAGE_TYPES.svg, exportedElements)
- }
- >
- SVG
-
- {/* firefox supports clipboard API under a flag,
- so let's throw and tell people what they can do */}
- {(probablySupportsClipboardBlob || isFirefox) && (
-
- onExportImage(EXPORT_IMAGE_TYPES.clipboard, exportedElements)
- }
- color="gray"
- shade={7}
+
+
{t("imageExportDialog.header")}
+ {someElementIsSelected && (
+
- {clipboard}
-
+ {
+ setExportSelected(checked);
+ }}
+ />
+
)}
-
+
+ {
+ setExportWithBackground(checked);
+ actionManager.executeAction(
+ actionChangeExportBackground,
+ "ui",
+ checked,
+ );
+ }}
+ />
+
+ {supportsContextFilters && (
+
+ {
+ setExportDarkMode(checked);
+ actionManager.executeAction(
+ actionExportWithDarkMode,
+ "ui",
+ checked,
+ );
+ }}
+ />
+
+ )}
+
+ {
+ setEmbedScene(checked);
+ actionManager.executeAction(
+ actionChangeExportEmbedScene,
+ "ui",
+ checked,
+ );
+ }}
+ />
+
+
+ {
+ setExportScale(scale);
+ actionManager.executeAction(actionChangeExportScale, "ui", scale);
+ }}
+ choices={EXPORT_SCALES.map((scale) => ({
+ value: scale,
+ label: `${scale}\u00d7`,
+ }))}
+ />
+
+
+
+
+
+ {(probablySupportsClipboardBlob || isFirefox) && (
+
+ )}
+
+
+
+ );
+};
+
+type ExportSettingProps = {
+ label: string;
+ children: React.ReactNode;
+ tooltip?: string;
+ name?: string;
+};
+
+const ExportSetting = ({
+ label,
+ children,
+ tooltip,
+ name,
+}: ExportSettingProps) => {
+ return (
+
+
+
+ {children}
+
);
};
@@ -225,7 +327,7 @@ export const ImageExportDialog = ({
}
return (
-
void;
label: string;
isNameEditable: boolean;
+ ignoreFocus?: boolean;
};
export const ProjectName = (props: Props) => {
@@ -19,7 +20,9 @@ export const ProjectName = (props: Props) => {
const [fileName, setFileName] = useState
(props.value);
const handleBlur = (event: any) => {
- focusNearestParent(event.target);
+ if (!props.ignoreFocus) {
+ focusNearestParent(event.target);
+ }
const value = event.target.value;
if (value !== props.value) {
props.onChange(value);
diff --git a/src/components/RadioGroup.scss b/src/components/RadioGroup.scss
new file mode 100644
index 00000000..86064826
--- /dev/null
+++ b/src/components/RadioGroup.scss
@@ -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;
+ }
+ }
+ }
+}
diff --git a/src/components/RadioGroup.tsx b/src/components/RadioGroup.tsx
new file mode 100644
index 00000000..40c6551f
--- /dev/null
+++ b/src/components/RadioGroup.tsx
@@ -0,0 +1,42 @@
+import clsx from "clsx";
+import "./RadioGroup.scss";
+
+export type RadioGroupChoice = {
+ value: T;
+ label: string;
+};
+
+export type RadioGroupProps = {
+ choices: RadioGroupChoice[];
+ value: T;
+ onChange: (value: T) => void;
+ name: string;
+};
+
+export const RadioGroup = function ({
+ onChange,
+ value,
+ choices,
+ name,
+}: RadioGroupProps) {
+ return (
+
+ {choices.map((choice) => (
+
+ onChange(choice.value)}
+ />
+ {choice.label}
+
+ ))}
+
+ );
+};
diff --git a/src/components/Switch.scss b/src/components/Switch.scss
new file mode 100644
index 00000000..ab98bad6
--- /dev/null
+++ b/src/components/Switch.scss
@@ -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;
+ }
+ }
+ }
+}
diff --git a/src/components/Switch.tsx b/src/components/Switch.tsx
new file mode 100644
index 00000000..dfbf332f
--- /dev/null
+++ b/src/components/Switch.tsx
@@ -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 (
+
+ onChange(!checked)}
+ onKeyDown={(e) => {
+ if (e.key === " ") {
+ onChange(!checked);
+ }
+ }}
+ />
+
+ );
+};
diff --git a/src/components/icons.tsx b/src/components/icons.tsx
index 784e8102..3841d030 100644
--- a/src/components/icons.tsx
+++ b/src/components/icons.tsx
@@ -1550,3 +1550,32 @@ export const handIcon = createIcon(
,
tablerIconProps,
);
+
+export const downloadIcon = createIcon(
+ <>
+
+
+
+
+ >,
+ tablerIconProps,
+);
+
+export const copyIcon = createIcon(
+ <>
+
+
+
+ >,
+ tablerIconProps,
+);
+
+export const helpIcon = createIcon(
+ <>
+
+
+
+
+ >,
+ tablerIconProps,
+);
diff --git a/src/excalidraw-app/collab/RoomDialog.tsx b/src/excalidraw-app/collab/RoomDialog.tsx
index 4810b5a5..05772774 100644
--- a/src/excalidraw-app/collab/RoomDialog.tsx
+++ b/src/excalidraw-app/collab/RoomDialog.tsx
@@ -180,7 +180,7 @@ const RoomDialog = ({
};
return (
", () => {
toggleMenu(container);
fireEvent.click(queryByTestId(container, "image-export-button")!);
const textInput: HTMLInputElement | null = document.querySelector(
- ".ExportDialog .ProjectName .TextInput",
+ ".ImageExportModal .ImageExportModal__preview__filename .TextInput",
);
expect(textInput?.value).toContain(`${t("labels.untitled")}`);
expect(textInput?.nodeName).toBe("INPUT");
@@ -303,10 +303,11 @@ describe("", () => {
toggleMenu(container);
await fireEvent.click(queryByTestId(container, "image-export-button")!);
const textInput = document.querySelector(
- ".ExportDialog .ProjectName .TextInput--readonly",
- );
- expect(textInput?.textContent).toEqual(name);
- expect(textInput?.nodeName).toBe("SPAN");
+ ".ImageExportModal .ImageExportModal__preview__filename .TextInput",
+ ) as HTMLInputElement;
+ expect(textInput?.value).toEqual(name);
+ expect(textInput?.nodeName).toBe("INPUT");
+ expect(textInput?.disabled).toBe(true);
});
});