mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-11-17 19:24:30 +01:00
feat: Settings menu
This commit is contained in:
@@ -37,6 +37,7 @@ export const AppMainMenu: React.FC<{
|
|||||||
)}
|
)}
|
||||||
<MainMenu.DefaultItems.CommandPalette className="highlighted" />
|
<MainMenu.DefaultItems.CommandPalette className="highlighted" />
|
||||||
<MainMenu.DefaultItems.SearchMenu />
|
<MainMenu.DefaultItems.SearchMenu />
|
||||||
|
<MainMenu.DefaultItems.SettingsMenu />
|
||||||
<MainMenu.DefaultItems.Help />
|
<MainMenu.DefaultItems.Help />
|
||||||
<MainMenu.DefaultItems.ClearCanvas />
|
<MainMenu.DefaultItems.ClearCanvas />
|
||||||
<MainMenu.Separator />
|
<MainMenu.Separator />
|
||||||
|
|||||||
@@ -1326,7 +1326,7 @@ type FEATURE_FLAGS = {
|
|||||||
COMPLEX_BINDINGS: boolean;
|
COMPLEX_BINDINGS: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const FEATURE_FLAGS_STORAGE_KEY = "feature-flags";
|
const FEATURE_FLAGS_STORAGE_KEY = "excalidraw-feature-flags";
|
||||||
const DEFAULT_FEATURE_FLAGS: FEATURE_FLAGS = {
|
const DEFAULT_FEATURE_FLAGS: FEATURE_FLAGS = {
|
||||||
COMPLEX_BINDINGS: false,
|
COMPLEX_BINDINGS: false,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ import { ImageExportDialog } from "./ImageExportDialog";
|
|||||||
import { Island } from "./Island";
|
import { Island } from "./Island";
|
||||||
import { JSONExportDialog } from "./JSONExportDialog";
|
import { JSONExportDialog } from "./JSONExportDialog";
|
||||||
import { LaserPointerButton } from "./LaserPointerButton";
|
import { LaserPointerButton } from "./LaserPointerButton";
|
||||||
|
import { Settings } from "./Settings";
|
||||||
|
|
||||||
import "./LayerUI.scss";
|
import "./LayerUI.scss";
|
||||||
import "./Toolbar.scss";
|
import "./Toolbar.scss";
|
||||||
@@ -548,6 +549,7 @@ const LayerUI = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<ActiveConfirmDialog />
|
<ActiveConfirmDialog />
|
||||||
|
<Settings />
|
||||||
{appState.openDialog?.name === "elementLinkSelector" && (
|
{appState.openDialog?.name === "elementLinkSelector" && (
|
||||||
<ElementLinkDialog
|
<ElementLinkDialog
|
||||||
sourceElementId={appState.openDialog.sourceElementId}
|
sourceElementId={appState.openDialog.sourceElementId}
|
||||||
|
|||||||
59
packages/excalidraw/components/Settings/Settings.scss
Normal file
59
packages/excalidraw/components/Settings/Settings.scss
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
.settings-dialog {
|
||||||
|
.Dialog__close {
|
||||||
|
margin-top: 30px;
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-category {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-category-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-category-items {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-item {
|
||||||
|
padding: 0.5rem 0 0.5rem 1rem;
|
||||||
|
color: var(--popup-text-color);
|
||||||
|
height: 2.5rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: var(--border-radius-lg);
|
||||||
|
|
||||||
|
.Checkbox {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-right: 0;
|
||||||
|
|
||||||
|
.Checkbox-box {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
130
packages/excalidraw/components/Settings/Settings.tsx
Normal file
130
packages/excalidraw/components/Settings/Settings.tsx
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { getFeatureFlag, setFeatureFlag } from "@excalidraw/common";
|
||||||
|
|
||||||
|
import { CheckboxItem } from "../CheckboxItem";
|
||||||
|
import { Dialog } from "../Dialog";
|
||||||
|
import { CloseIcon } from "../icons";
|
||||||
|
import { useExcalidrawSetAppState } from "../App";
|
||||||
|
import { useUIAppState } from "../../context/ui-appState";
|
||||||
|
import { t } from "../../i18n";
|
||||||
|
|
||||||
|
import "./Settings.scss";
|
||||||
|
|
||||||
|
export const DEFAULT_SETTINGS_CATEGORIES = {
|
||||||
|
experimental: t("settings.experimental"),
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCategoryOrder = (category: string) => {
|
||||||
|
switch (category) {
|
||||||
|
case DEFAULT_SETTINGS_CATEGORIES.experimental:
|
||||||
|
return 1;
|
||||||
|
default:
|
||||||
|
return 10;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
type SettingItem = {
|
||||||
|
label: string;
|
||||||
|
category: string;
|
||||||
|
flagKey: "COMPLEX_BINDINGS";
|
||||||
|
getValue: () => boolean;
|
||||||
|
setValue: (value: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Settings = () => {
|
||||||
|
const uiAppState = useUIAppState();
|
||||||
|
const setAppState = useExcalidrawSetAppState();
|
||||||
|
|
||||||
|
const settings: SettingItem[] = [
|
||||||
|
{
|
||||||
|
label: t("settings.binding"),
|
||||||
|
category: DEFAULT_SETTINGS_CATEGORIES.experimental,
|
||||||
|
flagKey: "COMPLEX_BINDINGS",
|
||||||
|
getValue: () => getFeatureFlag("COMPLEX_BINDINGS"),
|
||||||
|
setValue: (value: boolean) => setFeatureFlag("COMPLEX_BINDINGS", value),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const [settingStates, setSettingStates] = useState<Record<string, boolean>>(
|
||||||
|
() => {
|
||||||
|
const initialStates: Record<string, boolean> = {};
|
||||||
|
settings.forEach((setting) => {
|
||||||
|
initialStates[setting.flagKey] = setting.getValue();
|
||||||
|
});
|
||||||
|
return initialStates;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (uiAppState.openDialog?.name !== "settings") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeSettings = () => {
|
||||||
|
setAppState({
|
||||||
|
openDialog: null,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggle = (setting: SettingItem, checked: boolean) => {
|
||||||
|
setting.setValue(checked);
|
||||||
|
setSettingStates((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[setting.flagKey]: checked,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const settingsByCategory = settings
|
||||||
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
getCategoryOrder(a.category) - getCategoryOrder(b.category) ||
|
||||||
|
a.label.localeCompare(b.label),
|
||||||
|
)
|
||||||
|
.reduce((acc, setting) => {
|
||||||
|
if (!acc[setting.category]) {
|
||||||
|
acc[setting.category] = [];
|
||||||
|
}
|
||||||
|
acc[setting.category].push(setting);
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, SettingItem[]>);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
onCloseRequest={closeSettings}
|
||||||
|
closeOnClickOutside
|
||||||
|
title={t("settings.title")}
|
||||||
|
size={720}
|
||||||
|
autofocus
|
||||||
|
className="settings-dialog"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="Dialog__close"
|
||||||
|
onClick={closeSettings}
|
||||||
|
title={t("buttons.close")}
|
||||||
|
aria-label={t("buttons.close")}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{CloseIcon}
|
||||||
|
</button>
|
||||||
|
<div className="settings-content">
|
||||||
|
{Object.entries(settingsByCategory).map(([category, items]) => (
|
||||||
|
<div key={category} className="settings-category">
|
||||||
|
<div className="settings-category-title">{category}</div>
|
||||||
|
<div className="settings-category-items">
|
||||||
|
{items.map((setting) => (
|
||||||
|
<div key={setting.flagKey} className="settings-item">
|
||||||
|
<CheckboxItem
|
||||||
|
checked={settingStates[setting.flagKey] ?? false}
|
||||||
|
onChange={(checked) => handleToggle(setting, checked)}
|
||||||
|
>
|
||||||
|
{setting.label}
|
||||||
|
</CheckboxItem>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
1
packages/excalidraw/components/Settings/index.tsx
Normal file
1
packages/excalidraw/components/Settings/index.tsx
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { Settings } from "./Settings";
|
||||||
@@ -522,6 +522,16 @@ export const ExportIcon = createIcon(
|
|||||||
modifiedTablerIconProps,
|
modifiedTablerIconProps,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// tabler-icons: settings
|
||||||
|
export const settingsIcon = createIcon(
|
||||||
|
<g strokeWidth="1.5">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z" />
|
||||||
|
<circle cx="12" cy="12" r="3" />
|
||||||
|
</g>,
|
||||||
|
tablerIconProps,
|
||||||
|
);
|
||||||
|
|
||||||
export const HelpIcon = createIcon(
|
export const HelpIcon = createIcon(
|
||||||
<g strokeWidth="1.5">
|
<g strokeWidth="1.5">
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import {
|
|||||||
MoonIcon,
|
MoonIcon,
|
||||||
save,
|
save,
|
||||||
searchIcon,
|
searchIcon,
|
||||||
|
settingsIcon,
|
||||||
SunIcon,
|
SunIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
usersIcon,
|
usersIcon,
|
||||||
@@ -170,6 +171,26 @@ export const SearchMenu = (opts?: { className?: string }) => {
|
|||||||
};
|
};
|
||||||
SearchMenu.displayName = "SearchMenu";
|
SearchMenu.displayName = "SearchMenu";
|
||||||
|
|
||||||
|
export const SettingsMenu = (opts?: { className?: string }) => {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const setAppState = useExcalidrawSetAppState();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem
|
||||||
|
icon={settingsIcon}
|
||||||
|
data-testid="settings-menu-button"
|
||||||
|
onSelect={() => {
|
||||||
|
setAppState({ openDialog: { name: "settings" } });
|
||||||
|
}}
|
||||||
|
aria-label={t("settings.title")}
|
||||||
|
className={opts?.className}
|
||||||
|
>
|
||||||
|
{t("settings.title")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
SettingsMenu.displayName = "SettingsMenu";
|
||||||
|
|
||||||
export const Help = () => {
|
export const Help = () => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
|||||||
@@ -198,6 +198,11 @@
|
|||||||
"frames": "Frames",
|
"frames": "Frames",
|
||||||
"texts": "Texts"
|
"texts": "Texts"
|
||||||
},
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "Settings",
|
||||||
|
"experimental": "Experimental",
|
||||||
|
"binding": "Complex bindings"
|
||||||
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Reset the canvas",
|
"clearReset": "Reset the canvas",
|
||||||
"exportJSON": "Export to file",
|
"exportJSON": "Export to file",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -95,141 +95,3 @@ exports[`move element > rectangle 5`] = `
|
|||||||
"y": 40,
|
"y": 40,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`move element > rectangles with binding arrow 5`] = `
|
|
||||||
{
|
|
||||||
"angle": 0,
|
|
||||||
"backgroundColor": "transparent",
|
|
||||||
"boundElements": [
|
|
||||||
{
|
|
||||||
"id": "id6",
|
|
||||||
"type": "arrow",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"customData": undefined,
|
|
||||||
"fillStyle": "solid",
|
|
||||||
"frameId": null,
|
|
||||||
"groupIds": [],
|
|
||||||
"height": 100,
|
|
||||||
"id": "id0",
|
|
||||||
"index": "a0",
|
|
||||||
"isDeleted": false,
|
|
||||||
"link": null,
|
|
||||||
"locked": false,
|
|
||||||
"opacity": 100,
|
|
||||||
"roughness": 1,
|
|
||||||
"roundness": null,
|
|
||||||
"seed": 1278240551,
|
|
||||||
"strokeColor": "#1e1e1e",
|
|
||||||
"strokeStyle": "solid",
|
|
||||||
"strokeWidth": 2,
|
|
||||||
"type": "rectangle",
|
|
||||||
"updated": 1,
|
|
||||||
"version": 4,
|
|
||||||
"versionNonce": 640725609,
|
|
||||||
"width": 100,
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`move element > rectangles with binding arrow 6`] = `
|
|
||||||
{
|
|
||||||
"angle": 0,
|
|
||||||
"backgroundColor": "transparent",
|
|
||||||
"boundElements": [
|
|
||||||
{
|
|
||||||
"id": "id6",
|
|
||||||
"type": "arrow",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"customData": undefined,
|
|
||||||
"fillStyle": "solid",
|
|
||||||
"frameId": null,
|
|
||||||
"groupIds": [],
|
|
||||||
"height": 300,
|
|
||||||
"id": "id3",
|
|
||||||
"index": "a1",
|
|
||||||
"isDeleted": false,
|
|
||||||
"link": null,
|
|
||||||
"locked": false,
|
|
||||||
"opacity": 100,
|
|
||||||
"roughness": 1,
|
|
||||||
"roundness": null,
|
|
||||||
"seed": 1116226695,
|
|
||||||
"strokeColor": "#1e1e1e",
|
|
||||||
"strokeStyle": "solid",
|
|
||||||
"strokeWidth": 2,
|
|
||||||
"type": "rectangle",
|
|
||||||
"updated": 1,
|
|
||||||
"version": 7,
|
|
||||||
"versionNonce": 1051383431,
|
|
||||||
"width": 300,
|
|
||||||
"x": 201,
|
|
||||||
"y": 2,
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`move element > rectangles with binding arrow 7`] = `
|
|
||||||
{
|
|
||||||
"angle": 0,
|
|
||||||
"backgroundColor": "transparent",
|
|
||||||
"boundElements": null,
|
|
||||||
"customData": undefined,
|
|
||||||
"elbowed": false,
|
|
||||||
"endArrowhead": "arrow",
|
|
||||||
"endBinding": {
|
|
||||||
"elementId": "id3",
|
|
||||||
"fixedPoint": [
|
|
||||||
"-0.03333",
|
|
||||||
"0.43333",
|
|
||||||
],
|
|
||||||
"mode": "orbit",
|
|
||||||
},
|
|
||||||
"fillStyle": "solid",
|
|
||||||
"frameId": null,
|
|
||||||
"groupIds": [],
|
|
||||||
"height": "90.03375",
|
|
||||||
"id": "id6",
|
|
||||||
"index": "a2",
|
|
||||||
"isDeleted": false,
|
|
||||||
"link": null,
|
|
||||||
"locked": false,
|
|
||||||
"moveMidPointsWithElement": false,
|
|
||||||
"opacity": 100,
|
|
||||||
"points": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
89,
|
|
||||||
"90.03375",
|
|
||||||
],
|
|
||||||
],
|
|
||||||
"roughness": 1,
|
|
||||||
"roundness": {
|
|
||||||
"type": 2,
|
|
||||||
},
|
|
||||||
"seed": 23633383,
|
|
||||||
"startArrowhead": null,
|
|
||||||
"startBinding": {
|
|
||||||
"elementId": "id0",
|
|
||||||
"fixedPoint": [
|
|
||||||
"1.10000",
|
|
||||||
"0.50010",
|
|
||||||
],
|
|
||||||
"mode": "orbit",
|
|
||||||
},
|
|
||||||
"strokeColor": "#1e1e1e",
|
|
||||||
"strokeStyle": "solid",
|
|
||||||
"strokeWidth": 2,
|
|
||||||
"type": "arrow",
|
|
||||||
"updated": 1,
|
|
||||||
"version": 9,
|
|
||||||
"versionNonce": 1996028265,
|
|
||||||
"width": 89,
|
|
||||||
"x": 106,
|
|
||||||
"y": "46.01049",
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|||||||
@@ -373,6 +373,7 @@ export interface AppState {
|
|||||||
| { name: "imageExport" | "help" | "jsonExport" }
|
| { name: "imageExport" | "help" | "jsonExport" }
|
||||||
| { name: "ttd"; tab: "text-to-diagram" | "mermaid" }
|
| { name: "ttd"; tab: "text-to-diagram" | "mermaid" }
|
||||||
| { name: "commandPalette" }
|
| { name: "commandPalette" }
|
||||||
|
| { name: "settings" }
|
||||||
| { name: "elementLinkSelector"; sourceElementId: ExcalidrawElement["id"] };
|
| { name: "elementLinkSelector"; sourceElementId: ExcalidrawElement["id"] };
|
||||||
/**
|
/**
|
||||||
* Reflects user preference for whether the default sidebar should be docked.
|
* Reflects user preference for whether the default sidebar should be docked.
|
||||||
|
|||||||
Reference in New Issue
Block a user