mirror of
				https://github.com/excalidraw/excalidraw.git
				synced 2025-11-04 12:54:23 +01:00 
			
		
		
		
	Compare commits
	
		
			27 Commits
		
	
	
		
			dependabot
			...
			fix-zsvicz
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					b412e742e6 | ||
| 
						 | 
					c246ccf9d9 | ||
| 
						 | 
					3c0b29d85f | ||
| 
						 | 
					bfbaeae67f | ||
| 
						 | 
					74b9885955 | ||
| 
						 | 
					2cbe869a13 | ||
| 
						 | 
					a48607eb25 | ||
| 
						 | 
					7831b6e74b | ||
| 
						 | 
					640affe7c0 | ||
| 
						 | 
					335aff8838 | ||
| 
						 | 
					dc97dc30bf | ||
| 
						 | 
					a0ecfed4cd | ||
| 
						 | 
					e201e79cd0 | ||
| 
						 | 
					e1c5c706c6 | ||
| 
						 | 
					bdc56090d7 | ||
| 
						 | 
					58accc9310 | ||
| 
						 | 
					b91158198e | ||
| 
						 | 
					938ce241ff | ||
| 
						 | 
					0228646507 | ||
| 
						 | 
					25ea97d0f9 | ||
| 
						 | 
					8d5d68e589 | ||
| 
						 | 
					6c15d9948b | ||
| 
						 | 
					e8fba43cf6 | ||
| 
						 | 
					2e5c798c71 | ||
| 
						 | 
					8c298336fc | ||
| 
						 | 
					7f91cdc0c9 | ||
| 
						 | 
					6334bd832f | 
@@ -1,5 +1,6 @@
 | 
			
		||||
*
 | 
			
		||||
!.env
 | 
			
		||||
!.env.development
 | 
			
		||||
!.env.production
 | 
			
		||||
!.eslintrc.json
 | 
			
		||||
!.npmrc
 | 
			
		||||
!.prettierrc
 | 
			
		||||
 
 | 
			
		||||
@@ -20,3 +20,5 @@ REACT_APP_DEV_ENABLE_SW=
 | 
			
		||||
# whether to disable live reload / HMR. Usuaully what you want to do when
 | 
			
		||||
# debugging Service Workers.
 | 
			
		||||
REACT_APP_DEV_DISABLE_LIVE_RELOAD=
 | 
			
		||||
 | 
			
		||||
FAST_REFRESH=false
 | 
			
		||||
 
 | 
			
		||||
@@ -4755,9 +4755,9 @@ loader-runner@^4.2.0:
 | 
			
		||||
  integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==
 | 
			
		||||
 | 
			
		||||
loader-utils@^2.0.0:
 | 
			
		||||
  version "2.0.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.2.tgz#d6e3b4fb81870721ae4e0868ab11dd638368c129"
 | 
			
		||||
  integrity sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==
 | 
			
		||||
  version "2.0.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.3.tgz#d4b15b8504c63d1fc3f2ade52d41bc8459d6ede1"
 | 
			
		||||
  integrity sha512-THWqIsn8QRnvLl0shHYVBN9syumU8pYWEHPTmkiVGd+7K5eFNVSY6AJhRvgGF70gg1Dz+l/k8WicvFCxdEs60A==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    big.js "^5.2.2"
 | 
			
		||||
    emojis-list "^3.0.0"
 | 
			
		||||
 
 | 
			
		||||
@@ -35,7 +35,7 @@
 | 
			
		||||
    "firebase": "8.3.3",
 | 
			
		||||
    "i18next-browser-languagedetector": "6.1.4",
 | 
			
		||||
    "idb-keyval": "6.0.3",
 | 
			
		||||
    "image-blob-reduce": "4.1.0",
 | 
			
		||||
    "image-blob-reduce": "3.0.1",
 | 
			
		||||
    "jotai": "1.6.4",
 | 
			
		||||
    "lodash.throttle": "4.1.1",
 | 
			
		||||
    "nanoid": "3.3.3",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								public/Assistant-Bold.woff2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/Assistant-Bold.woff2
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								public/Assistant-Medium.woff2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/Assistant-Medium.woff2
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								public/Assistant-Regular.woff2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/Assistant-Regular.woff2
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								public/Assistant-SemiBold.woff2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/Assistant-SemiBold.woff2
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							@@ -11,3 +11,28 @@
 | 
			
		||||
  src: url("Cascadia.woff2");
 | 
			
		||||
  font-display: swap;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@font-face {
 | 
			
		||||
  font-family: "Assistant";
 | 
			
		||||
  src: url("Assistant-Regular.woff2");
 | 
			
		||||
  font-display: swap;
 | 
			
		||||
  font-weight: 400;
 | 
			
		||||
}
 | 
			
		||||
@font-face {
 | 
			
		||||
  font-family: "Assistant";
 | 
			
		||||
  src: url("Assistant-Medium.woff2");
 | 
			
		||||
  font-display: swap;
 | 
			
		||||
  font-weight: 500;
 | 
			
		||||
}
 | 
			
		||||
@font-face {
 | 
			
		||||
  font-family: "Assistant";
 | 
			
		||||
  src: url("Assistant-SemiBold.woff2");
 | 
			
		||||
  font-display: swap;
 | 
			
		||||
  font-weight: 600;
 | 
			
		||||
}
 | 
			
		||||
@font-face {
 | 
			
		||||
  font-family: "Assistant";
 | 
			
		||||
  src: url("Assistant-Bold.woff2");
 | 
			
		||||
  font-display: swap;
 | 
			
		||||
  font-weight: 700;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,49 +8,57 @@
 | 
			
		||||
      content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover, shrink-to-fit=no"
 | 
			
		||||
    />
 | 
			
		||||
    <meta name="referrer" content="origin" />
 | 
			
		||||
 | 
			
		||||
    <meta name="mobile-web-app-capable" content="yes" />
 | 
			
		||||
    <meta name="theme-color" content="#121212" />
 | 
			
		||||
 | 
			
		||||
    <meta name="theme-color" content="#000" />
 | 
			
		||||
    <!-- Primary Meta Tags -->
 | 
			
		||||
    <meta
 | 
			
		||||
      name="title"
 | 
			
		||||
      content="Excalidraw — Collaborative whiteboarding made easy"
 | 
			
		||||
    />
 | 
			
		||||
    <meta
 | 
			
		||||
      name="description"
 | 
			
		||||
      content="Excalidraw is a virtual collaborative whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them."
 | 
			
		||||
    />
 | 
			
		||||
    <meta name="image" content="https://excalidraw.com/og-general-v1.png" />
 | 
			
		||||
 | 
			
		||||
    <!-- Open Graph / Facebook -->
 | 
			
		||||
    <meta property="og:site_name" content="Excalidraw" />
 | 
			
		||||
    <meta property="og:type" content="website" />
 | 
			
		||||
    <meta property="og:url" content="https://excalidraw.com" />
 | 
			
		||||
    <meta
 | 
			
		||||
      property="og:title"
 | 
			
		||||
      content="Excalidraw — Collaborative whiteboarding made easy"
 | 
			
		||||
    />
 | 
			
		||||
    <meta property="og:image:alt" content="Excalidraw logo" />
 | 
			
		||||
    <meta
 | 
			
		||||
      property="og:description"
 | 
			
		||||
      content="Excalidraw is a virtual collaborative whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them."
 | 
			
		||||
    />
 | 
			
		||||
    <meta property="og:image" content="https://excalidraw.com/og-fb-v1.png" />
 | 
			
		||||
 | 
			
		||||
    <!-- Twitter -->
 | 
			
		||||
    <meta property="twitter:card" content="summary_large_image" />
 | 
			
		||||
    <meta property="twitter:site" content="@excalidraw" />
 | 
			
		||||
    <meta property="twitter:url" content="https://excalidraw.com" />
 | 
			
		||||
    <meta
 | 
			
		||||
      property="twitter:title"
 | 
			
		||||
      content="Excalidraw — Collaborative whiteboarding made easy"
 | 
			
		||||
    />
 | 
			
		||||
    <meta
 | 
			
		||||
      property="twitter:description"
 | 
			
		||||
      content="Excalidraw is a virtual collaborative whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them."
 | 
			
		||||
    />
 | 
			
		||||
    <meta
 | 
			
		||||
      property="twitter:image"
 | 
			
		||||
      content="https://excalidraw.com/og-twitter-v1.png"
 | 
			
		||||
    />
 | 
			
		||||
 | 
			
		||||
    <!-- General tags -->
 | 
			
		||||
    <meta
 | 
			
		||||
      name="description"
 | 
			
		||||
      content="Excalidraw is a virtual collaborative whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them."
 | 
			
		||||
    />
 | 
			
		||||
    <meta name="image" content="og-image.png" />
 | 
			
		||||
 | 
			
		||||
    <!-- OpenGraph tags -->
 | 
			
		||||
    <meta property="og:url" content="https://excalidraw.com" />
 | 
			
		||||
    <meta property="og:site_name" content="Excalidraw" />
 | 
			
		||||
    <meta property="og:type" content="website" />
 | 
			
		||||
    <meta property="og:title" content="Excalidraw" />
 | 
			
		||||
    <meta
 | 
			
		||||
      property="og:description"
 | 
			
		||||
      content="Excalidraw is a whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them."
 | 
			
		||||
    />
 | 
			
		||||
    <!-- OG tags require an absolute url for images -->
 | 
			
		||||
    <meta
 | 
			
		||||
      property="og:image"
 | 
			
		||||
      name="twitter:image"
 | 
			
		||||
      content="https://excalidraw.com/og-image.png"
 | 
			
		||||
    />
 | 
			
		||||
    <meta
 | 
			
		||||
      property="og:image:secure_url"
 | 
			
		||||
      name="twitter:image"
 | 
			
		||||
      content="https://excalidraw.com/og-image.png"
 | 
			
		||||
    />
 | 
			
		||||
    <meta property="og:image:width" content="1280" />
 | 
			
		||||
    <meta property="og:image:height" content="669" />
 | 
			
		||||
    <meta property="og:image:alt" content="Excalidraw logo with byline." />
 | 
			
		||||
 | 
			
		||||
    <!-- Twitter Card tags -->
 | 
			
		||||
    <meta name="twitter:card" content="summary_large_image" />
 | 
			
		||||
    <meta name="twitter:title" content="Excalidraw" />
 | 
			
		||||
    <meta
 | 
			
		||||
      name="twitter:description"
 | 
			
		||||
      content="Excalidraw is a whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them."
 | 
			
		||||
    />
 | 
			
		||||
 | 
			
		||||
    <!------------------------------------------------------------------------->
 | 
			
		||||
    <!--   to minimize white flash on load when user has dark mode enabled   -->
 | 
			
		||||
@@ -158,8 +166,8 @@
 | 
			
		||||
      body,
 | 
			
		||||
      html {
 | 
			
		||||
        margin: 0;
 | 
			
		||||
        --ui-font: system-ui, BlinkMacSystemFont, -apple-system, Segoe UI,
 | 
			
		||||
          Roboto, Helvetica, Arial, sans-serif;
 | 
			
		||||
        --ui-font: Assistant, system-ui, BlinkMacSystemFont, -apple-system,
 | 
			
		||||
          Segoe UI, Roboto, Helvetica, Arial, sans-serif;
 | 
			
		||||
        font-family: var(--ui-font);
 | 
			
		||||
        -webkit-text-size-adjust: 100%;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								public/og-fb-v1.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/og-fb-v1.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 26 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								public/og-general-v1.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/og-general-v1.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 26 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								public/og-twitter-v1.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/og-twitter-v1.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 27 KiB  | 
@@ -1,3 +1,9 @@
 | 
			
		||||
User-agent: Twitterbot
 | 
			
		||||
Disallow:
 | 
			
		||||
 | 
			
		||||
User-agent: facebookexternalhit
 | 
			
		||||
Disallow:
 | 
			
		||||
 | 
			
		||||
user-agent: *
 | 
			
		||||
Allow: /$
 | 
			
		||||
Disallow: /
 | 
			
		||||
 
 | 
			
		||||
@@ -60,7 +60,7 @@ export const actionAlignTop = register({
 | 
			
		||||
    <ToolButton
 | 
			
		||||
      hidden={!enableActionGroup(elements, appState)}
 | 
			
		||||
      type="button"
 | 
			
		||||
      icon={<AlignTopIcon theme={appState.theme} />}
 | 
			
		||||
      icon={AlignTopIcon}
 | 
			
		||||
      onClick={() => updateData(null)}
 | 
			
		||||
      title={`${t("labels.alignTop")} — ${getShortcutKey(
 | 
			
		||||
        "CtrlOrCmd+Shift+Up",
 | 
			
		||||
@@ -90,7 +90,7 @@ export const actionAlignBottom = register({
 | 
			
		||||
    <ToolButton
 | 
			
		||||
      hidden={!enableActionGroup(elements, appState)}
 | 
			
		||||
      type="button"
 | 
			
		||||
      icon={<AlignBottomIcon theme={appState.theme} />}
 | 
			
		||||
      icon={AlignBottomIcon}
 | 
			
		||||
      onClick={() => updateData(null)}
 | 
			
		||||
      title={`${t("labels.alignBottom")} — ${getShortcutKey(
 | 
			
		||||
        "CtrlOrCmd+Shift+Down",
 | 
			
		||||
@@ -120,7 +120,7 @@ export const actionAlignLeft = register({
 | 
			
		||||
    <ToolButton
 | 
			
		||||
      hidden={!enableActionGroup(elements, appState)}
 | 
			
		||||
      type="button"
 | 
			
		||||
      icon={<AlignLeftIcon theme={appState.theme} />}
 | 
			
		||||
      icon={AlignLeftIcon}
 | 
			
		||||
      onClick={() => updateData(null)}
 | 
			
		||||
      title={`${t("labels.alignLeft")} — ${getShortcutKey(
 | 
			
		||||
        "CtrlOrCmd+Shift+Left",
 | 
			
		||||
@@ -151,7 +151,7 @@ export const actionAlignRight = register({
 | 
			
		||||
    <ToolButton
 | 
			
		||||
      hidden={!enableActionGroup(elements, appState)}
 | 
			
		||||
      type="button"
 | 
			
		||||
      icon={<AlignRightIcon theme={appState.theme} />}
 | 
			
		||||
      icon={AlignRightIcon}
 | 
			
		||||
      onClick={() => updateData(null)}
 | 
			
		||||
      title={`${t("labels.alignRight")} — ${getShortcutKey(
 | 
			
		||||
        "CtrlOrCmd+Shift+Right",
 | 
			
		||||
@@ -180,7 +180,7 @@ export const actionAlignVerticallyCentered = register({
 | 
			
		||||
    <ToolButton
 | 
			
		||||
      hidden={!enableActionGroup(elements, appState)}
 | 
			
		||||
      type="button"
 | 
			
		||||
      icon={<CenterVerticallyIcon theme={appState.theme} />}
 | 
			
		||||
      icon={CenterVerticallyIcon}
 | 
			
		||||
      onClick={() => updateData(null)}
 | 
			
		||||
      title={t("labels.centerVertically")}
 | 
			
		||||
      aria-label={t("labels.centerVertically")}
 | 
			
		||||
@@ -206,7 +206,7 @@ export const actionAlignHorizontallyCentered = register({
 | 
			
		||||
    <ToolButton
 | 
			
		||||
      hidden={!enableActionGroup(elements, appState)}
 | 
			
		||||
      type="button"
 | 
			
		||||
      icon={<CenterHorizontallyIcon theme={appState.theme} />}
 | 
			
		||||
      icon={CenterHorizontallyIcon}
 | 
			
		||||
      onClick={() => updateData(null)}
 | 
			
		||||
      title={t("labels.centerHorizontally")}
 | 
			
		||||
      aria-label={t("labels.centerHorizontally")}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,12 @@
 | 
			
		||||
import { ColorPicker } from "../components/ColorPicker";
 | 
			
		||||
import { eraser, zoomIn, zoomOut } from "../components/icons";
 | 
			
		||||
import {
 | 
			
		||||
  eraser,
 | 
			
		||||
  MoonIcon,
 | 
			
		||||
  SunIcon,
 | 
			
		||||
  ZoomInIcon,
 | 
			
		||||
  ZoomOutIcon,
 | 
			
		||||
} from "../components/icons";
 | 
			
		||||
import { ToolButton } from "../components/ToolButton";
 | 
			
		||||
import { DarkModeToggle } from "../components/DarkModeToggle";
 | 
			
		||||
import { MIN_ZOOM, THEME, ZOOM_STEP } from "../constants";
 | 
			
		||||
import { getCommonBounds, getNonDeletedElements } from "../element";
 | 
			
		||||
import { ExcalidrawElement } from "../element/types";
 | 
			
		||||
@@ -18,6 +23,8 @@ import { newElementWith } from "../element/mutateElement";
 | 
			
		||||
import { getDefaultAppState, isEraserActive } from "../appState";
 | 
			
		||||
import ClearCanvas from "../components/ClearCanvas";
 | 
			
		||||
import clsx from "clsx";
 | 
			
		||||
import MenuItem from "../components/MenuItem";
 | 
			
		||||
import { getShortcutFromShortcutName } from "./shortcuts";
 | 
			
		||||
 | 
			
		||||
export const actionChangeViewBackgroundColor = register({
 | 
			
		||||
  name: "changeViewBackgroundColor",
 | 
			
		||||
@@ -103,13 +110,13 @@ export const actionZoomIn = register({
 | 
			
		||||
  PanelComponent: ({ updateData }) => (
 | 
			
		||||
    <ToolButton
 | 
			
		||||
      type="button"
 | 
			
		||||
      icon={zoomIn}
 | 
			
		||||
      className="zoom-in-button zoom-button"
 | 
			
		||||
      icon={ZoomInIcon}
 | 
			
		||||
      title={`${t("buttons.zoomIn")} — ${getShortcutKey("CtrlOrCmd++")}`}
 | 
			
		||||
      aria-label={t("buttons.zoomIn")}
 | 
			
		||||
      onClick={() => {
 | 
			
		||||
        updateData(null);
 | 
			
		||||
      }}
 | 
			
		||||
      size="small"
 | 
			
		||||
    />
 | 
			
		||||
  ),
 | 
			
		||||
  keyTest: (event) =>
 | 
			
		||||
@@ -139,13 +146,13 @@ export const actionZoomOut = register({
 | 
			
		||||
  PanelComponent: ({ updateData }) => (
 | 
			
		||||
    <ToolButton
 | 
			
		||||
      type="button"
 | 
			
		||||
      icon={zoomOut}
 | 
			
		||||
      className="zoom-out-button zoom-button"
 | 
			
		||||
      icon={ZoomOutIcon}
 | 
			
		||||
      title={`${t("buttons.zoomOut")} — ${getShortcutKey("CtrlOrCmd+-")}`}
 | 
			
		||||
      aria-label={t("buttons.zoomOut")}
 | 
			
		||||
      onClick={() => {
 | 
			
		||||
        updateData(null);
 | 
			
		||||
      }}
 | 
			
		||||
      size="small"
 | 
			
		||||
    />
 | 
			
		||||
  ),
 | 
			
		||||
  keyTest: (event) =>
 | 
			
		||||
@@ -176,13 +183,12 @@ export const actionResetZoom = register({
 | 
			
		||||
    <Tooltip label={t("buttons.resetZoom")} style={{ height: "100%" }}>
 | 
			
		||||
      <ToolButton
 | 
			
		||||
        type="button"
 | 
			
		||||
        className="reset-zoom-button"
 | 
			
		||||
        className="reset-zoom-button zoom-button"
 | 
			
		||||
        title={t("buttons.resetZoom")}
 | 
			
		||||
        aria-label={t("buttons.resetZoom")}
 | 
			
		||||
        onClick={() => {
 | 
			
		||||
          updateData(null);
 | 
			
		||||
        }}
 | 
			
		||||
        size="small"
 | 
			
		||||
      >
 | 
			
		||||
        {(appState.zoom.value * 100).toFixed(0)}%
 | 
			
		||||
      </ToolButton>
 | 
			
		||||
@@ -288,14 +294,19 @@ export const actionToggleTheme = register({
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  PanelComponent: ({ appState, updateData }) => (
 | 
			
		||||
    <div style={{ marginInlineStart: "0.25rem" }}>
 | 
			
		||||
      <DarkModeToggle
 | 
			
		||||
        value={appState.theme}
 | 
			
		||||
        onChange={(theme) => {
 | 
			
		||||
          updateData(theme);
 | 
			
		||||
        }}
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
    <MenuItem
 | 
			
		||||
      label={
 | 
			
		||||
        appState.theme === "dark"
 | 
			
		||||
          ? t("buttons.lightMode")
 | 
			
		||||
          : t("buttons.darkMode")
 | 
			
		||||
      }
 | 
			
		||||
      onClick={() => {
 | 
			
		||||
        updateData(appState.theme === THEME.LIGHT ? THEME.DARK : THEME.LIGHT);
 | 
			
		||||
      }}
 | 
			
		||||
      icon={appState.theme === "dark" ? SunIcon : MoonIcon}
 | 
			
		||||
      dataTestId="toggle-dark-mode"
 | 
			
		||||
      shortcut={getShortcutFromShortcutName("toggleTheme")}
 | 
			
		||||
    />
 | 
			
		||||
  ),
 | 
			
		||||
  keyTest: (event) => event.altKey && event.shiftKey && event.code === CODES.D,
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
import { isSomeElementSelected } from "../scene";
 | 
			
		||||
import { KEYS } from "../keys";
 | 
			
		||||
import { ToolButton } from "../components/ToolButton";
 | 
			
		||||
import { trash } from "../components/icons";
 | 
			
		||||
import { t } from "../i18n";
 | 
			
		||||
import { register } from "./register";
 | 
			
		||||
import { getNonDeletedElements } from "../element";
 | 
			
		||||
@@ -13,6 +12,7 @@ import { LinearElementEditor } from "../element/linearElementEditor";
 | 
			
		||||
import { fixBindingsAfterDeletion } from "../element/binding";
 | 
			
		||||
import { isBoundToContainer } from "../element/typeChecks";
 | 
			
		||||
import { updateActiveTool } from "../utils";
 | 
			
		||||
import { TrashIcon } from "../components/icons";
 | 
			
		||||
 | 
			
		||||
const deleteSelectedElements = (
 | 
			
		||||
  elements: readonly ExcalidrawElement[],
 | 
			
		||||
@@ -72,13 +72,22 @@ export const actionDeleteSelected = register({
 | 
			
		||||
      if (!element) {
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
      if (
 | 
			
		||||
        // case: no point selected → delete whole element
 | 
			
		||||
        selectedPointsIndices == null ||
 | 
			
		||||
        // case: deleting last remaining point
 | 
			
		||||
        element.points.length < 2
 | 
			
		||||
      ) {
 | 
			
		||||
        const nextElements = elements.filter((el) => el.id !== element.id);
 | 
			
		||||
      // case: no point selected → do nothing, as deleting the whole element
 | 
			
		||||
      // is most likely a mistake, where you wanted to delete a specific point
 | 
			
		||||
      // but failed to select it (or you thought it's selected, while it was
 | 
			
		||||
      // only in a hover state)
 | 
			
		||||
      if (selectedPointsIndices == null) {
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // case: deleting last remaining point
 | 
			
		||||
      if (element.points.length < 2) {
 | 
			
		||||
        const nextElements = elements.map((el) => {
 | 
			
		||||
          if (el.id === element.id) {
 | 
			
		||||
            return newElementWith(el, { isDeleted: true });
 | 
			
		||||
          }
 | 
			
		||||
          return el;
 | 
			
		||||
        });
 | 
			
		||||
        const nextAppState = handleGroupEditingState(appState, nextElements);
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
@@ -149,7 +158,7 @@ export const actionDeleteSelected = register({
 | 
			
		||||
  PanelComponent: ({ elements, appState, updateData }) => (
 | 
			
		||||
    <ToolButton
 | 
			
		||||
      type="button"
 | 
			
		||||
      icon={trash}
 | 
			
		||||
      icon={TrashIcon}
 | 
			
		||||
      title={t("labels.delete")}
 | 
			
		||||
      aria-label={t("labels.delete")}
 | 
			
		||||
      onClick={() => updateData(null)}
 | 
			
		||||
 
 | 
			
		||||
@@ -56,7 +56,7 @@ export const distributeHorizontally = register({
 | 
			
		||||
    <ToolButton
 | 
			
		||||
      hidden={!enableActionGroup(elements, appState)}
 | 
			
		||||
      type="button"
 | 
			
		||||
      icon={<DistributeHorizontallyIcon theme={appState.theme} />}
 | 
			
		||||
      icon={DistributeHorizontallyIcon}
 | 
			
		||||
      onClick={() => updateData(null)}
 | 
			
		||||
      title={`${t("labels.distributeHorizontally")} — ${getShortcutKey(
 | 
			
		||||
        "Alt+H",
 | 
			
		||||
@@ -86,7 +86,7 @@ export const distributeVertically = register({
 | 
			
		||||
    <ToolButton
 | 
			
		||||
      hidden={!enableActionGroup(elements, appState)}
 | 
			
		||||
      type="button"
 | 
			
		||||
      icon={<DistributeVerticallyIcon theme={appState.theme} />}
 | 
			
		||||
      icon={DistributeVerticallyIcon}
 | 
			
		||||
      onClick={() => updateData(null)}
 | 
			
		||||
      title={`${t("labels.distributeVertically")} — ${getShortcutKey("Alt+V")}`}
 | 
			
		||||
      aria-label={t("labels.distributeVertically")}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,6 @@ import { ExcalidrawElement } from "../element/types";
 | 
			
		||||
import { duplicateElement, getNonDeletedElements } from "../element";
 | 
			
		||||
import { getSelectedElements, isSomeElementSelected } from "../scene";
 | 
			
		||||
import { ToolButton } from "../components/ToolButton";
 | 
			
		||||
import { clone } from "../components/icons";
 | 
			
		||||
import { t } from "../i18n";
 | 
			
		||||
import { arrayToMap, getShortcutKey } from "../utils";
 | 
			
		||||
import { LinearElementEditor } from "../element/linearElementEditor";
 | 
			
		||||
@@ -19,6 +18,7 @@ import { ActionResult } from "./types";
 | 
			
		||||
import { GRID_SIZE } from "../constants";
 | 
			
		||||
import { bindTextToShapeAfterDuplication } from "../element/textElement";
 | 
			
		||||
import { isBoundToContainer } from "../element/typeChecks";
 | 
			
		||||
import { DuplicateIcon } from "../components/icons";
 | 
			
		||||
 | 
			
		||||
export const actionDuplicateSelection = register({
 | 
			
		||||
  name: "duplicateSelection",
 | 
			
		||||
@@ -49,7 +49,7 @@ export const actionDuplicateSelection = register({
 | 
			
		||||
  PanelComponent: ({ elements, appState, updateData }) => (
 | 
			
		||||
    <ToolButton
 | 
			
		||||
      type="button"
 | 
			
		||||
      icon={clone}
 | 
			
		||||
      icon={DuplicateIcon}
 | 
			
		||||
      title={`${t("labels.duplicateSelection")} — ${getShortcutKey(
 | 
			
		||||
        "CtrlOrCmd+D",
 | 
			
		||||
      )}`}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import { load, questionCircle, saveAs } from "../components/icons";
 | 
			
		||||
import { LoadIcon, questionCircle, saveAs } from "../components/icons";
 | 
			
		||||
import { ProjectName } from "../components/ProjectName";
 | 
			
		||||
import { ToolButton } from "../components/ToolButton";
 | 
			
		||||
import "../components/ToolIcon.scss";
 | 
			
		||||
@@ -19,6 +19,8 @@ import { ActiveFile } from "../components/ActiveFile";
 | 
			
		||||
import { isImageFileHandle } from "../data/blob";
 | 
			
		||||
import { nativeFileSystemSupported } from "../data/filesystem";
 | 
			
		||||
import { Theme } from "../element/types";
 | 
			
		||||
import MenuItem from "../components/MenuItem";
 | 
			
		||||
import { getShortcutFromShortcutName } from "./shortcuts";
 | 
			
		||||
 | 
			
		||||
export const actionChangeProjectName = register({
 | 
			
		||||
  name: "changeProjectName",
 | 
			
		||||
@@ -245,14 +247,12 @@ export const actionLoadScene = register({
 | 
			
		||||
  },
 | 
			
		||||
  keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.O,
 | 
			
		||||
  PanelComponent: ({ updateData }) => (
 | 
			
		||||
    <ToolButton
 | 
			
		||||
      type="button"
 | 
			
		||||
      icon={load}
 | 
			
		||||
      title={t("buttons.load")}
 | 
			
		||||
      aria-label={t("buttons.load")}
 | 
			
		||||
      showAriaLabel={useDevice().isMobile}
 | 
			
		||||
    <MenuItem
 | 
			
		||||
      label={t("buttons.load")}
 | 
			
		||||
      icon={LoadIcon}
 | 
			
		||||
      onClick={updateData}
 | 
			
		||||
      data-testid="load-button"
 | 
			
		||||
      dataTestId="load-button"
 | 
			
		||||
      shortcut={getShortcutFromShortcutName("loadScene")}
 | 
			
		||||
    />
 | 
			
		||||
  ),
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import { Action, ActionResult } from "./types";
 | 
			
		||||
import { undo, redo } from "../components/icons";
 | 
			
		||||
import { UndoIcon, RedoIcon } from "../components/icons";
 | 
			
		||||
import { ToolButton } from "../components/ToolButton";
 | 
			
		||||
import { t } from "../i18n";
 | 
			
		||||
import History, { HistoryEntry } from "../history";
 | 
			
		||||
@@ -72,7 +72,7 @@ export const createUndoAction: ActionCreator = (history) => ({
 | 
			
		||||
  PanelComponent: ({ updateData, data }) => (
 | 
			
		||||
    <ToolButton
 | 
			
		||||
      type="button"
 | 
			
		||||
      icon={undo}
 | 
			
		||||
      icon={UndoIcon}
 | 
			
		||||
      aria-label={t("buttons.undo")}
 | 
			
		||||
      onClick={updateData}
 | 
			
		||||
      size={data?.size || "medium"}
 | 
			
		||||
@@ -94,7 +94,7 @@ export const createRedoAction: ActionCreator = (history) => ({
 | 
			
		||||
  PanelComponent: ({ updateData, data }) => (
 | 
			
		||||
    <ToolButton
 | 
			
		||||
      type="button"
 | 
			
		||||
      icon={redo}
 | 
			
		||||
      icon={RedoIcon}
 | 
			
		||||
      aria-label={t("buttons.redo")}
 | 
			
		||||
      onClick={updateData}
 | 
			
		||||
      size={data?.size || "medium"}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,12 @@
 | 
			
		||||
import { menu, palette } from "../components/icons";
 | 
			
		||||
import { HamburgerMenuIcon, HelpIcon, palette } from "../components/icons";
 | 
			
		||||
import { ToolButton } from "../components/ToolButton";
 | 
			
		||||
import { t } from "../i18n";
 | 
			
		||||
import { showSelectedShapeActions, getNonDeletedElements } from "../element";
 | 
			
		||||
import { register } from "./register";
 | 
			
		||||
import { allowFullScreen, exitFullScreen, isFullScreen } from "../utils";
 | 
			
		||||
import { KEYS } from "../keys";
 | 
			
		||||
import { HelpIcon } from "../components/HelpIcon";
 | 
			
		||||
import { HelpButton } from "../components/HelpButton";
 | 
			
		||||
import MenuItem from "../components/MenuItem";
 | 
			
		||||
 | 
			
		||||
export const actionToggleCanvasMenu = register({
 | 
			
		||||
  name: "toggleCanvasMenu",
 | 
			
		||||
@@ -20,7 +21,7 @@ export const actionToggleCanvasMenu = register({
 | 
			
		||||
  PanelComponent: ({ appState, updateData }) => (
 | 
			
		||||
    <ToolButton
 | 
			
		||||
      type="button"
 | 
			
		||||
      icon={menu}
 | 
			
		||||
      icon={HamburgerMenuIcon}
 | 
			
		||||
      aria-label={t("buttons.menu")}
 | 
			
		||||
      onClick={updateData}
 | 
			
		||||
      selected={appState.openMenu === "canvas"}
 | 
			
		||||
@@ -74,19 +75,28 @@ export const actionShortcuts = register({
 | 
			
		||||
  name: "toggleShortcuts",
 | 
			
		||||
  trackEvent: { category: "menu", action: "toggleHelpDialog" },
 | 
			
		||||
  perform: (_elements, appState, _, { focusContainer }) => {
 | 
			
		||||
    if (appState.showHelpDialog) {
 | 
			
		||||
    if (appState.openDialog === "help") {
 | 
			
		||||
      focusContainer();
 | 
			
		||||
    }
 | 
			
		||||
    return {
 | 
			
		||||
      appState: {
 | 
			
		||||
        ...appState,
 | 
			
		||||
        showHelpDialog: !appState.showHelpDialog,
 | 
			
		||||
        openDialog: appState.openDialog === "help" ? null : "help",
 | 
			
		||||
      },
 | 
			
		||||
      commitToHistory: false,
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  PanelComponent: ({ updateData }) => (
 | 
			
		||||
    <HelpIcon title={t("helpDialog.title")} onClick={updateData} />
 | 
			
		||||
  ),
 | 
			
		||||
  PanelComponent: ({ updateData, isInHamburgerMenu }) =>
 | 
			
		||||
    isInHamburgerMenu ? (
 | 
			
		||||
      <MenuItem
 | 
			
		||||
        label={t("helpDialog.title")}
 | 
			
		||||
        dataTestId="help-menu-item"
 | 
			
		||||
        icon={HelpIcon}
 | 
			
		||||
        onClick={updateData}
 | 
			
		||||
        shortcut="?"
 | 
			
		||||
      />
 | 
			
		||||
    ) : (
 | 
			
		||||
      <HelpButton title={t("helpDialog.title")} onClick={updateData} />
 | 
			
		||||
    ),
 | 
			
		||||
  keyTest: (event) => event.key === KEYS.QUESTION_MARK,
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -2,37 +2,41 @@ import { AppState } from "../../src/types";
 | 
			
		||||
import { ButtonIconSelect } from "../components/ButtonIconSelect";
 | 
			
		||||
import { ColorPicker } from "../components/ColorPicker";
 | 
			
		||||
import { IconPicker } from "../components/IconPicker";
 | 
			
		||||
// TODO barnabasmolnar/editor-redesign
 | 
			
		||||
// TextAlignTopIcon, TextAlignBottomIcon,TextAlignMiddleIcon,
 | 
			
		||||
// ArrowHead icons
 | 
			
		||||
import {
 | 
			
		||||
  ArrowheadArrowIcon,
 | 
			
		||||
  ArrowheadBarIcon,
 | 
			
		||||
  ArrowheadDotIcon,
 | 
			
		||||
  ArrowheadTriangleIcon,
 | 
			
		||||
  ArrowheadNoneIcon,
 | 
			
		||||
  EdgeRoundIcon,
 | 
			
		||||
  EdgeSharpIcon,
 | 
			
		||||
  FillCrossHatchIcon,
 | 
			
		||||
  FillHachureIcon,
 | 
			
		||||
  FillSolidIcon,
 | 
			
		||||
  FontFamilyCodeIcon,
 | 
			
		||||
  FontFamilyHandDrawnIcon,
 | 
			
		||||
  FontFamilyNormalIcon,
 | 
			
		||||
  FontSizeExtraLargeIcon,
 | 
			
		||||
  FontSizeLargeIcon,
 | 
			
		||||
  FontSizeMediumIcon,
 | 
			
		||||
  FontSizeSmallIcon,
 | 
			
		||||
  SloppinessArchitectIcon,
 | 
			
		||||
  SloppinessArtistIcon,
 | 
			
		||||
  SloppinessCartoonistIcon,
 | 
			
		||||
  StrokeStyleDashedIcon,
 | 
			
		||||
  StrokeStyleDottedIcon,
 | 
			
		||||
  StrokeStyleSolidIcon,
 | 
			
		||||
  StrokeWidthIcon,
 | 
			
		||||
  TextAlignCenterIcon,
 | 
			
		||||
  TextAlignLeftIcon,
 | 
			
		||||
  TextAlignRightIcon,
 | 
			
		||||
  TextAlignTopIcon,
 | 
			
		||||
  TextAlignBottomIcon,
 | 
			
		||||
  TextAlignMiddleIcon,
 | 
			
		||||
  FillHachureIcon,
 | 
			
		||||
  FillCrossHatchIcon,
 | 
			
		||||
  FillSolidIcon,
 | 
			
		||||
  SloppinessArchitectIcon,
 | 
			
		||||
  SloppinessArtistIcon,
 | 
			
		||||
  SloppinessCartoonistIcon,
 | 
			
		||||
  StrokeWidthBaseIcon,
 | 
			
		||||
  StrokeWidthBoldIcon,
 | 
			
		||||
  StrokeWidthExtraBoldIcon,
 | 
			
		||||
  FontSizeSmallIcon,
 | 
			
		||||
  FontSizeMediumIcon,
 | 
			
		||||
  FontSizeLargeIcon,
 | 
			
		||||
  FontSizeExtraLargeIcon,
 | 
			
		||||
  EdgeSharpIcon,
 | 
			
		||||
  EdgeRoundIcon,
 | 
			
		||||
  FreedrawIcon,
 | 
			
		||||
  FontFamilyNormalIcon,
 | 
			
		||||
  FontFamilyCodeIcon,
 | 
			
		||||
  TextAlignLeftIcon,
 | 
			
		||||
  TextAlignCenterIcon,
 | 
			
		||||
  TextAlignRightIcon,
 | 
			
		||||
} from "../components/icons";
 | 
			
		||||
import {
 | 
			
		||||
  DEFAULT_FONT_FAMILY,
 | 
			
		||||
@@ -307,17 +311,17 @@ export const actionChangeFillStyle = register({
 | 
			
		||||
          {
 | 
			
		||||
            value: "hachure",
 | 
			
		||||
            text: t("labels.hachure"),
 | 
			
		||||
            icon: <FillHachureIcon theme={appState.theme} />,
 | 
			
		||||
            icon: FillHachureIcon,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            value: "cross-hatch",
 | 
			
		||||
            text: t("labels.crossHatch"),
 | 
			
		||||
            icon: <FillCrossHatchIcon theme={appState.theme} />,
 | 
			
		||||
            icon: FillCrossHatchIcon,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            value: "solid",
 | 
			
		||||
            text: t("labels.solid"),
 | 
			
		||||
            icon: <FillSolidIcon theme={appState.theme} />,
 | 
			
		||||
            icon: FillSolidIcon,
 | 
			
		||||
          },
 | 
			
		||||
        ]}
 | 
			
		||||
        group="fill"
 | 
			
		||||
@@ -358,17 +362,17 @@ export const actionChangeStrokeWidth = register({
 | 
			
		||||
          {
 | 
			
		||||
            value: 1,
 | 
			
		||||
            text: t("labels.thin"),
 | 
			
		||||
            icon: <StrokeWidthIcon theme={appState.theme} strokeWidth={2} />,
 | 
			
		||||
            icon: StrokeWidthBaseIcon,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            value: 2,
 | 
			
		||||
            text: t("labels.bold"),
 | 
			
		||||
            icon: <StrokeWidthIcon theme={appState.theme} strokeWidth={6} />,
 | 
			
		||||
            icon: StrokeWidthBoldIcon,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            value: 4,
 | 
			
		||||
            text: t("labels.extraBold"),
 | 
			
		||||
            icon: <StrokeWidthIcon theme={appState.theme} strokeWidth={10} />,
 | 
			
		||||
            icon: StrokeWidthExtraBoldIcon,
 | 
			
		||||
          },
 | 
			
		||||
        ]}
 | 
			
		||||
        value={getFormValue(
 | 
			
		||||
@@ -407,17 +411,17 @@ export const actionChangeSloppiness = register({
 | 
			
		||||
          {
 | 
			
		||||
            value: 0,
 | 
			
		||||
            text: t("labels.architect"),
 | 
			
		||||
            icon: <SloppinessArchitectIcon theme={appState.theme} />,
 | 
			
		||||
            icon: SloppinessArchitectIcon,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            value: 1,
 | 
			
		||||
            text: t("labels.artist"),
 | 
			
		||||
            icon: <SloppinessArtistIcon theme={appState.theme} />,
 | 
			
		||||
            icon: SloppinessArtistIcon,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            value: 2,
 | 
			
		||||
            text: t("labels.cartoonist"),
 | 
			
		||||
            icon: <SloppinessCartoonistIcon theme={appState.theme} />,
 | 
			
		||||
            icon: SloppinessCartoonistIcon,
 | 
			
		||||
          },
 | 
			
		||||
        ]}
 | 
			
		||||
        value={getFormValue(
 | 
			
		||||
@@ -455,17 +459,17 @@ export const actionChangeStrokeStyle = register({
 | 
			
		||||
          {
 | 
			
		||||
            value: "solid",
 | 
			
		||||
            text: t("labels.strokeStyle_solid"),
 | 
			
		||||
            icon: <StrokeStyleSolidIcon theme={appState.theme} />,
 | 
			
		||||
            icon: StrokeWidthBaseIcon,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            value: "dashed",
 | 
			
		||||
            text: t("labels.strokeStyle_dashed"),
 | 
			
		||||
            icon: <StrokeStyleDashedIcon theme={appState.theme} />,
 | 
			
		||||
            icon: StrokeStyleDashedIcon,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            value: "dotted",
 | 
			
		||||
            text: t("labels.strokeStyle_dotted"),
 | 
			
		||||
            icon: <StrokeStyleDottedIcon theme={appState.theme} />,
 | 
			
		||||
            icon: StrokeStyleDottedIcon,
 | 
			
		||||
          },
 | 
			
		||||
        ]}
 | 
			
		||||
        value={getFormValue(
 | 
			
		||||
@@ -535,25 +539,25 @@ export const actionChangeFontSize = register({
 | 
			
		||||
          {
 | 
			
		||||
            value: 16,
 | 
			
		||||
            text: t("labels.small"),
 | 
			
		||||
            icon: <FontSizeSmallIcon theme={appState.theme} />,
 | 
			
		||||
            icon: FontSizeSmallIcon,
 | 
			
		||||
            testId: "fontSize-small",
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            value: 20,
 | 
			
		||||
            text: t("labels.medium"),
 | 
			
		||||
            icon: <FontSizeMediumIcon theme={appState.theme} />,
 | 
			
		||||
            icon: FontSizeMediumIcon,
 | 
			
		||||
            testId: "fontSize-medium",
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            value: 28,
 | 
			
		||||
            text: t("labels.large"),
 | 
			
		||||
            icon: <FontSizeLargeIcon theme={appState.theme} />,
 | 
			
		||||
            icon: FontSizeLargeIcon,
 | 
			
		||||
            testId: "fontSize-large",
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            value: 36,
 | 
			
		||||
            text: t("labels.veryLarge"),
 | 
			
		||||
            icon: <FontSizeExtraLargeIcon theme={appState.theme} />,
 | 
			
		||||
            icon: FontSizeExtraLargeIcon,
 | 
			
		||||
            testId: "fontSize-veryLarge",
 | 
			
		||||
          },
 | 
			
		||||
        ]}
 | 
			
		||||
@@ -658,17 +662,17 @@ export const actionChangeFontFamily = register({
 | 
			
		||||
      {
 | 
			
		||||
        value: FONT_FAMILY.Virgil,
 | 
			
		||||
        text: t("labels.handDrawn"),
 | 
			
		||||
        icon: <FontFamilyHandDrawnIcon theme={appState.theme} />,
 | 
			
		||||
        icon: FreedrawIcon,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        value: FONT_FAMILY.Helvetica,
 | 
			
		||||
        text: t("labels.normal"),
 | 
			
		||||
        icon: <FontFamilyNormalIcon theme={appState.theme} />,
 | 
			
		||||
        icon: FontFamilyNormalIcon,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        value: FONT_FAMILY.Cascadia,
 | 
			
		||||
        text: t("labels.code"),
 | 
			
		||||
        icon: <FontFamilyCodeIcon theme={appState.theme} />,
 | 
			
		||||
        icon: FontFamilyCodeIcon,
 | 
			
		||||
      },
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
@@ -739,17 +743,17 @@ export const actionChangeTextAlign = register({
 | 
			
		||||
            {
 | 
			
		||||
              value: "left",
 | 
			
		||||
              text: t("labels.left"),
 | 
			
		||||
              icon: <TextAlignLeftIcon theme={appState.theme} />,
 | 
			
		||||
              icon: TextAlignLeftIcon,
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              value: "center",
 | 
			
		||||
              text: t("labels.center"),
 | 
			
		||||
              icon: <TextAlignCenterIcon theme={appState.theme} />,
 | 
			
		||||
              icon: TextAlignCenterIcon,
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              value: "right",
 | 
			
		||||
              text: t("labels.right"),
 | 
			
		||||
              icon: <TextAlignRightIcon theme={appState.theme} />,
 | 
			
		||||
              icon: TextAlignRightIcon,
 | 
			
		||||
            },
 | 
			
		||||
          ]}
 | 
			
		||||
          value={getFormValue(
 | 
			
		||||
@@ -882,12 +886,12 @@ export const actionChangeSharpness = register({
 | 
			
		||||
          {
 | 
			
		||||
            value: "sharp",
 | 
			
		||||
            text: t("labels.sharp"),
 | 
			
		||||
            icon: <EdgeSharpIcon theme={appState.theme} />,
 | 
			
		||||
            icon: EdgeSharpIcon,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            value: "round",
 | 
			
		||||
            text: t("labels.round"),
 | 
			
		||||
            icon: <EdgeRoundIcon theme={appState.theme} />,
 | 
			
		||||
            icon: EdgeRoundIcon,
 | 
			
		||||
          },
 | 
			
		||||
        ]}
 | 
			
		||||
        value={getFormValue(
 | 
			
		||||
@@ -949,42 +953,38 @@ export const actionChangeArrowhead = register({
 | 
			
		||||
    return (
 | 
			
		||||
      <fieldset>
 | 
			
		||||
        <legend>{t("labels.arrowheads")}</legend>
 | 
			
		||||
        <div className="iconSelectList">
 | 
			
		||||
        <div className="iconSelectList buttonList">
 | 
			
		||||
          <IconPicker
 | 
			
		||||
            label="arrowhead_start"
 | 
			
		||||
            options={[
 | 
			
		||||
              {
 | 
			
		||||
                value: null,
 | 
			
		||||
                text: t("labels.arrowhead_none"),
 | 
			
		||||
                icon: <ArrowheadNoneIcon theme={appState.theme} />,
 | 
			
		||||
                icon: ArrowheadNoneIcon,
 | 
			
		||||
                keyBinding: "q",
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                value: "arrow",
 | 
			
		||||
                text: t("labels.arrowhead_arrow"),
 | 
			
		||||
                icon: (
 | 
			
		||||
                  <ArrowheadArrowIcon theme={appState.theme} flip={!isRTL} />
 | 
			
		||||
                ),
 | 
			
		||||
                icon: <ArrowheadArrowIcon flip={!isRTL} />,
 | 
			
		||||
                keyBinding: "w",
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                value: "bar",
 | 
			
		||||
                text: t("labels.arrowhead_bar"),
 | 
			
		||||
                icon: <ArrowheadBarIcon theme={appState.theme} flip={!isRTL} />,
 | 
			
		||||
                icon: <ArrowheadBarIcon flip={!isRTL} />,
 | 
			
		||||
                keyBinding: "e",
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                value: "dot",
 | 
			
		||||
                text: t("labels.arrowhead_dot"),
 | 
			
		||||
                icon: <ArrowheadDotIcon theme={appState.theme} flip={!isRTL} />,
 | 
			
		||||
                icon: <ArrowheadDotIcon flip={!isRTL} />,
 | 
			
		||||
                keyBinding: "r",
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                value: "triangle",
 | 
			
		||||
                text: t("labels.arrowhead_triangle"),
 | 
			
		||||
                icon: (
 | 
			
		||||
                  <ArrowheadTriangleIcon theme={appState.theme} flip={!isRTL} />
 | 
			
		||||
                ),
 | 
			
		||||
                icon: <ArrowheadTriangleIcon flip={!isRTL} />,
 | 
			
		||||
                keyBinding: "t",
 | 
			
		||||
              },
 | 
			
		||||
            ]}
 | 
			
		||||
@@ -1007,34 +1007,30 @@ export const actionChangeArrowhead = register({
 | 
			
		||||
                value: null,
 | 
			
		||||
                text: t("labels.arrowhead_none"),
 | 
			
		||||
                keyBinding: "q",
 | 
			
		||||
                icon: <ArrowheadNoneIcon theme={appState.theme} />,
 | 
			
		||||
                icon: ArrowheadNoneIcon,
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                value: "arrow",
 | 
			
		||||
                text: t("labels.arrowhead_arrow"),
 | 
			
		||||
                keyBinding: "w",
 | 
			
		||||
                icon: (
 | 
			
		||||
                  <ArrowheadArrowIcon theme={appState.theme} flip={isRTL} />
 | 
			
		||||
                ),
 | 
			
		||||
                icon: <ArrowheadArrowIcon flip={isRTL} />,
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                value: "bar",
 | 
			
		||||
                text: t("labels.arrowhead_bar"),
 | 
			
		||||
                keyBinding: "e",
 | 
			
		||||
                icon: <ArrowheadBarIcon theme={appState.theme} flip={isRTL} />,
 | 
			
		||||
                icon: <ArrowheadBarIcon flip={isRTL} />,
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                value: "dot",
 | 
			
		||||
                text: t("labels.arrowhead_dot"),
 | 
			
		||||
                keyBinding: "r",
 | 
			
		||||
                icon: <ArrowheadDotIcon theme={appState.theme} flip={isRTL} />,
 | 
			
		||||
                icon: <ArrowheadDotIcon flip={isRTL} />,
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                value: "triangle",
 | 
			
		||||
                text: t("labels.arrowhead_triangle"),
 | 
			
		||||
                icon: (
 | 
			
		||||
                  <ArrowheadTriangleIcon theme={appState.theme} flip={isRTL} />
 | 
			
		||||
                ),
 | 
			
		||||
                icon: <ArrowheadTriangleIcon flip={isRTL} />,
 | 
			
		||||
                keyBinding: "t",
 | 
			
		||||
              },
 | 
			
		||||
            ]}
 | 
			
		||||
 
 | 
			
		||||
@@ -10,10 +10,10 @@ import { t } from "../i18n";
 | 
			
		||||
import { getShortcutKey } from "../utils";
 | 
			
		||||
import { register } from "./register";
 | 
			
		||||
import {
 | 
			
		||||
  SendBackwardIcon,
 | 
			
		||||
  BringToFrontIcon,
 | 
			
		||||
  SendToBackIcon,
 | 
			
		||||
  BringForwardIcon,
 | 
			
		||||
  BringToFrontIcon,
 | 
			
		||||
  SendBackwardIcon,
 | 
			
		||||
  SendToBackIcon,
 | 
			
		||||
} from "../components/icons";
 | 
			
		||||
 | 
			
		||||
export const actionSendBackward = register({
 | 
			
		||||
@@ -39,7 +39,7 @@ export const actionSendBackward = register({
 | 
			
		||||
      onClick={() => updateData(null)}
 | 
			
		||||
      title={`${t("labels.sendBackward")} — ${getShortcutKey("CtrlOrCmd+[")}`}
 | 
			
		||||
    >
 | 
			
		||||
      <SendBackwardIcon theme={appState.theme} />
 | 
			
		||||
      {SendBackwardIcon}
 | 
			
		||||
    </button>
 | 
			
		||||
  ),
 | 
			
		||||
});
 | 
			
		||||
@@ -67,7 +67,7 @@ export const actionBringForward = register({
 | 
			
		||||
      onClick={() => updateData(null)}
 | 
			
		||||
      title={`${t("labels.bringForward")} — ${getShortcutKey("CtrlOrCmd+]")}`}
 | 
			
		||||
    >
 | 
			
		||||
      <BringForwardIcon theme={appState.theme} />
 | 
			
		||||
      {BringForwardIcon}
 | 
			
		||||
    </button>
 | 
			
		||||
  ),
 | 
			
		||||
});
 | 
			
		||||
@@ -102,7 +102,7 @@ export const actionSendToBack = register({
 | 
			
		||||
          : getShortcutKey("CtrlOrCmd+Shift+[")
 | 
			
		||||
      }`}
 | 
			
		||||
    >
 | 
			
		||||
      <SendToBackIcon theme={appState.theme} />
 | 
			
		||||
      {SendToBackIcon}
 | 
			
		||||
    </button>
 | 
			
		||||
  ),
 | 
			
		||||
});
 | 
			
		||||
@@ -138,7 +138,7 @@ export const actionBringToFront = register({
 | 
			
		||||
          : getShortcutKey("CtrlOrCmd+Shift+]")
 | 
			
		||||
      }`}
 | 
			
		||||
    >
 | 
			
		||||
      <BringToFrontIcon theme={appState.theme} />
 | 
			
		||||
      {BringToFrontIcon}
 | 
			
		||||
    </button>
 | 
			
		||||
  ),
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -135,8 +135,13 @@ export class ActionManager {
 | 
			
		||||
  /**
 | 
			
		||||
   * @param data additional data sent to the PanelComponent
 | 
			
		||||
   */
 | 
			
		||||
  renderAction = (name: ActionName, data?: PanelComponentProps["data"]) => {
 | 
			
		||||
  renderAction = (
 | 
			
		||||
    name: ActionName,
 | 
			
		||||
    data?: PanelComponentProps["data"],
 | 
			
		||||
    isInHamburgerMenu = false,
 | 
			
		||||
  ) => {
 | 
			
		||||
    const canvasActions = this.app.props.UIOptions.canvasActions;
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
      this.actions[name] &&
 | 
			
		||||
      "PanelComponent" in this.actions[name] &&
 | 
			
		||||
@@ -169,6 +174,7 @@ export class ActionManager {
 | 
			
		||||
          updateData={updateData}
 | 
			
		||||
          appProps={this.app.props}
 | 
			
		||||
          data={data}
 | 
			
		||||
          isInHamburgerMenu={isInHamburgerMenu}
 | 
			
		||||
        />
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -3,36 +3,45 @@ import { isDarwin } from "../keys";
 | 
			
		||||
import { getShortcutKey } from "../utils";
 | 
			
		||||
import { ActionName } from "./types";
 | 
			
		||||
 | 
			
		||||
export type ShortcutName = SubtypeOf<
 | 
			
		||||
  ActionName,
 | 
			
		||||
  | "cut"
 | 
			
		||||
  | "copy"
 | 
			
		||||
  | "paste"
 | 
			
		||||
  | "copyStyles"
 | 
			
		||||
  | "pasteStyles"
 | 
			
		||||
  | "selectAll"
 | 
			
		||||
  | "deleteSelectedElements"
 | 
			
		||||
  | "duplicateSelection"
 | 
			
		||||
  | "sendBackward"
 | 
			
		||||
  | "bringForward"
 | 
			
		||||
  | "sendToBack"
 | 
			
		||||
  | "bringToFront"
 | 
			
		||||
  | "copyAsPng"
 | 
			
		||||
  | "copyAsSvg"
 | 
			
		||||
  | "group"
 | 
			
		||||
  | "ungroup"
 | 
			
		||||
  | "gridMode"
 | 
			
		||||
  | "zenMode"
 | 
			
		||||
  | "stats"
 | 
			
		||||
  | "addToLibrary"
 | 
			
		||||
  | "viewMode"
 | 
			
		||||
  | "flipHorizontal"
 | 
			
		||||
  | "flipVertical"
 | 
			
		||||
  | "hyperlink"
 | 
			
		||||
  | "toggleLock"
 | 
			
		||||
>;
 | 
			
		||||
export type ShortcutName =
 | 
			
		||||
  | SubtypeOf<
 | 
			
		||||
      ActionName,
 | 
			
		||||
      | "toggleTheme"
 | 
			
		||||
      | "loadScene"
 | 
			
		||||
      | "cut"
 | 
			
		||||
      | "copy"
 | 
			
		||||
      | "paste"
 | 
			
		||||
      | "copyStyles"
 | 
			
		||||
      | "pasteStyles"
 | 
			
		||||
      | "selectAll"
 | 
			
		||||
      | "deleteSelectedElements"
 | 
			
		||||
      | "duplicateSelection"
 | 
			
		||||
      | "sendBackward"
 | 
			
		||||
      | "bringForward"
 | 
			
		||||
      | "sendToBack"
 | 
			
		||||
      | "bringToFront"
 | 
			
		||||
      | "copyAsPng"
 | 
			
		||||
      | "copyAsSvg"
 | 
			
		||||
      | "group"
 | 
			
		||||
      | "ungroup"
 | 
			
		||||
      | "gridMode"
 | 
			
		||||
      | "zenMode"
 | 
			
		||||
      | "stats"
 | 
			
		||||
      | "addToLibrary"
 | 
			
		||||
      | "viewMode"
 | 
			
		||||
      | "flipHorizontal"
 | 
			
		||||
      | "flipVertical"
 | 
			
		||||
      | "hyperlink"
 | 
			
		||||
      | "toggleLock"
 | 
			
		||||
    >
 | 
			
		||||
  | "saveScene"
 | 
			
		||||
  | "imageExport";
 | 
			
		||||
 | 
			
		||||
const shortcutMap: Record<ShortcutName, string[]> = {
 | 
			
		||||
  toggleTheme: [getShortcutKey("Shift+Alt+D")],
 | 
			
		||||
  saveScene: [getShortcutKey("CtrlOrCmd+S")],
 | 
			
		||||
  loadScene: [getShortcutKey("CtrlOrCmd+O")],
 | 
			
		||||
  imageExport: [getShortcutKey("CtrlOrCmd+Shift+E")],
 | 
			
		||||
  cut: [getShortcutKey("CtrlOrCmd+X")],
 | 
			
		||||
  copy: [getShortcutKey("CtrlOrCmd+C")],
 | 
			
		||||
  paste: [getShortcutKey("CtrlOrCmd+V")],
 | 
			
		||||
 
 | 
			
		||||
@@ -124,7 +124,9 @@ export type PanelComponentProps = {
 | 
			
		||||
 | 
			
		||||
export interface Action {
 | 
			
		||||
  name: ActionName;
 | 
			
		||||
  PanelComponent?: React.FC<PanelComponentProps>;
 | 
			
		||||
  PanelComponent?: React.FC<
 | 
			
		||||
    PanelComponentProps & { isInHamburgerMenu: boolean }
 | 
			
		||||
  >;
 | 
			
		||||
  perform: ActionFn;
 | 
			
		||||
  keyPriority?: number;
 | 
			
		||||
  keyTest?: (
 | 
			
		||||
 
 | 
			
		||||
@@ -19,6 +19,7 @@ export const getDefaultAppState = (): Omit<
 | 
			
		||||
  "offsetTop" | "offsetLeft" | "width" | "height"
 | 
			
		||||
> => {
 | 
			
		||||
  return {
 | 
			
		||||
    showWelcomeScreen: false,
 | 
			
		||||
    theme: THEME.LIGHT,
 | 
			
		||||
    collaborators: new Map(),
 | 
			
		||||
    currentChartType: "bar",
 | 
			
		||||
@@ -67,6 +68,7 @@ export const getDefaultAppState = (): Omit<
 | 
			
		||||
    openMenu: null,
 | 
			
		||||
    openPopup: null,
 | 
			
		||||
    openSidebar: null,
 | 
			
		||||
    openDialog: null,
 | 
			
		||||
    pasteDialog: { shown: false, data: null },
 | 
			
		||||
    previousSelectedElementIds: {},
 | 
			
		||||
    resizingElement: null,
 | 
			
		||||
@@ -77,7 +79,6 @@ export const getDefaultAppState = (): Omit<
 | 
			
		||||
    selectedGroupIds: {},
 | 
			
		||||
    selectionElement: null,
 | 
			
		||||
    shouldCacheIgnoreZoom: false,
 | 
			
		||||
    showHelpDialog: false,
 | 
			
		||||
    showStats: false,
 | 
			
		||||
    startBoundElement: null,
 | 
			
		||||
    suggestedBindings: [],
 | 
			
		||||
@@ -110,6 +111,7 @@ const APP_STATE_STORAGE_CONF = (<
 | 
			
		||||
  T extends Record<keyof AppState, Values>,
 | 
			
		||||
>(config: { [K in keyof T]: K extends keyof AppState ? T[K] : never }) =>
 | 
			
		||||
  config)({
 | 
			
		||||
  showWelcomeScreen: { browser: true, export: false, server: false },
 | 
			
		||||
  theme: { browser: true, export: false, server: false },
 | 
			
		||||
  collaborators: { browser: false, export: false, server: false },
 | 
			
		||||
  currentChartType: { browser: true, export: false, server: false },
 | 
			
		||||
@@ -160,6 +162,7 @@ const APP_STATE_STORAGE_CONF = (<
 | 
			
		||||
  openMenu: { browser: true, export: false, server: false },
 | 
			
		||||
  openPopup: { browser: false, export: false, server: false },
 | 
			
		||||
  openSidebar: { browser: true, export: false, server: false },
 | 
			
		||||
  openDialog: { browser: false, export: false, server: false },
 | 
			
		||||
  pasteDialog: { browser: false, export: false, server: false },
 | 
			
		||||
  previousSelectedElementIds: { browser: true, export: false, server: false },
 | 
			
		||||
  resizingElement: { browser: false, export: false, server: false },
 | 
			
		||||
@@ -170,7 +173,6 @@ const APP_STATE_STORAGE_CONF = (<
 | 
			
		||||
  selectedGroupIds: { browser: true, export: false, server: false },
 | 
			
		||||
  selectionElement: { browser: false, export: false, server: false },
 | 
			
		||||
  shouldCacheIgnoreZoom: { browser: true, export: false, server: false },
 | 
			
		||||
  showHelpDialog: { browser: false, export: false, server: false },
 | 
			
		||||
  showStats: { browser: true, export: false, server: false },
 | 
			
		||||
  startBoundElement: { browser: false, export: false, server: false },
 | 
			
		||||
  suggestedBindings: { browser: false, export: false, server: false },
 | 
			
		||||
 
 | 
			
		||||
@@ -11,27 +11,18 @@ export const getClientColors = (clientId: string, appState: AppState) => {
 | 
			
		||||
  // Naive way of getting an integer out of the clientId
 | 
			
		||||
  const sum = clientId.split("").reduce((a, str) => a + str.charCodeAt(0), 0);
 | 
			
		||||
 | 
			
		||||
  // Skip transparent background.
 | 
			
		||||
  const backgrounds = colors.elementBackground.slice(1);
 | 
			
		||||
  const strokes = colors.elementStroke.slice(1);
 | 
			
		||||
  // Skip transparent & gray colors
 | 
			
		||||
  const backgrounds = colors.elementBackground.slice(3);
 | 
			
		||||
  const strokes = colors.elementStroke.slice(3);
 | 
			
		||||
  return {
 | 
			
		||||
    background: backgrounds[sum % backgrounds.length],
 | 
			
		||||
    stroke: strokes[sum % strokes.length],
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getClientInitials = (username?: string | null) => {
 | 
			
		||||
  if (!username) {
 | 
			
		||||
export const getClientInitials = (userName?: string | null) => {
 | 
			
		||||
  if (!userName) {
 | 
			
		||||
    return "?";
 | 
			
		||||
  }
 | 
			
		||||
  const names = username.trim().split(" ");
 | 
			
		||||
 | 
			
		||||
  if (names.length < 2) {
 | 
			
		||||
    return names[0].substring(0, 2).toUpperCase();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const firstName = names[0];
 | 
			
		||||
  const lastName = names[names.length - 1];
 | 
			
		||||
 | 
			
		||||
  return (firstName[0] + lastName[0]).toUpperCase();
 | 
			
		||||
  return userName.trim()[0].toUpperCase();
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										27
									
								
								src/clipboard.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/clipboard.test.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
			
		||||
import { parseClipboard } from "./clipboard";
 | 
			
		||||
 | 
			
		||||
describe("Test parseClipboard", () => {
 | 
			
		||||
  it("should parse valid json correctly", async () => {
 | 
			
		||||
    let text = "123";
 | 
			
		||||
 | 
			
		||||
    let clipboardData = await parseClipboard({
 | 
			
		||||
      //@ts-ignore
 | 
			
		||||
      clipboardData: {
 | 
			
		||||
        getData: () => text,
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    expect(clipboardData.text).toBe(text);
 | 
			
		||||
 | 
			
		||||
    text = "[123]";
 | 
			
		||||
 | 
			
		||||
    clipboardData = await parseClipboard({
 | 
			
		||||
      //@ts-ignore
 | 
			
		||||
      clipboardData: {
 | 
			
		||||
        getData: () => text,
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    expect(clipboardData.text).toBe(text);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@@ -156,15 +156,13 @@ export const parseClipboard = async (
 | 
			
		||||
        files: systemClipboardData.files,
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
    return appClipboardData;
 | 
			
		||||
  } catch {
 | 
			
		||||
    // system clipboard doesn't contain excalidraw elements → return plaintext
 | 
			
		||||
    // unless we set a flag to prefer in-app clipboard because browser didn't
 | 
			
		||||
    // support storing to system clipboard on copy
 | 
			
		||||
    return PREFER_APP_CLIPBOARD && appClipboardData.elements
 | 
			
		||||
      ? appClipboardData
 | 
			
		||||
      : { text: systemClipboard };
 | 
			
		||||
  }
 | 
			
		||||
  } catch (e) {}
 | 
			
		||||
  // system clipboard doesn't contain excalidraw elements → return plaintext
 | 
			
		||||
  // unless we set a flag to prefer in-app clipboard because browser didn't
 | 
			
		||||
  // support storing to system clipboard on copy
 | 
			
		||||
  return PREFER_APP_CLIPBOARD && appClipboardData.elements
 | 
			
		||||
    ? appClipboardData
 | 
			
		||||
    : { text: systemClipboard };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const copyBlobToClipboardAsPng = async (blob: Blob | Promise<Blob>) => {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										92
									
								
								src/components/Actions.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								src/components/Actions.scss
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,92 @@
 | 
			
		||||
.zoom-actions,
 | 
			
		||||
.undo-redo-buttons {
 | 
			
		||||
  background-color: var(--island-bg-color);
 | 
			
		||||
  border-radius: var(--border-radius-lg);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.zoom-button,
 | 
			
		||||
.undo-redo-buttons button {
 | 
			
		||||
  border: 1px solid var(--default-border-color) !important;
 | 
			
		||||
  border-radius: 0 !important;
 | 
			
		||||
  background-color: transparent !important;
 | 
			
		||||
  font-size: 0.875rem !important;
 | 
			
		||||
  width: var(--lg-button-size);
 | 
			
		||||
  height: var(--lg-button-size);
 | 
			
		||||
  svg {
 | 
			
		||||
    width: var(--lg-icon-size) !important;
 | 
			
		||||
    height: var(--lg-icon-size) !important;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .ToolIcon__icon {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.reset-zoom-button {
 | 
			
		||||
  border-left: 0 !important;
 | 
			
		||||
  border-right: 0 !important;
 | 
			
		||||
  padding: 0 0.625rem !important;
 | 
			
		||||
  width: 3.75rem !important;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  color: var(--text-primary-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.zoom-out-button {
 | 
			
		||||
  border-top-left-radius: var(--border-radius-lg) !important;
 | 
			
		||||
  border-bottom-left-radius: var(--border-radius-lg) !important;
 | 
			
		||||
 | 
			
		||||
  :root[dir="rtl"] & {
 | 
			
		||||
    transform: scaleX(-1);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .ToolIcon__icon {
 | 
			
		||||
    border-top-right-radius: 0 !important;
 | 
			
		||||
    border-bottom-right-radius: 0 !important;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.zoom-in-button {
 | 
			
		||||
  border-top-right-radius: var(--border-radius-lg) !important;
 | 
			
		||||
  border-bottom-right-radius: var(--border-radius-lg) !important;
 | 
			
		||||
 | 
			
		||||
  :root[dir="rtl"] & {
 | 
			
		||||
    transform: scaleX(-1);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .ToolIcon__icon {
 | 
			
		||||
    border-top-left-radius: 0 !important;
 | 
			
		||||
    border-bottom-left-radius: 0 !important;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.undo-redo-buttons {
 | 
			
		||||
  .undo-button-container button {
 | 
			
		||||
    border-top-left-radius: var(--border-radius-lg) !important;
 | 
			
		||||
    border-bottom-left-radius: var(--border-radius-lg) !important;
 | 
			
		||||
    border-right: 0 !important;
 | 
			
		||||
 | 
			
		||||
    :root[dir="rtl"] & {
 | 
			
		||||
      transform: scaleX(-1);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .ToolIcon__icon {
 | 
			
		||||
      border-top-right-radius: 0 !important;
 | 
			
		||||
      border-bottom-right-radius: 0 !important;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .redo-button-container button {
 | 
			
		||||
    border-top-right-radius: var(--border-radius-lg) !important;
 | 
			
		||||
    border-bottom-right-radius: var(--border-radius-lg) !important;
 | 
			
		||||
 | 
			
		||||
    :root[dir="rtl"] & {
 | 
			
		||||
      transform: scaleX(-1);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .ToolIcon__icon {
 | 
			
		||||
      border-top-left-radius: 0 !important;
 | 
			
		||||
      border-bottom-left-radius: 0 !important;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -28,6 +28,8 @@ import { trackEvent } from "../analytics";
 | 
			
		||||
import { hasBoundTextElement, isBoundToContainer } from "../element/typeChecks";
 | 
			
		||||
import clsx from "clsx";
 | 
			
		||||
import { actionToggleZenMode } from "../actions";
 | 
			
		||||
import "./Actions.scss";
 | 
			
		||||
import { Tooltip } from "./Tooltip";
 | 
			
		||||
 | 
			
		||||
export const SelectedShapeActions = ({
 | 
			
		||||
  appState,
 | 
			
		||||
@@ -79,12 +81,16 @@ export const SelectedShapeActions = ({
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="panelColumn">
 | 
			
		||||
      {((hasStrokeColor(appState.activeTool.type) &&
 | 
			
		||||
        appState.activeTool.type !== "image" &&
 | 
			
		||||
        commonSelectedType !== "image") ||
 | 
			
		||||
        targetElements.some((element) => hasStrokeColor(element.type))) &&
 | 
			
		||||
        renderAction("changeStrokeColor")}
 | 
			
		||||
      {showChangeBackgroundIcons && renderAction("changeBackgroundColor")}
 | 
			
		||||
      <div>
 | 
			
		||||
        {((hasStrokeColor(appState.activeTool.type) &&
 | 
			
		||||
          appState.activeTool.type !== "image" &&
 | 
			
		||||
          commonSelectedType !== "image") ||
 | 
			
		||||
          targetElements.some((element) => hasStrokeColor(element.type))) &&
 | 
			
		||||
          renderAction("changeStrokeColor")}
 | 
			
		||||
      </div>
 | 
			
		||||
      {showChangeBackgroundIcons && (
 | 
			
		||||
        <div>{renderAction("changeBackgroundColor")}</div>
 | 
			
		||||
      )}
 | 
			
		||||
      {showFillIcons && renderAction("changeFillStyle")}
 | 
			
		||||
 | 
			
		||||
      {(hasStrokeWidth(appState.activeTool.type) ||
 | 
			
		||||
@@ -163,7 +169,16 @@ export const SelectedShapeActions = ({
 | 
			
		||||
            )}
 | 
			
		||||
            {targetElements.length > 2 &&
 | 
			
		||||
              renderAction("distributeHorizontally")}
 | 
			
		||||
            <div className="iconRow">
 | 
			
		||||
            {/* breaks the row ˇˇ */}
 | 
			
		||||
            <div style={{ flexBasis: "100%", height: 0 }} />
 | 
			
		||||
            <div
 | 
			
		||||
              style={{
 | 
			
		||||
                display: "flex",
 | 
			
		||||
                flexWrap: "wrap",
 | 
			
		||||
                gap: ".5rem",
 | 
			
		||||
                marginTop: "-0.5rem",
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              {renderAction("alignTop")}
 | 
			
		||||
              {renderAction("alignVerticallyCentered")}
 | 
			
		||||
              {renderAction("alignBottom")}
 | 
			
		||||
@@ -203,25 +218,25 @@ export const ShapesSwitcher = ({
 | 
			
		||||
  appState: AppState;
 | 
			
		||||
}) => (
 | 
			
		||||
  <>
 | 
			
		||||
    {SHAPES.map(({ value, icon, key }, index) => {
 | 
			
		||||
    {SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => {
 | 
			
		||||
      const label = t(`toolBar.${value}`);
 | 
			
		||||
      const letter = key && (typeof key === "string" ? key : key[0]);
 | 
			
		||||
      const shortcut = letter
 | 
			
		||||
        ? `${capitalizeString(letter)} ${t("helpDialog.or")} ${index + 1}`
 | 
			
		||||
        : `${index + 1}`;
 | 
			
		||||
        ? `${capitalizeString(letter)} ${t("helpDialog.or")} ${numericKey}`
 | 
			
		||||
        : `${numericKey}`;
 | 
			
		||||
      return (
 | 
			
		||||
        <ToolButton
 | 
			
		||||
          className="Shape"
 | 
			
		||||
          className={clsx("Shape", { fillable })}
 | 
			
		||||
          key={value}
 | 
			
		||||
          type="radio"
 | 
			
		||||
          icon={icon}
 | 
			
		||||
          checked={activeTool.type === value}
 | 
			
		||||
          name="editor-current-shape"
 | 
			
		||||
          title={`${capitalizeString(label)} — ${shortcut}`}
 | 
			
		||||
          keyBindingLabel={`${index + 1}`}
 | 
			
		||||
          keyBindingLabel={numericKey}
 | 
			
		||||
          aria-label={capitalizeString(label)}
 | 
			
		||||
          aria-keyshortcuts={shortcut}
 | 
			
		||||
          data-testid={value}
 | 
			
		||||
          data-testid={`toolbar-${value}`}
 | 
			
		||||
          onPointerDown={({ pointerType }) => {
 | 
			
		||||
            if (!appState.penDetected && pointerType === "pen") {
 | 
			
		||||
              setAppState({
 | 
			
		||||
@@ -263,11 +278,11 @@ export const ZoomActions = ({
 | 
			
		||||
  renderAction: ActionManager["renderAction"];
 | 
			
		||||
  zoom: Zoom;
 | 
			
		||||
}) => (
 | 
			
		||||
  <Stack.Col gap={1}>
 | 
			
		||||
    <Stack.Row gap={1} align="center">
 | 
			
		||||
  <Stack.Col gap={1} className="zoom-actions">
 | 
			
		||||
    <Stack.Row align="center">
 | 
			
		||||
      {renderAction("zoomOut")}
 | 
			
		||||
      {renderAction("zoomIn")}
 | 
			
		||||
      {renderAction("resetZoom")}
 | 
			
		||||
      {renderAction("zoomIn")}
 | 
			
		||||
    </Stack.Row>
 | 
			
		||||
  </Stack.Col>
 | 
			
		||||
);
 | 
			
		||||
@@ -280,8 +295,12 @@ export const UndoRedoActions = ({
 | 
			
		||||
  className?: string;
 | 
			
		||||
}) => (
 | 
			
		||||
  <div className={`undo-redo-buttons ${className}`}>
 | 
			
		||||
    {renderAction("undo", { size: "small" })}
 | 
			
		||||
    {renderAction("redo", { size: "small" })}
 | 
			
		||||
    <div className="undo-button-container">
 | 
			
		||||
      <Tooltip label={t("buttons.undo")}>{renderAction("undo")}</Tooltip>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div className="redo-button-container">
 | 
			
		||||
      <Tooltip label={t("buttons.redo")}> {renderAction("redo")}</Tooltip>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,11 @@
 | 
			
		||||
import Stack from "../components/Stack";
 | 
			
		||||
import { ToolButton } from "../components/ToolButton";
 | 
			
		||||
import { save, file } from "../components/icons";
 | 
			
		||||
// TODO barnabasmolnar/editor-redesign
 | 
			
		||||
// this icon is not great
 | 
			
		||||
import { getShortcutFromShortcutName } from "../actions/shortcuts";
 | 
			
		||||
import { save } from "../components/icons";
 | 
			
		||||
import { t } from "../i18n";
 | 
			
		||||
 | 
			
		||||
import "./ActiveFile.scss";
 | 
			
		||||
import MenuItem from "./MenuItem";
 | 
			
		||||
 | 
			
		||||
type ActiveFileProps = {
 | 
			
		||||
  fileName?: string;
 | 
			
		||||
@@ -11,18 +13,11 @@ type ActiveFileProps = {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const ActiveFile = ({ fileName, onSave }: ActiveFileProps) => (
 | 
			
		||||
  <Stack.Row className="ActiveFile" gap={1} align="center">
 | 
			
		||||
    <span className="ActiveFile__fileName">
 | 
			
		||||
      {file}
 | 
			
		||||
      <span>{fileName}</span>
 | 
			
		||||
    </span>
 | 
			
		||||
    <ToolButton
 | 
			
		||||
      type="icon"
 | 
			
		||||
      icon={save}
 | 
			
		||||
      title={t("buttons.save")}
 | 
			
		||||
      aria-label={t("buttons.save")}
 | 
			
		||||
      onClick={onSave}
 | 
			
		||||
      data-testid="save-button"
 | 
			
		||||
    />
 | 
			
		||||
  </Stack.Row>
 | 
			
		||||
  <MenuItem
 | 
			
		||||
    label={`${t("buttons.save")}`}
 | 
			
		||||
    shortcut={getShortcutFromShortcutName("saveScene")}
 | 
			
		||||
    dataTestId="save-button"
 | 
			
		||||
    onClick={onSave}
 | 
			
		||||
    icon={save}
 | 
			
		||||
  />
 | 
			
		||||
);
 | 
			
		||||
 
 | 
			
		||||
@@ -266,6 +266,10 @@ import {
 | 
			
		||||
  isLocalLink,
 | 
			
		||||
} from "../element/Hyperlink";
 | 
			
		||||
import { shouldShowBoundingBox } from "../element/transformHandles";
 | 
			
		||||
import { atom } from "jotai";
 | 
			
		||||
 | 
			
		||||
export const isMenuOpenAtom = atom(false);
 | 
			
		||||
export const isDropdownOpenAtom = atom(false);
 | 
			
		||||
 | 
			
		||||
const deviceContextInitialValue = {
 | 
			
		||||
  isSmScreen: false,
 | 
			
		||||
@@ -571,6 +575,11 @@ class App extends React.Component<AppProps, AppState> {
 | 
			
		||||
                    library={this.library}
 | 
			
		||||
                    id={this.id}
 | 
			
		||||
                    onImageAction={this.onImageAction}
 | 
			
		||||
                    renderWelcomeScreen={
 | 
			
		||||
                      this.state.showWelcomeScreen &&
 | 
			
		||||
                      this.state.activeTool.type === "selection" &&
 | 
			
		||||
                      !this.scene.getElementsIncludingDeleted().length
 | 
			
		||||
                    }
 | 
			
		||||
                  />
 | 
			
		||||
                  <div className="excalidraw-textEditorContainer" />
 | 
			
		||||
                  <div className="excalidraw-contextMenuContainer" />
 | 
			
		||||
@@ -1085,6 +1094,13 @@ class App extends React.Component<AppProps, AppState> {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentDidUpdate(prevProps: AppProps, prevState: AppState) {
 | 
			
		||||
    if (
 | 
			
		||||
      !this.state.showWelcomeScreen &&
 | 
			
		||||
      !this.scene.getElementsIncludingDeleted().length
 | 
			
		||||
    ) {
 | 
			
		||||
      this.setState({ showWelcomeScreen: true });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
      this.excalidrawContainerRef.current &&
 | 
			
		||||
      prevProps.UIOptions.dockedSidebarBreakpoint !==
 | 
			
		||||
@@ -1276,6 +1292,10 @@ class App extends React.Component<AppProps, AppState> {
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
    const selectionColor = getComputedStyle(
 | 
			
		||||
      document.querySelector(".excalidraw")!,
 | 
			
		||||
    ).getPropertyValue("--color-selection");
 | 
			
		||||
 | 
			
		||||
    renderScene(
 | 
			
		||||
      {
 | 
			
		||||
        elements: renderingElements,
 | 
			
		||||
@@ -1284,6 +1304,7 @@ class App extends React.Component<AppProps, AppState> {
 | 
			
		||||
        rc: this.rc!,
 | 
			
		||||
        canvas: this.canvas!,
 | 
			
		||||
        renderConfig: {
 | 
			
		||||
          selectionColor,
 | 
			
		||||
          scrollX: this.state.scrollX,
 | 
			
		||||
          scrollY: this.state.scrollY,
 | 
			
		||||
          viewBackgroundColor: this.state.viewBackgroundColor,
 | 
			
		||||
@@ -1867,8 +1888,16 @@ class App extends React.Component<AppProps, AppState> {
 | 
			
		||||
 | 
			
		||||
      if (event.key === KEYS.QUESTION_MARK) {
 | 
			
		||||
        this.setState({
 | 
			
		||||
          showHelpDialog: true,
 | 
			
		||||
          openDialog: "help",
 | 
			
		||||
        });
 | 
			
		||||
        return;
 | 
			
		||||
      } else if (
 | 
			
		||||
        event.key.toLowerCase() === KEYS.E &&
 | 
			
		||||
        event.shiftKey &&
 | 
			
		||||
        event[KEYS.CTRL_OR_CMD]
 | 
			
		||||
      ) {
 | 
			
		||||
        this.setState({ openDialog: "imageExport" });
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (this.actionManager.handleKeyDown(event)) {
 | 
			
		||||
@@ -1883,18 +1912,6 @@ class App extends React.Component<AppProps, AppState> {
 | 
			
		||||
        this.setState({ isBindingEnabled: false });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (event.code === CODES.ZERO) {
 | 
			
		||||
        const nextState = this.toggleMenu("library");
 | 
			
		||||
        // track only openings
 | 
			
		||||
        if (nextState) {
 | 
			
		||||
          trackEvent(
 | 
			
		||||
            "library",
 | 
			
		||||
            "toggleLibrary (open)",
 | 
			
		||||
            `keyboard (${this.device.isMobile ? "mobile" : "desktop"})`,
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (isArrowKey(event.key)) {
 | 
			
		||||
        const step =
 | 
			
		||||
          (this.state.gridSize &&
 | 
			
		||||
@@ -4807,10 +4824,6 @@ class App extends React.Component<AppProps, AppState> {
 | 
			
		||||
          } else {
 | 
			
		||||
            this.setState((prevState) => ({
 | 
			
		||||
              draggingElement: null,
 | 
			
		||||
              selectedElementIds: {
 | 
			
		||||
                ...prevState.selectedElementIds,
 | 
			
		||||
                [draggingElement.id]: true,
 | 
			
		||||
              },
 | 
			
		||||
            }));
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
@@ -5218,6 +5231,7 @@ class App extends React.Component<AppProps, AppState> {
 | 
			
		||||
              id: fileId,
 | 
			
		||||
              dataURL,
 | 
			
		||||
              created: Date.now(),
 | 
			
		||||
              lastRetrieved: Date.now(),
 | 
			
		||||
            },
 | 
			
		||||
          };
 | 
			
		||||
          const cachedImageData = this.imageCache.get(fileId);
 | 
			
		||||
 
 | 
			
		||||
@@ -2,16 +2,19 @@
 | 
			
		||||
 | 
			
		||||
.excalidraw {
 | 
			
		||||
  .Avatar {
 | 
			
		||||
    width: 2.5rem;
 | 
			
		||||
    height: 2.5rem;
 | 
			
		||||
    border-radius: 1.25rem;
 | 
			
		||||
    width: 1.25rem;
 | 
			
		||||
    height: 1.25rem;
 | 
			
		||||
    border-radius: 100%;
 | 
			
		||||
    outline: 2px solid var(--avatar-border-color);
 | 
			
		||||
    outline-offset: 2px;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    color: $oc-white;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    font-size: 0.8rem;
 | 
			
		||||
    font-size: 0.625rem;
 | 
			
		||||
    font-weight: 500;
 | 
			
		||||
    line-height: 1;
 | 
			
		||||
 | 
			
		||||
    &-img {
 | 
			
		||||
      width: 100%;
 | 
			
		||||
 
 | 
			
		||||
@@ -11,13 +11,11 @@ type AvatarProps = {
 | 
			
		||||
  src?: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const Avatar = ({ color, border, onClick, name, src }: AvatarProps) => {
 | 
			
		||||
export const Avatar = ({ color, onClick, name, src }: AvatarProps) => {
 | 
			
		||||
  const shortName = getClientInitials(name);
 | 
			
		||||
  const [error, setError] = useState(false);
 | 
			
		||||
  const loadImg = !error && src;
 | 
			
		||||
  const style = loadImg
 | 
			
		||||
    ? undefined
 | 
			
		||||
    : { background: color, border: `1px solid ${border}` };
 | 
			
		||||
  const style = loadImg ? undefined : { background: color };
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="Avatar" style={style} onClick={onClick}>
 | 
			
		||||
      {loadImg ? (
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +0,0 @@
 | 
			
		||||
import { ActionManager } from "../actions/manager";
 | 
			
		||||
 | 
			
		||||
export const BackgroundPickerAndDarkModeToggle = ({
 | 
			
		||||
  actionManager,
 | 
			
		||||
}: {
 | 
			
		||||
  actionManager: ActionManager;
 | 
			
		||||
}) => (
 | 
			
		||||
  <div style={{ display: "flex" }}>
 | 
			
		||||
    {actionManager.renderAction("changeViewBackgroundColor")}
 | 
			
		||||
    {actionManager.renderAction("toggleTheme")}
 | 
			
		||||
  </div>
 | 
			
		||||
);
 | 
			
		||||
@@ -64,6 +64,8 @@
 | 
			
		||||
 | 
			
		||||
      color: #{$oc-blue-7};
 | 
			
		||||
 | 
			
		||||
      border: 0;
 | 
			
		||||
 | 
			
		||||
      &:focus {
 | 
			
		||||
        box-shadow: 0 0 0 3px #{$oc-blue-7};
 | 
			
		||||
      }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,9 @@
 | 
			
		||||
import { useState } from "react";
 | 
			
		||||
import { t } from "../i18n";
 | 
			
		||||
import { useDevice } from "./App";
 | 
			
		||||
import { trash } from "./icons";
 | 
			
		||||
import { ToolButton } from "./ToolButton";
 | 
			
		||||
import { TrashIcon } from "./icons";
 | 
			
		||||
 | 
			
		||||
import ConfirmDialog from "./ConfirmDialog";
 | 
			
		||||
import MenuItem from "./MenuItem";
 | 
			
		||||
 | 
			
		||||
const ClearCanvas = ({ onConfirm }: { onConfirm: () => void }) => {
 | 
			
		||||
  const [showDialog, setShowDialog] = useState(false);
 | 
			
		||||
@@ -14,14 +13,11 @@ const ClearCanvas = ({ onConfirm }: { onConfirm: () => void }) => {
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <ToolButton
 | 
			
		||||
        type="button"
 | 
			
		||||
        icon={trash}
 | 
			
		||||
        title={t("buttons.clearReset")}
 | 
			
		||||
        aria-label={t("buttons.clearReset")}
 | 
			
		||||
        showAriaLabel={useDevice().isMobile}
 | 
			
		||||
      <MenuItem
 | 
			
		||||
        label={t("buttons.clearReset")}
 | 
			
		||||
        icon={TrashIcon}
 | 
			
		||||
        onClick={toggleDialog}
 | 
			
		||||
        data-testid="clear-canvas-button"
 | 
			
		||||
        dataTestId="clear-canvas-button"
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
      {showDialog && (
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,51 @@
 | 
			
		||||
@import "../css/variables.module";
 | 
			
		||||
 | 
			
		||||
.excalidraw {
 | 
			
		||||
  .collab-button {
 | 
			
		||||
    @include outlineButtonStyles;
 | 
			
		||||
    width: var(--lg-button-size);
 | 
			
		||||
    height: var(--lg-button-size);
 | 
			
		||||
 | 
			
		||||
    svg {
 | 
			
		||||
      width: var(--lg-icon-size);
 | 
			
		||||
      height: var(--lg-icon-size);
 | 
			
		||||
    }
 | 
			
		||||
    background-color: var(--color-primary);
 | 
			
		||||
    border-color: var(--color-primary);
 | 
			
		||||
    color: white;
 | 
			
		||||
    flex-shrink: 0;
 | 
			
		||||
 | 
			
		||||
    &:hover {
 | 
			
		||||
      background-color: var(--color-primary-darker);
 | 
			
		||||
      border-color: var(--color-primary-darker);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &:active {
 | 
			
		||||
      background-color: var(--color-primary-darker);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.active {
 | 
			
		||||
      background-color: #0fb884;
 | 
			
		||||
      border-color: #0fb884;
 | 
			
		||||
 | 
			
		||||
      svg {
 | 
			
		||||
        color: #fff;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      &:hover,
 | 
			
		||||
      &:active {
 | 
			
		||||
        background-color: #0fb884;
 | 
			
		||||
        border-color: #0fb884;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &.theme--dark {
 | 
			
		||||
    .collab-button {
 | 
			
		||||
      color: var(--color-gray-90);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .CollabButton.is-collaborating {
 | 
			
		||||
    background-color: var(--button-special-active-bg-color);
 | 
			
		||||
 | 
			
		||||
@@ -24,9 +69,9 @@
 | 
			
		||||
    bottom: -5px;
 | 
			
		||||
    padding: 3px;
 | 
			
		||||
    border-radius: 50%;
 | 
			
		||||
    background-color: $oc-green-6;
 | 
			
		||||
    color: $oc-white;
 | 
			
		||||
    font-size: 0.6em;
 | 
			
		||||
    background-color: $oc-green-2;
 | 
			
		||||
    color: $oc-green-9;
 | 
			
		||||
    font-size: 0.6rem;
 | 
			
		||||
    font-family: "Cascadia";
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,37 +1,47 @@
 | 
			
		||||
import clsx from "clsx";
 | 
			
		||||
import { ToolButton } from "./ToolButton";
 | 
			
		||||
import { t } from "../i18n";
 | 
			
		||||
import { useDevice } from "../components/App";
 | 
			
		||||
import { users } from "./icons";
 | 
			
		||||
import { UsersIcon } from "./icons";
 | 
			
		||||
 | 
			
		||||
import "./CollabButton.scss";
 | 
			
		||||
import MenuItem from "./MenuItem";
 | 
			
		||||
import clsx from "clsx";
 | 
			
		||||
 | 
			
		||||
const CollabButton = ({
 | 
			
		||||
  isCollaborating,
 | 
			
		||||
  collaboratorCount,
 | 
			
		||||
  onClick,
 | 
			
		||||
  isInHamburgerMenu = true,
 | 
			
		||||
}: {
 | 
			
		||||
  isCollaborating: boolean;
 | 
			
		||||
  collaboratorCount: number;
 | 
			
		||||
  onClick: () => void;
 | 
			
		||||
  isInHamburgerMenu?: boolean;
 | 
			
		||||
}) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <ToolButton
 | 
			
		||||
        className={clsx("CollabButton", {
 | 
			
		||||
          "is-collaborating": isCollaborating,
 | 
			
		||||
        })}
 | 
			
		||||
        onClick={onClick}
 | 
			
		||||
        icon={users}
 | 
			
		||||
        type="button"
 | 
			
		||||
        title={t("labels.liveCollaboration")}
 | 
			
		||||
        aria-label={t("labels.liveCollaboration")}
 | 
			
		||||
        showAriaLabel={useDevice().isMobile}
 | 
			
		||||
      >
 | 
			
		||||
        {isCollaborating && (
 | 
			
		||||
          <div className="CollabButton-collaborators">{collaboratorCount}</div>
 | 
			
		||||
        )}
 | 
			
		||||
      </ToolButton>
 | 
			
		||||
      {isInHamburgerMenu ? (
 | 
			
		||||
        <MenuItem
 | 
			
		||||
          label={t("labels.liveCollaboration")}
 | 
			
		||||
          dataTestId="collab-button"
 | 
			
		||||
          icon={UsersIcon}
 | 
			
		||||
          onClick={onClick}
 | 
			
		||||
          isCollaborating={isCollaborating}
 | 
			
		||||
        />
 | 
			
		||||
      ) : (
 | 
			
		||||
        <button
 | 
			
		||||
          className={clsx("collab-button", { active: isCollaborating })}
 | 
			
		||||
          type="button"
 | 
			
		||||
          onClick={onClick}
 | 
			
		||||
          style={{ position: "relative" }}
 | 
			
		||||
          title={t("labels.liveCollaboration")}
 | 
			
		||||
        >
 | 
			
		||||
          {UsersIcon}
 | 
			
		||||
          {collaboratorCount > 0 && (
 | 
			
		||||
            <div className="CollabButton-collaborators">
 | 
			
		||||
              {collaboratorCount}
 | 
			
		||||
            </div>
 | 
			
		||||
          )}
 | 
			
		||||
        </button>
 | 
			
		||||
      )}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -21,6 +21,23 @@
 | 
			
		||||
    display: grid;
 | 
			
		||||
    grid-template-columns: auto 1fr;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    column-gap: 0.5rem;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .color-picker-control-container + .popover {
 | 
			
		||||
    position: static;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .color-picker-popover-container {
 | 
			
		||||
    margin-top: -0.25rem;
 | 
			
		||||
 | 
			
		||||
    :root[dir="ltr"] & {
 | 
			
		||||
      margin-left: 0.5rem;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    :root[dir="rtl"] & {
 | 
			
		||||
      margin-left: -3rem;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .color-picker-triangle {
 | 
			
		||||
@@ -30,20 +47,29 @@
 | 
			
		||||
    border-width: 0 9px 10px;
 | 
			
		||||
    border-color: transparent transparent var(--popup-bg-color);
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: -10px;
 | 
			
		||||
    top: 10px;
 | 
			
		||||
 | 
			
		||||
    :root[dir="ltr"] & {
 | 
			
		||||
      left: 12px;
 | 
			
		||||
      transform: rotate(270deg);
 | 
			
		||||
      left: -14px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    :root[dir="rtl"] & {
 | 
			
		||||
      right: 12px;
 | 
			
		||||
      transform: rotate(90deg);
 | 
			
		||||
      right: -14px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .color-picker-triangle-shadow {
 | 
			
		||||
    border-color: transparent transparent transparentize($oc-black, 0.9);
 | 
			
		||||
    top: -11px;
 | 
			
		||||
 | 
			
		||||
    :root[dir="ltr"] & {
 | 
			
		||||
      left: -14px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    :root[dir="rtl"] & {
 | 
			
		||||
      right: -16px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .color-picker-content--default {
 | 
			
		||||
@@ -119,16 +145,21 @@
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .color-picker-hash {
 | 
			
		||||
    background: var(--input-border-color);
 | 
			
		||||
    height: 1.875rem;
 | 
			
		||||
    width: 1.875rem;
 | 
			
		||||
    height: var(--default-button-size);
 | 
			
		||||
    flex-shrink: 0;
 | 
			
		||||
    padding: 0.5rem 0.5rem 0.5rem 0.75rem;
 | 
			
		||||
    border: 1px solid var(--default-border-color);
 | 
			
		||||
    border-right: 0;
 | 
			
		||||
    box-sizing: border-box;
 | 
			
		||||
 | 
			
		||||
    :root[dir="ltr"] & {
 | 
			
		||||
      border-radius: 4px 0 0 4px;
 | 
			
		||||
      border-radius: var(--border-radius-lg) 0 0 var(--border-radius-lg);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    :root[dir="rtl"] & {
 | 
			
		||||
      border-radius: 0 4px 4px 0;
 | 
			
		||||
      border-radius: 0 var(--border-radius-lg) var(--border-radius-lg) 0;
 | 
			
		||||
      border-right: 1px solid var(--default-border-color);
 | 
			
		||||
      border-left: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    color: var(--input-label-color);
 | 
			
		||||
@@ -138,81 +169,64 @@
 | 
			
		||||
    position: relative;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .color-input-container:focus-within .color-picker-hash {
 | 
			
		||||
    box-shadow: 0 0 0 2px var(--focus-highlight-color);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .color-input-container:focus-within .color-picker-hash::before,
 | 
			
		||||
  .color-input-container:focus-within .color-picker-hash::after {
 | 
			
		||||
    content: "";
 | 
			
		||||
    width: 1px;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .color-input-container:focus-within .color-picker-hash::before {
 | 
			
		||||
    background: var(--input-border-color);
 | 
			
		||||
 | 
			
		||||
    :root[dir="ltr"] & {
 | 
			
		||||
      right: -1px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    :root[dir="rtl"] & {
 | 
			
		||||
      left: -1px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .color-input-container:focus-within .color-picker-hash::after {
 | 
			
		||||
    background: var(--input-bg-color);
 | 
			
		||||
 | 
			
		||||
    :root[dir="ltr"] & {
 | 
			
		||||
      right: -2px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    :root[dir="rtl"] & {
 | 
			
		||||
      left: -2px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .color-input-container {
 | 
			
		||||
    display: flex;
 | 
			
		||||
 | 
			
		||||
    &:focus-within {
 | 
			
		||||
      box-shadow: 0 0 0 1px var(--color-primary-darkest);
 | 
			
		||||
      border-radius: var(--border-radius-lg);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .color-picker-input {
 | 
			
		||||
    width: 11ch; /* length of `transparent` */
 | 
			
		||||
    box-sizing: border-box;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    font-size: 1rem;
 | 
			
		||||
    background-color: var(--input-bg-color);
 | 
			
		||||
    font-size: 0.875rem;
 | 
			
		||||
    background-color: transparent;
 | 
			
		||||
    color: var(--text-primary-color);
 | 
			
		||||
    border: 0;
 | 
			
		||||
    outline: none;
 | 
			
		||||
    height: 1.75em;
 | 
			
		||||
    box-shadow: var(--input-border-color) 0 0 0 1px inset;
 | 
			
		||||
    height: var(--default-button-size);
 | 
			
		||||
    border: 1px solid var(--default-border-color);
 | 
			
		||||
    border-left: 0;
 | 
			
		||||
    letter-spacing: 0.4px;
 | 
			
		||||
 | 
			
		||||
    :root[dir="ltr"] & {
 | 
			
		||||
      border-radius: 0 4px 4px 0;
 | 
			
		||||
      border-radius: 0 var(--border-radius-lg) var(--border-radius-lg) 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    :root[dir="rtl"] & {
 | 
			
		||||
      border-radius: 4px 0 0 4px;
 | 
			
		||||
      border-radius: var(--border-radius-lg) 0 0 var(--border-radius-lg);
 | 
			
		||||
      border-left: 1px solid var(--default-border-color);
 | 
			
		||||
      border-right: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    float: left;
 | 
			
		||||
    padding: 1px;
 | 
			
		||||
    padding-inline-start: 0.5em;
 | 
			
		||||
    padding: 0.5rem;
 | 
			
		||||
    padding-left: 0.25rem;
 | 
			
		||||
    appearance: none;
 | 
			
		||||
 | 
			
		||||
    &:focus-visible {
 | 
			
		||||
      box-shadow: none;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .color-picker-label-swatch-container {
 | 
			
		||||
    border: 1px solid var(--default-border-color);
 | 
			
		||||
    border-radius: var(--border-radius-lg);
 | 
			
		||||
    width: var(--default-button-size);
 | 
			
		||||
    height: var(--default-button-size);
 | 
			
		||||
    box-sizing: border-box;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .color-picker-label-swatch {
 | 
			
		||||
    height: 1.875rem;
 | 
			
		||||
    width: 1.875rem;
 | 
			
		||||
    margin-inline-end: 0.25rem;
 | 
			
		||||
    border: 1px solid $oc-gray-3;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    @include outlineButtonStyles;
 | 
			
		||||
    background-color: var(--swatch-color) !important;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    background-color: transparent !important;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    filter: var(--theme-filter);
 | 
			
		||||
    border: 0 !important;
 | 
			
		||||
 | 
			
		||||
    &:after {
 | 
			
		||||
      content: "";
 | 
			
		||||
 
 | 
			
		||||
@@ -365,17 +365,20 @@ export const ColorPicker = ({
 | 
			
		||||
  appState: AppState;
 | 
			
		||||
}) => {
 | 
			
		||||
  const pickerButton = React.useRef<HTMLButtonElement>(null);
 | 
			
		||||
  const coords = pickerButton.current?.getBoundingClientRect();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      <div className="color-picker-control-container">
 | 
			
		||||
        <button
 | 
			
		||||
          className="color-picker-label-swatch"
 | 
			
		||||
          aria-label={label}
 | 
			
		||||
          style={color ? { "--swatch-color": color } : undefined}
 | 
			
		||||
          onClick={() => setActive(!isActive)}
 | 
			
		||||
          ref={pickerButton}
 | 
			
		||||
        />
 | 
			
		||||
        <div className="color-picker-label-swatch-container">
 | 
			
		||||
          <button
 | 
			
		||||
            className="color-picker-label-swatch"
 | 
			
		||||
            aria-label={label}
 | 
			
		||||
            style={color ? { "--swatch-color": color } : undefined}
 | 
			
		||||
            onClick={() => setActive(!isActive)}
 | 
			
		||||
            ref={pickerButton}
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
        <ColorInput
 | 
			
		||||
          color={color}
 | 
			
		||||
          label={label}
 | 
			
		||||
@@ -386,27 +389,37 @@ export const ColorPicker = ({
 | 
			
		||||
      </div>
 | 
			
		||||
      <React.Suspense fallback="">
 | 
			
		||||
        {isActive ? (
 | 
			
		||||
          <Popover
 | 
			
		||||
            onCloseRequest={(event) =>
 | 
			
		||||
              event.target !== pickerButton.current && setActive(false)
 | 
			
		||||
            }
 | 
			
		||||
          <div
 | 
			
		||||
            className="color-picker-popover-container"
 | 
			
		||||
            style={{
 | 
			
		||||
              position: "fixed",
 | 
			
		||||
              top: coords?.top,
 | 
			
		||||
              left: coords?.right,
 | 
			
		||||
              zIndex: 1,
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <Picker
 | 
			
		||||
              colors={colors[type]}
 | 
			
		||||
              color={color || null}
 | 
			
		||||
              onChange={(changedColor) => {
 | 
			
		||||
                onChange(changedColor);
 | 
			
		||||
              }}
 | 
			
		||||
              onClose={() => {
 | 
			
		||||
                setActive(false);
 | 
			
		||||
                pickerButton.current?.focus();
 | 
			
		||||
              }}
 | 
			
		||||
              label={label}
 | 
			
		||||
              showInput={false}
 | 
			
		||||
              type={type}
 | 
			
		||||
              elements={elements}
 | 
			
		||||
            />
 | 
			
		||||
          </Popover>
 | 
			
		||||
            <Popover
 | 
			
		||||
              onCloseRequest={(event) =>
 | 
			
		||||
                event.target !== pickerButton.current && setActive(false)
 | 
			
		||||
              }
 | 
			
		||||
            >
 | 
			
		||||
              <Picker
 | 
			
		||||
                colors={colors[type]}
 | 
			
		||||
                color={color || null}
 | 
			
		||||
                onChange={(changedColor) => {
 | 
			
		||||
                  onChange(changedColor);
 | 
			
		||||
                }}
 | 
			
		||||
                onClose={() => {
 | 
			
		||||
                  setActive(false);
 | 
			
		||||
                  pickerButton.current?.focus();
 | 
			
		||||
                }}
 | 
			
		||||
                label={label}
 | 
			
		||||
                showInput={false}
 | 
			
		||||
                type={type}
 | 
			
		||||
                elements={elements}
 | 
			
		||||
              />
 | 
			
		||||
            </Popover>
 | 
			
		||||
          </div>
 | 
			
		||||
        ) : null}
 | 
			
		||||
      </React.Suspense>
 | 
			
		||||
    </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -4,34 +4,8 @@
 | 
			
		||||
  .confirm-dialog {
 | 
			
		||||
    &-buttons {
 | 
			
		||||
      display: flex;
 | 
			
		||||
      padding: 0.2rem 0;
 | 
			
		||||
      column-gap: 0.5rem;
 | 
			
		||||
      justify-content: flex-end;
 | 
			
		||||
    }
 | 
			
		||||
    .ToolIcon__icon {
 | 
			
		||||
      min-width: 2.5rem;
 | 
			
		||||
      width: auto;
 | 
			
		||||
      font-size: 1rem;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .ToolIcon_type_button {
 | 
			
		||||
      margin-left: 0.8rem;
 | 
			
		||||
      padding: 0 0.5rem;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &__content {
 | 
			
		||||
      font-size: 1rem;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &--confirm.ToolIcon_type_button {
 | 
			
		||||
      background-color: $oc-red-6;
 | 
			
		||||
 | 
			
		||||
      &:hover {
 | 
			
		||||
        background-color: $oc-red-8;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .ToolIcon__icon {
 | 
			
		||||
        color: $oc-white;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,11 @@
 | 
			
		||||
import { t } from "../i18n";
 | 
			
		||||
import { Dialog, DialogProps } from "./Dialog";
 | 
			
		||||
import { ToolButton } from "./ToolButton";
 | 
			
		||||
 | 
			
		||||
import "./ConfirmDialog.scss";
 | 
			
		||||
import DialogActionButton from "./DialogActionButton";
 | 
			
		||||
import { isMenuOpenAtom } from "./App";
 | 
			
		||||
import { isDropdownOpenAtom } from "./App";
 | 
			
		||||
import { useSetAtom } from "jotai";
 | 
			
		||||
 | 
			
		||||
interface Props extends Omit<DialogProps, "onCloseRequest"> {
 | 
			
		||||
  onConfirm: () => void;
 | 
			
		||||
@@ -20,6 +23,10 @@ const ConfirmDialog = (props: Props) => {
 | 
			
		||||
    className = "",
 | 
			
		||||
    ...rest
 | 
			
		||||
  } = props;
 | 
			
		||||
 | 
			
		||||
  const setIsMenuOpen = useSetAtom(isMenuOpenAtom);
 | 
			
		||||
  const setIsDropdownOpen = useSetAtom(isDropdownOpenAtom);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Dialog
 | 
			
		||||
      onCloseRequest={onCancel}
 | 
			
		||||
@@ -29,21 +36,22 @@ const ConfirmDialog = (props: Props) => {
 | 
			
		||||
    >
 | 
			
		||||
      {children}
 | 
			
		||||
      <div className="confirm-dialog-buttons">
 | 
			
		||||
        <ToolButton
 | 
			
		||||
          type="button"
 | 
			
		||||
          title={cancelText}
 | 
			
		||||
          aria-label={cancelText}
 | 
			
		||||
        <DialogActionButton
 | 
			
		||||
          label={cancelText}
 | 
			
		||||
          onClick={onCancel}
 | 
			
		||||
          className="confirm-dialog--cancel"
 | 
			
		||||
          onClick={() => {
 | 
			
		||||
            setIsMenuOpen(false);
 | 
			
		||||
            setIsDropdownOpen(false);
 | 
			
		||||
            onCancel();
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
        <ToolButton
 | 
			
		||||
          type="button"
 | 
			
		||||
          title={confirmText}
 | 
			
		||||
          aria-label={confirmText}
 | 
			
		||||
        <DialogActionButton
 | 
			
		||||
          label={confirmText}
 | 
			
		||||
          onClick={onConfirm}
 | 
			
		||||
          className="confirm-dialog--confirm"
 | 
			
		||||
          onClick={() => {
 | 
			
		||||
            setIsMenuOpen(false);
 | 
			
		||||
            setIsDropdownOpen(false);
 | 
			
		||||
            onConfirm();
 | 
			
		||||
          }}
 | 
			
		||||
          actionType="danger"
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
    </Dialog>
 | 
			
		||||
 
 | 
			
		||||
@@ -7,68 +7,11 @@
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .Dialog__title {
 | 
			
		||||
    display: grid;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    margin-top: 0;
 | 
			
		||||
    grid-template-columns: 1fr calc(var(--space-factor) * 7);
 | 
			
		||||
    grid-gap: var(--metric);
 | 
			
		||||
    padding: calc(var(--space-factor) * 2);
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    font-variant: small-caps;
 | 
			
		||||
    font-size: 1.2em;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .Dialog__titleContent {
 | 
			
		||||
    flex: 1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .Dialog .Modal__close {
 | 
			
		||||
    color: var(--icon-fill-color);
 | 
			
		||||
    margin: 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .Dialog__content {
 | 
			
		||||
    padding: 0 16px 16px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @include isMobile {
 | 
			
		||||
    .Dialog {
 | 
			
		||||
      --metric: calc(var(--space-factor) * 4);
 | 
			
		||||
      --inset-left: #{"max(var(--metric), var(--sal))"};
 | 
			
		||||
      --inset-right: #{"max(var(--metric), var(--sar))"};
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .Dialog__title {
 | 
			
		||||
      grid-template-columns: calc(var(--space-factor) * 7) 1fr calc(
 | 
			
		||||
          var(--space-factor) * 7
 | 
			
		||||
        );
 | 
			
		||||
      position: sticky;
 | 
			
		||||
      top: 0;
 | 
			
		||||
      padding: calc(var(--space-factor) * 2);
 | 
			
		||||
      background: var(--island-bg-color);
 | 
			
		||||
      font-size: 1.25em;
 | 
			
		||||
 | 
			
		||||
      box-sizing: border-box;
 | 
			
		||||
      border-bottom: 1px solid var(--button-gray-2);
 | 
			
		||||
      z-index: 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .Dialog__titleContent {
 | 
			
		||||
      text-align: center;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .Dialog .Island {
 | 
			
		||||
      width: 100vw;
 | 
			
		||||
      height: 100%;
 | 
			
		||||
      box-sizing: border-box;
 | 
			
		||||
      overflow-y: auto;
 | 
			
		||||
      padding-left: #{"max(calc(var(--padding) * var(--space-factor)), var(--sal))"};
 | 
			
		||||
      padding-right: #{"max(calc(var(--padding) * var(--space-factor)), var(--sar))"};
 | 
			
		||||
      padding-bottom: #{"max(calc(var(--padding) * var(--space-factor)), var(--sab))"};
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .Dialog .Modal__close {
 | 
			
		||||
      order: -1;
 | 
			
		||||
    }
 | 
			
		||||
    text-align: left;
 | 
			
		||||
    font-size: 1.25rem;
 | 
			
		||||
    border-bottom: 1px solid var(--dialog-border-color);
 | 
			
		||||
    padding: 0 0 0.75rem;
 | 
			
		||||
    margin-bottom: 1.5rem;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,11 +5,13 @@ import { t } from "../i18n";
 | 
			
		||||
import { useExcalidrawContainer, useDevice } from "../components/App";
 | 
			
		||||
import { KEYS } from "../keys";
 | 
			
		||||
import "./Dialog.scss";
 | 
			
		||||
import { back, close } from "./icons";
 | 
			
		||||
import { back, CloseIcon } from "./icons";
 | 
			
		||||
import { Island } from "./Island";
 | 
			
		||||
import { Modal } from "./Modal";
 | 
			
		||||
import { AppState } from "../types";
 | 
			
		||||
import { queryFocusableElements } from "../utils";
 | 
			
		||||
import { isMenuOpenAtom, isDropdownOpenAtom } from "./App";
 | 
			
		||||
import { useSetAtom } from "jotai";
 | 
			
		||||
 | 
			
		||||
export interface DialogProps {
 | 
			
		||||
  children: React.ReactNode;
 | 
			
		||||
@@ -65,7 +67,12 @@ export const Dialog = (props: DialogProps) => {
 | 
			
		||||
    return () => islandNode.removeEventListener("keydown", handleKeyDown);
 | 
			
		||||
  }, [islandNode, props.autofocus]);
 | 
			
		||||
 | 
			
		||||
  const setIsMenuOpen = useSetAtom(isMenuOpenAtom);
 | 
			
		||||
  const setIsDropdownOpen = useSetAtom(isDropdownOpenAtom);
 | 
			
		||||
 | 
			
		||||
  const onClose = () => {
 | 
			
		||||
    setIsMenuOpen(false);
 | 
			
		||||
    setIsDropdownOpen(false);
 | 
			
		||||
    (lastActiveElement as HTMLElement).focus();
 | 
			
		||||
    props.onCloseRequest();
 | 
			
		||||
  };
 | 
			
		||||
@@ -88,7 +95,7 @@ export const Dialog = (props: DialogProps) => {
 | 
			
		||||
            title={t("buttons.close")}
 | 
			
		||||
            aria-label={t("buttons.close")}
 | 
			
		||||
          >
 | 
			
		||||
            {useDevice().isMobile ? back : close}
 | 
			
		||||
            {useDevice().isMobile ? back : CloseIcon}
 | 
			
		||||
          </button>
 | 
			
		||||
        </h2>
 | 
			
		||||
        <div className="Dialog__content">{props.children}</div>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										47
									
								
								src/components/DialogActionButton.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/components/DialogActionButton.scss
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,47 @@
 | 
			
		||||
.excalidraw {
 | 
			
		||||
  .Dialog__action-button {
 | 
			
		||||
    position: relative;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    column-gap: 0.5rem;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    padding: 0.5rem 1.5rem;
 | 
			
		||||
    border: 1px solid var(--default-border-color);
 | 
			
		||||
    background-color: transparent;
 | 
			
		||||
    height: 3rem;
 | 
			
		||||
    border-radius: var(--border-radius-lg);
 | 
			
		||||
    letter-spacing: 0.4px;
 | 
			
		||||
    color: inherit;
 | 
			
		||||
    font-family: inherit;
 | 
			
		||||
    font-size: 0.875rem;
 | 
			
		||||
    font-weight: 600;
 | 
			
		||||
    user-select: none;
 | 
			
		||||
 | 
			
		||||
    svg {
 | 
			
		||||
      display: block;
 | 
			
		||||
      width: 1rem;
 | 
			
		||||
      height: 1rem;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &--danger {
 | 
			
		||||
      background-color: var(--color-danger);
 | 
			
		||||
      border-color: var(--color-danger);
 | 
			
		||||
      color: #fff;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &--primary {
 | 
			
		||||
      background-color: var(--color-primary);
 | 
			
		||||
      border-color: var(--color-primary);
 | 
			
		||||
      color: #fff;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &.theme--dark {
 | 
			
		||||
    .Dialog__action-button--danger {
 | 
			
		||||
      color: var(--color-gray-100);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .Dialog__action-button--primary {
 | 
			
		||||
      color: var(--color-gray-100);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										46
									
								
								src/components/DialogActionButton.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/components/DialogActionButton.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,46 @@
 | 
			
		||||
import clsx from "clsx";
 | 
			
		||||
import { ReactNode } from "react";
 | 
			
		||||
import "./DialogActionButton.scss";
 | 
			
		||||
import Spinner from "./Spinner";
 | 
			
		||||
 | 
			
		||||
interface DialogActionButtonProps {
 | 
			
		||||
  label: string;
 | 
			
		||||
  children?: ReactNode;
 | 
			
		||||
  actionType?: "primary" | "danger";
 | 
			
		||||
  isLoading?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const DialogActionButton = ({
 | 
			
		||||
  label,
 | 
			
		||||
  onClick,
 | 
			
		||||
  className,
 | 
			
		||||
  children,
 | 
			
		||||
  actionType,
 | 
			
		||||
  type = "button",
 | 
			
		||||
  isLoading,
 | 
			
		||||
  ...rest
 | 
			
		||||
}: DialogActionButtonProps & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
 | 
			
		||||
  const cs = actionType ? `Dialog__action-button--${actionType}` : "";
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <button
 | 
			
		||||
      className={clsx("Dialog__action-button", cs, className)}
 | 
			
		||||
      type={type}
 | 
			
		||||
      aria-label={label}
 | 
			
		||||
      onClick={onClick}
 | 
			
		||||
      {...rest}
 | 
			
		||||
    >
 | 
			
		||||
      {children && (
 | 
			
		||||
        <div style={isLoading ? { visibility: "hidden" } : {}}>{children}</div>
 | 
			
		||||
      )}
 | 
			
		||||
      <div style={isLoading ? { visibility: "hidden" } : {}}>{label}</div>
 | 
			
		||||
      {isLoading && (
 | 
			
		||||
        <div style={{ position: "absolute", inset: 0 }}>
 | 
			
		||||
          <Spinner />
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
    </button>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default DialogActionButton;
 | 
			
		||||
							
								
								
									
										19
									
								
								src/components/EncryptedIcon.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/components/EncryptedIcon.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
import { t } from "../i18n";
 | 
			
		||||
import { shield } from "./icons";
 | 
			
		||||
import { Tooltip } from "./Tooltip";
 | 
			
		||||
 | 
			
		||||
const EncryptedIcon = () => (
 | 
			
		||||
  <a
 | 
			
		||||
    className="encrypted-icon tooltip"
 | 
			
		||||
    href="https://blog.excalidraw.com/end-to-end-encryption/"
 | 
			
		||||
    target="_blank"
 | 
			
		||||
    rel="noopener noreferrer"
 | 
			
		||||
    aria-label={t("encrypted.link")}
 | 
			
		||||
  >
 | 
			
		||||
    <Tooltip label={t("encrypted.tooltip")} long={true}>
 | 
			
		||||
      {shield}
 | 
			
		||||
    </Tooltip>
 | 
			
		||||
  </a>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export default EncryptedIcon;
 | 
			
		||||
@@ -91,6 +91,8 @@
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  button.ExportDialog-imageExportButton {
 | 
			
		||||
    border: 0;
 | 
			
		||||
 | 
			
		||||
    width: 5rem;
 | 
			
		||||
    height: 5rem;
 | 
			
		||||
    margin: 0 0.2em;
 | 
			
		||||
 
 | 
			
		||||
@@ -9,9 +9,10 @@
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .FixedSideContainer_side_top {
 | 
			
		||||
    left: var(--space-factor);
 | 
			
		||||
    top: var(--space-factor);
 | 
			
		||||
    right: var(--space-factor);
 | 
			
		||||
    left: 1rem;
 | 
			
		||||
    top: 1rem;
 | 
			
		||||
    right: 1rem;
 | 
			
		||||
    bottom: 1rem;
 | 
			
		||||
    z-index: 2;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import clsx from "clsx";
 | 
			
		||||
import { ActionManager } from "../actions/manager";
 | 
			
		||||
import { t } from "../i18n";
 | 
			
		||||
import { AppState, ExcalidrawProps } from "../types";
 | 
			
		||||
import {
 | 
			
		||||
  ExitZenModeAction,
 | 
			
		||||
@@ -8,20 +9,23 @@ import {
 | 
			
		||||
  ZoomActions,
 | 
			
		||||
} from "./Actions";
 | 
			
		||||
import { useDevice } from "./App";
 | 
			
		||||
import { Island } from "./Island";
 | 
			
		||||
import { WelcomeScreenHelpArrow } from "./icons";
 | 
			
		||||
import { Section } from "./Section";
 | 
			
		||||
import Stack from "./Stack";
 | 
			
		||||
import WelcomeScreenDecor from "./WelcomeScreenDecor";
 | 
			
		||||
 | 
			
		||||
const Footer = ({
 | 
			
		||||
  appState,
 | 
			
		||||
  actionManager,
 | 
			
		||||
  renderCustomFooter,
 | 
			
		||||
  showExitZenModeBtn,
 | 
			
		||||
  renderWelcomeScreen,
 | 
			
		||||
}: {
 | 
			
		||||
  appState: AppState;
 | 
			
		||||
  actionManager: ActionManager;
 | 
			
		||||
  renderCustomFooter?: ExcalidrawProps["renderFooter"];
 | 
			
		||||
  showExitZenModeBtn: boolean;
 | 
			
		||||
  renderWelcomeScreen: boolean;
 | 
			
		||||
}) => {
 | 
			
		||||
  const device = useDevice();
 | 
			
		||||
  const showFinalize =
 | 
			
		||||
@@ -39,31 +43,19 @@ const Footer = ({
 | 
			
		||||
      >
 | 
			
		||||
        <Stack.Col gap={2}>
 | 
			
		||||
          <Section heading="canvasActions">
 | 
			
		||||
            <Island padding={1}>
 | 
			
		||||
              <ZoomActions
 | 
			
		||||
                renderAction={actionManager.renderAction}
 | 
			
		||||
                zoom={appState.zoom}
 | 
			
		||||
              />
 | 
			
		||||
            </Island>
 | 
			
		||||
            {!appState.viewModeEnabled && (
 | 
			
		||||
              <>
 | 
			
		||||
                <UndoRedoActions
 | 
			
		||||
                  renderAction={actionManager.renderAction}
 | 
			
		||||
                  className={clsx("zen-mode-transition", {
 | 
			
		||||
                    "layer-ui__wrapper__footer-left--transition-bottom":
 | 
			
		||||
                      appState.zenModeEnabled,
 | 
			
		||||
                  })}
 | 
			
		||||
                />
 | 
			
		||||
            <ZoomActions
 | 
			
		||||
              renderAction={actionManager.renderAction}
 | 
			
		||||
              zoom={appState.zoom}
 | 
			
		||||
            />
 | 
			
		||||
 | 
			
		||||
                <div
 | 
			
		||||
                  className={clsx("eraser-buttons zen-mode-transition", {
 | 
			
		||||
                    "layer-ui__wrapper__footer-left--transition-left":
 | 
			
		||||
                      appState.zenModeEnabled,
 | 
			
		||||
                  })}
 | 
			
		||||
                >
 | 
			
		||||
                  {actionManager.renderAction("eraser", { size: "small" })}
 | 
			
		||||
                </div>
 | 
			
		||||
              </>
 | 
			
		||||
            {!appState.viewModeEnabled && (
 | 
			
		||||
              <UndoRedoActions
 | 
			
		||||
                renderAction={actionManager.renderAction}
 | 
			
		||||
                className={clsx("zen-mode-transition", {
 | 
			
		||||
                  "layer-ui__wrapper__footer-left--transition-bottom":
 | 
			
		||||
                    appState.zenModeEnabled,
 | 
			
		||||
                })}
 | 
			
		||||
              />
 | 
			
		||||
            )}
 | 
			
		||||
            {showFinalize && (
 | 
			
		||||
              <FinalizeAction
 | 
			
		||||
@@ -93,7 +85,18 @@ const Footer = ({
 | 
			
		||||
          "transition-right disable-pointerEvents": appState.zenModeEnabled,
 | 
			
		||||
        })}
 | 
			
		||||
      >
 | 
			
		||||
        {actionManager.renderAction("toggleShortcuts")}
 | 
			
		||||
        <div style={{ position: "relative" }}>
 | 
			
		||||
          <WelcomeScreenDecor
 | 
			
		||||
            shouldRender={renderWelcomeScreen && !appState.isLoading}
 | 
			
		||||
          >
 | 
			
		||||
            <div className="virgil WelcomeScreen-decor WelcomeScreen-decor--help-pointer">
 | 
			
		||||
              <div>{t("welcomeScreen.helpHints")}</div>
 | 
			
		||||
              {WelcomeScreenHelpArrow}
 | 
			
		||||
            </div>
 | 
			
		||||
          </WelcomeScreenDecor>
 | 
			
		||||
 | 
			
		||||
          {actionManager.renderAction("toggleShortcuts")}
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <ExitZenModeAction
 | 
			
		||||
        actionManager={actionManager}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,13 @@
 | 
			
		||||
import { questionCircle } from "../components/icons";
 | 
			
		||||
import { HelpIcon } from "./icons";
 | 
			
		||||
 | 
			
		||||
type HelpIconProps = {
 | 
			
		||||
type HelpButtonProps = {
 | 
			
		||||
  title?: string;
 | 
			
		||||
  name?: string;
 | 
			
		||||
  id?: string;
 | 
			
		||||
  onClick?(): void;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const HelpIcon = (props: HelpIconProps) => (
 | 
			
		||||
export const HelpButton = (props: HelpButtonProps) => (
 | 
			
		||||
  <button
 | 
			
		||||
    className="help-icon"
 | 
			
		||||
    onClick={props.onClick}
 | 
			
		||||
@@ -15,6 +15,6 @@ export const HelpIcon = (props: HelpIconProps) => (
 | 
			
		||||
    title={`${props.title} — ?`}
 | 
			
		||||
    aria-label={props.title}
 | 
			
		||||
  >
 | 
			
		||||
    {questionCircle}
 | 
			
		||||
    {HelpIcon}
 | 
			
		||||
  </button>
 | 
			
		||||
);
 | 
			
		||||
@@ -1,56 +1,115 @@
 | 
			
		||||
@import "../css/variables.module";
 | 
			
		||||
 | 
			
		||||
.excalidraw {
 | 
			
		||||
  .HelpDialog h3 {
 | 
			
		||||
    border-bottom: 1px solid var(--button-gray-2);
 | 
			
		||||
    padding-bottom: 4px;
 | 
			
		||||
  }
 | 
			
		||||
  .HelpDialog {
 | 
			
		||||
    .Modal__content {
 | 
			
		||||
      max-width: 960px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  .HelpDialog--island {
 | 
			
		||||
    border: 1px solid var(--button-gray-2);
 | 
			
		||||
    margin-bottom: 16px;
 | 
			
		||||
  }
 | 
			
		||||
    h3 {
 | 
			
		||||
      margin: 1.5rem 0;
 | 
			
		||||
      font-weight: bold;
 | 
			
		||||
      font-size: 1.125rem;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  .HelpDialog--island-title {
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    padding: 4px;
 | 
			
		||||
    background-color: var(--button-gray-1);
 | 
			
		||||
    text-align: center;
 | 
			
		||||
  }
 | 
			
		||||
    &__header {
 | 
			
		||||
      display: flex;
 | 
			
		||||
      flex-wrap: wrap;
 | 
			
		||||
      gap: 0.75rem;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  .HelpDialog--shortcut {
 | 
			
		||||
    border-top: 1px solid var(--button-gray-2);
 | 
			
		||||
  }
 | 
			
		||||
    &__btn {
 | 
			
		||||
      display: flex;
 | 
			
		||||
      column-gap: 0.5rem;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
      border: 1px solid var(--default-border-color);
 | 
			
		||||
      padding: 0.625rem 1rem;
 | 
			
		||||
      border-radius: var(--border-radius-lg);
 | 
			
		||||
      color: var(--text-primary-color);
 | 
			
		||||
      font-weight: 600;
 | 
			
		||||
      font-size: 0.75rem;
 | 
			
		||||
      letter-spacing: 0.4px;
 | 
			
		||||
 | 
			
		||||
  .HelpDialog--key {
 | 
			
		||||
    word-break: keep-all;
 | 
			
		||||
    border: 1px solid var(--button-gray-2);
 | 
			
		||||
    padding: 2px 8px;
 | 
			
		||||
    margin: auto 4px;
 | 
			
		||||
    background-color: var(--button-gray-1);
 | 
			
		||||
    border-radius: 2px;
 | 
			
		||||
    font-size: 0.8em;
 | 
			
		||||
    min-height: 26px;
 | 
			
		||||
    box-sizing: border-box;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    font-family: inherit;
 | 
			
		||||
  }
 | 
			
		||||
      &:hover {
 | 
			
		||||
        text-decoration: none;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  .HelpDialog--header {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: row;
 | 
			
		||||
    justify-content: space-evenly;
 | 
			
		||||
    margin-bottom: 32px;
 | 
			
		||||
    padding-bottom: 16px;
 | 
			
		||||
  }
 | 
			
		||||
    &__link-icon {
 | 
			
		||||
      line-height: 0;
 | 
			
		||||
      svg {
 | 
			
		||||
        width: 1rem;
 | 
			
		||||
        height: 1rem;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  .HelpDialog--btn {
 | 
			
		||||
    border: 1px solid var(--link-color);
 | 
			
		||||
    padding: 8px 32px;
 | 
			
		||||
    border-radius: 4px;
 | 
			
		||||
  }
 | 
			
		||||
  .HelpDialog--btn:hover {
 | 
			
		||||
    text-decoration: none;
 | 
			
		||||
    &__islands-container {
 | 
			
		||||
      display: grid;
 | 
			
		||||
      @media screen and (min-width: 1024px) {
 | 
			
		||||
        grid-template-columns: 1fr 1fr;
 | 
			
		||||
      }
 | 
			
		||||
      grid-column-gap: 1.5rem;
 | 
			
		||||
      grid-row-gap: 2rem;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @media screen and (min-width: 1024px) {
 | 
			
		||||
      &__island--tools {
 | 
			
		||||
        grid-area: 1 / 1 / 2 / 2;
 | 
			
		||||
      }
 | 
			
		||||
      &__island--view {
 | 
			
		||||
        grid-area: 2 / 1 / 3 / 2;
 | 
			
		||||
      }
 | 
			
		||||
      &__island--editor {
 | 
			
		||||
        grid-area: 1 / 2 / 3 / 3;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &__island {
 | 
			
		||||
      h4 {
 | 
			
		||||
        font-size: 1rem;
 | 
			
		||||
        font-weight: bold;
 | 
			
		||||
        margin: 0;
 | 
			
		||||
        margin-bottom: 0.625rem;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      &-content {
 | 
			
		||||
        border: 1px solid var(--dialog-border-color);
 | 
			
		||||
        border-radius: var(--border-radius-lg);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &__shortcut {
 | 
			
		||||
      border-bottom: 1px solid var(--dialog-border-color);
 | 
			
		||||
      padding: 0.375rem 0.75rem;
 | 
			
		||||
      display: flex;
 | 
			
		||||
      justify-content: space-between;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
      font-size: 0.875rem;
 | 
			
		||||
      column-gap: 0.5rem;
 | 
			
		||||
 | 
			
		||||
      &:last-child {
 | 
			
		||||
        border-bottom: none;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &__key-container {
 | 
			
		||||
      display: flex;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
      column-gap: 0.25rem;
 | 
			
		||||
      flex-shrink: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &__key {
 | 
			
		||||
      display: flex;
 | 
			
		||||
      box-sizing: border-box;
 | 
			
		||||
      font-size: 0.625rem;
 | 
			
		||||
      background-color: var(--color-primary-light);
 | 
			
		||||
      border-radius: var(--border-radius-md);
 | 
			
		||||
      padding: 0.5rem;
 | 
			
		||||
      word-break: keep-all;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
      font-family: inherit;
 | 
			
		||||
      line-height: 1;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,35 +1,39 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { t } from "../i18n";
 | 
			
		||||
import { isDarwin, isWindows } from "../keys";
 | 
			
		||||
import { isDarwin, isWindows, KEYS } from "../keys";
 | 
			
		||||
import { Dialog } from "./Dialog";
 | 
			
		||||
import { getShortcutKey } from "../utils";
 | 
			
		||||
import "./HelpDialog.scss";
 | 
			
		||||
import { ExternalLinkIcon } from "./icons";
 | 
			
		||||
 | 
			
		||||
const Header = () => (
 | 
			
		||||
  <div className="HelpDialog--header">
 | 
			
		||||
  <div className="HelpDialog__header">
 | 
			
		||||
    <a
 | 
			
		||||
      className="HelpDialog--btn"
 | 
			
		||||
      className="HelpDialog__btn"
 | 
			
		||||
      href="https://github.com/excalidraw/excalidraw#documentation"
 | 
			
		||||
      target="_blank"
 | 
			
		||||
      rel="noopener noreferrer"
 | 
			
		||||
    >
 | 
			
		||||
      {t("helpDialog.documentation")}
 | 
			
		||||
      <div className="HelpDialog__link-icon">{ExternalLinkIcon}</div>
 | 
			
		||||
    </a>
 | 
			
		||||
    <a
 | 
			
		||||
      className="HelpDialog--btn"
 | 
			
		||||
      className="HelpDialog__btn"
 | 
			
		||||
      href="https://blog.excalidraw.com"
 | 
			
		||||
      target="_blank"
 | 
			
		||||
      rel="noopener noreferrer"
 | 
			
		||||
    >
 | 
			
		||||
      {t("helpDialog.blog")}
 | 
			
		||||
      <div className="HelpDialog__link-icon">{ExternalLinkIcon}</div>
 | 
			
		||||
    </a>
 | 
			
		||||
    <a
 | 
			
		||||
      className="HelpDialog--btn"
 | 
			
		||||
      className="HelpDialog__btn"
 | 
			
		||||
      href="https://github.com/excalidraw/excalidraw/issues"
 | 
			
		||||
      target="_blank"
 | 
			
		||||
      rel="noopener noreferrer"
 | 
			
		||||
    >
 | 
			
		||||
      {t("helpDialog.github")}
 | 
			
		||||
      <div className="HelpDialog__link-icon">{ExternalLinkIcon}</div>
 | 
			
		||||
    </a>
 | 
			
		||||
  </div>
 | 
			
		||||
);
 | 
			
		||||
@@ -37,88 +41,61 @@ const Header = () => (
 | 
			
		||||
const Section = (props: { title: string; children: React.ReactNode }) => (
 | 
			
		||||
  <>
 | 
			
		||||
    <h3>{props.title}</h3>
 | 
			
		||||
    {props.children}
 | 
			
		||||
    <div className="HelpDialog__islands-container">{props.children}</div>
 | 
			
		||||
  </>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const Columns = (props: { children: React.ReactNode }) => (
 | 
			
		||||
  <div
 | 
			
		||||
    style={{
 | 
			
		||||
      display: "flex",
 | 
			
		||||
      flexDirection: "row",
 | 
			
		||||
      flexWrap: "wrap",
 | 
			
		||||
      justifyContent: "space-between",
 | 
			
		||||
    }}
 | 
			
		||||
  >
 | 
			
		||||
    {props.children}
 | 
			
		||||
  </div>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const Column = (props: { children: React.ReactNode }) => (
 | 
			
		||||
  <div style={{ width: "49%" }}>{props.children}</div>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const ShortcutIsland = (props: {
 | 
			
		||||
  caption: string;
 | 
			
		||||
  children: React.ReactNode;
 | 
			
		||||
  className?: string;
 | 
			
		||||
}) => (
 | 
			
		||||
  <div className="HelpDialog--island">
 | 
			
		||||
    <h3 className="HelpDialog--island-title">{props.caption}</h3>
 | 
			
		||||
    {props.children}
 | 
			
		||||
  <div className={`HelpDialog__island ${props.className}`}>
 | 
			
		||||
    <h4 className="HelpDialog__island-title">{props.caption}</h4>
 | 
			
		||||
    <div className="HelpDialog__island-content">{props.children}</div>
 | 
			
		||||
  </div>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const Shortcut = (props: {
 | 
			
		||||
function* intersperse(as: JSX.Element[][], delim: string | null) {
 | 
			
		||||
  let first = true;
 | 
			
		||||
  for (const x of as) {
 | 
			
		||||
    if (!first) {
 | 
			
		||||
      yield delim;
 | 
			
		||||
    }
 | 
			
		||||
    first = false;
 | 
			
		||||
    yield x;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Shortcut = ({
 | 
			
		||||
  label,
 | 
			
		||||
  shortcuts,
 | 
			
		||||
  isOr = true,
 | 
			
		||||
}: {
 | 
			
		||||
  label: string;
 | 
			
		||||
  shortcuts: string[];
 | 
			
		||||
  isOr: boolean;
 | 
			
		||||
  isOr?: boolean;
 | 
			
		||||
}) => {
 | 
			
		||||
  const splitShortcutKeys = shortcuts.map((shortcut) => {
 | 
			
		||||
    const keys = shortcut.endsWith("++")
 | 
			
		||||
      ? [...shortcut.slice(0, -2).split("+"), "+"]
 | 
			
		||||
      : shortcut.split("+");
 | 
			
		||||
 | 
			
		||||
    return keys.map((key) => <ShortcutKey key={key}>{key}</ShortcutKey>);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="HelpDialog--shortcut">
 | 
			
		||||
      <div
 | 
			
		||||
        style={{
 | 
			
		||||
          display: "flex",
 | 
			
		||||
          margin: "0",
 | 
			
		||||
          padding: "4px 8px",
 | 
			
		||||
          alignItems: "center",
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <div
 | 
			
		||||
          style={{
 | 
			
		||||
            lineHeight: 1.4,
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          {props.label}
 | 
			
		||||
        </div>
 | 
			
		||||
        <div
 | 
			
		||||
          style={{
 | 
			
		||||
            display: "flex",
 | 
			
		||||
            flex: "0 0 auto",
 | 
			
		||||
            justifyContent: "flex-end",
 | 
			
		||||
            marginInlineStart: "auto",
 | 
			
		||||
            minWidth: "30%",
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          {props.shortcuts.map((shortcut, index) => (
 | 
			
		||||
            <React.Fragment key={index}>
 | 
			
		||||
              <ShortcutKey>{shortcut}</ShortcutKey>
 | 
			
		||||
              {props.isOr &&
 | 
			
		||||
                index !== props.shortcuts.length - 1 &&
 | 
			
		||||
                t("helpDialog.or")}
 | 
			
		||||
            </React.Fragment>
 | 
			
		||||
          ))}
 | 
			
		||||
        </div>
 | 
			
		||||
    <div className="HelpDialog__shortcut">
 | 
			
		||||
      <div>{label}</div>
 | 
			
		||||
      <div className="HelpDialog__key-container">
 | 
			
		||||
        {[...intersperse(splitShortcutKeys, isOr ? t("helpDialog.or") : null)]}
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
Shortcut.defaultProps = {
 | 
			
		||||
  isOr: true,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const ShortcutKey = (props: { children: React.ReactNode }) => (
 | 
			
		||||
  <kbd className="HelpDialog--key" {...props} />
 | 
			
		||||
  <kbd className="HelpDialog__key" {...props} />
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
 | 
			
		||||
@@ -137,286 +114,296 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
 | 
			
		||||
      >
 | 
			
		||||
        <Header />
 | 
			
		||||
        <Section title={t("helpDialog.shortcuts")}>
 | 
			
		||||
          <Columns>
 | 
			
		||||
            <Column>
 | 
			
		||||
              <ShortcutIsland caption={t("helpDialog.tools")}>
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("toolBar.selection")}
 | 
			
		||||
                  shortcuts={["V", "1"]}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("toolBar.rectangle")}
 | 
			
		||||
                  shortcuts={["R", "2"]}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut label={t("toolBar.diamond")} shortcuts={["D", "3"]} />
 | 
			
		||||
                <Shortcut label={t("toolBar.ellipse")} shortcuts={["O", "4"]} />
 | 
			
		||||
                <Shortcut label={t("toolBar.arrow")} shortcuts={["A", "5"]} />
 | 
			
		||||
                <Shortcut label={t("toolBar.line")} shortcuts={["P", "6"]} />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("toolBar.freedraw")}
 | 
			
		||||
                  shortcuts={["Shift + P", "X", "7"]}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut label={t("toolBar.text")} shortcuts={["T", "8"]} />
 | 
			
		||||
                <Shortcut label={t("toolBar.image")} shortcuts={["9"]} />
 | 
			
		||||
                <Shortcut label={t("toolBar.library")} shortcuts={["0"]} />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("toolBar.eraser")}
 | 
			
		||||
                  shortcuts={[getShortcutKey("E")]}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("helpDialog.editSelectedShape")}
 | 
			
		||||
                  shortcuts={[
 | 
			
		||||
                    getShortcutKey("Enter"),
 | 
			
		||||
                    t("helpDialog.doubleClick"),
 | 
			
		||||
                  ]}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("helpDialog.textNewLine")}
 | 
			
		||||
                  shortcuts={[
 | 
			
		||||
                    getShortcutKey("Enter"),
 | 
			
		||||
                    getShortcutKey("Shift+Enter"),
 | 
			
		||||
                  ]}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("helpDialog.textFinish")}
 | 
			
		||||
                  shortcuts={[
 | 
			
		||||
                    getShortcutKey("Esc"),
 | 
			
		||||
                    getShortcutKey("CtrlOrCmd+Enter"),
 | 
			
		||||
                  ]}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("helpDialog.curvedArrow")}
 | 
			
		||||
                  shortcuts={[
 | 
			
		||||
                    "A",
 | 
			
		||||
                    t("helpDialog.click"),
 | 
			
		||||
                    t("helpDialog.click"),
 | 
			
		||||
                    t("helpDialog.click"),
 | 
			
		||||
                  ]}
 | 
			
		||||
                  isOr={false}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("helpDialog.curvedLine")}
 | 
			
		||||
                  shortcuts={[
 | 
			
		||||
                    "L",
 | 
			
		||||
                    t("helpDialog.click"),
 | 
			
		||||
                    t("helpDialog.click"),
 | 
			
		||||
                    t("helpDialog.click"),
 | 
			
		||||
                  ]}
 | 
			
		||||
                  isOr={false}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut label={t("toolBar.lock")} shortcuts={["Q"]} />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("helpDialog.preventBinding")}
 | 
			
		||||
                  shortcuts={[getShortcutKey("CtrlOrCmd")]}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("toolBar.link")}
 | 
			
		||||
                  shortcuts={[getShortcutKey("CtrlOrCmd+K")]}
 | 
			
		||||
                />
 | 
			
		||||
              </ShortcutIsland>
 | 
			
		||||
              <ShortcutIsland caption={t("helpDialog.view")}>
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("buttons.zoomIn")}
 | 
			
		||||
                  shortcuts={[getShortcutKey("CtrlOrCmd++")]}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("buttons.zoomOut")}
 | 
			
		||||
                  shortcuts={[getShortcutKey("CtrlOrCmd+-")]}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("buttons.resetZoom")}
 | 
			
		||||
                  shortcuts={[getShortcutKey("CtrlOrCmd+0")]}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("helpDialog.zoomToFit")}
 | 
			
		||||
                  shortcuts={["Shift+1"]}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("helpDialog.zoomToSelection")}
 | 
			
		||||
                  shortcuts={["Shift+2"]}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut label={t("buttons.fullScreen")} shortcuts={["F"]} />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("buttons.zenMode")}
 | 
			
		||||
                  shortcuts={[getShortcutKey("Alt+Z")]}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("labels.showGrid")}
 | 
			
		||||
                  shortcuts={[getShortcutKey("CtrlOrCmd+'")]}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("labels.viewMode")}
 | 
			
		||||
                  shortcuts={[getShortcutKey("Alt+R")]}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("labels.toggleTheme")}
 | 
			
		||||
                  shortcuts={[getShortcutKey("Alt+Shift+D")]}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("stats.title")}
 | 
			
		||||
                  shortcuts={[getShortcutKey("Alt+/")]}
 | 
			
		||||
                />
 | 
			
		||||
              </ShortcutIsland>
 | 
			
		||||
            </Column>
 | 
			
		||||
            <Column>
 | 
			
		||||
              <ShortcutIsland caption={t("helpDialog.editor")}>
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("labels.selectAll")}
 | 
			
		||||
                  shortcuts={[getShortcutKey("CtrlOrCmd+A")]}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("labels.multiSelect")}
 | 
			
		||||
                  shortcuts={[getShortcutKey(`Shift+${t("helpDialog.click")}`)]}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("helpDialog.deepSelect")}
 | 
			
		||||
                  shortcuts={[
 | 
			
		||||
                    getShortcutKey(`CtrlOrCmd+${t("helpDialog.click")}`),
 | 
			
		||||
                  ]}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("helpDialog.deepBoxSelect")}
 | 
			
		||||
                  shortcuts={[
 | 
			
		||||
                    getShortcutKey(`CtrlOrCmd+${t("helpDialog.drag")}`),
 | 
			
		||||
                  ]}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("labels.moveCanvas")}
 | 
			
		||||
                  shortcuts={[
 | 
			
		||||
                    getShortcutKey(`Space+${t("helpDialog.drag")}`),
 | 
			
		||||
                    getShortcutKey(`Wheel+${t("helpDialog.drag")}`),
 | 
			
		||||
                  ]}
 | 
			
		||||
                  isOr={true}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("labels.cut")}
 | 
			
		||||
                  shortcuts={[getShortcutKey("CtrlOrCmd+X")]}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("labels.copy")}
 | 
			
		||||
                  shortcuts={[getShortcutKey("CtrlOrCmd+C")]}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("labels.paste")}
 | 
			
		||||
                  shortcuts={[getShortcutKey("CtrlOrCmd+V")]}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("labels.copyAsPng")}
 | 
			
		||||
                  shortcuts={[getShortcutKey("Shift+Alt+C")]}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("labels.copyStyles")}
 | 
			
		||||
                  shortcuts={[getShortcutKey("CtrlOrCmd+Alt+C")]}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("labels.pasteStyles")}
 | 
			
		||||
                  shortcuts={[getShortcutKey("CtrlOrCmd+Alt+V")]}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("labels.delete")}
 | 
			
		||||
                  shortcuts={[getShortcutKey("Del")]}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("labels.sendToBack")}
 | 
			
		||||
                  shortcuts={[
 | 
			
		||||
                    isDarwin
 | 
			
		||||
                      ? getShortcutKey("CtrlOrCmd+Alt+[")
 | 
			
		||||
                      : getShortcutKey("CtrlOrCmd+Shift+["),
 | 
			
		||||
                  ]}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("labels.bringToFront")}
 | 
			
		||||
                  shortcuts={[
 | 
			
		||||
                    isDarwin
 | 
			
		||||
                      ? getShortcutKey("CtrlOrCmd+Alt+]")
 | 
			
		||||
                      : getShortcutKey("CtrlOrCmd+Shift+]"),
 | 
			
		||||
                  ]}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("labels.sendBackward")}
 | 
			
		||||
                  shortcuts={[getShortcutKey("CtrlOrCmd+[")]}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("labels.bringForward")}
 | 
			
		||||
                  shortcuts={[getShortcutKey("CtrlOrCmd+]")]}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("labels.alignTop")}
 | 
			
		||||
                  shortcuts={[getShortcutKey("CtrlOrCmd+Shift+Up")]}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("labels.alignBottom")}
 | 
			
		||||
                  shortcuts={[getShortcutKey("CtrlOrCmd+Shift+Down")]}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("labels.alignLeft")}
 | 
			
		||||
                  shortcuts={[getShortcutKey("CtrlOrCmd+Shift+Left")]}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("labels.alignRight")}
 | 
			
		||||
                  shortcuts={[getShortcutKey("CtrlOrCmd+Shift+Right")]}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("labels.duplicateSelection")}
 | 
			
		||||
                  shortcuts={[
 | 
			
		||||
                    getShortcutKey("CtrlOrCmd+D"),
 | 
			
		||||
                    getShortcutKey(`Alt+${t("helpDialog.drag")}`),
 | 
			
		||||
                  ]}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("helpDialog.toggleElementLock")}
 | 
			
		||||
                  shortcuts={[getShortcutKey("CtrlOrCmd+Shift+L")]}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("buttons.undo")}
 | 
			
		||||
                  shortcuts={[getShortcutKey("CtrlOrCmd+Z")]}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("buttons.redo")}
 | 
			
		||||
                  shortcuts={
 | 
			
		||||
                    isWindows
 | 
			
		||||
                      ? [
 | 
			
		||||
                          getShortcutKey("CtrlOrCmd+Y"),
 | 
			
		||||
                          getShortcutKey("CtrlOrCmd+Shift+Z"),
 | 
			
		||||
                        ]
 | 
			
		||||
                      : [getShortcutKey("CtrlOrCmd+Shift+Z")]
 | 
			
		||||
                  }
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("labels.group")}
 | 
			
		||||
                  shortcuts={[getShortcutKey("CtrlOrCmd+G")]}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("labels.ungroup")}
 | 
			
		||||
                  shortcuts={[getShortcutKey("CtrlOrCmd+Shift+G")]}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("labels.flipHorizontal")}
 | 
			
		||||
                  shortcuts={[getShortcutKey("Shift+H")]}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("labels.flipVertical")}
 | 
			
		||||
                  shortcuts={[getShortcutKey("Shift+V")]}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("labels.showStroke")}
 | 
			
		||||
                  shortcuts={[getShortcutKey("S")]}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("labels.showBackground")}
 | 
			
		||||
                  shortcuts={[getShortcutKey("G")]}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("labels.decreaseFontSize")}
 | 
			
		||||
                  shortcuts={[getShortcutKey("CtrlOrCmd+Shift+<")]}
 | 
			
		||||
                />
 | 
			
		||||
                <Shortcut
 | 
			
		||||
                  label={t("labels.increaseFontSize")}
 | 
			
		||||
                  shortcuts={[getShortcutKey("CtrlOrCmd+Shift+>")]}
 | 
			
		||||
                />
 | 
			
		||||
              </ShortcutIsland>
 | 
			
		||||
            </Column>
 | 
			
		||||
          </Columns>
 | 
			
		||||
          <ShortcutIsland
 | 
			
		||||
            className="HelpDialog__island--tools"
 | 
			
		||||
            caption={t("helpDialog.tools")}
 | 
			
		||||
          >
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("toolBar.selection")}
 | 
			
		||||
              shortcuts={[KEYS.V, KEYS["1"]]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("toolBar.rectangle")}
 | 
			
		||||
              shortcuts={[KEYS.R, KEYS["2"]]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("toolBar.diamond")}
 | 
			
		||||
              shortcuts={[KEYS.D, KEYS["3"]]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("toolBar.ellipse")}
 | 
			
		||||
              shortcuts={[KEYS.O, KEYS["4"]]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("toolBar.arrow")}
 | 
			
		||||
              shortcuts={[KEYS.A, KEYS["5"]]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("toolBar.line")}
 | 
			
		||||
              shortcuts={[KEYS.P, KEYS["6"]]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("toolBar.freedraw")}
 | 
			
		||||
              shortcuts={["Shift + P", KEYS["7"]]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("toolBar.text")}
 | 
			
		||||
              shortcuts={[KEYS.T, KEYS["8"]]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut label={t("toolBar.image")} shortcuts={[KEYS["9"]]} />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("toolBar.eraser")}
 | 
			
		||||
              shortcuts={[KEYS.E, KEYS["0"]]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("helpDialog.editSelectedShape")}
 | 
			
		||||
              shortcuts={[getShortcutKey("Enter"), t("helpDialog.doubleClick")]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("helpDialog.textNewLine")}
 | 
			
		||||
              shortcuts={[
 | 
			
		||||
                getShortcutKey("Enter"),
 | 
			
		||||
                getShortcutKey("Shift+Enter"),
 | 
			
		||||
              ]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("helpDialog.textFinish")}
 | 
			
		||||
              shortcuts={[
 | 
			
		||||
                getShortcutKey("Esc"),
 | 
			
		||||
                getShortcutKey("CtrlOrCmd+Enter"),
 | 
			
		||||
              ]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("helpDialog.curvedArrow")}
 | 
			
		||||
              shortcuts={[
 | 
			
		||||
                "A",
 | 
			
		||||
                t("helpDialog.click"),
 | 
			
		||||
                t("helpDialog.click"),
 | 
			
		||||
                t("helpDialog.click"),
 | 
			
		||||
              ]}
 | 
			
		||||
              isOr={false}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("helpDialog.curvedLine")}
 | 
			
		||||
              shortcuts={[
 | 
			
		||||
                "L",
 | 
			
		||||
                t("helpDialog.click"),
 | 
			
		||||
                t("helpDialog.click"),
 | 
			
		||||
                t("helpDialog.click"),
 | 
			
		||||
              ]}
 | 
			
		||||
              isOr={false}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut label={t("toolBar.lock")} shortcuts={[KEYS.Q]} />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("helpDialog.preventBinding")}
 | 
			
		||||
              shortcuts={[getShortcutKey("CtrlOrCmd")]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("toolBar.link")}
 | 
			
		||||
              shortcuts={[getShortcutKey("CtrlOrCmd+K")]}
 | 
			
		||||
            />
 | 
			
		||||
          </ShortcutIsland>
 | 
			
		||||
          <ShortcutIsland
 | 
			
		||||
            className="HelpDialog__island--view"
 | 
			
		||||
            caption={t("helpDialog.view")}
 | 
			
		||||
          >
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("buttons.zoomIn")}
 | 
			
		||||
              shortcuts={[getShortcutKey("CtrlOrCmd++")]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("buttons.zoomOut")}
 | 
			
		||||
              shortcuts={[getShortcutKey("CtrlOrCmd+-")]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("buttons.resetZoom")}
 | 
			
		||||
              shortcuts={[getShortcutKey("CtrlOrCmd+0")]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("helpDialog.zoomToFit")}
 | 
			
		||||
              shortcuts={["Shift+1"]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("helpDialog.zoomToSelection")}
 | 
			
		||||
              shortcuts={["Shift+2"]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut label={t("buttons.fullScreen")} shortcuts={["F"]} />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("buttons.zenMode")}
 | 
			
		||||
              shortcuts={[getShortcutKey("Alt+Z")]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("labels.showGrid")}
 | 
			
		||||
              shortcuts={[getShortcutKey("CtrlOrCmd+'")]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("labels.viewMode")}
 | 
			
		||||
              shortcuts={[getShortcutKey("Alt+R")]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("labels.toggleTheme")}
 | 
			
		||||
              shortcuts={[getShortcutKey("Alt+Shift+D")]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("stats.title")}
 | 
			
		||||
              shortcuts={[getShortcutKey("Alt+/")]}
 | 
			
		||||
            />
 | 
			
		||||
          </ShortcutIsland>
 | 
			
		||||
          <ShortcutIsland
 | 
			
		||||
            className="HelpDialog__island--editor"
 | 
			
		||||
            caption={t("helpDialog.editor")}
 | 
			
		||||
          >
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("labels.selectAll")}
 | 
			
		||||
              shortcuts={[getShortcutKey("CtrlOrCmd+A")]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("labels.multiSelect")}
 | 
			
		||||
              shortcuts={[getShortcutKey(`Shift+${t("helpDialog.click")}`)]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("helpDialog.deepSelect")}
 | 
			
		||||
              shortcuts={[getShortcutKey(`CtrlOrCmd+${t("helpDialog.click")}`)]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("helpDialog.deepBoxSelect")}
 | 
			
		||||
              shortcuts={[getShortcutKey(`CtrlOrCmd+${t("helpDialog.drag")}`)]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("labels.moveCanvas")}
 | 
			
		||||
              shortcuts={[
 | 
			
		||||
                getShortcutKey(`Space+${t("helpDialog.drag")}`),
 | 
			
		||||
                getShortcutKey(`Wheel+${t("helpDialog.drag")}`),
 | 
			
		||||
              ]}
 | 
			
		||||
              isOr={true}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("labels.cut")}
 | 
			
		||||
              shortcuts={[getShortcutKey("CtrlOrCmd+X")]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("labels.copy")}
 | 
			
		||||
              shortcuts={[getShortcutKey("CtrlOrCmd+C")]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("labels.paste")}
 | 
			
		||||
              shortcuts={[getShortcutKey("CtrlOrCmd+V")]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("labels.copyAsPng")}
 | 
			
		||||
              shortcuts={[getShortcutKey("Shift+Alt+C")]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("labels.copyStyles")}
 | 
			
		||||
              shortcuts={[getShortcutKey("CtrlOrCmd+Alt+C")]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("labels.pasteStyles")}
 | 
			
		||||
              shortcuts={[getShortcutKey("CtrlOrCmd+Alt+V")]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("labels.delete")}
 | 
			
		||||
              shortcuts={[getShortcutKey("Del")]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("labels.sendToBack")}
 | 
			
		||||
              shortcuts={[
 | 
			
		||||
                isDarwin
 | 
			
		||||
                  ? getShortcutKey("CtrlOrCmd+Alt+[")
 | 
			
		||||
                  : getShortcutKey("CtrlOrCmd+Shift+["),
 | 
			
		||||
              ]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("labels.bringToFront")}
 | 
			
		||||
              shortcuts={[
 | 
			
		||||
                isDarwin
 | 
			
		||||
                  ? getShortcutKey("CtrlOrCmd+Alt+]")
 | 
			
		||||
                  : getShortcutKey("CtrlOrCmd+Shift+]"),
 | 
			
		||||
              ]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("labels.sendBackward")}
 | 
			
		||||
              shortcuts={[getShortcutKey("CtrlOrCmd+[")]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("labels.bringForward")}
 | 
			
		||||
              shortcuts={[getShortcutKey("CtrlOrCmd+]")]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("labels.alignTop")}
 | 
			
		||||
              shortcuts={[getShortcutKey("CtrlOrCmd+Shift+Up")]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("labels.alignBottom")}
 | 
			
		||||
              shortcuts={[getShortcutKey("CtrlOrCmd+Shift+Down")]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("labels.alignLeft")}
 | 
			
		||||
              shortcuts={[getShortcutKey("CtrlOrCmd+Shift+Left")]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("labels.alignRight")}
 | 
			
		||||
              shortcuts={[getShortcutKey("CtrlOrCmd+Shift+Right")]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("labels.duplicateSelection")}
 | 
			
		||||
              shortcuts={[
 | 
			
		||||
                getShortcutKey("CtrlOrCmd+D"),
 | 
			
		||||
                getShortcutKey(`Alt+${t("helpDialog.drag")}`),
 | 
			
		||||
              ]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("helpDialog.toggleElementLock")}
 | 
			
		||||
              shortcuts={[getShortcutKey("CtrlOrCmd+Shift+L")]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("buttons.undo")}
 | 
			
		||||
              shortcuts={[getShortcutKey("CtrlOrCmd+Z")]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("buttons.redo")}
 | 
			
		||||
              shortcuts={
 | 
			
		||||
                isWindows
 | 
			
		||||
                  ? [
 | 
			
		||||
                      getShortcutKey("CtrlOrCmd+Y"),
 | 
			
		||||
                      getShortcutKey("CtrlOrCmd+Shift+Z"),
 | 
			
		||||
                    ]
 | 
			
		||||
                  : [getShortcutKey("CtrlOrCmd+Shift+Z")]
 | 
			
		||||
              }
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("labels.group")}
 | 
			
		||||
              shortcuts={[getShortcutKey("CtrlOrCmd+G")]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("labels.ungroup")}
 | 
			
		||||
              shortcuts={[getShortcutKey("CtrlOrCmd+Shift+G")]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("labels.flipHorizontal")}
 | 
			
		||||
              shortcuts={[getShortcutKey("Shift+H")]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("labels.flipVertical")}
 | 
			
		||||
              shortcuts={[getShortcutKey("Shift+V")]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("labels.showStroke")}
 | 
			
		||||
              shortcuts={[getShortcutKey("S")]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("labels.showBackground")}
 | 
			
		||||
              shortcuts={[getShortcutKey("G")]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("labels.decreaseFontSize")}
 | 
			
		||||
              shortcuts={[getShortcutKey("CtrlOrCmd+Shift+<")]}
 | 
			
		||||
            />
 | 
			
		||||
            <Shortcut
 | 
			
		||||
              label={t("labels.increaseFontSize")}
 | 
			
		||||
              shortcuts={[getShortcutKey("CtrlOrCmd+Shift+>")]}
 | 
			
		||||
            />
 | 
			
		||||
          </ShortcutIsland>
 | 
			
		||||
        </Section>
 | 
			
		||||
      </Dialog>
 | 
			
		||||
    </>
 | 
			
		||||
 
 | 
			
		||||
@@ -14,20 +14,24 @@ $wide-viewport-width: 1000px;
 | 
			
		||||
    top: 100%;
 | 
			
		||||
    max-width: 100%;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    margin-top: 6px;
 | 
			
		||||
    margin-top: 0.5rem;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    color: $oc-gray-6;
 | 
			
		||||
    font-size: 0.8rem;
 | 
			
		||||
    color: var(--color-gray-40);
 | 
			
		||||
    font-size: 0.75rem;
 | 
			
		||||
 | 
			
		||||
    @include isMobile {
 | 
			
		||||
      position: static;
 | 
			
		||||
      padding-right: 2em;
 | 
			
		||||
      padding-right: 2rem;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    > span {
 | 
			
		||||
      padding: 0.2rem 0.4rem;
 | 
			
		||||
      background-color: var(--overlay-bg-color);
 | 
			
		||||
      border-radius: 4px;
 | 
			
		||||
      padding: 0.25rem;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &.theme--dark {
 | 
			
		||||
    .HintViewer {
 | 
			
		||||
      color: var(--color-gray-60);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,8 @@
 | 
			
		||||
  .picker {
 | 
			
		||||
    background: var(--popup-bg-color);
 | 
			
		||||
    border: 0 solid transparentize($oc-white, 0.75);
 | 
			
		||||
    box-shadow: transparentize($oc-black, 0.75) 0 1px 4px;
 | 
			
		||||
    // ˇˇ yeah, i dunno, open to suggestions here :D
 | 
			
		||||
    box-shadow: rgb(0 0 0 / 25%) 2px 2px 4px 2px;
 | 
			
		||||
    border-radius: 4px;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
  }
 | 
			
		||||
@@ -46,7 +47,6 @@
 | 
			
		||||
      margin: 0;
 | 
			
		||||
      width: 36px;
 | 
			
		||||
      height: 18px;
 | 
			
		||||
      opacity: 0.6;
 | 
			
		||||
      pointer-events: none;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@ import { Popover } from "./Popover";
 | 
			
		||||
import "./IconPicker.scss";
 | 
			
		||||
import { isArrowKey, KEYS } from "../keys";
 | 
			
		||||
import { getLanguage } from "../i18n";
 | 
			
		||||
import clsx from "clsx";
 | 
			
		||||
 | 
			
		||||
function Picker<T>({
 | 
			
		||||
  options,
 | 
			
		||||
@@ -102,7 +103,9 @@ function Picker<T>({
 | 
			
		||||
      <div className="picker-content" ref={rGallery}>
 | 
			
		||||
        {options.map((option, i) => (
 | 
			
		||||
          <button
 | 
			
		||||
            className="picker-option"
 | 
			
		||||
            className={clsx("picker-option", {
 | 
			
		||||
              active: value === option.value,
 | 
			
		||||
            })}
 | 
			
		||||
            onClick={(event) => {
 | 
			
		||||
              (event.currentTarget as HTMLButtonElement).focus();
 | 
			
		||||
              onChange(option.value);
 | 
			
		||||
@@ -150,7 +153,7 @@ export function IconPicker<T>({
 | 
			
		||||
  const isRTL = getLanguage().rtl;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <label className={"picker-container"}>
 | 
			
		||||
    <div>
 | 
			
		||||
      <button
 | 
			
		||||
        name={group}
 | 
			
		||||
        className={isActive ? "active" : ""}
 | 
			
		||||
@@ -184,6 +187,6 @@ export function IconPicker<T>({
 | 
			
		||||
          </>
 | 
			
		||||
        ) : null}
 | 
			
		||||
      </React.Suspense>
 | 
			
		||||
    </label>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,14 +5,12 @@ import { canvasToBlob } from "../data/blob";
 | 
			
		||||
import { NonDeletedExcalidrawElement } from "../element/types";
 | 
			
		||||
import { CanvasError } from "../errors";
 | 
			
		||||
import { t } from "../i18n";
 | 
			
		||||
import { useDevice } from "./App";
 | 
			
		||||
import { getSelectedElements, isSomeElementSelected } from "../scene";
 | 
			
		||||
import { exportToCanvas } from "../scene/export";
 | 
			
		||||
import { AppState, BinaryFiles } from "../types";
 | 
			
		||||
import { Dialog } from "./Dialog";
 | 
			
		||||
import { clipboard, exportImage } from "./icons";
 | 
			
		||||
import { clipboard } from "./icons";
 | 
			
		||||
import Stack from "./Stack";
 | 
			
		||||
import { ToolButton } from "./ToolButton";
 | 
			
		||||
import "./ExportDialog.scss";
 | 
			
		||||
import OpenColor from "open-color";
 | 
			
		||||
import { CheckboxItem } from "./CheckboxItem";
 | 
			
		||||
@@ -221,6 +219,7 @@ const ImageExportModal = ({
 | 
			
		||||
export const ImageExportDialog = ({
 | 
			
		||||
  elements,
 | 
			
		||||
  appState,
 | 
			
		||||
  setAppState,
 | 
			
		||||
  files,
 | 
			
		||||
  exportPadding = DEFAULT_EXPORT_PADDING,
 | 
			
		||||
  actionManager,
 | 
			
		||||
@@ -229,6 +228,7 @@ export const ImageExportDialog = ({
 | 
			
		||||
  onExportToClipboard,
 | 
			
		||||
}: {
 | 
			
		||||
  appState: AppState;
 | 
			
		||||
  setAppState: React.Component<any, AppState>["setState"];
 | 
			
		||||
  elements: readonly NonDeletedExcalidrawElement[];
 | 
			
		||||
  files: BinaryFiles;
 | 
			
		||||
  exportPadding?: number;
 | 
			
		||||
@@ -237,26 +237,13 @@ export const ImageExportDialog = ({
 | 
			
		||||
  onExportToSvg: ExportCB;
 | 
			
		||||
  onExportToClipboard: ExportCB;
 | 
			
		||||
}) => {
 | 
			
		||||
  const [modalIsShown, setModalIsShown] = useState(false);
 | 
			
		||||
 | 
			
		||||
  const handleClose = React.useCallback(() => {
 | 
			
		||||
    setModalIsShown(false);
 | 
			
		||||
  }, []);
 | 
			
		||||
    setAppState({ openDialog: null });
 | 
			
		||||
  }, [setAppState]);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <ToolButton
 | 
			
		||||
        onClick={() => {
 | 
			
		||||
          setModalIsShown(true);
 | 
			
		||||
        }}
 | 
			
		||||
        data-testid="image-export-button"
 | 
			
		||||
        icon={exportImage}
 | 
			
		||||
        type="button"
 | 
			
		||||
        aria-label={t("buttons.exportImage")}
 | 
			
		||||
        showAriaLabel={useDevice().isMobile}
 | 
			
		||||
        title={t("buttons.exportImage")}
 | 
			
		||||
      />
 | 
			
		||||
      {modalIsShown && (
 | 
			
		||||
      {appState.openDialog === "imageExport" && (
 | 
			
		||||
        <Dialog onCloseRequest={handleClose} title={t("buttons.exportImage")}>
 | 
			
		||||
          <ImageExportModal
 | 
			
		||||
            elements={elements}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
.excalidraw {
 | 
			
		||||
  .Island {
 | 
			
		||||
    --padding: 0;
 | 
			
		||||
    box-sizing: border-box;
 | 
			
		||||
    background-color: var(--island-bg-color);
 | 
			
		||||
    box-shadow: var(--shadow-island);
 | 
			
		||||
    border-radius: var(--border-radius-lg);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,10 @@
 | 
			
		||||
import React, { useState } from "react";
 | 
			
		||||
import { NonDeletedExcalidrawElement } from "../element/types";
 | 
			
		||||
import { t } from "../i18n";
 | 
			
		||||
import { useDevice } from "./App";
 | 
			
		||||
 | 
			
		||||
import { AppState, ExportOpts, BinaryFiles } from "../types";
 | 
			
		||||
import { Dialog } from "./Dialog";
 | 
			
		||||
import { exportFile, exportToFileIcon, link } from "./icons";
 | 
			
		||||
import { ExportIcon, exportToFileIcon, LinkIcon } from "./icons";
 | 
			
		||||
import { ToolButton } from "./ToolButton";
 | 
			
		||||
import { actionSaveFileToDisk } from "../actions/actionExport";
 | 
			
		||||
import { Card } from "./Card";
 | 
			
		||||
@@ -14,6 +14,7 @@ import { nativeFileSystemSupported } from "../data/filesystem";
 | 
			
		||||
import { trackEvent } from "../analytics";
 | 
			
		||||
import { ActionManager } from "../actions/manager";
 | 
			
		||||
import { getFrame } from "../utils";
 | 
			
		||||
import MenuItem from "./MenuItem";
 | 
			
		||||
 | 
			
		||||
export type ExportCB = (
 | 
			
		||||
  elements: readonly NonDeletedExcalidrawElement[],
 | 
			
		||||
@@ -63,7 +64,7 @@ const JSONExportModal = ({
 | 
			
		||||
        )}
 | 
			
		||||
        {onExportToBackend && (
 | 
			
		||||
          <Card color="pink">
 | 
			
		||||
            <div className="Card-icon">{link}</div>
 | 
			
		||||
            <div className="Card-icon">{LinkIcon}</div>
 | 
			
		||||
            <h2>{t("exportDialog.link_title")}</h2>
 | 
			
		||||
            <div className="Card-details">{t("exportDialog.link_details")}</div>
 | 
			
		||||
            <ToolButton
 | 
			
		||||
@@ -109,16 +110,13 @@ export const JSONExportDialog = ({
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <ToolButton
 | 
			
		||||
      <MenuItem
 | 
			
		||||
        icon={ExportIcon}
 | 
			
		||||
        label={t("buttons.export")}
 | 
			
		||||
        onClick={() => {
 | 
			
		||||
          setModalIsShown(true);
 | 
			
		||||
        }}
 | 
			
		||||
        data-testid="json-export-button"
 | 
			
		||||
        icon={exportFile}
 | 
			
		||||
        type="button"
 | 
			
		||||
        aria-label={t("buttons.export")}
 | 
			
		||||
        showAriaLabel={useDevice().isMobile}
 | 
			
		||||
        title={t("buttons.export")}
 | 
			
		||||
        dataTestId="json-export-button"
 | 
			
		||||
      />
 | 
			
		||||
      {modalIsShown && (
 | 
			
		||||
        <Dialog onCloseRequest={handleClose} title={t("buttons.export")}>
 | 
			
		||||
 
 | 
			
		||||
@@ -16,8 +16,10 @@
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    pointer-events: none;
 | 
			
		||||
    z-index: var(--zIndex-layerUI);
 | 
			
		||||
 | 
			
		||||
    &__top-right {
 | 
			
		||||
      display: flex;
 | 
			
		||||
      gap: 0.75rem;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &__footer {
 | 
			
		||||
@@ -48,13 +50,6 @@
 | 
			
		||||
        transform: translate(-999px, 0);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      :root[dir="ltr"] &.layer-ui__wrapper__footer-left--transition-left {
 | 
			
		||||
        transform: translate(-76px, 0);
 | 
			
		||||
      }
 | 
			
		||||
      :root[dir="rtl"] &.layer-ui__wrapper__footer-left--transition-left {
 | 
			
		||||
        transform: translate(76px, 0);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      &.layer-ui__wrapper__footer-left--transition-bottom {
 | 
			
		||||
        transform: translate(0, 92px);
 | 
			
		||||
      }
 | 
			
		||||
@@ -97,14 +92,9 @@
 | 
			
		||||
      pointer-events: all;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .layer-ui__wrapper__footer-left {
 | 
			
		||||
      margin-bottom: 0.2em;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .layer-ui__wrapper__footer-right {
 | 
			
		||||
      margin-top: auto;
 | 
			
		||||
      margin-bottom: auto;
 | 
			
		||||
      margin-inline-end: 1em;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,6 @@ import { ExportType } from "../scene/types";
 | 
			
		||||
import { AppProps, AppState, ExcalidrawProps, BinaryFiles } from "../types";
 | 
			
		||||
import { muteFSAbortError } from "../utils";
 | 
			
		||||
import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
 | 
			
		||||
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
 | 
			
		||||
import CollabButton from "./CollabButton";
 | 
			
		||||
import { ErrorDialog } from "./ErrorDialog";
 | 
			
		||||
import { ExportCB, ImageExportDialog } from "./ImageExportDialog";
 | 
			
		||||
@@ -36,13 +35,26 @@ import "./LayerUI.scss";
 | 
			
		||||
import "./Toolbar.scss";
 | 
			
		||||
import { PenModeButton } from "./PenModeButton";
 | 
			
		||||
import { trackEvent } from "../analytics";
 | 
			
		||||
import { useDevice } from "../components/App";
 | 
			
		||||
import { isMenuOpenAtom, useDevice } from "../components/App";
 | 
			
		||||
import { Stats } from "./Stats";
 | 
			
		||||
import { actionToggleStats } from "../actions/actionToggleStats";
 | 
			
		||||
import Footer from "./Footer";
 | 
			
		||||
import {
 | 
			
		||||
  ExportImageIcon,
 | 
			
		||||
  HamburgerMenuIcon,
 | 
			
		||||
  WelcomeScreenMenuArrow,
 | 
			
		||||
  WelcomeScreenTopToolbarArrow,
 | 
			
		||||
} from "./icons";
 | 
			
		||||
import { MenuLinks, Separator } from "./MenuUtils";
 | 
			
		||||
import { useOutsideClickHook } from "../hooks/useOutsideClick";
 | 
			
		||||
import WelcomeScreen from "./WelcomeScreen";
 | 
			
		||||
import { hostSidebarCountersAtom } from "./Sidebar/Sidebar";
 | 
			
		||||
import { jotaiScope } from "../jotai";
 | 
			
		||||
import { useAtom } from "jotai";
 | 
			
		||||
import { LanguageList } from "../excalidraw-app/components/LanguageList";
 | 
			
		||||
import WelcomeScreenDecor from "./WelcomeScreenDecor";
 | 
			
		||||
import { getShortcutFromShortcutName } from "../actions/shortcuts";
 | 
			
		||||
import MenuItem from "./MenuItem";
 | 
			
		||||
 | 
			
		||||
interface LayerUIProps {
 | 
			
		||||
  actionManager: ActionManager;
 | 
			
		||||
@@ -68,6 +80,7 @@ interface LayerUIProps {
 | 
			
		||||
  library: Library;
 | 
			
		||||
  id: string;
 | 
			
		||||
  onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
 | 
			
		||||
  renderWelcomeScreen: boolean;
 | 
			
		||||
}
 | 
			
		||||
const LayerUI = ({
 | 
			
		||||
  actionManager,
 | 
			
		||||
@@ -92,6 +105,7 @@ const LayerUI = ({
 | 
			
		||||
  library,
 | 
			
		||||
  id,
 | 
			
		||||
  onImageAction,
 | 
			
		||||
  renderWelcomeScreen,
 | 
			
		||||
}: LayerUIProps) => {
 | 
			
		||||
  const device = useDevice();
 | 
			
		||||
 | 
			
		||||
@@ -151,6 +165,7 @@ const LayerUI = ({
 | 
			
		||||
      <ImageExportDialog
 | 
			
		||||
        elements={elements}
 | 
			
		||||
        appState={appState}
 | 
			
		||||
        setAppState={setAppState}
 | 
			
		||||
        files={files}
 | 
			
		||||
        actionManager={actionManager}
 | 
			
		||||
        onExportToPng={createExporter("png")}
 | 
			
		||||
@@ -160,71 +175,107 @@ const LayerUI = ({
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const Separator = () => {
 | 
			
		||||
    return <div style={{ width: ".625em" }} />;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const renderViewModeCanvasActions = () => {
 | 
			
		||||
    return (
 | 
			
		||||
      <Section
 | 
			
		||||
        heading="canvasActions"
 | 
			
		||||
        className={clsx("zen-mode-transition", {
 | 
			
		||||
          "transition-left": appState.zenModeEnabled,
 | 
			
		||||
        })}
 | 
			
		||||
      >
 | 
			
		||||
        {/* the zIndex ensures this menu has higher stacking order,
 | 
			
		||||
         see https://github.com/excalidraw/excalidraw/pull/1445 */}
 | 
			
		||||
        <Island padding={2} style={{ zIndex: 1 }}>
 | 
			
		||||
          <Stack.Col gap={4}>
 | 
			
		||||
            <Stack.Row gap={1} justifyContent="space-between">
 | 
			
		||||
              {renderJSONExportDialog()}
 | 
			
		||||
              {renderImageExportDialog()}
 | 
			
		||||
            </Stack.Row>
 | 
			
		||||
          </Stack.Col>
 | 
			
		||||
        </Island>
 | 
			
		||||
      </Section>
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
  const [isMenuOpen, setIsMenuOpen] = useAtom(isMenuOpenAtom);
 | 
			
		||||
  const menuRef = useOutsideClickHook(() => setIsMenuOpen(false));
 | 
			
		||||
 | 
			
		||||
  const renderCanvasActions = () => (
 | 
			
		||||
    <Section
 | 
			
		||||
      heading="canvasActions"
 | 
			
		||||
      className={clsx("zen-mode-transition", {
 | 
			
		||||
        "transition-left": appState.zenModeEnabled,
 | 
			
		||||
      })}
 | 
			
		||||
    >
 | 
			
		||||
      {/* the zIndex ensures this menu has higher stacking order,
 | 
			
		||||
    <div style={{ position: "relative" }}>
 | 
			
		||||
      <WelcomeScreenDecor
 | 
			
		||||
        shouldRender={renderWelcomeScreen && !appState.isLoading}
 | 
			
		||||
      >
 | 
			
		||||
        <div className="virgil WelcomeScreen-decor WelcomeScreen-decor--menu-pointer">
 | 
			
		||||
          {WelcomeScreenMenuArrow}
 | 
			
		||||
          <div>{t("welcomeScreen.menuHints")}</div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </WelcomeScreenDecor>
 | 
			
		||||
 | 
			
		||||
      <button
 | 
			
		||||
        data-prevent-outside-click
 | 
			
		||||
        className={clsx("menu-button", "zen-mode-transition", {
 | 
			
		||||
          "transition-left": appState.zenModeEnabled,
 | 
			
		||||
        })}
 | 
			
		||||
        onClick={() => setIsMenuOpen(!isMenuOpen)}
 | 
			
		||||
        type="button"
 | 
			
		||||
        data-testid="menu-button"
 | 
			
		||||
      >
 | 
			
		||||
        {HamburgerMenuIcon}
 | 
			
		||||
      </button>
 | 
			
		||||
 | 
			
		||||
      {isMenuOpen && (
 | 
			
		||||
        <div
 | 
			
		||||
          ref={menuRef}
 | 
			
		||||
          style={{ position: "absolute", top: "100%", marginTop: ".25rem" }}
 | 
			
		||||
        >
 | 
			
		||||
          <Section heading="canvasActions">
 | 
			
		||||
            {/* the zIndex ensures this menu has higher stacking order,
 | 
			
		||||
         see https://github.com/excalidraw/excalidraw/pull/1445 */}
 | 
			
		||||
      <Island padding={2} style={{ zIndex: 1 }}>
 | 
			
		||||
        <Stack.Col gap={4}>
 | 
			
		||||
          <Stack.Row gap={1} justifyContent="space-between">
 | 
			
		||||
            {actionManager.renderAction("clearCanvas")}
 | 
			
		||||
            <Separator />
 | 
			
		||||
            {actionManager.renderAction("loadScene")}
 | 
			
		||||
            {renderJSONExportDialog()}
 | 
			
		||||
            {renderImageExportDialog()}
 | 
			
		||||
            <Separator />
 | 
			
		||||
            {onCollabButtonClick && (
 | 
			
		||||
              <CollabButton
 | 
			
		||||
                isCollaborating={isCollaborating}
 | 
			
		||||
                collaboratorCount={appState.collaborators.size}
 | 
			
		||||
                onClick={onCollabButtonClick}
 | 
			
		||||
              />
 | 
			
		||||
            )}
 | 
			
		||||
          </Stack.Row>
 | 
			
		||||
          <BackgroundPickerAndDarkModeToggle actionManager={actionManager} />
 | 
			
		||||
          {appState.fileHandle && (
 | 
			
		||||
            <>{actionManager.renderAction("saveToActiveFile")}</>
 | 
			
		||||
          )}
 | 
			
		||||
        </Stack.Col>
 | 
			
		||||
      </Island>
 | 
			
		||||
    </Section>
 | 
			
		||||
            <Island
 | 
			
		||||
              className="menu-container"
 | 
			
		||||
              padding={2}
 | 
			
		||||
              style={{ zIndex: 1 }}
 | 
			
		||||
            >
 | 
			
		||||
              {!appState.viewModeEnabled &&
 | 
			
		||||
                actionManager.renderAction("loadScene")}
 | 
			
		||||
              {/* // TODO barnabasmolnar/editor-redesign  */}
 | 
			
		||||
              {/* is this fine here? */}
 | 
			
		||||
              {appState.fileHandle &&
 | 
			
		||||
                actionManager.renderAction("saveToActiveFile")}
 | 
			
		||||
              {renderJSONExportDialog()}
 | 
			
		||||
              {UIOptions.canvasActions.saveAsImage && (
 | 
			
		||||
                <MenuItem
 | 
			
		||||
                  label={t("buttons.exportImage")}
 | 
			
		||||
                  icon={ExportImageIcon}
 | 
			
		||||
                  dataTestId="image-export-button"
 | 
			
		||||
                  onClick={() => setAppState({ openDialog: "imageExport" })}
 | 
			
		||||
                  shortcut={getShortcutFromShortcutName("imageExport")}
 | 
			
		||||
                />
 | 
			
		||||
              )}
 | 
			
		||||
              {onCollabButtonClick && (
 | 
			
		||||
                <CollabButton
 | 
			
		||||
                  isCollaborating={isCollaborating}
 | 
			
		||||
                  collaboratorCount={appState.collaborators.size}
 | 
			
		||||
                  onClick={onCollabButtonClick}
 | 
			
		||||
                />
 | 
			
		||||
              )}
 | 
			
		||||
              {actionManager.renderAction("toggleShortcuts", undefined, true)}
 | 
			
		||||
              {!appState.viewModeEnabled &&
 | 
			
		||||
                actionManager.renderAction("clearCanvas")}
 | 
			
		||||
              <Separator />
 | 
			
		||||
              <MenuLinks />
 | 
			
		||||
              <Separator />
 | 
			
		||||
              <div
 | 
			
		||||
                style={{
 | 
			
		||||
                  display: "flex",
 | 
			
		||||
                  flexDirection: "column",
 | 
			
		||||
                  rowGap: ".5rem",
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                <div>{actionManager.renderAction("toggleTheme")}</div>
 | 
			
		||||
                <div style={{ padding: "0 0.625rem" }}>
 | 
			
		||||
                  <LanguageList style={{ width: "100%" }} />
 | 
			
		||||
                </div>
 | 
			
		||||
                {!appState.viewModeEnabled && (
 | 
			
		||||
                  <div>
 | 
			
		||||
                    <div style={{ fontSize: ".75rem", marginBottom: ".5rem" }}>
 | 
			
		||||
                      {t("labels.canvasBackground")}
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div style={{ padding: "0 0.625rem" }}>
 | 
			
		||||
                      {actionManager.renderAction("changeViewBackgroundColor")}
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                )}
 | 
			
		||||
              </div>
 | 
			
		||||
            </Island>
 | 
			
		||||
          </Section>
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const renderSelectedShapeActions = () => (
 | 
			
		||||
    <Section
 | 
			
		||||
      heading="selectedShapeActions"
 | 
			
		||||
      className={clsx("zen-mode-transition", {
 | 
			
		||||
      className={clsx("selected-shape-actions zen-mode-transition", {
 | 
			
		||||
        "transition-left": appState.zenModeEnabled,
 | 
			
		||||
      })}
 | 
			
		||||
    >
 | 
			
		||||
@@ -232,10 +283,9 @@ const LayerUI = ({
 | 
			
		||||
        className={CLASSES.SHAPE_ACTIONS_MENU}
 | 
			
		||||
        padding={2}
 | 
			
		||||
        style={{
 | 
			
		||||
          // we want to make sure this doesn't overflow so subtracting 200
 | 
			
		||||
          // which is approximately height of zoom footer and top left menu items with some buffer
 | 
			
		||||
          // if active file name is displayed, subtracting 248 to account for its height
 | 
			
		||||
          maxHeight: `${appState.height - (appState.fileHandle ? 248 : 200)}px`,
 | 
			
		||||
          // we want to make sure this doesn't overflow so subtracting the
 | 
			
		||||
          // approximate height of hamburgerMenu + footer
 | 
			
		||||
          maxHeight: `${appState.height - 166}px`,
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <SelectedShapeActions
 | 
			
		||||
@@ -255,74 +305,89 @@ const LayerUI = ({
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <FixedSideContainer side="top">
 | 
			
		||||
        {renderWelcomeScreen && !appState.isLoading && (
 | 
			
		||||
          <WelcomeScreen appState={appState} actionManager={actionManager} />
 | 
			
		||||
        )}
 | 
			
		||||
        <div className="App-menu App-menu_top">
 | 
			
		||||
          <Stack.Col
 | 
			
		||||
            gap={4}
 | 
			
		||||
            className={clsx({
 | 
			
		||||
            gap={6}
 | 
			
		||||
            className={clsx("App-menu_top__left", {
 | 
			
		||||
              "disable-pointerEvents": appState.zenModeEnabled,
 | 
			
		||||
            })}
 | 
			
		||||
          >
 | 
			
		||||
            {appState.viewModeEnabled
 | 
			
		||||
              ? renderViewModeCanvasActions()
 | 
			
		||||
              : renderCanvasActions()}
 | 
			
		||||
            {renderCanvasActions()}
 | 
			
		||||
            {shouldRenderSelectedShapeActions && renderSelectedShapeActions()}
 | 
			
		||||
          </Stack.Col>
 | 
			
		||||
          {!appState.viewModeEnabled && (
 | 
			
		||||
            <Section heading="shapes">
 | 
			
		||||
            <Section heading="shapes" className="shapes-section">
 | 
			
		||||
              {(heading: React.ReactNode) => (
 | 
			
		||||
                <Stack.Col gap={4} align="start">
 | 
			
		||||
                  <Stack.Row
 | 
			
		||||
                    gap={1}
 | 
			
		||||
                    className={clsx("App-toolbar-container", {
 | 
			
		||||
                      "zen-mode": appState.zenModeEnabled,
 | 
			
		||||
                    })}
 | 
			
		||||
                <div style={{ position: "relative" }}>
 | 
			
		||||
                  <WelcomeScreenDecor
 | 
			
		||||
                    shouldRender={renderWelcomeScreen && !appState.isLoading}
 | 
			
		||||
                  >
 | 
			
		||||
                    <PenModeButton
 | 
			
		||||
                      zenModeEnabled={appState.zenModeEnabled}
 | 
			
		||||
                      checked={appState.penMode}
 | 
			
		||||
                      onChange={onPenModeToggle}
 | 
			
		||||
                      title={t("toolBar.penMode")}
 | 
			
		||||
                      penDetected={appState.penDetected}
 | 
			
		||||
                    />
 | 
			
		||||
                    <LockButton
 | 
			
		||||
                      zenModeEnabled={appState.zenModeEnabled}
 | 
			
		||||
                      checked={appState.activeTool.locked}
 | 
			
		||||
                      onChange={() => onLockToggle()}
 | 
			
		||||
                      title={t("toolBar.lock")}
 | 
			
		||||
                    />
 | 
			
		||||
                    <Island
 | 
			
		||||
                      padding={1}
 | 
			
		||||
                      className={clsx("App-toolbar", {
 | 
			
		||||
                    <div className="virgil WelcomeScreen-decor WelcomeScreen-decor--top-toolbar-pointer">
 | 
			
		||||
                      <div className="WelcomeScreen-decor--top-toolbar-pointer__label">
 | 
			
		||||
                        {t("welcomeScreen.toolbarHints")}
 | 
			
		||||
                      </div>
 | 
			
		||||
                      {WelcomeScreenTopToolbarArrow}
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </WelcomeScreenDecor>
 | 
			
		||||
 | 
			
		||||
                  <Stack.Col gap={4} align="start">
 | 
			
		||||
                    <Stack.Row
 | 
			
		||||
                      gap={1}
 | 
			
		||||
                      className={clsx("App-toolbar-container", {
 | 
			
		||||
                        "zen-mode": appState.zenModeEnabled,
 | 
			
		||||
                      })}
 | 
			
		||||
                    >
 | 
			
		||||
                      <HintViewer
 | 
			
		||||
                        appState={appState}
 | 
			
		||||
                        elements={elements}
 | 
			
		||||
                        isMobile={device.isMobile}
 | 
			
		||||
                        device={device}
 | 
			
		||||
                      />
 | 
			
		||||
                      {heading}
 | 
			
		||||
                      <Stack.Row gap={1}>
 | 
			
		||||
                        <ShapesSwitcher
 | 
			
		||||
                      <Island
 | 
			
		||||
                        padding={1}
 | 
			
		||||
                        className={clsx("App-toolbar", {
 | 
			
		||||
                          "zen-mode": appState.zenModeEnabled,
 | 
			
		||||
                        })}
 | 
			
		||||
                      >
 | 
			
		||||
                        <HintViewer
 | 
			
		||||
                          appState={appState}
 | 
			
		||||
                          canvas={canvas}
 | 
			
		||||
                          activeTool={appState.activeTool}
 | 
			
		||||
                          setAppState={setAppState}
 | 
			
		||||
                          onImageAction={({ pointerType }) => {
 | 
			
		||||
                            onImageAction({
 | 
			
		||||
                              insertOnCanvasDirectly: pointerType !== "mouse",
 | 
			
		||||
                            });
 | 
			
		||||
                          }}
 | 
			
		||||
                          elements={elements}
 | 
			
		||||
                          isMobile={device.isMobile}
 | 
			
		||||
                          device={device}
 | 
			
		||||
                        />
 | 
			
		||||
                      </Stack.Row>
 | 
			
		||||
                    </Island>
 | 
			
		||||
                    <LibraryButton
 | 
			
		||||
                      appState={appState}
 | 
			
		||||
                      setAppState={setAppState}
 | 
			
		||||
                    />
 | 
			
		||||
                  </Stack.Row>
 | 
			
		||||
                </Stack.Col>
 | 
			
		||||
                        {heading}
 | 
			
		||||
                        <Stack.Row gap={1}>
 | 
			
		||||
                          <PenModeButton
 | 
			
		||||
                            zenModeEnabled={appState.zenModeEnabled}
 | 
			
		||||
                            checked={appState.penMode}
 | 
			
		||||
                            onChange={onPenModeToggle}
 | 
			
		||||
                            title={t("toolBar.penMode")}
 | 
			
		||||
                            penDetected={appState.penDetected}
 | 
			
		||||
                          />
 | 
			
		||||
                          <LockButton
 | 
			
		||||
                            zenModeEnabled={appState.zenModeEnabled}
 | 
			
		||||
                            checked={appState.activeTool.locked}
 | 
			
		||||
                            onChange={() => onLockToggle()}
 | 
			
		||||
                            title={t("toolBar.lock")}
 | 
			
		||||
                          />
 | 
			
		||||
                          <div className="App-toolbar__divider"></div>
 | 
			
		||||
 | 
			
		||||
                          <ShapesSwitcher
 | 
			
		||||
                            appState={appState}
 | 
			
		||||
                            canvas={canvas}
 | 
			
		||||
                            activeTool={appState.activeTool}
 | 
			
		||||
                            setAppState={setAppState}
 | 
			
		||||
                            onImageAction={({ pointerType }) => {
 | 
			
		||||
                              onImageAction({
 | 
			
		||||
                                insertOnCanvasDirectly: pointerType !== "mouse",
 | 
			
		||||
                              });
 | 
			
		||||
                            }}
 | 
			
		||||
                          />
 | 
			
		||||
                          {/* {actionManager.renderAction("eraser", {
 | 
			
		||||
                          // size: "small",
 | 
			
		||||
                        })} */}
 | 
			
		||||
                        </Stack.Row>
 | 
			
		||||
                      </Island>
 | 
			
		||||
                    </Stack.Row>
 | 
			
		||||
                  </Stack.Col>
 | 
			
		||||
                </div>
 | 
			
		||||
              )}
 | 
			
		||||
            </Section>
 | 
			
		||||
          )}
 | 
			
		||||
@@ -338,7 +403,19 @@ const LayerUI = ({
 | 
			
		||||
              collaborators={appState.collaborators}
 | 
			
		||||
              actionManager={actionManager}
 | 
			
		||||
            />
 | 
			
		||||
            {renderTopRightUI?.(device.isMobile, appState)}
 | 
			
		||||
            {onCollabButtonClick && (
 | 
			
		||||
              <CollabButton
 | 
			
		||||
                isInHamburgerMenu={false}
 | 
			
		||||
                isCollaborating={isCollaborating}
 | 
			
		||||
                collaboratorCount={appState.collaborators.size}
 | 
			
		||||
                onClick={onCollabButtonClick}
 | 
			
		||||
              />
 | 
			
		||||
            )}
 | 
			
		||||
            {!appState.viewModeEnabled &&
 | 
			
		||||
              renderTopRightUI?.(device.isMobile, appState)}
 | 
			
		||||
            {!appState.viewModeEnabled && (
 | 
			
		||||
              <LibraryButton appState={appState} setAppState={setAppState} />
 | 
			
		||||
            )}
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </FixedSideContainer>
 | 
			
		||||
@@ -371,13 +448,14 @@ const LayerUI = ({
 | 
			
		||||
          onClose={() => setAppState({ errorMessage: null })}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
      {appState.showHelpDialog && (
 | 
			
		||||
      {appState.openDialog === "help" && (
 | 
			
		||||
        <HelpDialog
 | 
			
		||||
          onClose={() => {
 | 
			
		||||
            setAppState({ showHelpDialog: false });
 | 
			
		||||
            setAppState({ openDialog: null });
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
      {renderImageExportDialog()}
 | 
			
		||||
      {appState.pasteDialog.shown && (
 | 
			
		||||
        <PasteChartDialog
 | 
			
		||||
          setAppState={setAppState}
 | 
			
		||||
@@ -392,6 +470,7 @@ const LayerUI = ({
 | 
			
		||||
      )}
 | 
			
		||||
      {device.isMobile && (
 | 
			
		||||
        <MobileMenu
 | 
			
		||||
          renderWelcomeScreen={renderWelcomeScreen}
 | 
			
		||||
          appState={appState}
 | 
			
		||||
          elements={elements}
 | 
			
		||||
          actionManager={actionManager}
 | 
			
		||||
@@ -433,6 +512,7 @@ const LayerUI = ({
 | 
			
		||||
          >
 | 
			
		||||
            {renderFixedSideContainer()}
 | 
			
		||||
            <Footer
 | 
			
		||||
              renderWelcomeScreen={renderWelcomeScreen}
 | 
			
		||||
              appState={appState}
 | 
			
		||||
              actionManager={actionManager}
 | 
			
		||||
              renderCustomFooter={renderCustomFooter}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										32
									
								
								src/components/LibraryButton.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/components/LibraryButton.scss
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
			
		||||
@import "../css/variables.module";
 | 
			
		||||
 | 
			
		||||
.library-button {
 | 
			
		||||
  @include outlineButtonStyles;
 | 
			
		||||
 | 
			
		||||
  background-color: var(--island-bg-color);
 | 
			
		||||
 | 
			
		||||
  width: auto;
 | 
			
		||||
  height: var(--lg-button-size);
 | 
			
		||||
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  gap: 0.5rem;
 | 
			
		||||
 | 
			
		||||
  line-height: 0;
 | 
			
		||||
 | 
			
		||||
  font-size: 0.75rem;
 | 
			
		||||
  letter-spacing: 0.4px;
 | 
			
		||||
 | 
			
		||||
  svg {
 | 
			
		||||
    width: var(--lg-icon-size);
 | 
			
		||||
    height: var(--lg-icon-size);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__label {
 | 
			
		||||
    display: none;
 | 
			
		||||
 | 
			
		||||
    @media screen and (min-width: 1024px) {
 | 
			
		||||
      display: block;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,19 +1,11 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import clsx from "clsx";
 | 
			
		||||
import { t } from "../i18n";
 | 
			
		||||
import { AppState } from "../types";
 | 
			
		||||
import { capitalizeString } from "../utils";
 | 
			
		||||
import { trackEvent } from "../analytics";
 | 
			
		||||
import { useDevice } from "./App";
 | 
			
		||||
 | 
			
		||||
const LIBRARY_ICON = (
 | 
			
		||||
  <svg viewBox="0 0 576 512">
 | 
			
		||||
    <path
 | 
			
		||||
      fill="currentColor"
 | 
			
		||||
      d="M542.22 32.05c-54.8 3.11-163.72 14.43-230.96 55.59-4.64 2.84-7.27 7.89-7.27 13.17v363.87c0 11.55 12.63 18.85 23.28 13.49 69.18-34.82 169.23-44.32 218.7-46.92 16.89-.89 30.02-14.43 30.02-30.66V62.75c.01-17.71-15.35-31.74-33.77-30.7zM264.73 87.64C197.5 46.48 88.58 35.17 33.78 32.05 15.36 31.01 0 45.04 0 62.75V400.6c0 16.24 13.13 29.78 30.02 30.66 49.49 2.6 149.59 12.11 218.77 46.95 10.62 5.35 23.21-1.94 23.21-13.46V100.63c0-5.29-2.62-10.14-7.27-12.99z"
 | 
			
		||||
    ></path>
 | 
			
		||||
  </svg>
 | 
			
		||||
);
 | 
			
		||||
import "./LibraryButton.scss";
 | 
			
		||||
import { LibraryIcon } from "./icons";
 | 
			
		||||
 | 
			
		||||
export const LibraryButton: React.FC<{
 | 
			
		||||
  appState: AppState;
 | 
			
		||||
@@ -21,17 +13,16 @@ export const LibraryButton: React.FC<{
 | 
			
		||||
  isMobile?: boolean;
 | 
			
		||||
}> = ({ appState, setAppState, isMobile }) => {
 | 
			
		||||
  const device = useDevice();
 | 
			
		||||
  const showLabel = !isMobile;
 | 
			
		||||
 | 
			
		||||
  // TODO barnabasmolnar/redesign
 | 
			
		||||
  // not great, toolbar jumps in a jarring manner
 | 
			
		||||
  if (appState.isSidebarDocked && appState.openSidebar === "library") {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <label
 | 
			
		||||
      className={clsx(
 | 
			
		||||
        "ToolIcon ToolIcon_type_floating ToolIcon__library",
 | 
			
		||||
        `ToolIcon_size_medium`,
 | 
			
		||||
        {
 | 
			
		||||
          "is-mobile": isMobile,
 | 
			
		||||
        },
 | 
			
		||||
      )}
 | 
			
		||||
      title={`${capitalizeString(t("toolBar.library"))} — 0`}
 | 
			
		||||
    >
 | 
			
		||||
    <label title={`${capitalizeString(t("toolBar.library"))}`}>
 | 
			
		||||
      <input
 | 
			
		||||
        className="ToolIcon_type_checkbox"
 | 
			
		||||
        type="checkbox"
 | 
			
		||||
@@ -55,7 +46,12 @@ export const LibraryButton: React.FC<{
 | 
			
		||||
        aria-label={capitalizeString(t("toolBar.library"))}
 | 
			
		||||
        aria-keyshortcuts="0"
 | 
			
		||||
      />
 | 
			
		||||
      <div className="ToolIcon__icon">{LIBRARY_ICON}</div>
 | 
			
		||||
      <div className="library-button">
 | 
			
		||||
        <div>{LibraryIcon}</div>
 | 
			
		||||
        {showLabel && (
 | 
			
		||||
          <div className="library-button__label">{t("toolBar.library")}</div>
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
    </label>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -35,103 +35,32 @@
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .library-actions {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
  .library-actions-counter {
 | 
			
		||||
    background-color: var(--color-primary);
 | 
			
		||||
    color: var(--color-primary-light);
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    margin-right: auto;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
 | 
			
		||||
    button .library-actions-counter {
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      right: 2px;
 | 
			
		||||
      bottom: 2px;
 | 
			
		||||
      border-radius: 50%;
 | 
			
		||||
      width: 1em;
 | 
			
		||||
      height: 1em;
 | 
			
		||||
      padding: 1px;
 | 
			
		||||
      font-size: 0.7rem;
 | 
			
		||||
      background: #fff;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &--remove {
 | 
			
		||||
      background-color: $oc-red-7;
 | 
			
		||||
      &:hover {
 | 
			
		||||
        background-color: $oc-red-8;
 | 
			
		||||
      }
 | 
			
		||||
      &:active {
 | 
			
		||||
        background-color: $oc-red-9;
 | 
			
		||||
      }
 | 
			
		||||
      svg {
 | 
			
		||||
        color: $oc-white;
 | 
			
		||||
      }
 | 
			
		||||
      .library-actions-counter {
 | 
			
		||||
        color: $oc-red-7;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &--export {
 | 
			
		||||
      background-color: $oc-lime-5;
 | 
			
		||||
 | 
			
		||||
      &:hover {
 | 
			
		||||
        background-color: $oc-lime-7;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      &:active {
 | 
			
		||||
        background-color: $oc-lime-8;
 | 
			
		||||
      }
 | 
			
		||||
      svg {
 | 
			
		||||
        color: $oc-white;
 | 
			
		||||
      }
 | 
			
		||||
      .library-actions-counter {
 | 
			
		||||
        color: $oc-lime-5;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &--publish {
 | 
			
		||||
      background-color: $oc-cyan-6;
 | 
			
		||||
      &:hover {
 | 
			
		||||
        background-color: $oc-cyan-7;
 | 
			
		||||
      }
 | 
			
		||||
      &:active {
 | 
			
		||||
        background-color: $oc-cyan-9;
 | 
			
		||||
      }
 | 
			
		||||
      svg {
 | 
			
		||||
        color: $oc-white;
 | 
			
		||||
      }
 | 
			
		||||
      label {
 | 
			
		||||
        margin-left: -0.2em;
 | 
			
		||||
        margin-right: 1.1em;
 | 
			
		||||
        color: $oc-white;
 | 
			
		||||
        font-size: 0.86em;
 | 
			
		||||
      }
 | 
			
		||||
      .library-actions-counter {
 | 
			
		||||
        color: $oc-cyan-6;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &--load {
 | 
			
		||||
      background-color: $oc-blue-6;
 | 
			
		||||
      &:hover {
 | 
			
		||||
        background-color: $oc-blue-7;
 | 
			
		||||
      }
 | 
			
		||||
      &:active {
 | 
			
		||||
        background-color: $oc-blue-9;
 | 
			
		||||
      }
 | 
			
		||||
      svg {
 | 
			
		||||
        color: $oc-white;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    border-radius: 50%;
 | 
			
		||||
    width: 1rem;
 | 
			
		||||
    height: 1rem;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    bottom: -0.25rem;
 | 
			
		||||
    right: -0.25rem;
 | 
			
		||||
    font-size: 0.625rem;
 | 
			
		||||
    pointer-events: none;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .layer-ui__library-message {
 | 
			
		||||
    padding: 2em 4em;
 | 
			
		||||
    padding: 2rem;
 | 
			
		||||
    min-width: 200px;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    .Spinner {
 | 
			
		||||
      margin-bottom: 1em;
 | 
			
		||||
    }
 | 
			
		||||
    flex-grow: 1;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
 | 
			
		||||
    span {
 | 
			
		||||
      font-size: 0.8em;
 | 
			
		||||
    }
 | 
			
		||||
@@ -159,11 +88,10 @@
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .library-menu-browse-button {
 | 
			
		||||
    width: 80%;
 | 
			
		||||
    min-height: 22px;
 | 
			
		||||
    margin: 0 auto;
 | 
			
		||||
    margin-top: 1rem;
 | 
			
		||||
    padding: 10px;
 | 
			
		||||
    margin: 1rem auto;
 | 
			
		||||
 | 
			
		||||
    padding: 0.875rem 1rem;
 | 
			
		||||
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
@@ -176,6 +104,10 @@
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    white-space: nowrap;
 | 
			
		||||
    text-decoration: none !important;
 | 
			
		||||
 | 
			
		||||
    font-weight: 600;
 | 
			
		||||
    font-size: 0.75rem;
 | 
			
		||||
 | 
			
		||||
    &:hover {
 | 
			
		||||
      background-color: var(--color-primary-darker);
 | 
			
		||||
    }
 | 
			
		||||
@@ -184,6 +116,12 @@
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &.theme--dark {
 | 
			
		||||
    .library-menu-browse-button {
 | 
			
		||||
      color: var(--color-gray-100);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .library-menu-browse-button--mobile {
 | 
			
		||||
    min-height: 22px;
 | 
			
		||||
    margin-left: auto;
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,7 @@ import { LibraryItems, LibraryItem, AppState, ExcalidrawProps } from "../types";
 | 
			
		||||
 | 
			
		||||
import "./LibraryMenu.scss";
 | 
			
		||||
import LibraryMenuItems from "./LibraryMenuItems";
 | 
			
		||||
import { EVENT, VERSIONS } from "../constants";
 | 
			
		||||
import { EVENT } from "../constants";
 | 
			
		||||
import { KEYS } from "../keys";
 | 
			
		||||
import { trackEvent } from "../analytics";
 | 
			
		||||
import { useAtom } from "jotai";
 | 
			
		||||
@@ -31,6 +31,7 @@ import { Sidebar } from "./Sidebar/Sidebar";
 | 
			
		||||
import { getSelectedElements } from "../scene";
 | 
			
		||||
import { NonDeletedExcalidrawElement } from "../element/types";
 | 
			
		||||
import { LibraryMenuHeader } from "./LibraryMenuHeaderContent";
 | 
			
		||||
import LibraryMenuBrowseButton from "./LibraryMenuBrowseButton";
 | 
			
		||||
 | 
			
		||||
const useOnClickOutside = (
 | 
			
		||||
  ref: RefObject<HTMLElement>,
 | 
			
		||||
@@ -94,9 +95,6 @@ export const LibraryMenuContent = ({
 | 
			
		||||
  selectedItems: LibraryItem["id"][];
 | 
			
		||||
  onSelectItems: (id: LibraryItem["id"][]) => void;
 | 
			
		||||
}) => {
 | 
			
		||||
  const referrer =
 | 
			
		||||
    libraryReturnUrl || window.location.origin + window.location.pathname;
 | 
			
		||||
 | 
			
		||||
  const [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope);
 | 
			
		||||
 | 
			
		||||
  const addToLibrary = useCallback(
 | 
			
		||||
@@ -131,13 +129,18 @@ export const LibraryMenuContent = ({
 | 
			
		||||
    return (
 | 
			
		||||
      <LibraryMenuWrapper>
 | 
			
		||||
        <div className="layer-ui__library-message">
 | 
			
		||||
          <Spinner size="2em" />
 | 
			
		||||
          <span>{t("labels.libraryLoadingMessage")}</span>
 | 
			
		||||
          <div>
 | 
			
		||||
            <Spinner size="2em" />
 | 
			
		||||
            <span>{t("labels.libraryLoadingMessage")}</span>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </LibraryMenuWrapper>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const showBtn =
 | 
			
		||||
    libraryItemsData.libraryItems.length > 0 || pendingElements.length > 0;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <LibraryMenuWrapper>
 | 
			
		||||
      <LibraryMenuItems
 | 
			
		||||
@@ -150,18 +153,17 @@ export const LibraryMenuContent = ({
 | 
			
		||||
        pendingElements={pendingElements}
 | 
			
		||||
        selectedItems={selectedItems}
 | 
			
		||||
        onSelectItems={onSelectItems}
 | 
			
		||||
        id={id}
 | 
			
		||||
        libraryReturnUrl={libraryReturnUrl}
 | 
			
		||||
        theme={appState.theme}
 | 
			
		||||
      />
 | 
			
		||||
      <a
 | 
			
		||||
        className="library-menu-browse-button"
 | 
			
		||||
        href={`${process.env.REACT_APP_LIBRARY_URL}?target=${
 | 
			
		||||
          window.name || "_blank"
 | 
			
		||||
        }&referrer=${referrer}&useHash=true&token=${id}&theme=${
 | 
			
		||||
          appState.theme
 | 
			
		||||
        }&version=${VERSIONS.excalidrawLibrary}`}
 | 
			
		||||
        target="_excalidraw_libraries"
 | 
			
		||||
      >
 | 
			
		||||
        {t("labels.libraries")}
 | 
			
		||||
      </a>
 | 
			
		||||
      {showBtn && (
 | 
			
		||||
        <LibraryMenuBrowseButton
 | 
			
		||||
          id={id}
 | 
			
		||||
          libraryReturnUrl={libraryReturnUrl}
 | 
			
		||||
          theme={appState.theme}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
    </LibraryMenuWrapper>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@@ -265,6 +267,7 @@ export const LibraryMenu: React.FC<{
 | 
			
		||||
      // is colled correctly
 | 
			
		||||
      key="library"
 | 
			
		||||
      className="layer-ui__library-sidebar"
 | 
			
		||||
      initialDockedState={appState.isSidebarDocked}
 | 
			
		||||
      onDock={(docked) => {
 | 
			
		||||
        trackEvent(
 | 
			
		||||
          "library",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										31
									
								
								src/components/LibraryMenuBrowseButton.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/components/LibraryMenuBrowseButton.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
			
		||||
import { VERSIONS } from "../constants";
 | 
			
		||||
import { t } from "../i18n";
 | 
			
		||||
import { AppState, ExcalidrawProps } from "../types";
 | 
			
		||||
 | 
			
		||||
const LibraryMenuBrowseButton = ({
 | 
			
		||||
  theme,
 | 
			
		||||
  id,
 | 
			
		||||
  libraryReturnUrl,
 | 
			
		||||
}: {
 | 
			
		||||
  libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
 | 
			
		||||
  theme: AppState["theme"];
 | 
			
		||||
  id: string;
 | 
			
		||||
}) => {
 | 
			
		||||
  const referrer =
 | 
			
		||||
    libraryReturnUrl || window.location.origin + window.location.pathname;
 | 
			
		||||
  return (
 | 
			
		||||
    <a
 | 
			
		||||
      className="library-menu-browse-button"
 | 
			
		||||
      href={`${process.env.REACT_APP_LIBRARY_URL}?target=${
 | 
			
		||||
        window.name || "_blank"
 | 
			
		||||
      }&referrer=${referrer}&useHash=true&token=${id}&theme=${theme}&version=${
 | 
			
		||||
        VERSIONS.excalidrawLibrary
 | 
			
		||||
      }`}
 | 
			
		||||
      target="_excalidraw_libraries"
 | 
			
		||||
    >
 | 
			
		||||
      {t("labels.libraries")}
 | 
			
		||||
    </a>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default LibraryMenuBrowseButton;
 | 
			
		||||
@@ -3,9 +3,14 @@ import { saveLibraryAsJSON } from "../data/json";
 | 
			
		||||
import Library, { libraryItemsAtom } from "../data/library";
 | 
			
		||||
import { t } from "../i18n";
 | 
			
		||||
import { AppState, LibraryItem, LibraryItems } from "../types";
 | 
			
		||||
import { exportToFileIcon, load, publishIcon, trash } from "./icons";
 | 
			
		||||
import {
 | 
			
		||||
  DotsIcon,
 | 
			
		||||
  ExportIcon,
 | 
			
		||||
  LoadIcon,
 | 
			
		||||
  publishIcon,
 | 
			
		||||
  TrashIcon,
 | 
			
		||||
} from "./icons";
 | 
			
		||||
import { ToolButton } from "./ToolButton";
 | 
			
		||||
import { Tooltip } from "./Tooltip";
 | 
			
		||||
import { fileOpen } from "../data/filesystem";
 | 
			
		||||
import { muteFSAbortError } from "../utils";
 | 
			
		||||
import { useAtom } from "jotai";
 | 
			
		||||
@@ -13,6 +18,9 @@ import { jotaiScope } from "../jotai";
 | 
			
		||||
import ConfirmDialog from "./ConfirmDialog";
 | 
			
		||||
import PublishLibrary from "./PublishLibrary";
 | 
			
		||||
import { Dialog } from "./Dialog";
 | 
			
		||||
import { useOutsideClickHook } from "../hooks/useOutsideClick";
 | 
			
		||||
import MenuItem from "./MenuItem";
 | 
			
		||||
import { isDropdownOpenAtom } from "./App";
 | 
			
		||||
 | 
			
		||||
const getSelectedItems = (
 | 
			
		||||
  libraryItems: LibraryItems,
 | 
			
		||||
@@ -165,93 +173,84 @@ export const LibraryMenuHeader: React.FC<{
 | 
			
		||||
      });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const [isDropdownOpen, setIsDropdownOpen] = useAtom(isDropdownOpenAtom);
 | 
			
		||||
  const dropdownRef = useOutsideClickHook(() => setIsDropdownOpen(false));
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="library-actions">
 | 
			
		||||
      {showRemoveLibAlert && renderRemoveLibAlert()}
 | 
			
		||||
      {showPublishLibraryDialog && (
 | 
			
		||||
        <PublishLibrary
 | 
			
		||||
          onClose={() => setShowPublishLibraryDialog(false)}
 | 
			
		||||
          libraryItems={getSelectedItems(
 | 
			
		||||
            libraryItemsData.libraryItems,
 | 
			
		||||
            selectedItems,
 | 
			
		||||
    <div style={{ position: "relative" }}>
 | 
			
		||||
      <button
 | 
			
		||||
        type="button"
 | 
			
		||||
        className="Sidebar__dropdown-btn"
 | 
			
		||||
        data-prevent-outside-click
 | 
			
		||||
        onClick={() => setIsDropdownOpen(!isDropdownOpen)}
 | 
			
		||||
      >
 | 
			
		||||
        {DotsIcon}
 | 
			
		||||
      </button>
 | 
			
		||||
 | 
			
		||||
      {selectedItems.length > 0 && (
 | 
			
		||||
        <div className="library-actions-counter">{selectedItems.length}</div>
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      {isDropdownOpen && (
 | 
			
		||||
        <div
 | 
			
		||||
          className="Sidebar__dropdown-content menu-container"
 | 
			
		||||
          ref={dropdownRef}
 | 
			
		||||
        >
 | 
			
		||||
          {!itemsSelected && (
 | 
			
		||||
            <MenuItem
 | 
			
		||||
              label={t("buttons.load")}
 | 
			
		||||
              icon={LoadIcon}
 | 
			
		||||
              dataTestId="lib-dropdown--load"
 | 
			
		||||
              onClick={onLibraryImport}
 | 
			
		||||
            />
 | 
			
		||||
          )}
 | 
			
		||||
          appState={appState}
 | 
			
		||||
          onSuccess={(data) =>
 | 
			
		||||
            onPublishLibSuccess(data, libraryItemsData.libraryItems)
 | 
			
		||||
          }
 | 
			
		||||
          onError={(error) => window.alert(error)}
 | 
			
		||||
          updateItemsInStorage={() =>
 | 
			
		||||
            library.setLibrary(libraryItemsData.libraryItems)
 | 
			
		||||
          }
 | 
			
		||||
          onRemove={(id: string) =>
 | 
			
		||||
            onSelectItems(selectedItems.filter((_id) => _id !== id))
 | 
			
		||||
          }
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
      {publishLibSuccess && renderPublishSuccess()}
 | 
			
		||||
      {!itemsSelected && (
 | 
			
		||||
        <ToolButton
 | 
			
		||||
          key="import"
 | 
			
		||||
          type="button"
 | 
			
		||||
          title={t("buttons.load")}
 | 
			
		||||
          aria-label={t("buttons.load")}
 | 
			
		||||
          icon={load}
 | 
			
		||||
          onClick={onLibraryImport}
 | 
			
		||||
          className="library-actions--load"
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
      {!!items.length && (
 | 
			
		||||
        <>
 | 
			
		||||
          <ToolButton
 | 
			
		||||
            key="export"
 | 
			
		||||
            type="button"
 | 
			
		||||
            title={t("buttons.export")}
 | 
			
		||||
            aria-label={t("buttons.export")}
 | 
			
		||||
            icon={exportToFileIcon}
 | 
			
		||||
            onClick={onLibraryExport}
 | 
			
		||||
            className="library-actions--export"
 | 
			
		||||
          >
 | 
			
		||||
            {selectedItems.length > 0 && (
 | 
			
		||||
              <span className="library-actions-counter">
 | 
			
		||||
                {selectedItems.length}
 | 
			
		||||
              </span>
 | 
			
		||||
            )}
 | 
			
		||||
          </ToolButton>
 | 
			
		||||
          <ToolButton
 | 
			
		||||
            key="reset"
 | 
			
		||||
            type="button"
 | 
			
		||||
            title={resetLabel}
 | 
			
		||||
            aria-label={resetLabel}
 | 
			
		||||
            icon={trash}
 | 
			
		||||
            onClick={() => setShowRemoveLibAlert(true)}
 | 
			
		||||
            className="library-actions--remove"
 | 
			
		||||
          >
 | 
			
		||||
            {selectedItems.length > 0 && (
 | 
			
		||||
              <span className="library-actions-counter">
 | 
			
		||||
                {selectedItems.length}
 | 
			
		||||
              </span>
 | 
			
		||||
            )}
 | 
			
		||||
          </ToolButton>
 | 
			
		||||
        </>
 | 
			
		||||
      )}
 | 
			
		||||
      {itemsSelected && (
 | 
			
		||||
        <Tooltip label={t("hints.publishLibrary")}>
 | 
			
		||||
          <ToolButton
 | 
			
		||||
            type="button"
 | 
			
		||||
            aria-label={t("buttons.publishLibrary")}
 | 
			
		||||
            label={t("buttons.publishLibrary")}
 | 
			
		||||
            icon={publishIcon}
 | 
			
		||||
            className="library-actions--publish"
 | 
			
		||||
            onClick={() => setShowPublishLibraryDialog(true)}
 | 
			
		||||
          >
 | 
			
		||||
            <label>{t("buttons.publishLibrary")}</label>
 | 
			
		||||
            {selectedItems.length > 0 && (
 | 
			
		||||
              <span className="library-actions-counter">
 | 
			
		||||
                {selectedItems.length}
 | 
			
		||||
              </span>
 | 
			
		||||
            )}
 | 
			
		||||
          </ToolButton>
 | 
			
		||||
        </Tooltip>
 | 
			
		||||
          {showRemoveLibAlert && renderRemoveLibAlert()}
 | 
			
		||||
          {showPublishLibraryDialog && (
 | 
			
		||||
            <PublishLibrary
 | 
			
		||||
              onClose={() => setShowPublishLibraryDialog(false)}
 | 
			
		||||
              libraryItems={getSelectedItems(
 | 
			
		||||
                libraryItemsData.libraryItems,
 | 
			
		||||
                selectedItems,
 | 
			
		||||
              )}
 | 
			
		||||
              appState={appState}
 | 
			
		||||
              onSuccess={(data) =>
 | 
			
		||||
                onPublishLibSuccess(data, libraryItemsData.libraryItems)
 | 
			
		||||
              }
 | 
			
		||||
              onError={(error) => window.alert(error)}
 | 
			
		||||
              updateItemsInStorage={() =>
 | 
			
		||||
                library.setLibrary(libraryItemsData.libraryItems)
 | 
			
		||||
              }
 | 
			
		||||
              onRemove={(id: string) =>
 | 
			
		||||
                onSelectItems(selectedItems.filter((_id) => _id !== id))
 | 
			
		||||
              }
 | 
			
		||||
            />
 | 
			
		||||
          )}
 | 
			
		||||
          {publishLibSuccess && renderPublishSuccess()}
 | 
			
		||||
          {!!items.length && (
 | 
			
		||||
            <>
 | 
			
		||||
              <MenuItem
 | 
			
		||||
                label={t("buttons.export")}
 | 
			
		||||
                icon={ExportIcon}
 | 
			
		||||
                onClick={onLibraryExport}
 | 
			
		||||
                dataTestId="lib-dropdown--export"
 | 
			
		||||
              />
 | 
			
		||||
              <MenuItem
 | 
			
		||||
                label={resetLabel}
 | 
			
		||||
                icon={TrashIcon}
 | 
			
		||||
                onClick={() => setShowRemoveLibAlert(true)}
 | 
			
		||||
                dataTestId="lib-dropdown--remove"
 | 
			
		||||
              />
 | 
			
		||||
            </>
 | 
			
		||||
          )}
 | 
			
		||||
          {itemsSelected && (
 | 
			
		||||
            <MenuItem
 | 
			
		||||
              label={t("buttons.publishLibrary")}
 | 
			
		||||
              icon={publishIcon}
 | 
			
		||||
              dataTestId="lib-dropdown--publish"
 | 
			
		||||
              onClick={() => setShowPublishLibraryDialog(true)}
 | 
			
		||||
            />
 | 
			
		||||
          )}
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
 
 | 
			
		||||
@@ -1,18 +1,70 @@
 | 
			
		||||
@import "open-color/open-color";
 | 
			
		||||
 | 
			
		||||
.excalidraw {
 | 
			
		||||
  --container-padding-y: 1.5rem;
 | 
			
		||||
  --container-padding-x: 0.75rem;
 | 
			
		||||
 | 
			
		||||
  .library-menu-items__no-items {
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    color: var(--color-gray-70);
 | 
			
		||||
    line-height: 1.5;
 | 
			
		||||
    font-size: 0.875rem;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
 | 
			
		||||
    &__label {
 | 
			
		||||
      color: var(--color-primary);
 | 
			
		||||
      font-weight: bold;
 | 
			
		||||
      font-size: 1.125rem;
 | 
			
		||||
      margin-bottom: 0.75rem;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &.theme--dark {
 | 
			
		||||
    .library-menu-items__no-items {
 | 
			
		||||
      color: var(--color-gray-40);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .library-menu-items-container {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-grow: 1;
 | 
			
		||||
    flex-shrink: 1;
 | 
			
		||||
    flex-basis: 0;
 | 
			
		||||
    overflow-y: auto;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    border-bottom: 1px solid var(--sidebar-border-color);
 | 
			
		||||
 | 
			
		||||
    position: relative;
 | 
			
		||||
 | 
			
		||||
    &__row {
 | 
			
		||||
      display: grid;
 | 
			
		||||
      grid-template-columns: repeat(4, 1fr);
 | 
			
		||||
      gap: 1rem;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &__items {
 | 
			
		||||
      row-gap: 0.5rem;
 | 
			
		||||
      padding: var(--container-padding-y) var(--container-padding-x);
 | 
			
		||||
      flex: 1;
 | 
			
		||||
      overflow-y: auto;
 | 
			
		||||
      overflow-x: hidden;
 | 
			
		||||
      margin-bottom: 1rem;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &__header {
 | 
			
		||||
      color: var(--color-primary);
 | 
			
		||||
      font-size: 1.125rem;
 | 
			
		||||
      font-weight: bold;
 | 
			
		||||
      margin-bottom: 0.75rem;
 | 
			
		||||
 | 
			
		||||
      &--excal {
 | 
			
		||||
        margin-top: 2.5rem;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .separator {
 | 
			
		||||
      width: 100%;
 | 
			
		||||
      display: flex;
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@ import React, { useState } from "react";
 | 
			
		||||
import { serializeLibraryAsJSON } from "../data/json";
 | 
			
		||||
import { ExcalidrawElement, NonDeleted } from "../element/types";
 | 
			
		||||
import { t } from "../i18n";
 | 
			
		||||
import { LibraryItem, LibraryItems } from "../types";
 | 
			
		||||
import { AppState, ExcalidrawProps, LibraryItem, LibraryItems } from "../types";
 | 
			
		||||
import { arrayToMap, chunk } from "../utils";
 | 
			
		||||
import { LibraryUnit } from "./LibraryUnit";
 | 
			
		||||
import Stack from "./Stack";
 | 
			
		||||
@@ -10,6 +10,8 @@ import Stack from "./Stack";
 | 
			
		||||
import "./LibraryMenuItems.scss";
 | 
			
		||||
import { MIME_TYPES } from "../constants";
 | 
			
		||||
import Spinner from "./Spinner";
 | 
			
		||||
import LibraryMenuBrowseButton from "./LibraryMenuBrowseButton";
 | 
			
		||||
import clsx from "clsx";
 | 
			
		||||
 | 
			
		||||
const CELLS_PER_ROW = 4;
 | 
			
		||||
 | 
			
		||||
@@ -21,6 +23,9 @@ const LibraryMenuItems = ({
 | 
			
		||||
  pendingElements,
 | 
			
		||||
  selectedItems,
 | 
			
		||||
  onSelectItems,
 | 
			
		||||
  theme,
 | 
			
		||||
  id,
 | 
			
		||||
  libraryReturnUrl,
 | 
			
		||||
}: {
 | 
			
		||||
  isLoading: boolean;
 | 
			
		||||
  libraryItems: LibraryItems;
 | 
			
		||||
@@ -29,6 +34,9 @@ const LibraryMenuItems = ({
 | 
			
		||||
  onAddToLibrary: (elements: LibraryItem["elements"]) => void;
 | 
			
		||||
  selectedItems: LibraryItem["id"][];
 | 
			
		||||
  onSelectItems: (id: LibraryItem["id"][]) => void;
 | 
			
		||||
  libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
 | 
			
		||||
  theme: AppState["theme"];
 | 
			
		||||
  id: string;
 | 
			
		||||
}) => {
 | 
			
		||||
  const [lastSelectedItem, setLastSelectedItem] = useState<
 | 
			
		||||
    LibraryItem["id"] | null
 | 
			
		||||
@@ -167,7 +175,11 @@ const LibraryMenuItems = ({
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      return (
 | 
			
		||||
        <Stack.Row align="center" gap={1} key={index}>
 | 
			
		||||
        <Stack.Row
 | 
			
		||||
          align="center"
 | 
			
		||||
          key={index}
 | 
			
		||||
          className="library-menu-items-container__row"
 | 
			
		||||
        >
 | 
			
		||||
          {rowItems}
 | 
			
		||||
        </Stack.Row>
 | 
			
		||||
      );
 | 
			
		||||
@@ -181,19 +193,21 @@ const LibraryMenuItems = ({
 | 
			
		||||
    (item) => item.status === "published",
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const showBtn =
 | 
			
		||||
    !libraryItems.length &&
 | 
			
		||||
    !unpublishedItems.length &&
 | 
			
		||||
    !publishedItems.length &&
 | 
			
		||||
    !pendingElements.length;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      className="library-menu-items-container"
 | 
			
		||||
      style={
 | 
			
		||||
        publishedItems.length || unpublishedItems.length
 | 
			
		||||
          ? {
 | 
			
		||||
              flex: "1 1 0",
 | 
			
		||||
              overflowY: "auto",
 | 
			
		||||
            }
 | 
			
		||||
          : {
 | 
			
		||||
              marginBottom: "2rem",
 | 
			
		||||
              flex: 0,
 | 
			
		||||
            }
 | 
			
		||||
        pendingElements.length ||
 | 
			
		||||
        unpublishedItems.length ||
 | 
			
		||||
        publishedItems.length
 | 
			
		||||
          ? { justifyContent: "flex-start" }
 | 
			
		||||
          : {}
 | 
			
		||||
      }
 | 
			
		||||
    >
 | 
			
		||||
      <Stack.Col
 | 
			
		||||
@@ -206,49 +220,37 @@ const LibraryMenuItems = ({
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <>
 | 
			
		||||
          <div className="separator">
 | 
			
		||||
          <div>
 | 
			
		||||
            {(pendingElements.length > 0 ||
 | 
			
		||||
              unpublishedItems.length > 0 ||
 | 
			
		||||
              publishedItems.length > 0) && (
 | 
			
		||||
              <div>{t("labels.personalLib")}</div>
 | 
			
		||||
              <div className="library-menu-items-container__header">
 | 
			
		||||
                {t("labels.personalLib")}
 | 
			
		||||
              </div>
 | 
			
		||||
            )}
 | 
			
		||||
            {isLoading && (
 | 
			
		||||
              <div
 | 
			
		||||
                style={{
 | 
			
		||||
                  marginLeft: "auto",
 | 
			
		||||
                  marginRight: "1rem",
 | 
			
		||||
                  display: "flex",
 | 
			
		||||
                  alignItems: "center",
 | 
			
		||||
                  fontWeight: "normal",
 | 
			
		||||
                  position: "absolute",
 | 
			
		||||
                  top: "var(--container-padding-y)",
 | 
			
		||||
                  right: "var(--container-padding-x)",
 | 
			
		||||
                  transform: "translateY(50%)",
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                <div style={{ transform: "translateY(2px)" }}>
 | 
			
		||||
                  <Spinner />
 | 
			
		||||
                </div>
 | 
			
		||||
                <Spinner />
 | 
			
		||||
              </div>
 | 
			
		||||
            )}
 | 
			
		||||
          </div>
 | 
			
		||||
          {!pendingElements.length && !unpublishedItems.length ? (
 | 
			
		||||
            <div
 | 
			
		||||
              style={{
 | 
			
		||||
                height: 65,
 | 
			
		||||
                display: "flex",
 | 
			
		||||
                flexDirection: "column",
 | 
			
		||||
                alignItems: "center",
 | 
			
		||||
                justifyContent: "center",
 | 
			
		||||
                width: "100%",
 | 
			
		||||
                fontSize: ".9rem",
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              {t("library.noItems")}
 | 
			
		||||
            <div className="library-menu-items__no-items">
 | 
			
		||||
              <div
 | 
			
		||||
                style={{
 | 
			
		||||
                  margin: ".6rem 0",
 | 
			
		||||
                  fontSize: ".8em",
 | 
			
		||||
                  width: "70%",
 | 
			
		||||
                  textAlign: "center",
 | 
			
		||||
                }}
 | 
			
		||||
                className={clsx({
 | 
			
		||||
                  "library-menu-items__no-items__label": showBtn,
 | 
			
		||||
                })}
 | 
			
		||||
              >
 | 
			
		||||
                {t("library.noItems")}
 | 
			
		||||
              </div>
 | 
			
		||||
              <div className="library-menu-items__no-items__hint">
 | 
			
		||||
                {publishedItems.length > 0
 | 
			
		||||
                  ? t("library.hint_emptyPrivateLibrary")
 | 
			
		||||
                  : t("library.hint_emptyLibrary")}
 | 
			
		||||
@@ -269,7 +271,9 @@ const LibraryMenuItems = ({
 | 
			
		||||
          {(publishedItems.length > 0 ||
 | 
			
		||||
            pendingElements.length > 0 ||
 | 
			
		||||
            unpublishedItems.length > 0) && (
 | 
			
		||||
            <div className="separator">{t("labels.excalidrawLib")}</div>
 | 
			
		||||
            <div className="library-menu-items-container__header library-menu-items-container__header--excal">
 | 
			
		||||
              {t("labels.excalidrawLib")}
 | 
			
		||||
            </div>
 | 
			
		||||
          )}
 | 
			
		||||
          {publishedItems.length > 0 ? (
 | 
			
		||||
            renderLibrarySection(publishedItems)
 | 
			
		||||
@@ -289,6 +293,14 @@ const LibraryMenuItems = ({
 | 
			
		||||
            </div>
 | 
			
		||||
          ) : null}
 | 
			
		||||
        </>
 | 
			
		||||
 | 
			
		||||
        {showBtn && (
 | 
			
		||||
          <LibraryMenuBrowseButton
 | 
			
		||||
            id={id}
 | 
			
		||||
            libraryReturnUrl={libraryReturnUrl}
 | 
			
		||||
            theme={theme}
 | 
			
		||||
          />
 | 
			
		||||
        )}
 | 
			
		||||
      </Stack.Col>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
 
 | 
			
		||||
@@ -7,17 +7,18 @@
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    width: 63px;
 | 
			
		||||
    height: 63px; // match width
 | 
			
		||||
    width: 55px;
 | 
			
		||||
    height: 55px;
 | 
			
		||||
    box-sizing: border-box;
 | 
			
		||||
    border-radius: var(--border-radius-lg);
 | 
			
		||||
 | 
			
		||||
    &--hover {
 | 
			
		||||
      box-shadow: inset 0px 0px 0px 2px $oc-blue-5;
 | 
			
		||||
      border-color: $oc-blue-5;
 | 
			
		||||
      border-color: var(--color-primary);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &--selected {
 | 
			
		||||
      box-shadow: inset 0px 0px 0px 2px $oc-blue-8;
 | 
			
		||||
      border-color: $oc-blue-8;
 | 
			
		||||
      border-color: var(--color-primary);
 | 
			
		||||
      border-width: 1px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -59,20 +60,34 @@
 | 
			
		||||
 | 
			
		||||
  .library-unit__checkbox {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    left: 2.3rem;
 | 
			
		||||
    bottom: 2.3rem;
 | 
			
		||||
    top: 0.125rem;
 | 
			
		||||
    right: 0.125rem;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
 | 
			
		||||
    .Checkbox-box {
 | 
			
		||||
      width: 13px;
 | 
			
		||||
      height: 13px;
 | 
			
		||||
      border-radius: 2px;
 | 
			
		||||
      margin: 0.5em 0.5em 0.2em 0.2em;
 | 
			
		||||
      background-color: $oc-blue-1;
 | 
			
		||||
      margin: 0;
 | 
			
		||||
      width: 1rem;
 | 
			
		||||
      height: 1rem;
 | 
			
		||||
      border-radius: 4px;
 | 
			
		||||
      background-color: var(--color-primary-light);
 | 
			
		||||
      border: 1px solid var(--color-primary);
 | 
			
		||||
      box-shadow: none !important;
 | 
			
		||||
      padding: 2px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.Checkbox:hover {
 | 
			
		||||
      .Checkbox-box {
 | 
			
		||||
        background-color: $oc-blue-2;
 | 
			
		||||
        background-color: var(--color-primary-light);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.is-checked {
 | 
			
		||||
      .Checkbox-box {
 | 
			
		||||
        background-color: var(--color-primary) !important;
 | 
			
		||||
 | 
			
		||||
        svg {
 | 
			
		||||
          color: var(--color-primary-light);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@@ -85,25 +100,29 @@
 | 
			
		||||
  .library-unit__adder {
 | 
			
		||||
    transform: scale(1);
 | 
			
		||||
    animation: library-unit__adder-animation 1s ease-in infinite;
 | 
			
		||||
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    width: 1.5rem;
 | 
			
		||||
    height: 1.5rem;
 | 
			
		||||
    background-color: var(--color-primary);
 | 
			
		||||
    border-radius: var(--border-radius-md);
 | 
			
		||||
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
 | 
			
		||||
    pointer-events: none;
 | 
			
		||||
 | 
			
		||||
    svg {
 | 
			
		||||
      color: var(--color-primary-light);
 | 
			
		||||
      width: 1rem;
 | 
			
		||||
      height: 1rem;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .library-unit__adder {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    left: 40%;
 | 
			
		||||
    top: 40%;
 | 
			
		||||
    width: 2rem;
 | 
			
		||||
    height: 2rem;
 | 
			
		||||
    margin-left: -10px;
 | 
			
		||||
    margin-top: -10px;
 | 
			
		||||
    pointer-events: none;
 | 
			
		||||
  }
 | 
			
		||||
  .library-unit:hover .library-unit__adder {
 | 
			
		||||
    fill: $oc-blue-7;
 | 
			
		||||
  }
 | 
			
		||||
  .library-unit:active .library-unit__adder {
 | 
			
		||||
    animation: none;
 | 
			
		||||
    transform: scale(0.8);
 | 
			
		||||
    fill: $oc-black;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .library-unit__active {
 | 
			
		||||
 
 | 
			
		||||
@@ -6,19 +6,7 @@ import { exportToSvg } from "../scene/export";
 | 
			
		||||
import { LibraryItem } from "../types";
 | 
			
		||||
import "./LibraryUnit.scss";
 | 
			
		||||
import { CheckboxItem } from "./CheckboxItem";
 | 
			
		||||
 | 
			
		||||
const PLUS_ICON = (
 | 
			
		||||
  <svg viewBox="0 0 1792 1792">
 | 
			
		||||
    <path
 | 
			
		||||
      d="M1600 736v192c0 26.667-9.33 49.333-28 68-18.67 18.67-41.33 28-68 28h-416v416c0 26.67-9.33 49.33-28 68s-41.33 28-68 28H800c-26.667 0-49.333-9.33-68-28s-28-41.33-28-68v-416H288c-26.667 0-49.333-9.33-68-28-18.667-18.667-28-41.333-28-68V736c0-26.667 9.333-49.333 28-68s41.333-28 68-28h416V224c0-26.667 9.333-49.333 28-68s41.333-28 68-28h192c26.67 0 49.33 9.333 68 28s28 41.333 28 68v416h416c26.67 0 49.33 9.333 68 28s28 41.333 28 68Z"
 | 
			
		||||
      style={{
 | 
			
		||||
        stroke: "#fff",
 | 
			
		||||
        strokeWidth: 140,
 | 
			
		||||
      }}
 | 
			
		||||
      transform="translate(0 64)"
 | 
			
		||||
    />
 | 
			
		||||
  </svg>
 | 
			
		||||
);
 | 
			
		||||
import { PlusIcon } from "./icons";
 | 
			
		||||
 | 
			
		||||
export const LibraryUnit = ({
 | 
			
		||||
  id,
 | 
			
		||||
@@ -67,7 +55,7 @@ export const LibraryUnit = ({
 | 
			
		||||
  const [isHovered, setIsHovered] = useState(false);
 | 
			
		||||
  const isMobile = useDevice().isMobile;
 | 
			
		||||
  const adder = isPending && (
 | 
			
		||||
    <div className="library-unit__adder">{PLUS_ICON}</div>
 | 
			
		||||
    <div className="library-unit__adder">{PlusIcon}</div>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
import "./ToolIcon.scss";
 | 
			
		||||
 | 
			
		||||
import React from "react";
 | 
			
		||||
import clsx from "clsx";
 | 
			
		||||
import { ToolButtonSize } from "./ToolButton";
 | 
			
		||||
import { LockedIcon, UnlockedIcon } from "./icons";
 | 
			
		||||
 | 
			
		||||
type LockIconProps = {
 | 
			
		||||
  title?: string;
 | 
			
		||||
@@ -16,34 +16,15 @@ type LockIconProps = {
 | 
			
		||||
const DEFAULT_SIZE: ToolButtonSize = "medium";
 | 
			
		||||
 | 
			
		||||
const ICONS = {
 | 
			
		||||
  CHECKED: (
 | 
			
		||||
    <svg
 | 
			
		||||
      width="1792"
 | 
			
		||||
      height="1792"
 | 
			
		||||
      viewBox="0 0 1792 1792"
 | 
			
		||||
      xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
    >
 | 
			
		||||
      <path d="M640 768h512v-192q0-106-75-181t-181-75-181 75-75 181v192zm832 96v576q0 40-28 68t-68 28h-960q-40 0-68-28t-28-68v-576q0-40 28-68t68-28h32v-192q0-184 132-316t316-132 316 132 132 316v192h32q40 0 68 28t28 68z" />
 | 
			
		||||
    </svg>
 | 
			
		||||
  ),
 | 
			
		||||
  UNCHECKED: (
 | 
			
		||||
    <svg
 | 
			
		||||
      width="1792"
 | 
			
		||||
      height="1792"
 | 
			
		||||
      viewBox="0 0 1792 1792"
 | 
			
		||||
      xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
      className="unlocked-icon rtl-mirror"
 | 
			
		||||
    >
 | 
			
		||||
      <path d="M1728 576v256q0 26-19 45t-45 19h-64q-26 0-45-19t-19-45v-256q0-106-75-181t-181-75-181 75-75 181v192h96q40 0 68 28t28 68v576q0 40-28 68t-68 28h-960q-40 0-68-28t-28-68v-576q0-40 28-68t68-28h672v-192q0-185 131.5-316.5t316.5-131.5 316.5 131.5 131.5 316.5z" />
 | 
			
		||||
    </svg>
 | 
			
		||||
  ),
 | 
			
		||||
  CHECKED: LockedIcon,
 | 
			
		||||
  UNCHECKED: UnlockedIcon,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const LockButton = (props: LockIconProps) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <label
 | 
			
		||||
      className={clsx(
 | 
			
		||||
        "ToolIcon ToolIcon__lock ToolIcon_type_floating",
 | 
			
		||||
        "ToolIcon ToolIcon__lock",
 | 
			
		||||
        `ToolIcon_size_${DEFAULT_SIZE}`,
 | 
			
		||||
        {
 | 
			
		||||
          "is-mobile": props.isMobile,
 | 
			
		||||
@@ -58,6 +39,7 @@ export const LockButton = (props: LockIconProps) => {
 | 
			
		||||
        onChange={props.onChange}
 | 
			
		||||
        checked={props.checked}
 | 
			
		||||
        aria-label={props.title}
 | 
			
		||||
        data-testid="toolbar-lock"
 | 
			
		||||
      />
 | 
			
		||||
      <div className="ToolIcon__icon">
 | 
			
		||||
        {props.checked ? ICONS.CHECKED : ICONS.UNCHECKED}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										85
									
								
								src/components/Menu.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								src/components/Menu.scss
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,85 @@
 | 
			
		||||
@import "../css/variables.module";
 | 
			
		||||
 | 
			
		||||
.excalidraw {
 | 
			
		||||
  .menu-container {
 | 
			
		||||
    background-color: #fff !important;
 | 
			
		||||
    max-height: calc(100vh - 150px);
 | 
			
		||||
    overflow-y: auto;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .menu-button {
 | 
			
		||||
    @include outlineButtonStyles;
 | 
			
		||||
    background-color: var(--island-bg-color);
 | 
			
		||||
    width: var(--lg-button-size);
 | 
			
		||||
    height: var(--lg-button-size);
 | 
			
		||||
 | 
			
		||||
    svg {
 | 
			
		||||
      width: var(--lg-icon-size);
 | 
			
		||||
      height: var(--lg-icon-size);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .menu-item {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    background-color: transparent;
 | 
			
		||||
    border: 0;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    padding: 0 0.625rem;
 | 
			
		||||
    height: 2rem;
 | 
			
		||||
    column-gap: 0.625rem;
 | 
			
		||||
    font-size: 0.875rem;
 | 
			
		||||
    color: var(--color-gray-100);
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    border-radius: var(--border-radius-md);
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    box-sizing: border-box;
 | 
			
		||||
    font-weight: normal;
 | 
			
		||||
    font-family: inherit;
 | 
			
		||||
 | 
			
		||||
    @media screen and (min-width: 1921px) {
 | 
			
		||||
      height: 2.25rem;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &__text {
 | 
			
		||||
      text-overflow: ellipsis;
 | 
			
		||||
      overflow: hidden;
 | 
			
		||||
      white-space: nowrap;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &__shortcut {
 | 
			
		||||
      margin-inline-start: auto;
 | 
			
		||||
      opacity: 0.5;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &:hover {
 | 
			
		||||
      background-color: var(--button-hover);
 | 
			
		||||
      text-decoration: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    svg {
 | 
			
		||||
      width: 1rem;
 | 
			
		||||
      height: 1rem;
 | 
			
		||||
      display: block;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.active-collab {
 | 
			
		||||
      background-color: #ecfdf5;
 | 
			
		||||
      color: #064e3c;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &.theme--dark {
 | 
			
		||||
    .menu-item {
 | 
			
		||||
      color: var(--color-gray-40);
 | 
			
		||||
 | 
			
		||||
      &.active-collab {
 | 
			
		||||
        background-color: #064e3c;
 | 
			
		||||
        color: #ecfdf5;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .menu-container {
 | 
			
		||||
      background-color: var(--color-gray-90) !important;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										37
									
								
								src/components/MenuItem.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/components/MenuItem.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
			
		||||
import clsx from "clsx";
 | 
			
		||||
import "./Menu.scss";
 | 
			
		||||
 | 
			
		||||
interface MenuProps {
 | 
			
		||||
  icon: JSX.Element;
 | 
			
		||||
  onClick: () => void;
 | 
			
		||||
  label: string;
 | 
			
		||||
  dataTestId: string;
 | 
			
		||||
  shortcut?: string;
 | 
			
		||||
  isCollaborating?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const MenuItem = ({
 | 
			
		||||
  icon,
 | 
			
		||||
  onClick,
 | 
			
		||||
  label,
 | 
			
		||||
  dataTestId,
 | 
			
		||||
  shortcut,
 | 
			
		||||
  isCollaborating,
 | 
			
		||||
}: MenuProps) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <button
 | 
			
		||||
      className={clsx("menu-item", { "active-collab": isCollaborating })}
 | 
			
		||||
      aria-label={label}
 | 
			
		||||
      onClick={onClick}
 | 
			
		||||
      data-testid={dataTestId}
 | 
			
		||||
      title={label}
 | 
			
		||||
      type="button"
 | 
			
		||||
    >
 | 
			
		||||
      <div className="menu-item__icon">{icon}</div>
 | 
			
		||||
      <div className="menu-item__text">{label}</div>
 | 
			
		||||
      {shortcut && <div className="menu-item__shortcut">{shortcut}</div>}
 | 
			
		||||
    </button>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default MenuItem;
 | 
			
		||||
							
								
								
									
										53
									
								
								src/components/MenuUtils.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								src/components/MenuUtils.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,53 @@
 | 
			
		||||
import { GithubIcon, DiscordIcon, PlusPromoIcon, TwitterIcon } from "./icons";
 | 
			
		||||
 | 
			
		||||
export const MenuLinks = () => (
 | 
			
		||||
  <>
 | 
			
		||||
    <a
 | 
			
		||||
      href="https://plus.excalidraw.com/plus?utm_source=excalidraw&utm_medium=app&utm_content=hamburger"
 | 
			
		||||
      target="_blank"
 | 
			
		||||
      rel="noreferrer"
 | 
			
		||||
      className="menu-item"
 | 
			
		||||
      style={{ color: "var(--color-promo)" }}
 | 
			
		||||
    >
 | 
			
		||||
      <div className="menu-item__icon">{PlusPromoIcon}</div>
 | 
			
		||||
      <div className="menu-item__text">Excalidraw+</div>
 | 
			
		||||
    </a>
 | 
			
		||||
    <a
 | 
			
		||||
      className="menu-item"
 | 
			
		||||
      href="https://github.com/excalidraw/excalidraw"
 | 
			
		||||
      target="_blank"
 | 
			
		||||
      rel="noopener noreferrer"
 | 
			
		||||
    >
 | 
			
		||||
      <div className="menu-item__icon">{GithubIcon}</div>
 | 
			
		||||
      <div className="menu-item__text">GitHub</div>
 | 
			
		||||
    </a>
 | 
			
		||||
    <a
 | 
			
		||||
      className="menu-item"
 | 
			
		||||
      target="_blank"
 | 
			
		||||
      href="https://discord.gg/UexuTaE"
 | 
			
		||||
      rel="noopener noreferrer"
 | 
			
		||||
    >
 | 
			
		||||
      <div className="menu-item__icon">{DiscordIcon}</div>
 | 
			
		||||
      <div className="menu-item__text">Discord</div>
 | 
			
		||||
    </a>
 | 
			
		||||
    <a
 | 
			
		||||
      className="menu-item"
 | 
			
		||||
      target="_blank"
 | 
			
		||||
      href="https://twitter.com/excalidraw"
 | 
			
		||||
      rel="noopener noreferrer"
 | 
			
		||||
    >
 | 
			
		||||
      <div className="menu-item__icon">{TwitterIcon}</div>
 | 
			
		||||
      <div className="menu-item__text">Twitter</div>
 | 
			
		||||
    </a>
 | 
			
		||||
  </>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export const Separator = () => (
 | 
			
		||||
  <div
 | 
			
		||||
    style={{
 | 
			
		||||
      height: "1px",
 | 
			
		||||
      backgroundColor: "var(--default-border-color)",
 | 
			
		||||
      margin: ".5rem 0",
 | 
			
		||||
    }}
 | 
			
		||||
  />
 | 
			
		||||
);
 | 
			
		||||
@@ -8,18 +8,21 @@ import { NonDeletedExcalidrawElement } from "../element/types";
 | 
			
		||||
import { FixedSideContainer } from "./FixedSideContainer";
 | 
			
		||||
import { Island } from "./Island";
 | 
			
		||||
import { HintViewer } from "./HintViewer";
 | 
			
		||||
import { calculateScrollCenter, getSelectedElements } from "../scene";
 | 
			
		||||
import { calculateScrollCenter } from "../scene";
 | 
			
		||||
import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
 | 
			
		||||
import { Section } from "./Section";
 | 
			
		||||
import CollabButton from "./CollabButton";
 | 
			
		||||
import { SCROLLBAR_WIDTH, SCROLLBAR_MARGIN } from "../scene/scrollbars";
 | 
			
		||||
import { LockButton } from "./LockButton";
 | 
			
		||||
import { UserList } from "./UserList";
 | 
			
		||||
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
 | 
			
		||||
import { LibraryButton } from "./LibraryButton";
 | 
			
		||||
import { PenModeButton } from "./PenModeButton";
 | 
			
		||||
import { Stats } from "./Stats";
 | 
			
		||||
import { actionToggleStats } from "../actions";
 | 
			
		||||
import { MenuLinks, Separator } from "./MenuUtils";
 | 
			
		||||
import WelcomeScreen from "./WelcomeScreen";
 | 
			
		||||
import MenuItem from "./MenuItem";
 | 
			
		||||
import { ExportImageIcon } from "./icons";
 | 
			
		||||
 | 
			
		||||
type MobileMenuProps = {
 | 
			
		||||
  appState: AppState;
 | 
			
		||||
@@ -45,6 +48,7 @@ type MobileMenuProps = {
 | 
			
		||||
  renderCustomStats?: ExcalidrawProps["renderCustomStats"];
 | 
			
		||||
  renderSidebars: () => JSX.Element | null;
 | 
			
		||||
  device: Device;
 | 
			
		||||
  renderWelcomeScreen?: boolean;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const MobileMenu = ({
 | 
			
		||||
@@ -65,17 +69,35 @@ export const MobileMenu = ({
 | 
			
		||||
  renderCustomStats,
 | 
			
		||||
  renderSidebars,
 | 
			
		||||
  device,
 | 
			
		||||
  renderWelcomeScreen,
 | 
			
		||||
}: MobileMenuProps) => {
 | 
			
		||||
  const renderToolbar = () => {
 | 
			
		||||
    return (
 | 
			
		||||
      <FixedSideContainer side="top" className="App-top-bar">
 | 
			
		||||
        {renderWelcomeScreen && !appState.isLoading && (
 | 
			
		||||
          <WelcomeScreen appState={appState} actionManager={actionManager} />
 | 
			
		||||
        )}
 | 
			
		||||
        <Section heading="shapes">
 | 
			
		||||
          {(heading: React.ReactNode) => (
 | 
			
		||||
            <Stack.Col gap={4} align="center">
 | 
			
		||||
              <Stack.Row gap={1} className="App-toolbar-container">
 | 
			
		||||
                <Island padding={1} className="App-toolbar">
 | 
			
		||||
                <Island padding={1} className="App-toolbar App-toolbar--mobile">
 | 
			
		||||
                  {heading}
 | 
			
		||||
                  <Stack.Row gap={1}>
 | 
			
		||||
                    {/* <PenModeButton
 | 
			
		||||
                      checked={appState.penMode}
 | 
			
		||||
                      onChange={onPenModeToggle}
 | 
			
		||||
                      title={t("toolBar.penMode")}
 | 
			
		||||
                      isMobile
 | 
			
		||||
                      penDetected={appState.penDetected}
 | 
			
		||||
                    />
 | 
			
		||||
                    <LockButton
 | 
			
		||||
                      checked={appState.activeTool.locked}
 | 
			
		||||
                      onChange={onLockToggle}
 | 
			
		||||
                      title={t("toolBar.lock")}
 | 
			
		||||
                      isMobile
 | 
			
		||||
                    />
 | 
			
		||||
                    <div className="App-toolbar__divider"></div> */}
 | 
			
		||||
                    <ShapesSwitcher
 | 
			
		||||
                      appState={appState}
 | 
			
		||||
                      canvas={canvas}
 | 
			
		||||
@@ -89,25 +111,31 @@ export const MobileMenu = ({
 | 
			
		||||
                    />
 | 
			
		||||
                  </Stack.Row>
 | 
			
		||||
                </Island>
 | 
			
		||||
                {renderTopRightUI && renderTopRightUI(true, appState)}
 | 
			
		||||
                <LockButton
 | 
			
		||||
                  checked={appState.activeTool.locked}
 | 
			
		||||
                  onChange={onLockToggle}
 | 
			
		||||
                  title={t("toolBar.lock")}
 | 
			
		||||
                  isMobile
 | 
			
		||||
                />
 | 
			
		||||
                <LibraryButton
 | 
			
		||||
                  appState={appState}
 | 
			
		||||
                  setAppState={setAppState}
 | 
			
		||||
                  isMobile
 | 
			
		||||
                />
 | 
			
		||||
                <PenModeButton
 | 
			
		||||
                  checked={appState.penMode}
 | 
			
		||||
                  onChange={onPenModeToggle}
 | 
			
		||||
                  title={t("toolBar.penMode")}
 | 
			
		||||
                  isMobile
 | 
			
		||||
                  penDetected={appState.penDetected}
 | 
			
		||||
                />
 | 
			
		||||
                <div className="mobile-misc-tools-container">
 | 
			
		||||
                  {!appState.viewModeEnabled &&
 | 
			
		||||
                    renderTopRightUI?.(true, appState)}
 | 
			
		||||
                  <PenModeButton
 | 
			
		||||
                    checked={appState.penMode}
 | 
			
		||||
                    onChange={onPenModeToggle}
 | 
			
		||||
                    title={t("toolBar.penMode")}
 | 
			
		||||
                    isMobile
 | 
			
		||||
                    penDetected={appState.penDetected}
 | 
			
		||||
                    // penDetected={true}
 | 
			
		||||
                  />
 | 
			
		||||
                  <LockButton
 | 
			
		||||
                    checked={appState.activeTool.locked}
 | 
			
		||||
                    onChange={onLockToggle}
 | 
			
		||||
                    title={t("toolBar.lock")}
 | 
			
		||||
                    isMobile
 | 
			
		||||
                  />
 | 
			
		||||
                  {!appState.viewModeEnabled && (
 | 
			
		||||
                    <LibraryButton
 | 
			
		||||
                      appState={appState}
 | 
			
		||||
                      setAppState={setAppState}
 | 
			
		||||
                      isMobile
 | 
			
		||||
                    />
 | 
			
		||||
                  )}
 | 
			
		||||
                </div>
 | 
			
		||||
              </Stack.Row>
 | 
			
		||||
            </Stack.Col>
 | 
			
		||||
          )}
 | 
			
		||||
@@ -123,11 +151,6 @@ export const MobileMenu = ({
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const renderAppToolbar = () => {
 | 
			
		||||
    // Render eraser conditionally in mobile
 | 
			
		||||
    const showEraser =
 | 
			
		||||
      !appState.editingElement &&
 | 
			
		||||
      getSelectedElements(elements, appState).length === 0;
 | 
			
		||||
 | 
			
		||||
    if (appState.viewModeEnabled) {
 | 
			
		||||
      return (
 | 
			
		||||
        <div className="App-toolbar-content">
 | 
			
		||||
@@ -140,14 +163,11 @@ export const MobileMenu = ({
 | 
			
		||||
      <div className="App-toolbar-content">
 | 
			
		||||
        {actionManager.renderAction("toggleCanvasMenu")}
 | 
			
		||||
        {actionManager.renderAction("toggleEditMenu")}
 | 
			
		||||
 | 
			
		||||
        {actionManager.renderAction("undo")}
 | 
			
		||||
        {actionManager.renderAction("redo")}
 | 
			
		||||
        {showEraser
 | 
			
		||||
          ? actionManager.renderAction("eraser")
 | 
			
		||||
          : actionManager.renderAction(
 | 
			
		||||
              appState.multiElement ? "finalize" : "duplicateSelection",
 | 
			
		||||
            )}
 | 
			
		||||
        {actionManager.renderAction(
 | 
			
		||||
          appState.multiElement ? "finalize" : "duplicateSelection",
 | 
			
		||||
        )}
 | 
			
		||||
        {actionManager.renderAction("deleteSelectedElements")}
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
@@ -158,16 +178,27 @@ export const MobileMenu = ({
 | 
			
		||||
      return (
 | 
			
		||||
        <>
 | 
			
		||||
          {renderJSONExportDialog()}
 | 
			
		||||
          <MenuItem
 | 
			
		||||
            label={t("buttons.exportImage")}
 | 
			
		||||
            icon={ExportImageIcon}
 | 
			
		||||
            dataTestId="image-export-button"
 | 
			
		||||
            onClick={() => setAppState({ openDialog: "imageExport" })}
 | 
			
		||||
          />
 | 
			
		||||
          {renderImageExportDialog()}
 | 
			
		||||
        </>
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    return (
 | 
			
		||||
      <>
 | 
			
		||||
        {actionManager.renderAction("clearCanvas")}
 | 
			
		||||
        {actionManager.renderAction("loadScene")}
 | 
			
		||||
        {!appState.viewModeEnabled && actionManager.renderAction("loadScene")}
 | 
			
		||||
        {renderJSONExportDialog()}
 | 
			
		||||
        {renderImageExportDialog()}
 | 
			
		||||
        <MenuItem
 | 
			
		||||
          label={t("buttons.exportImage")}
 | 
			
		||||
          icon={ExportImageIcon}
 | 
			
		||||
          dataTestId="image-export-button"
 | 
			
		||||
          onClick={() => setAppState({ openDialog: "imageExport" })}
 | 
			
		||||
        />
 | 
			
		||||
        {onCollabButtonClick && (
 | 
			
		||||
          <CollabButton
 | 
			
		||||
            isCollaborating={isCollaborating}
 | 
			
		||||
@@ -175,7 +206,22 @@ export const MobileMenu = ({
 | 
			
		||||
            onClick={onCollabButtonClick}
 | 
			
		||||
          />
 | 
			
		||||
        )}
 | 
			
		||||
        {<BackgroundPickerAndDarkModeToggle actionManager={actionManager} />}
 | 
			
		||||
        {actionManager.renderAction("toggleShortcuts", undefined, true)}
 | 
			
		||||
        {!appState.viewModeEnabled && actionManager.renderAction("clearCanvas")}
 | 
			
		||||
        <Separator />
 | 
			
		||||
        <MenuLinks />
 | 
			
		||||
        <Separator />
 | 
			
		||||
        {!appState.viewModeEnabled && (
 | 
			
		||||
          <div style={{ marginBottom: ".5rem" }}>
 | 
			
		||||
            <div style={{ fontSize: ".75rem", marginBottom: ".5rem" }}>
 | 
			
		||||
              {t("labels.canvasBackground")}
 | 
			
		||||
            </div>
 | 
			
		||||
            <div style={{ padding: "0 0.625rem" }}>
 | 
			
		||||
              {actionManager.renderAction("changeViewBackgroundColor")}
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        )}
 | 
			
		||||
        {actionManager.renderAction("toggleTheme")}
 | 
			
		||||
      </>
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
@@ -206,7 +252,7 @@ export const MobileMenu = ({
 | 
			
		||||
          {appState.openMenu === "canvas" ? (
 | 
			
		||||
            <Section className="App-mobile-menu" heading="canvasActions">
 | 
			
		||||
              <div className="panelColumn">
 | 
			
		||||
                <Stack.Col gap={4}>
 | 
			
		||||
                <Stack.Col gap={2}>
 | 
			
		||||
                  {renderCanvasActions()}
 | 
			
		||||
                  {renderCustomFooter?.(true, appState)}
 | 
			
		||||
                  {appState.collaborators.size > 0 && (
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,10 @@
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    overflow: auto;
 | 
			
		||||
    padding: calc(var(--space-factor) * 10);
 | 
			
		||||
 | 
			
		||||
    .Island {
 | 
			
		||||
      padding: 2.5rem !important;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .Modal__background {
 | 
			
		||||
@@ -26,7 +30,7 @@
 | 
			
		||||
    right: 0;
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
    z-index: 1;
 | 
			
		||||
    background-color: transparentize($oc-black, 0.3);
 | 
			
		||||
    background-color: rgba(#121212, 0.2);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .Modal__content {
 | 
			
		||||
@@ -46,7 +50,7 @@
 | 
			
		||||
    background: var(--island-bg-color);
 | 
			
		||||
 | 
			
		||||
    border: 1px solid var(--dialog-border-color);
 | 
			
		||||
    box-shadow: 0 2px 10px transparentize($oc-black, 0.75);
 | 
			
		||||
    box-shadow: var(--modal-shadow);
 | 
			
		||||
    border-radius: 6px;
 | 
			
		||||
    box-sizing: border-box;
 | 
			
		||||
 | 
			
		||||
@@ -73,14 +77,20 @@
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .Modal__close {
 | 
			
		||||
    width: calc(var(--space-factor) * 7);
 | 
			
		||||
    height: calc(var(--space-factor) * 7);
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    color: var(--icon-fill-color);
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    padding: 0.375rem;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: 1rem;
 | 
			
		||||
    right: 1rem;
 | 
			
		||||
    border: 0;
 | 
			
		||||
    background-color: transparent;
 | 
			
		||||
    line-height: 0;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
 | 
			
		||||
    svg {
 | 
			
		||||
      height: calc(var(--space-factor) * 5);
 | 
			
		||||
      width: 1.5rem;
 | 
			
		||||
      height: 1.5rem;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -39,6 +39,7 @@ export const Modal: React.FC<{
 | 
			
		||||
      aria-modal="true"
 | 
			
		||||
      onKeyDown={handleKeydown}
 | 
			
		||||
      aria-labelledby={props.labelledBy}
 | 
			
		||||
      data-prevent-outside-click
 | 
			
		||||
    >
 | 
			
		||||
      <div
 | 
			
		||||
        className="Modal__background"
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ import "./ToolIcon.scss";
 | 
			
		||||
 | 
			
		||||
import clsx from "clsx";
 | 
			
		||||
import { ToolButtonSize } from "./ToolButton";
 | 
			
		||||
import { PenModeIcon } from "./icons";
 | 
			
		||||
 | 
			
		||||
type PenModeIconProps = {
 | 
			
		||||
  title?: string;
 | 
			
		||||
@@ -15,59 +16,15 @@ type PenModeIconProps = {
 | 
			
		||||
 | 
			
		||||
const DEFAULT_SIZE: ToolButtonSize = "medium";
 | 
			
		||||
 | 
			
		||||
const ICONS = {
 | 
			
		||||
  CHECKED: (
 | 
			
		||||
    <svg
 | 
			
		||||
      width="205"
 | 
			
		||||
      height="205"
 | 
			
		||||
      viewBox="0 0 205 205"
 | 
			
		||||
      xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
    >
 | 
			
		||||
      <path d="m35 195-25-29.17V50h50v115l-25 30" />
 | 
			
		||||
      <path d="M10 40V10h50v30H10" />
 | 
			
		||||
      <path d="M125 145h70v50h-70" />
 | 
			
		||||
      <path d="M190 145v-30l-10-20h-40l-10 20v30h15v-30l5-5h20l5 5v30h15" />
 | 
			
		||||
    </svg>
 | 
			
		||||
  ),
 | 
			
		||||
  UNCHECKED: (
 | 
			
		||||
    <svg
 | 
			
		||||
      width="205"
 | 
			
		||||
      height="205"
 | 
			
		||||
      viewBox="0 0 205 205"
 | 
			
		||||
      xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
      className="unlocked-icon rtl-mirror"
 | 
			
		||||
    >
 | 
			
		||||
      <path d="m35 195-25-29.17V50h50v115l-25 30" />
 | 
			
		||||
      <path d="M10 40V10h50v30H10" />
 | 
			
		||||
      <path d="M125 145h70v50h-70" />
 | 
			
		||||
      <path d="M145 145v-30l-10-20H95l-10 20v30h15v-30l5-5h20l5 5v30h15" />
 | 
			
		||||
    </svg>
 | 
			
		||||
  ),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const PenModeButton = (props: PenModeIconProps) => {
 | 
			
		||||
  if (!props.penDetected) {
 | 
			
		||||
    if (props.isMobile) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
    return (
 | 
			
		||||
      <label
 | 
			
		||||
        className={clsx(
 | 
			
		||||
          "ToolIcon ToolIcon__penMode ToolIcon_type_floating",
 | 
			
		||||
          `ToolIcon_size_${DEFAULT_SIZE}`,
 | 
			
		||||
          {
 | 
			
		||||
            "is-mobile": props.isMobile,
 | 
			
		||||
          },
 | 
			
		||||
        )}
 | 
			
		||||
      >
 | 
			
		||||
        <div className="ToolIcon__icon ToolIcon__hidden" />
 | 
			
		||||
      </label>
 | 
			
		||||
    );
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <label
 | 
			
		||||
      className={clsx(
 | 
			
		||||
        "ToolIcon ToolIcon__penMode ToolIcon_type_floating",
 | 
			
		||||
        "ToolIcon ToolIcon__penMode",
 | 
			
		||||
        `ToolIcon_size_${DEFAULT_SIZE}`,
 | 
			
		||||
        {
 | 
			
		||||
          "is-mobile": props.isMobile,
 | 
			
		||||
@@ -83,9 +40,7 @@ export const PenModeButton = (props: PenModeIconProps) => {
 | 
			
		||||
        checked={props.checked}
 | 
			
		||||
        aria-label={props.title}
 | 
			
		||||
      />
 | 
			
		||||
      <div className="ToolIcon__icon">
 | 
			
		||||
        {props.checked ? ICONS.CHECKED : ICONS.UNCHECKED}
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="ToolIcon__icon">{PenModeIcon}</div>
 | 
			
		||||
    </label>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@
 | 
			
		||||
      flex-direction: column;
 | 
			
		||||
 | 
			
		||||
      label {
 | 
			
		||||
        padding: 1em;
 | 
			
		||||
        padding: 1em 0;
 | 
			
		||||
        display: flex;
 | 
			
		||||
        justify-content: space-between;
 | 
			
		||||
        align-items: center;
 | 
			
		||||
@@ -34,6 +34,7 @@
 | 
			
		||||
      display: flex;
 | 
			
		||||
      padding: 0.2rem 0;
 | 
			
		||||
      justify-content: flex-end;
 | 
			
		||||
      gap: 0.5rem;
 | 
			
		||||
 | 
			
		||||
      .ToolIcon__icon {
 | 
			
		||||
        min-width: 2.5rem;
 | 
			
		||||
@@ -74,7 +75,6 @@
 | 
			
		||||
 | 
			
		||||
    .selected-library-items {
 | 
			
		||||
      display: flex;
 | 
			
		||||
      padding: 0 0.8rem;
 | 
			
		||||
      flex-wrap: wrap;
 | 
			
		||||
 | 
			
		||||
      .single-library-item-wrapper {
 | 
			
		||||
@@ -87,7 +87,7 @@
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &-note {
 | 
			
		||||
      padding: 1em;
 | 
			
		||||
      padding: 1em 0;
 | 
			
		||||
      font-style: italic;
 | 
			
		||||
      font-size: 14px;
 | 
			
		||||
      display: block;
 | 
			
		||||
 
 | 
			
		||||
@@ -4,8 +4,6 @@ import OpenColor from "open-color";
 | 
			
		||||
import { Dialog } from "./Dialog";
 | 
			
		||||
import { t } from "../i18n";
 | 
			
		||||
 | 
			
		||||
import { ToolButton } from "./ToolButton";
 | 
			
		||||
 | 
			
		||||
import { AppState, LibraryItems, LibraryItem } from "../types";
 | 
			
		||||
import { exportToCanvas } from "../packages/utils";
 | 
			
		||||
import {
 | 
			
		||||
@@ -20,6 +18,7 @@ import "./PublishLibrary.scss";
 | 
			
		||||
import SingleLibraryItem from "./SingleLibraryItem";
 | 
			
		||||
import { canvasToBlob, resizeImageFile } from "../data/blob";
 | 
			
		||||
import { chunk } from "../utils";
 | 
			
		||||
import DialogActionButton from "./DialogActionButton";
 | 
			
		||||
 | 
			
		||||
interface PublishLibraryDataParams {
 | 
			
		||||
  authorName: string;
 | 
			
		||||
@@ -434,21 +433,15 @@ const PublishLibrary = ({
 | 
			
		||||
            </span>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div className="publish-library__buttons">
 | 
			
		||||
            <ToolButton
 | 
			
		||||
              type="button"
 | 
			
		||||
              title={t("buttons.cancel")}
 | 
			
		||||
              aria-label={t("buttons.cancel")}
 | 
			
		||||
            <DialogActionButton
 | 
			
		||||
              label={t("buttons.cancel")}
 | 
			
		||||
              onClick={onDialogClose}
 | 
			
		||||
              data-testid="cancel-clear-canvas-button"
 | 
			
		||||
              className="publish-library__buttons--cancel"
 | 
			
		||||
            />
 | 
			
		||||
            <ToolButton
 | 
			
		||||
            <DialogActionButton
 | 
			
		||||
              type="submit"
 | 
			
		||||
              title={t("buttons.submit")}
 | 
			
		||||
              aria-label={t("buttons.submit")}
 | 
			
		||||
              label={t("buttons.submit")}
 | 
			
		||||
              className="publish-library__buttons--confirm"
 | 
			
		||||
              actionType="primary"
 | 
			
		||||
              isLoading={isSubmitting}
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -2,20 +2,101 @@
 | 
			
		||||
@import "../../css/variables.module";
 | 
			
		||||
 | 
			
		||||
.excalidraw {
 | 
			
		||||
  .Sidebar {
 | 
			
		||||
    &__dropdown-content {
 | 
			
		||||
      z-index: 1;
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      top: 100%;
 | 
			
		||||
      left: 0;
 | 
			
		||||
 | 
			
		||||
      :root[dir="rtl"] & {
 | 
			
		||||
        right: 0;
 | 
			
		||||
        left: auto;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      margin-top: 0.25rem;
 | 
			
		||||
      width: 180px;
 | 
			
		||||
      box-shadow: var(--library-dropdown-shadow);
 | 
			
		||||
      border-radius: var(--border-radius-lg);
 | 
			
		||||
      padding: 0.25rem 0.5rem;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &__close-btn,
 | 
			
		||||
    &__pin-btn,
 | 
			
		||||
    &__dropdown-btn {
 | 
			
		||||
      @include outlineButtonStyles;
 | 
			
		||||
      width: var(--lg-button-size);
 | 
			
		||||
      height: var(--lg-button-size);
 | 
			
		||||
      padding: 0;
 | 
			
		||||
 | 
			
		||||
      svg {
 | 
			
		||||
        width: var(--lg-icon-size);
 | 
			
		||||
        height: var(--lg-icon-size);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &__pin-btn {
 | 
			
		||||
      &--pinned {
 | 
			
		||||
        background-color: var(--color-primary);
 | 
			
		||||
        border-color: var(--color-primary);
 | 
			
		||||
 | 
			
		||||
        svg {
 | 
			
		||||
          color: #fff;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        &:hover,
 | 
			
		||||
        &:active {
 | 
			
		||||
          background-color: var(--color-primary-darker);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &.theme--dark {
 | 
			
		||||
    .Sidebar {
 | 
			
		||||
      &__pin-btn {
 | 
			
		||||
        &--pinned {
 | 
			
		||||
          svg {
 | 
			
		||||
            color: var(--color-gray-90);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .layer-ui__sidebar {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: var(--sat);
 | 
			
		||||
    bottom: var(--sab);
 | 
			
		||||
    right: var(--sar);
 | 
			
		||||
    top: 0;
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
    right: 0;
 | 
			
		||||
    z-index: 5;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
 | 
			
		||||
    :root[dir="rtl"] & {
 | 
			
		||||
      left: 0;
 | 
			
		||||
      right: auto;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    background-color: var(--sidebar-bg-color);
 | 
			
		||||
 | 
			
		||||
    box-shadow: var(--sidebar-shadow);
 | 
			
		||||
 | 
			
		||||
    &--docked {
 | 
			
		||||
      box-shadow: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    box-shadow: var(--shadow-island);
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    border-radius: var(--border-radius-lg);
 | 
			
		||||
    margin: var(--space-factor);
 | 
			
		||||
    border-radius: 0;
 | 
			
		||||
    width: calc(#{$right-sidebar-width} - var(--space-factor) * 2);
 | 
			
		||||
 | 
			
		||||
    padding: 0.5rem;
 | 
			
		||||
    border-left: 1px solid var(--sidebar-border-color);
 | 
			
		||||
 | 
			
		||||
    :root[dir="rtl"] & {
 | 
			
		||||
      border-right: 1px solid var(--sidebar-border-color);
 | 
			
		||||
      border-left: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    padding: 0;
 | 
			
		||||
    box-sizing: border-box;
 | 
			
		||||
 | 
			
		||||
    .Island {
 | 
			
		||||
@@ -48,42 +129,18 @@
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .layer-ui__sidebar__header {
 | 
			
		||||
    box-sizing: border-box;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: space-between;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    margin: 2px 0 15px 0;
 | 
			
		||||
    &:empty {
 | 
			
		||||
      margin: 0;
 | 
			
		||||
    }
 | 
			
		||||
    button {
 | 
			
		||||
      // 2px from the left to account for focus border of left-most button
 | 
			
		||||
      margin: 0 2px;
 | 
			
		||||
    }
 | 
			
		||||
    padding: 1rem;
 | 
			
		||||
    border-bottom: 1px solid var(--sidebar-border-color);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .layer-ui__sidebar__header__buttons {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    margin-left: auto;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .layer-ui__sidebar-dock-button {
 | 
			
		||||
    @include toolbarButtonColorStates;
 | 
			
		||||
    margin-right: 0.2rem;
 | 
			
		||||
 | 
			
		||||
    .ToolIcon_type_floating .ToolIcon__icon {
 | 
			
		||||
      width: calc(var(--space-factor) * 7);
 | 
			
		||||
      height: calc(var(--space-factor) * 7);
 | 
			
		||||
      svg {
 | 
			
		||||
        // mirror
 | 
			
		||||
        transform: scale(-1, 1);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .ToolIcon_type_checkbox {
 | 
			
		||||
      &:not(.ToolIcon_toggle_opaque):checked + .ToolIcon__icon {
 | 
			
		||||
        background-color: var(--color-primary);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    gap: 0.625rem;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -90,10 +90,10 @@ describe("Sidebar", () => {
 | 
			
		||||
 | 
			
		||||
    const sidebar = container.querySelector<HTMLElement>(".test-sidebar");
 | 
			
		||||
    expect(sidebar).not.toBe(null);
 | 
			
		||||
    const closeButton = queryByTestId(sidebar!, "sidebar-close");
 | 
			
		||||
    const closeButton = queryByTestId(sidebar!, "sidebar-close")!;
 | 
			
		||||
    expect(closeButton).not.toBe(null);
 | 
			
		||||
 | 
			
		||||
    fireEvent.click(closeButton!.querySelector("button")!);
 | 
			
		||||
    fireEvent.click(closeButton);
 | 
			
		||||
    await waitFor(() => {
 | 
			
		||||
      expect(container.querySelector<HTMLElement>(".test-sidebar")).toBe(null);
 | 
			
		||||
      expect(onClose).toHaveBeenCalled();
 | 
			
		||||
 
 | 
			
		||||
@@ -33,6 +33,13 @@ export const Sidebar = Object.assign(
 | 
			
		||||
        onClose,
 | 
			
		||||
        onDock,
 | 
			
		||||
        docked,
 | 
			
		||||
        /** Undocumented, may be removed later. Generally should either be
 | 
			
		||||
         * `props.docked` or `appState.isSidebarDocked`. Currently serves to
 | 
			
		||||
         *  prevent unwanted animation of the shadow if initially docked. */
 | 
			
		||||
        //
 | 
			
		||||
        // NOTE we'll want to remove this after we sort out how to subscribe to
 | 
			
		||||
        // individual appState properties
 | 
			
		||||
        initialDockedState = docked,
 | 
			
		||||
        dockable = true,
 | 
			
		||||
        className,
 | 
			
		||||
        __isInternal,
 | 
			
		||||
@@ -52,7 +59,9 @@ export const Sidebar = Object.assign(
 | 
			
		||||
 | 
			
		||||
      const setAppState = useExcalidrawSetAppState();
 | 
			
		||||
 | 
			
		||||
      const [isDockedFallback, setIsDockedFallback] = useState(docked ?? false);
 | 
			
		||||
      const [isDockedFallback, setIsDockedFallback] = useState(
 | 
			
		||||
        docked ?? initialDockedState ?? false,
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      useLayoutEffect(() => {
 | 
			
		||||
        if (docked === undefined) {
 | 
			
		||||
@@ -119,8 +128,11 @@ export const Sidebar = Object.assign(
 | 
			
		||||
 | 
			
		||||
      return (
 | 
			
		||||
        <Island
 | 
			
		||||
          padding={2}
 | 
			
		||||
          className={clsx("layer-ui__sidebar", className)}
 | 
			
		||||
          className={clsx(
 | 
			
		||||
            "layer-ui__sidebar",
 | 
			
		||||
            { "layer-ui__sidebar--docked": isDockedFallback },
 | 
			
		||||
            className,
 | 
			
		||||
          )}
 | 
			
		||||
          ref={ref}
 | 
			
		||||
        >
 | 
			
		||||
          <SidebarPropsContext.Provider value={headerPropsRef.current}>
 | 
			
		||||
 
 | 
			
		||||
@@ -3,16 +3,10 @@ import { useContext } from "react";
 | 
			
		||||
import { t } from "../../i18n";
 | 
			
		||||
import { useDevice } from "../App";
 | 
			
		||||
import { SidebarPropsContext } from "./common";
 | 
			
		||||
import { close } from "../icons";
 | 
			
		||||
import { CloseIcon, PinIcon } from "../icons";
 | 
			
		||||
import { withUpstreamOverride } from "../hoc/withUpstreamOverride";
 | 
			
		||||
import { Tooltip } from "../Tooltip";
 | 
			
		||||
 | 
			
		||||
const SIDE_LIBRARY_TOGGLE_ICON = (
 | 
			
		||||
  <svg viewBox="0 0 24 24" fill="#ffffff">
 | 
			
		||||
    <path d="M19 22H5a3 3 0 01-3-3V5a3 3 0 013-3h14a3 3 0 013 3v14a3 3 0 01-3 3zm0-18h-9v16h9a1.01 1.01 0 001-1V5a1.01 1.01 0 00-1-1z"></path>
 | 
			
		||||
  </svg>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export const SidebarDockButton = (props: {
 | 
			
		||||
  checked: boolean;
 | 
			
		||||
  onChange?(): void;
 | 
			
		||||
@@ -33,8 +27,13 @@ export const SidebarDockButton = (props: {
 | 
			
		||||
            checked={props.checked}
 | 
			
		||||
            aria-label={t("labels.sidebarLock")}
 | 
			
		||||
          />{" "}
 | 
			
		||||
          <div className="ToolIcon__icon" tabIndex={0}>
 | 
			
		||||
            {SIDE_LIBRARY_TOGGLE_ICON}
 | 
			
		||||
          <div
 | 
			
		||||
            className={clsx("Sidebar__pin-btn", {
 | 
			
		||||
              "Sidebar__pin-btn--pinned": props.checked,
 | 
			
		||||
            })}
 | 
			
		||||
            tabIndex={0}
 | 
			
		||||
          >
 | 
			
		||||
            {PinIcon}
 | 
			
		||||
          </div>{" "}
 | 
			
		||||
        </label>{" "}
 | 
			
		||||
      </Tooltip>
 | 
			
		||||
@@ -64,24 +63,19 @@ const _SidebarHeader: React.FC<{
 | 
			
		||||
            <SidebarDockButton
 | 
			
		||||
              checked={!!props.docked}
 | 
			
		||||
              onChange={() => {
 | 
			
		||||
                document
 | 
			
		||||
                  .querySelector(".layer-ui__wrapper")
 | 
			
		||||
                  ?.classList.add("animate");
 | 
			
		||||
 | 
			
		||||
                props.onDock?.(!props.docked);
 | 
			
		||||
              }}
 | 
			
		||||
            />
 | 
			
		||||
          )}
 | 
			
		||||
          {renderCloseButton && (
 | 
			
		||||
            <div className="ToolIcon__icon__close" data-testid="sidebar-close">
 | 
			
		||||
              <button
 | 
			
		||||
                className="Modal__close"
 | 
			
		||||
                onClick={props.onClose}
 | 
			
		||||
                aria-label={t("buttons.close")}
 | 
			
		||||
              >
 | 
			
		||||
                {close}
 | 
			
		||||
              </button>
 | 
			
		||||
            </div>
 | 
			
		||||
            <button
 | 
			
		||||
              data-testid="sidebar-close"
 | 
			
		||||
              className="Sidebar__close-btn"
 | 
			
		||||
              onClick={props.onClose}
 | 
			
		||||
              aria-label={t("buttons.close")}
 | 
			
		||||
            >
 | 
			
		||||
              {CloseIcon}
 | 
			
		||||
            </button>
 | 
			
		||||
          )}
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@ export type SidebarProps<P = {}> = {
 | 
			
		||||
  /** if not supplied, sidebar won't be dockable */
 | 
			
		||||
  onDock?: (docked: boolean) => void;
 | 
			
		||||
  docked?: boolean;
 | 
			
		||||
  initialDockedState?: boolean;
 | 
			
		||||
  dockable?: boolean;
 | 
			
		||||
  className?: string;
 | 
			
		||||
} & P;
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@ import { useEffect, useRef } from "react";
 | 
			
		||||
import { t } from "../i18n";
 | 
			
		||||
import { exportToSvg } from "../packages/utils";
 | 
			
		||||
import { AppState, LibraryItem } from "../types";
 | 
			
		||||
import { close } from "./icons";
 | 
			
		||||
import { CloseIcon } from "./icons";
 | 
			
		||||
 | 
			
		||||
import "./SingleLibraryItem.scss";
 | 
			
		||||
import { ToolButton } from "./ToolButton";
 | 
			
		||||
@@ -54,7 +54,7 @@ const SingleLibraryItem = ({
 | 
			
		||||
      <ToolButton
 | 
			
		||||
        aria-label={t("buttons.remove")}
 | 
			
		||||
        type="button"
 | 
			
		||||
        icon={close}
 | 
			
		||||
        icon={CloseIcon}
 | 
			
		||||
        className="single-library-item--remove"
 | 
			
		||||
        onClick={onRemove.bind(null, libItem.id)}
 | 
			
		||||
        title={t("buttons.remove")}
 | 
			
		||||
@@ -62,7 +62,7 @@ const SingleLibraryItem = ({
 | 
			
		||||
      <div
 | 
			
		||||
        style={{
 | 
			
		||||
          display: "flex",
 | 
			
		||||
          margin: "0.8rem 0.3rem",
 | 
			
		||||
          margin: "0.8rem 0",
 | 
			
		||||
          width: "100%",
 | 
			
		||||
          fontSize: "14px",
 | 
			
		||||
          fontWeight: 500,
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ import { NonDeletedExcalidrawElement } from "../element/types";
 | 
			
		||||
import { t } from "../i18n";
 | 
			
		||||
import { getTargetElements } from "../scene";
 | 
			
		||||
import { AppState, ExcalidrawProps } from "../types";
 | 
			
		||||
import { close } from "./icons";
 | 
			
		||||
import { CloseIcon } from "./icons";
 | 
			
		||||
import { Island } from "./Island";
 | 
			
		||||
import "./Stats.scss";
 | 
			
		||||
 | 
			
		||||
@@ -23,7 +23,7 @@ export const Stats = (props: {
 | 
			
		||||
    <div className="Stats">
 | 
			
		||||
      <Island padding={2}>
 | 
			
		||||
        <div className="close" onClick={props.onClose}>
 | 
			
		||||
          {close}
 | 
			
		||||
          {CloseIcon}
 | 
			
		||||
        </div>
 | 
			
		||||
        <h3>{t("stats.title")}</h3>
 | 
			
		||||
        <table>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import { useCallback, useEffect, useRef } from "react";
 | 
			
		||||
import { close } from "./icons";
 | 
			
		||||
import { CloseIcon } from "./icons";
 | 
			
		||||
import "./Toast.scss";
 | 
			
		||||
import { ToolButton } from "./ToolButton";
 | 
			
		||||
 | 
			
		||||
@@ -47,7 +47,7 @@ export const Toast = ({
 | 
			
		||||
      <p className="Toast__message">{message}</p>
 | 
			
		||||
      {closable && (
 | 
			
		||||
        <ToolButton
 | 
			
		||||
          icon={close}
 | 
			
		||||
          icon={CloseIcon}
 | 
			
		||||
          aria-label="close"
 | 
			
		||||
          type="icon"
 | 
			
		||||
          onClick={onClose}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,12 +3,19 @@
 | 
			
		||||
 | 
			
		||||
.excalidraw {
 | 
			
		||||
  .ToolIcon {
 | 
			
		||||
    border-radius: var(--border-radius-lg);
 | 
			
		||||
    display: inline-flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    -webkit-tap-highlight-color: transparent;
 | 
			
		||||
    user-select: none;
 | 
			
		||||
 | 
			
		||||
    &__hidden {
 | 
			
		||||
      display: none !important;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @include toolbarButtonColorStates;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .ToolIcon--plain {
 | 
			
		||||
@@ -21,21 +28,15 @@
 | 
			
		||||
 | 
			
		||||
  .ToolIcon_type_radio,
 | 
			
		||||
  .ToolIcon_type_checkbox {
 | 
			
		||||
    & + .ToolIcon__icon {
 | 
			
		||||
      background-color: var(--button-gray-1);
 | 
			
		||||
 | 
			
		||||
      &:hover {
 | 
			
		||||
        background-color: var(--button-gray-2);
 | 
			
		||||
      }
 | 
			
		||||
      &:active {
 | 
			
		||||
        background-color: var(--button-gray-3);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    opacity: 0;
 | 
			
		||||
    pointer-events: none;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .ToolIcon__icon {
 | 
			
		||||
    width: 2.5rem;
 | 
			
		||||
    height: 2.5rem;
 | 
			
		||||
    box-sizing: border-box;
 | 
			
		||||
    width: var(--default-button-size);
 | 
			
		||||
    height: var(--default-button-size);
 | 
			
		||||
    color: var(--icon-fill-color);
 | 
			
		||||
 | 
			
		||||
    display: flex;
 | 
			
		||||
@@ -50,8 +51,8 @@
 | 
			
		||||
 | 
			
		||||
    svg {
 | 
			
		||||
      position: relative;
 | 
			
		||||
      height: 1em;
 | 
			
		||||
      fill: var(--icon-fill-color);
 | 
			
		||||
      width: var(--default-icon-size);
 | 
			
		||||
      height: var(--default-icon-size);
 | 
			
		||||
      color: var(--icon-fill-color);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@@ -75,13 +76,14 @@
 | 
			
		||||
    font-size: 0.8em;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .excalidraw .ToolIcon_type_button,
 | 
			
		||||
  .ToolIcon_type_button,
 | 
			
		||||
  .Modal .ToolIcon_type_button,
 | 
			
		||||
  .ToolIcon_type_button {
 | 
			
		||||
    padding: 0;
 | 
			
		||||
    border: none;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    font-size: inherit;
 | 
			
		||||
    background-color: initial;
 | 
			
		||||
 | 
			
		||||
    &:focus-visible {
 | 
			
		||||
      box-shadow: 0 0 0 2px var(--focus-highlight-color);
 | 
			
		||||
@@ -95,9 +97,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &:hover {
 | 
			
		||||
      background-color: var(--button-gray-2);
 | 
			
		||||
    }
 | 
			
		||||
    // &:hover {
 | 
			
		||||
    //   background-color: var(--button-gray-2);
 | 
			
		||||
    // }
 | 
			
		||||
 | 
			
		||||
    &:active {
 | 
			
		||||
      background-color: var(--button-gray-3);
 | 
			
		||||
@@ -108,29 +110,8 @@
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &--hide {
 | 
			
		||||
      visibility: hidden;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .ToolIcon_type_radio,
 | 
			
		||||
  .ToolIcon_type_checkbox {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    opacity: 0;
 | 
			
		||||
    pointer-events: none;
 | 
			
		||||
 | 
			
		||||
    &:not(.ToolIcon_toggle_opaque):checked + .ToolIcon__icon {
 | 
			
		||||
      background-color: var(--button-gray-2);
 | 
			
		||||
      &:active {
 | 
			
		||||
        background-color: var(--button-gray-3);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &:focus-visible + .ToolIcon__icon {
 | 
			
		||||
      box-shadow: 0 0 0 2px var(--focus-highlight-color);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &:active + .ToolIcon__icon {
 | 
			
		||||
      background-color: var(--button-gray-3);
 | 
			
		||||
      // visibility: hidden;
 | 
			
		||||
      display: none !important;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -163,66 +144,12 @@
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    bottom: 2px;
 | 
			
		||||
    right: 3px;
 | 
			
		||||
    font-size: 0.5em;
 | 
			
		||||
    font-size: 0.625rem;
 | 
			
		||||
    color: var(--keybinding-color);
 | 
			
		||||
    font-family: var(--ui-font);
 | 
			
		||||
    user-select: none;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // shrink shape icons on small viewports to make them fit
 | 
			
		||||
  @media (max-width: 425px) {
 | 
			
		||||
    .Shape .ToolIcon__icon {
 | 
			
		||||
      width: 2rem;
 | 
			
		||||
      height: 2rem;
 | 
			
		||||
 | 
			
		||||
      svg {
 | 
			
		||||
        height: 0.8em;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // move the lock button out of the way on small viewports
 | 
			
		||||
  // it begins to collide with the GitHub icon before we switch to mobile mode
 | 
			
		||||
  @media (max-width: 760px) {
 | 
			
		||||
    .ToolIcon.ToolIcon_type_floating {
 | 
			
		||||
      display: inline-block;
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      right: -8px;
 | 
			
		||||
 | 
			
		||||
      margin-left: 0;
 | 
			
		||||
      border-radius: 20px 0 0 20px;
 | 
			
		||||
      z-index: 1;
 | 
			
		||||
 | 
			
		||||
      background-color: var(--button-gray-1);
 | 
			
		||||
 | 
			
		||||
      &:hover {
 | 
			
		||||
        background-color: var(--button-gray-1);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      &:active {
 | 
			
		||||
        background-color: var(--button-gray-2);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .ToolIcon__icon {
 | 
			
		||||
        border-radius: inherit;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      svg {
 | 
			
		||||
        position: static;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    .ToolIcon.ToolIcon__library {
 | 
			
		||||
      top: calc(var(--sat) + 100px);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .ToolIcon.ToolIcon__lock {
 | 
			
		||||
      top: calc(var(--sat) + 60px);
 | 
			
		||||
    }
 | 
			
		||||
    .ToolIcon.ToolIcon__penMode {
 | 
			
		||||
      top: calc(var(--sat) + 140px);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .unlocked-icon {
 | 
			
		||||
    :root[dir="ltr"] & {
 | 
			
		||||
      left: 2px;
 | 
			
		||||
@@ -232,4 +159,16 @@
 | 
			
		||||
      right: 2px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .App-toolbar-container {
 | 
			
		||||
    .ToolIcon__icon {
 | 
			
		||||
      width: var(--lg-button-size);
 | 
			
		||||
      height: var(--lg-button-size);
 | 
			
		||||
 | 
			
		||||
      svg {
 | 
			
		||||
        width: var(--lg-icon-size);
 | 
			
		||||
        height: var(--lg-icon-size);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,101 +2,20 @@
 | 
			
		||||
@import "../css/variables.module";
 | 
			
		||||
 | 
			
		||||
.excalidraw {
 | 
			
		||||
  .App-toolbar-container {
 | 
			
		||||
    .ToolIcon_type_floating {
 | 
			
		||||
      @include toolbarButtonColorStates;
 | 
			
		||||
 | 
			
		||||
      &:not(.is-mobile) {
 | 
			
		||||
        .ToolIcon__icon {
 | 
			
		||||
          padding: 1px;
 | 
			
		||||
          background-color: var(--island-bg-color);
 | 
			
		||||
          box-shadow: 1px 3px 4px 0px rgb(0 0 0 / 15%);
 | 
			
		||||
          border-radius: 50%;
 | 
			
		||||
          transition: box-shadow 0.5s ease, transform 0.5s ease;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .ToolIcon_type_radio,
 | 
			
		||||
      .ToolIcon_type_checkbox {
 | 
			
		||||
        &:focus-within + .ToolIcon__icon {
 | 
			
		||||
          // override for custom floating button shadow
 | 
			
		||||
          box-shadow: 0 0 0 2px var(--focus-highlight-color);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .ToolIcon__hidden {
 | 
			
		||||
      box-shadow: none !important;
 | 
			
		||||
      background-color: transparent !important;
 | 
			
		||||
      pointer-events: none !important;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .ToolIcon.ToolIcon__lock {
 | 
			
		||||
      &.ToolIcon_type_floating {
 | 
			
		||||
        margin-left: 0.1rem;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .ToolIcon__library {
 | 
			
		||||
      margin-inline-start: var(--space-factor);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.zen-mode {
 | 
			
		||||
      .ToolIcon_type_floating {
 | 
			
		||||
        .ToolIcon__icon {
 | 
			
		||||
          box-shadow: none;
 | 
			
		||||
          transform: scale(0.9);
 | 
			
		||||
        }
 | 
			
		||||
        .ToolIcon_type_checkbox:not(:checked):not(:hover):not(:active) {
 | 
			
		||||
          & + .ToolIcon__icon {
 | 
			
		||||
            svg {
 | 
			
		||||
              fill: $oc-gray-5;
 | 
			
		||||
              color: $oc-gray-5;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .App-toolbar {
 | 
			
		||||
    border-radius: var(--border-radius-lg);
 | 
			
		||||
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.01), 1px 1px 5px rgb(0 0 0 / 15%);
 | 
			
		||||
 | 
			
		||||
    .ToolIcon {
 | 
			
		||||
      &:hover {
 | 
			
		||||
        --icon-fill-color: var(
 | 
			
		||||
          --color-primary-contrast-offset,
 | 
			
		||||
          var(--color-primary)
 | 
			
		||||
        );
 | 
			
		||||
        --keybinding-color: var(
 | 
			
		||||
          --color-primary-contrast-offset,
 | 
			
		||||
          var(--color-primary)
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      &:active {
 | 
			
		||||
        --icon-fill-color: #{$oc-gray-9};
 | 
			
		||||
        --keybinding-color: #{$oc-gray-9};
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .ToolIcon__icon {
 | 
			
		||||
        background: transparent;
 | 
			
		||||
        border-radius: var(--border-radius-lg);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      @include toolbarButtonColorStates;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.zen-mode {
 | 
			
		||||
      .ToolIcon__keybinding,
 | 
			
		||||
      .HintViewer {
 | 
			
		||||
        display: none;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &.theme--dark .App-toolbar .ToolIcon:active {
 | 
			
		||||
    --icon-fill-color: #{$oc-gray-3};
 | 
			
		||||
    --keybinding-color: #{$oc-gray-3};
 | 
			
		||||
    &__divider {
 | 
			
		||||
      width: 1px;
 | 
			
		||||
      height: 1.5rem;
 | 
			
		||||
      align-self: center;
 | 
			
		||||
      background-color: var(--default-border-color);
 | 
			
		||||
      margin: 0 0.5rem;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -7,23 +7,30 @@
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-wrap: wrap;
 | 
			
		||||
    justify-content: flex-end;
 | 
			
		||||
    gap: 0.625rem;
 | 
			
		||||
 | 
			
		||||
    &:empty {
 | 
			
		||||
      display: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // can fit max 5 avatars in a column
 | 
			
		||||
    max-height: 140px;
 | 
			
		||||
 | 
			
		||||
    // can fit max 10 avatars in a row when there's enough space
 | 
			
		||||
    max-width: 290px;
 | 
			
		||||
 | 
			
		||||
    // Tweak in 30px increments to fit more/fewer avatars in a row/column ^^
 | 
			
		||||
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .UserList > * {
 | 
			
		||||
    pointer-events: all;
 | 
			
		||||
    margin: 0 0 var(--space-factor) var(--space-factor);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .UserList_mobile {
 | 
			
		||||
    padding: 0;
 | 
			
		||||
    justify-content: normal;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .UserList_mobile > * {
 | 
			
		||||
    margin: 0 var(--space-factor) var(--space-factor) 0;
 | 
			
		||||
    margin: 0.5rem 0;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -44,6 +44,26 @@ export const UserList: React.FC<{
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
  // TODO barnabasmolnar/editor-redesign
 | 
			
		||||
  // probably remove before shipping :)
 | 
			
		||||
  // 20 fake collaborators; for easy, convenient debug purposes ˇˇ
 | 
			
		||||
  // const avatars = Array.from({ length: 20 }).map((_, index) => {
 | 
			
		||||
  //   const avatarJSX = actionManager.renderAction("goToCollaborator", [
 | 
			
		||||
  //     index.toString(),
 | 
			
		||||
  //     {
 | 
			
		||||
  //       username: `User ${index}`,
 | 
			
		||||
  //     },
 | 
			
		||||
  //   ]);
 | 
			
		||||
 | 
			
		||||
  //   return mobile ? (
 | 
			
		||||
  //     <Tooltip label={`User ${index}`} key={index}>
 | 
			
		||||
  //       {avatarJSX}
 | 
			
		||||
  //     </Tooltip>
 | 
			
		||||
  //   ) : (
 | 
			
		||||
  //     <React.Fragment key={index}>{avatarJSX}</React.Fragment>
 | 
			
		||||
  //   );
 | 
			
		||||
  // });
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className={clsx("UserList", className, { UserList_mobile: mobile })}>
 | 
			
		||||
      {avatars}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										273
									
								
								src/components/WelcomeScreen.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										273
									
								
								src/components/WelcomeScreen.scss
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,273 @@
 | 
			
		||||
.excalidraw {
 | 
			
		||||
  .virgil {
 | 
			
		||||
    font-family: "Virgil";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .WelcomeScreen-logo {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    column-gap: 0.75rem;
 | 
			
		||||
    font-size: 2.25rem;
 | 
			
		||||
 | 
			
		||||
    svg {
 | 
			
		||||
      width: 1.625rem;
 | 
			
		||||
      height: auto;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .WelcomeScreen-decor {
 | 
			
		||||
    pointer-events: none;
 | 
			
		||||
 | 
			
		||||
    color: var(--color-gray-40);
 | 
			
		||||
 | 
			
		||||
    &--subheading {
 | 
			
		||||
      font-size: 1.125rem;
 | 
			
		||||
      text-align: center;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &--help-pointer {
 | 
			
		||||
      display: flex;
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      right: 0;
 | 
			
		||||
      bottom: 100%;
 | 
			
		||||
 | 
			
		||||
      :root[dir="rtl"] & {
 | 
			
		||||
        left: 0;
 | 
			
		||||
        right: auto;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      svg {
 | 
			
		||||
        margin-top: 0.5rem;
 | 
			
		||||
        width: 85px;
 | 
			
		||||
        height: 71px;
 | 
			
		||||
 | 
			
		||||
        transform: scaleX(-1) rotate(80deg);
 | 
			
		||||
 | 
			
		||||
        :root[dir="rtl"] & {
 | 
			
		||||
          transform: rotate(80deg);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &--top-toolbar-pointer {
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      top: 100%;
 | 
			
		||||
      left: 50%;
 | 
			
		||||
      transform: translateX(-50%);
 | 
			
		||||
      margin-top: 2.5rem;
 | 
			
		||||
      display: flex;
 | 
			
		||||
      align-items: baseline;
 | 
			
		||||
 | 
			
		||||
      &__label {
 | 
			
		||||
        width: 120px;
 | 
			
		||||
        position: relative;
 | 
			
		||||
        top: -0.5rem;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      svg {
 | 
			
		||||
        width: 38px;
 | 
			
		||||
        height: 78px;
 | 
			
		||||
 | 
			
		||||
        :root[dir="rtl"] & {
 | 
			
		||||
          transform: scaleX(-1);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &--menu-pointer {
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      width: 320px;
 | 
			
		||||
      font-size: 1rem;
 | 
			
		||||
 | 
			
		||||
      top: 100%;
 | 
			
		||||
      margin-top: 0.25rem;
 | 
			
		||||
      margin-inline-start: 0.6rem;
 | 
			
		||||
 | 
			
		||||
      display: flex;
 | 
			
		||||
      align-items: flex-end;
 | 
			
		||||
      gap: 0.5rem;
 | 
			
		||||
 | 
			
		||||
      svg {
 | 
			
		||||
        width: 41px;
 | 
			
		||||
        height: 94px;
 | 
			
		||||
 | 
			
		||||
        :root[dir="rtl"] & {
 | 
			
		||||
          transform: scaleX(-1);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .WelcomeScreen-container {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    gap: 2rem;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    pointer-events: none;
 | 
			
		||||
    left: 1rem;
 | 
			
		||||
    top: 1rem;
 | 
			
		||||
    right: 1rem;
 | 
			
		||||
    bottom: 1rem;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .WelcomeScreen-items {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    gap: 2px;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .WelcomeScreen-item {
 | 
			
		||||
    box-sizing: border-box;
 | 
			
		||||
 | 
			
		||||
    pointer-events: all;
 | 
			
		||||
 | 
			
		||||
    color: var(--color-gray-50);
 | 
			
		||||
    font-size: 0.875rem;
 | 
			
		||||
 | 
			
		||||
    min-width: 300px;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    justify-content: space-between;
 | 
			
		||||
 | 
			
		||||
    background: none;
 | 
			
		||||
    border: none;
 | 
			
		||||
 | 
			
		||||
    padding: 0.75rem;
 | 
			
		||||
 | 
			
		||||
    border-radius: var(--border-radius-md);
 | 
			
		||||
 | 
			
		||||
    &__label {
 | 
			
		||||
      display: flex;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
      column-gap: 0.5rem;
 | 
			
		||||
 | 
			
		||||
      svg {
 | 
			
		||||
        width: var(--default-icon-size);
 | 
			
		||||
        height: var(--default-icon-size);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &__shortcut {
 | 
			
		||||
      color: var(--color-gray-40);
 | 
			
		||||
      font-size: 0.75rem;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &:not(:active) .WelcomeScreen-item:hover {
 | 
			
		||||
    text-decoration: none;
 | 
			
		||||
    background: var(--color-gray-10);
 | 
			
		||||
 | 
			
		||||
    .WelcomeScreen-item__shortcut {
 | 
			
		||||
      color: var(--color-gray-50);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .WelcomeScreen-item__label {
 | 
			
		||||
      color: var(--color-gray-100);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .WelcomeScreen-item:active {
 | 
			
		||||
    background: var(--color-gray-20);
 | 
			
		||||
 | 
			
		||||
    .WelcomeScreen-item__shortcut {
 | 
			
		||||
      color: var(--color-gray-50);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .WelcomeScreen-item__label {
 | 
			
		||||
      color: var(--color-gray-100);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &--promo {
 | 
			
		||||
      color: var(--color-promo) !important;
 | 
			
		||||
 | 
			
		||||
      &:hover {
 | 
			
		||||
        .WelcomeScreen-item__label {
 | 
			
		||||
          color: var(--color-promo) !important;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &.theme--dark {
 | 
			
		||||
    .WelcomeScreen-decor {
 | 
			
		||||
      color: var(--color-gray-60);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .WelcomeScreen-item {
 | 
			
		||||
      color: var(--color-gray-60);
 | 
			
		||||
 | 
			
		||||
      &__shortcut {
 | 
			
		||||
        color: var(--color-gray-60);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &:not(:active) .WelcomeScreen-item:hover {
 | 
			
		||||
      background: var(--color-gray-85);
 | 
			
		||||
 | 
			
		||||
      .WelcomeScreen-item__shortcut {
 | 
			
		||||
        color: var(--color-gray-50);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .WelcomeScreen-item__label {
 | 
			
		||||
        color: var(--color-gray-10);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .WelcomeScreen-item:active {
 | 
			
		||||
      background-color: var(--color-gray-90);
 | 
			
		||||
      .WelcomeScreen-item__label {
 | 
			
		||||
        color: var(--color-gray-10);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Can tweak these values but for an initial effort, it looks OK to me
 | 
			
		||||
  @media (max-width: 1024px) {
 | 
			
		||||
    .WelcomeScreen-decor {
 | 
			
		||||
      &--help-pointer,
 | 
			
		||||
      &--menu-pointer {
 | 
			
		||||
        display: none;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // @media (max-height: 400px) {
 | 
			
		||||
  //   .WelcomeScreen-container {
 | 
			
		||||
  //     margin-top: 0;
 | 
			
		||||
  //   }
 | 
			
		||||
  // }
 | 
			
		||||
  @media (max-height: 599px) {
 | 
			
		||||
    .WelcomeScreen-container {
 | 
			
		||||
      margin-top: 4rem;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  @media (min-height: 600px) and (max-height: 900px) {
 | 
			
		||||
    .WelcomeScreen-container {
 | 
			
		||||
      margin-top: 8rem;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  @media (max-height: 630px) {
 | 
			
		||||
    .WelcomeScreen-decor--top-toolbar-pointer {
 | 
			
		||||
      display: none;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  @media (max-height: 500px) {
 | 
			
		||||
    .WelcomeScreen-container {
 | 
			
		||||
      display: none;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // @media (max-height: 740px) {
 | 
			
		||||
  //   .WelcomeScreen-decor {
 | 
			
		||||
  //     &--help-pointer,
 | 
			
		||||
  //     &--top-toolbar-pointer,
 | 
			
		||||
  //     &--menu-pointer {
 | 
			
		||||
  //       display: none;
 | 
			
		||||
  //     }
 | 
			
		||||
  //   }
 | 
			
		||||
  // }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										141
									
								
								src/components/WelcomeScreen.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								src/components/WelcomeScreen.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,141 @@
 | 
			
		||||
import { useAtom } from "jotai";
 | 
			
		||||
import { actionLoadScene, actionShortcuts } from "../actions";
 | 
			
		||||
import { ActionManager } from "../actions/manager";
 | 
			
		||||
import { getShortcutFromShortcutName } from "../actions/shortcuts";
 | 
			
		||||
import { COOKIES } from "../constants";
 | 
			
		||||
import { collabDialogShownAtom } from "../excalidraw-app/collab/Collab";
 | 
			
		||||
import { t } from "../i18n";
 | 
			
		||||
import { AppState } from "../types";
 | 
			
		||||
import {
 | 
			
		||||
  ExcalLogo,
 | 
			
		||||
  HelpIcon,
 | 
			
		||||
  LoadIcon,
 | 
			
		||||
  PlusPromoIcon,
 | 
			
		||||
  UsersIcon,
 | 
			
		||||
} from "./icons";
 | 
			
		||||
import "./WelcomeScreen.scss";
 | 
			
		||||
 | 
			
		||||
const isExcalidrawPlusSignedUser = document.cookie.includes(
 | 
			
		||||
  COOKIES.AUTH_STATE_COOKIE,
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const WelcomeScreenItem = ({
 | 
			
		||||
  label,
 | 
			
		||||
  shortcut,
 | 
			
		||||
  onClick,
 | 
			
		||||
  icon,
 | 
			
		||||
  link,
 | 
			
		||||
}: {
 | 
			
		||||
  label: string;
 | 
			
		||||
  shortcut: string | null;
 | 
			
		||||
  onClick?: () => void;
 | 
			
		||||
  icon: JSX.Element;
 | 
			
		||||
  link?: string;
 | 
			
		||||
}) => {
 | 
			
		||||
  if (link) {
 | 
			
		||||
    return (
 | 
			
		||||
      <a
 | 
			
		||||
        className="WelcomeScreen-item"
 | 
			
		||||
        href={link}
 | 
			
		||||
        target="_blank"
 | 
			
		||||
        rel="noreferrer"
 | 
			
		||||
      >
 | 
			
		||||
        <div className="WelcomeScreen-item__label">
 | 
			
		||||
          {icon}
 | 
			
		||||
          {label}
 | 
			
		||||
        </div>
 | 
			
		||||
      </a>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <button className="WelcomeScreen-item" type="button" onClick={onClick}>
 | 
			
		||||
      <div className="WelcomeScreen-item__label">
 | 
			
		||||
        {icon}
 | 
			
		||||
        {label}
 | 
			
		||||
      </div>
 | 
			
		||||
      {shortcut && (
 | 
			
		||||
        <div className="WelcomeScreen-item__shortcut">{shortcut}</div>
 | 
			
		||||
      )}
 | 
			
		||||
    </button>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const WelcomeScreen = ({
 | 
			
		||||
  appState,
 | 
			
		||||
  actionManager,
 | 
			
		||||
}: {
 | 
			
		||||
  appState: AppState;
 | 
			
		||||
  actionManager: ActionManager;
 | 
			
		||||
}) => {
 | 
			
		||||
  const [, setCollabDialogShown] = useAtom(collabDialogShownAtom);
 | 
			
		||||
 | 
			
		||||
  let subheadingJSX;
 | 
			
		||||
 | 
			
		||||
  if (isExcalidrawPlusSignedUser) {
 | 
			
		||||
    subheadingJSX = t("welcomeScreen.switchToPlusApp")
 | 
			
		||||
      .split(/(Excalidraw\+)/)
 | 
			
		||||
      .map((bit, idx) => {
 | 
			
		||||
        if (bit === "Excalidraw+") {
 | 
			
		||||
          return (
 | 
			
		||||
            <a
 | 
			
		||||
              style={{ pointerEvents: "all" }}
 | 
			
		||||
              href={`${process.env.REACT_APP_PLUS_APP}?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenSignedInUser`}
 | 
			
		||||
              key={idx}
 | 
			
		||||
            >
 | 
			
		||||
              Excalidraw+
 | 
			
		||||
            </a>
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
        return bit;
 | 
			
		||||
      });
 | 
			
		||||
  } else {
 | 
			
		||||
    subheadingJSX = t("welcomeScreen.data");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="WelcomeScreen-container">
 | 
			
		||||
      <div className="WelcomeScreen-logo virgil WelcomeScreen-decor">
 | 
			
		||||
        {ExcalLogo} Excalidraw
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="virgil WelcomeScreen-decor WelcomeScreen-decor--subheading">
 | 
			
		||||
        {subheadingJSX}
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="WelcomeScreen-items">
 | 
			
		||||
        {!appState.viewModeEnabled && (
 | 
			
		||||
          <WelcomeScreenItem
 | 
			
		||||
            // TODO barnabasmolnar/editor-redesign
 | 
			
		||||
            // do we want the internationalized labels here that are currently
 | 
			
		||||
            // in use elsewhere or new ones?
 | 
			
		||||
            label={t("buttons.load")}
 | 
			
		||||
            onClick={() => actionManager.executeAction(actionLoadScene)}
 | 
			
		||||
            shortcut={getShortcutFromShortcutName("loadScene")}
 | 
			
		||||
            icon={LoadIcon}
 | 
			
		||||
          />
 | 
			
		||||
        )}
 | 
			
		||||
        <WelcomeScreenItem
 | 
			
		||||
          label={t("labels.liveCollaboration")}
 | 
			
		||||
          shortcut={null}
 | 
			
		||||
          onClick={() => setCollabDialogShown(true)}
 | 
			
		||||
          icon={UsersIcon}
 | 
			
		||||
        />
 | 
			
		||||
        <WelcomeScreenItem
 | 
			
		||||
          onClick={() => actionManager.executeAction(actionShortcuts)}
 | 
			
		||||
          label={t("helpDialog.title")}
 | 
			
		||||
          shortcut="?"
 | 
			
		||||
          icon={HelpIcon}
 | 
			
		||||
        />
 | 
			
		||||
        {!isExcalidrawPlusSignedUser && (
 | 
			
		||||
          <WelcomeScreenItem
 | 
			
		||||
            link="https://plus.excalidraw.com/plus?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenGuest"
 | 
			
		||||
            label="Try Excalidraw Plus!"
 | 
			
		||||
            shortcut={null}
 | 
			
		||||
            icon={PlusPromoIcon}
 | 
			
		||||
          />
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default WelcomeScreen;
 | 
			
		||||
							
								
								
									
										11
									
								
								src/components/WelcomeScreenDecor.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/components/WelcomeScreenDecor.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
import { ReactNode } from "react";
 | 
			
		||||
 | 
			
		||||
const WelcomeScreenDecor = ({
 | 
			
		||||
  children,
 | 
			
		||||
  shouldRender,
 | 
			
		||||
}: {
 | 
			
		||||
  children: ReactNode;
 | 
			
		||||
  shouldRender: boolean;
 | 
			
		||||
}) => (shouldRender ? <>{children}</> : null);
 | 
			
		||||
 | 
			
		||||
export default WelcomeScreenDecor;
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user