feat: editor redesign 🔥 (#5780)
* Placed eraser into shape switcher (top toolbar). Redesigned top toolbar. * Redesigned zoom and undo-redo buttons. * Started redesigning left toolbar. * Redesigned help dialog. * Colour picker now somewhat in line with new design * [WIP] Changed a bunch of icons. TODO: organise new icons. * [WIP] Organised a bunch of icons. Still some to do * [WIP] Started working on hamburger menu. * Fixed some bugs with hamburger menu. * Menu and left toolbar positioning. * Added some more items to hamburger menu. * Changed some icons. * Modal/dialog styling & bunch of fixes. * Some more dialog improvements & fixes. * Mobile menu changes. * Menu can now be closed with outside click. * Collab avatars and button changes. * Icon sizing. Left toolbar positioning. * Implemented welcome screen rendering logic. * [WIP] Welcome screen content + design. * Some more welcome screen content and design. * Merge fixes. * Tweaked icon set. * Welcome screen darkmode fix. * Content updates. * Various small fixes & adjustments. Moved language selection into menu. Fixed some problematic icons. Slightly moved encryption icon. * Sidebar header redesign. * Libraries content rendering logic + some styling. * Somem more library sidebar styling. * Publish library dialog styling. * scroll-back-to-content btn styling * ColorPicker positioning. * Library button styling. * ColorPicker positioning "fix". * Misc adjustments. * PenMode button changes. * Trying to make mobile somewhat usable. * Added a couple of icons. * Added some shortcuts. * Prevent welcome screen flickering. Fix issue with welcome screen interactivity. Don't show sidebar button when docked. * Icon sizing on smaller screens. * Sidebar styling changes. * Alignment button... well... alignments. * Fix inconsistent padding in left toolbar. * HintViewer changes. * Hamburger menu changes. * Move encryption badge back to its original pos. * Arrowhead changes. Active state, colours + stronger shadow. * Added new custom font. * Fixed bug with library button not rendering. * Fixed issue with lang selection colours. * Add tooltips for undo, redo. * Address some dark mode contrast issues. * (Re)introduce counter for selectedItems in sidebar * [WIP] Tweaked bounding box colour & padding. * Dashed bounding box for remote clients. * Some more bounding box tweaks. * Removed docking animation for now... * Address some RTL issues. * Welcome screen responsiveness. * use lighter selection color in dark mode & align naming * use rounded corners for transform handles * use lighter gray for welcomeScreen text in dark mode * disable selection on dialog buttons * change selection button icon * fix library item width being flexible * library: visually align spinner with first section heading * lint * fix scrollbar color in dark mode & make thinner * adapt properties panel max-height * add shrotcut label to save-to-current-file * fix unrelated `useOutsideClick` firing for active modal * add promo color to e+ menu item * fix type * lowered button size * fix transform handles raidus not accounting for zoom * attempt fix for excal logo on safari * final fix for excal logo on safari * fixing fhd resolution button sized * remove TODO shortcut * Collab related styling changes. Expanding avatar list no longer offsets top toolbar. Added active state & collaborator count badge for collab button. * Tweaked collab button active colours. * Added active style to collab btn in hamburger menu * Remove unnecessary comment. * Added back promo link for non (signed in) E+ users * Go to E+ button added for signed in E+ users. * Close menu & dropdown on modal close. * tweak icons & fix rendering on smaller sizes [part one] * align welcomeScreen icons with other UI * switch icon resize mq to `device-width` * disable welcomeScreen items `:hover` when selecting on canvas * change selection box color and style * reduce selection padding and fix group selection styling * improve collab cursor styling - make name borders round - hide status when "active" - remove black/gray colors * add Twitter to hamburger menu * align collab button * add shortcut for image export dialog * revert yarn.lock * fix more tabler icons * slightly better-looking penMode button * change penMode button & tooltip * revert hamburger menu icon * align padding on lang picker & canvas bg * updated robot txt to allow twitter bot and fb bot * added new OG and tweaked the OG state * add tooltip to collab button * align style for scroll-to-content button * fix pointer-events around toolbar * fix decor arrow positioning and RTL * fix welcomeScreen-item active state in dark mode * change `load` button copy * prevent shadow anim when opening a docked sidebar * update E+ links ga params * show redirect-to-eplus welcomeScreen subheading for signed-in users * make more generic * add ga for eplus redirect button * change copy and icons for hamburger export buttons * update snaps * trim the username to account for trailing spaces * tweaks around decor breakpoints * fix linear element editor test * remove .env change * remove `it.only` Co-authored-by: dwelle <luzar.david@gmail.com> Co-authored-by: Maielo <maielo.mv@gmail.com> Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
This commit is contained in:
parent
4d26993c8f
commit
6334bd832f
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");
|
src: url("Cascadia.woff2");
|
||||||
font-display: swap;
|
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"
|
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="referrer" content="origin" />
|
||||||
|
|
||||||
<meta name="mobile-web-app-capable" content="yes" />
|
<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 -->
|
<!-- General tags -->
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="Excalidraw is a virtual collaborative whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them."
|
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 -->
|
<!-- to minimize white flash on load when user has dark mode enabled -->
|
||||||
@ -158,8 +166,8 @@
|
|||||||
body,
|
body,
|
||||||
html {
|
html {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
--ui-font: system-ui, BlinkMacSystemFont, -apple-system, Segoe UI,
|
--ui-font: Assistant, system-ui, BlinkMacSystemFont, -apple-system,
|
||||||
Roboto, Helvetica, Arial, sans-serif;
|
Segoe UI, Roboto, Helvetica, Arial, sans-serif;
|
||||||
font-family: var(--ui-font);
|
font-family: var(--ui-font);
|
||||||
-webkit-text-size-adjust: 100%;
|
-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: *
|
user-agent: *
|
||||||
Allow: /$
|
Allow: /$
|
||||||
Disallow: /
|
Disallow: /
|
||||||
|
@ -60,7 +60,7 @@ export const actionAlignTop = register({
|
|||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!enableActionGroup(elements, appState)}
|
hidden={!enableActionGroup(elements, appState)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={<AlignTopIcon theme={appState.theme} />}
|
icon={AlignTopIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
title={`${t("labels.alignTop")} — ${getShortcutKey(
|
title={`${t("labels.alignTop")} — ${getShortcutKey(
|
||||||
"CtrlOrCmd+Shift+Up",
|
"CtrlOrCmd+Shift+Up",
|
||||||
@ -90,7 +90,7 @@ export const actionAlignBottom = register({
|
|||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!enableActionGroup(elements, appState)}
|
hidden={!enableActionGroup(elements, appState)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={<AlignBottomIcon theme={appState.theme} />}
|
icon={AlignBottomIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
title={`${t("labels.alignBottom")} — ${getShortcutKey(
|
title={`${t("labels.alignBottom")} — ${getShortcutKey(
|
||||||
"CtrlOrCmd+Shift+Down",
|
"CtrlOrCmd+Shift+Down",
|
||||||
@ -120,7 +120,7 @@ export const actionAlignLeft = register({
|
|||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!enableActionGroup(elements, appState)}
|
hidden={!enableActionGroup(elements, appState)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={<AlignLeftIcon theme={appState.theme} />}
|
icon={AlignLeftIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
title={`${t("labels.alignLeft")} — ${getShortcutKey(
|
title={`${t("labels.alignLeft")} — ${getShortcutKey(
|
||||||
"CtrlOrCmd+Shift+Left",
|
"CtrlOrCmd+Shift+Left",
|
||||||
@ -151,7 +151,7 @@ export const actionAlignRight = register({
|
|||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!enableActionGroup(elements, appState)}
|
hidden={!enableActionGroup(elements, appState)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={<AlignRightIcon theme={appState.theme} />}
|
icon={AlignRightIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
title={`${t("labels.alignRight")} — ${getShortcutKey(
|
title={`${t("labels.alignRight")} — ${getShortcutKey(
|
||||||
"CtrlOrCmd+Shift+Right",
|
"CtrlOrCmd+Shift+Right",
|
||||||
@ -180,7 +180,7 @@ export const actionAlignVerticallyCentered = register({
|
|||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!enableActionGroup(elements, appState)}
|
hidden={!enableActionGroup(elements, appState)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={<CenterVerticallyIcon theme={appState.theme} />}
|
icon={CenterVerticallyIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
title={t("labels.centerVertically")}
|
title={t("labels.centerVertically")}
|
||||||
aria-label={t("labels.centerVertically")}
|
aria-label={t("labels.centerVertically")}
|
||||||
@ -206,7 +206,7 @@ export const actionAlignHorizontallyCentered = register({
|
|||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!enableActionGroup(elements, appState)}
|
hidden={!enableActionGroup(elements, appState)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={<CenterHorizontallyIcon theme={appState.theme} />}
|
icon={CenterHorizontallyIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
title={t("labels.centerHorizontally")}
|
title={t("labels.centerHorizontally")}
|
||||||
aria-label={t("labels.centerHorizontally")}
|
aria-label={t("labels.centerHorizontally")}
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
import { ColorPicker } from "../components/ColorPicker";
|
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 { ToolButton } from "../components/ToolButton";
|
||||||
import { DarkModeToggle } from "../components/DarkModeToggle";
|
|
||||||
import { MIN_ZOOM, THEME, ZOOM_STEP } from "../constants";
|
import { MIN_ZOOM, THEME, ZOOM_STEP } from "../constants";
|
||||||
import { getCommonBounds, getNonDeletedElements } from "../element";
|
import { getCommonBounds, getNonDeletedElements } from "../element";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
@ -18,6 +23,8 @@ import { newElementWith } from "../element/mutateElement";
|
|||||||
import { getDefaultAppState, isEraserActive } from "../appState";
|
import { getDefaultAppState, isEraserActive } from "../appState";
|
||||||
import ClearCanvas from "../components/ClearCanvas";
|
import ClearCanvas from "../components/ClearCanvas";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import MenuItem from "../components/MenuItem";
|
||||||
|
import { getShortcutFromShortcutName } from "./shortcuts";
|
||||||
|
|
||||||
export const actionChangeViewBackgroundColor = register({
|
export const actionChangeViewBackgroundColor = register({
|
||||||
name: "changeViewBackgroundColor",
|
name: "changeViewBackgroundColor",
|
||||||
@ -103,13 +110,13 @@ export const actionZoomIn = register({
|
|||||||
PanelComponent: ({ updateData }) => (
|
PanelComponent: ({ updateData }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
type="button"
|
type="button"
|
||||||
icon={zoomIn}
|
className="zoom-in-button zoom-button"
|
||||||
|
icon={ZoomInIcon}
|
||||||
title={`${t("buttons.zoomIn")} — ${getShortcutKey("CtrlOrCmd++")}`}
|
title={`${t("buttons.zoomIn")} — ${getShortcutKey("CtrlOrCmd++")}`}
|
||||||
aria-label={t("buttons.zoomIn")}
|
aria-label={t("buttons.zoomIn")}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
updateData(null);
|
updateData(null);
|
||||||
}}
|
}}
|
||||||
size="small"
|
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
@ -139,13 +146,13 @@ export const actionZoomOut = register({
|
|||||||
PanelComponent: ({ updateData }) => (
|
PanelComponent: ({ updateData }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
type="button"
|
type="button"
|
||||||
icon={zoomOut}
|
className="zoom-out-button zoom-button"
|
||||||
|
icon={ZoomOutIcon}
|
||||||
title={`${t("buttons.zoomOut")} — ${getShortcutKey("CtrlOrCmd+-")}`}
|
title={`${t("buttons.zoomOut")} — ${getShortcutKey("CtrlOrCmd+-")}`}
|
||||||
aria-label={t("buttons.zoomOut")}
|
aria-label={t("buttons.zoomOut")}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
updateData(null);
|
updateData(null);
|
||||||
}}
|
}}
|
||||||
size="small"
|
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
@ -176,13 +183,12 @@ export const actionResetZoom = register({
|
|||||||
<Tooltip label={t("buttons.resetZoom")} style={{ height: "100%" }}>
|
<Tooltip label={t("buttons.resetZoom")} style={{ height: "100%" }}>
|
||||||
<ToolButton
|
<ToolButton
|
||||||
type="button"
|
type="button"
|
||||||
className="reset-zoom-button"
|
className="reset-zoom-button zoom-button"
|
||||||
title={t("buttons.resetZoom")}
|
title={t("buttons.resetZoom")}
|
||||||
aria-label={t("buttons.resetZoom")}
|
aria-label={t("buttons.resetZoom")}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
updateData(null);
|
updateData(null);
|
||||||
}}
|
}}
|
||||||
size="small"
|
|
||||||
>
|
>
|
||||||
{(appState.zoom.value * 100).toFixed(0)}%
|
{(appState.zoom.value * 100).toFixed(0)}%
|
||||||
</ToolButton>
|
</ToolButton>
|
||||||
@ -288,14 +294,19 @@ export const actionToggleTheme = register({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ appState, updateData }) => (
|
PanelComponent: ({ appState, updateData }) => (
|
||||||
<div style={{ marginInlineStart: "0.25rem" }}>
|
<MenuItem
|
||||||
<DarkModeToggle
|
label={
|
||||||
value={appState.theme}
|
appState.theme === "dark"
|
||||||
onChange={(theme) => {
|
? t("buttons.lightMode")
|
||||||
updateData(theme);
|
: 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")}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
),
|
),
|
||||||
keyTest: (event) => event.altKey && event.shiftKey && event.code === CODES.D,
|
keyTest: (event) => event.altKey && event.shiftKey && event.code === CODES.D,
|
||||||
});
|
});
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { isSomeElementSelected } from "../scene";
|
import { isSomeElementSelected } from "../scene";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import { trash } from "../components/icons";
|
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { getNonDeletedElements } from "../element";
|
import { getNonDeletedElements } from "../element";
|
||||||
@ -13,6 +12,7 @@ import { LinearElementEditor } from "../element/linearElementEditor";
|
|||||||
import { fixBindingsAfterDeletion } from "../element/binding";
|
import { fixBindingsAfterDeletion } from "../element/binding";
|
||||||
import { isBoundToContainer } from "../element/typeChecks";
|
import { isBoundToContainer } from "../element/typeChecks";
|
||||||
import { updateActiveTool } from "../utils";
|
import { updateActiveTool } from "../utils";
|
||||||
|
import { TrashIcon } from "../components/icons";
|
||||||
|
|
||||||
const deleteSelectedElements = (
|
const deleteSelectedElements = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
@ -149,7 +149,7 @@ export const actionDeleteSelected = register({
|
|||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
type="button"
|
type="button"
|
||||||
icon={trash}
|
icon={TrashIcon}
|
||||||
title={t("labels.delete")}
|
title={t("labels.delete")}
|
||||||
aria-label={t("labels.delete")}
|
aria-label={t("labels.delete")}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
|
@ -56,7 +56,7 @@ export const distributeHorizontally = register({
|
|||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!enableActionGroup(elements, appState)}
|
hidden={!enableActionGroup(elements, appState)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={<DistributeHorizontallyIcon theme={appState.theme} />}
|
icon={DistributeHorizontallyIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
title={`${t("labels.distributeHorizontally")} — ${getShortcutKey(
|
title={`${t("labels.distributeHorizontally")} — ${getShortcutKey(
|
||||||
"Alt+H",
|
"Alt+H",
|
||||||
@ -86,7 +86,7 @@ export const distributeVertically = register({
|
|||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!enableActionGroup(elements, appState)}
|
hidden={!enableActionGroup(elements, appState)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={<DistributeVerticallyIcon theme={appState.theme} />}
|
icon={DistributeVerticallyIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
title={`${t("labels.distributeVertically")} — ${getShortcutKey("Alt+V")}`}
|
title={`${t("labels.distributeVertically")} — ${getShortcutKey("Alt+V")}`}
|
||||||
aria-label={t("labels.distributeVertically")}
|
aria-label={t("labels.distributeVertically")}
|
||||||
|
@ -4,7 +4,6 @@ import { ExcalidrawElement } from "../element/types";
|
|||||||
import { duplicateElement, getNonDeletedElements } from "../element";
|
import { duplicateElement, getNonDeletedElements } from "../element";
|
||||||
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import { clone } from "../components/icons";
|
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { arrayToMap, getShortcutKey } from "../utils";
|
import { arrayToMap, getShortcutKey } from "../utils";
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
@ -19,6 +18,7 @@ import { ActionResult } from "./types";
|
|||||||
import { GRID_SIZE } from "../constants";
|
import { GRID_SIZE } from "../constants";
|
||||||
import { bindTextToShapeAfterDuplication } from "../element/textElement";
|
import { bindTextToShapeAfterDuplication } from "../element/textElement";
|
||||||
import { isBoundToContainer } from "../element/typeChecks";
|
import { isBoundToContainer } from "../element/typeChecks";
|
||||||
|
import { DuplicateIcon } from "../components/icons";
|
||||||
|
|
||||||
export const actionDuplicateSelection = register({
|
export const actionDuplicateSelection = register({
|
||||||
name: "duplicateSelection",
|
name: "duplicateSelection",
|
||||||
@ -49,7 +49,7 @@ export const actionDuplicateSelection = register({
|
|||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
type="button"
|
type="button"
|
||||||
icon={clone}
|
icon={DuplicateIcon}
|
||||||
title={`${t("labels.duplicateSelection")} — ${getShortcutKey(
|
title={`${t("labels.duplicateSelection")} — ${getShortcutKey(
|
||||||
"CtrlOrCmd+D",
|
"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 { ProjectName } from "../components/ProjectName";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import "../components/ToolIcon.scss";
|
import "../components/ToolIcon.scss";
|
||||||
@ -19,6 +19,8 @@ import { ActiveFile } from "../components/ActiveFile";
|
|||||||
import { isImageFileHandle } from "../data/blob";
|
import { isImageFileHandle } from "../data/blob";
|
||||||
import { nativeFileSystemSupported } from "../data/filesystem";
|
import { nativeFileSystemSupported } from "../data/filesystem";
|
||||||
import { Theme } from "../element/types";
|
import { Theme } from "../element/types";
|
||||||
|
import MenuItem from "../components/MenuItem";
|
||||||
|
import { getShortcutFromShortcutName } from "./shortcuts";
|
||||||
|
|
||||||
export const actionChangeProjectName = register({
|
export const actionChangeProjectName = register({
|
||||||
name: "changeProjectName",
|
name: "changeProjectName",
|
||||||
@ -245,14 +247,12 @@ export const actionLoadScene = register({
|
|||||||
},
|
},
|
||||||
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.O,
|
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.O,
|
||||||
PanelComponent: ({ updateData }) => (
|
PanelComponent: ({ updateData }) => (
|
||||||
<ToolButton
|
<MenuItem
|
||||||
type="button"
|
label={t("buttons.load")}
|
||||||
icon={load}
|
icon={LoadIcon}
|
||||||
title={t("buttons.load")}
|
|
||||||
aria-label={t("buttons.load")}
|
|
||||||
showAriaLabel={useDevice().isMobile}
|
|
||||||
onClick={updateData}
|
onClick={updateData}
|
||||||
data-testid="load-button"
|
dataTestId="load-button"
|
||||||
|
shortcut={getShortcutFromShortcutName("loadScene")}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Action, ActionResult } from "./types";
|
import { Action, ActionResult } from "./types";
|
||||||
import { undo, redo } from "../components/icons";
|
import { UndoIcon, RedoIcon } from "../components/icons";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import History, { HistoryEntry } from "../history";
|
import History, { HistoryEntry } from "../history";
|
||||||
@ -72,7 +72,7 @@ export const createUndoAction: ActionCreator = (history) => ({
|
|||||||
PanelComponent: ({ updateData, data }) => (
|
PanelComponent: ({ updateData, data }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
type="button"
|
type="button"
|
||||||
icon={undo}
|
icon={UndoIcon}
|
||||||
aria-label={t("buttons.undo")}
|
aria-label={t("buttons.undo")}
|
||||||
onClick={updateData}
|
onClick={updateData}
|
||||||
size={data?.size || "medium"}
|
size={data?.size || "medium"}
|
||||||
@ -94,7 +94,7 @@ export const createRedoAction: ActionCreator = (history) => ({
|
|||||||
PanelComponent: ({ updateData, data }) => (
|
PanelComponent: ({ updateData, data }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
type="button"
|
type="button"
|
||||||
icon={redo}
|
icon={RedoIcon}
|
||||||
aria-label={t("buttons.redo")}
|
aria-label={t("buttons.redo")}
|
||||||
onClick={updateData}
|
onClick={updateData}
|
||||||
size={data?.size || "medium"}
|
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 { ToolButton } from "../components/ToolButton";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { showSelectedShapeActions, getNonDeletedElements } from "../element";
|
import { showSelectedShapeActions, getNonDeletedElements } from "../element";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { allowFullScreen, exitFullScreen, isFullScreen } from "../utils";
|
import { allowFullScreen, exitFullScreen, isFullScreen } from "../utils";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { HelpIcon } from "../components/HelpIcon";
|
import { HelpButton } from "../components/HelpButton";
|
||||||
|
import MenuItem from "../components/MenuItem";
|
||||||
|
|
||||||
export const actionToggleCanvasMenu = register({
|
export const actionToggleCanvasMenu = register({
|
||||||
name: "toggleCanvasMenu",
|
name: "toggleCanvasMenu",
|
||||||
@ -20,7 +21,7 @@ export const actionToggleCanvasMenu = register({
|
|||||||
PanelComponent: ({ appState, updateData }) => (
|
PanelComponent: ({ appState, updateData }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
type="button"
|
type="button"
|
||||||
icon={menu}
|
icon={HamburgerMenuIcon}
|
||||||
aria-label={t("buttons.menu")}
|
aria-label={t("buttons.menu")}
|
||||||
onClick={updateData}
|
onClick={updateData}
|
||||||
selected={appState.openMenu === "canvas"}
|
selected={appState.openMenu === "canvas"}
|
||||||
@ -74,19 +75,28 @@ export const actionShortcuts = register({
|
|||||||
name: "toggleShortcuts",
|
name: "toggleShortcuts",
|
||||||
trackEvent: { category: "menu", action: "toggleHelpDialog" },
|
trackEvent: { category: "menu", action: "toggleHelpDialog" },
|
||||||
perform: (_elements, appState, _, { focusContainer }) => {
|
perform: (_elements, appState, _, { focusContainer }) => {
|
||||||
if (appState.showHelpDialog) {
|
if (appState.openDialog === "help") {
|
||||||
focusContainer();
|
focusContainer();
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
showHelpDialog: !appState.showHelpDialog,
|
openDialog: appState.openDialog === "help" ? null : "help",
|
||||||
},
|
},
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ updateData }) => (
|
PanelComponent: ({ updateData, isInHamburgerMenu }) =>
|
||||||
<HelpIcon title={t("helpDialog.title")} onClick={updateData} />
|
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,
|
keyTest: (event) => event.key === KEYS.QUESTION_MARK,
|
||||||
});
|
});
|
||||||
|
@ -2,37 +2,41 @@ import { AppState } from "../../src/types";
|
|||||||
import { ButtonIconSelect } from "../components/ButtonIconSelect";
|
import { ButtonIconSelect } from "../components/ButtonIconSelect";
|
||||||
import { ColorPicker } from "../components/ColorPicker";
|
import { ColorPicker } from "../components/ColorPicker";
|
||||||
import { IconPicker } from "../components/IconPicker";
|
import { IconPicker } from "../components/IconPicker";
|
||||||
|
// TODO barnabasmolnar/editor-redesign
|
||||||
|
// TextAlignTopIcon, TextAlignBottomIcon,TextAlignMiddleIcon,
|
||||||
|
// ArrowHead icons
|
||||||
import {
|
import {
|
||||||
ArrowheadArrowIcon,
|
ArrowheadArrowIcon,
|
||||||
ArrowheadBarIcon,
|
ArrowheadBarIcon,
|
||||||
ArrowheadDotIcon,
|
ArrowheadDotIcon,
|
||||||
ArrowheadTriangleIcon,
|
ArrowheadTriangleIcon,
|
||||||
ArrowheadNoneIcon,
|
ArrowheadNoneIcon,
|
||||||
EdgeRoundIcon,
|
|
||||||
EdgeSharpIcon,
|
|
||||||
FillCrossHatchIcon,
|
|
||||||
FillHachureIcon,
|
|
||||||
FillSolidIcon,
|
|
||||||
FontFamilyCodeIcon,
|
|
||||||
FontFamilyHandDrawnIcon,
|
|
||||||
FontFamilyNormalIcon,
|
|
||||||
FontSizeExtraLargeIcon,
|
|
||||||
FontSizeLargeIcon,
|
|
||||||
FontSizeMediumIcon,
|
|
||||||
FontSizeSmallIcon,
|
|
||||||
SloppinessArchitectIcon,
|
|
||||||
SloppinessArtistIcon,
|
|
||||||
SloppinessCartoonistIcon,
|
|
||||||
StrokeStyleDashedIcon,
|
StrokeStyleDashedIcon,
|
||||||
StrokeStyleDottedIcon,
|
StrokeStyleDottedIcon,
|
||||||
StrokeStyleSolidIcon,
|
|
||||||
StrokeWidthIcon,
|
|
||||||
TextAlignCenterIcon,
|
|
||||||
TextAlignLeftIcon,
|
|
||||||
TextAlignRightIcon,
|
|
||||||
TextAlignTopIcon,
|
TextAlignTopIcon,
|
||||||
TextAlignBottomIcon,
|
TextAlignBottomIcon,
|
||||||
TextAlignMiddleIcon,
|
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";
|
} from "../components/icons";
|
||||||
import {
|
import {
|
||||||
DEFAULT_FONT_FAMILY,
|
DEFAULT_FONT_FAMILY,
|
||||||
@ -307,17 +311,17 @@ export const actionChangeFillStyle = register({
|
|||||||
{
|
{
|
||||||
value: "hachure",
|
value: "hachure",
|
||||||
text: t("labels.hachure"),
|
text: t("labels.hachure"),
|
||||||
icon: <FillHachureIcon theme={appState.theme} />,
|
icon: FillHachureIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "cross-hatch",
|
value: "cross-hatch",
|
||||||
text: t("labels.crossHatch"),
|
text: t("labels.crossHatch"),
|
||||||
icon: <FillCrossHatchIcon theme={appState.theme} />,
|
icon: FillCrossHatchIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "solid",
|
value: "solid",
|
||||||
text: t("labels.solid"),
|
text: t("labels.solid"),
|
||||||
icon: <FillSolidIcon theme={appState.theme} />,
|
icon: FillSolidIcon,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
group="fill"
|
group="fill"
|
||||||
@ -358,17 +362,17 @@ export const actionChangeStrokeWidth = register({
|
|||||||
{
|
{
|
||||||
value: 1,
|
value: 1,
|
||||||
text: t("labels.thin"),
|
text: t("labels.thin"),
|
||||||
icon: <StrokeWidthIcon theme={appState.theme} strokeWidth={2} />,
|
icon: StrokeWidthBaseIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 2,
|
value: 2,
|
||||||
text: t("labels.bold"),
|
text: t("labels.bold"),
|
||||||
icon: <StrokeWidthIcon theme={appState.theme} strokeWidth={6} />,
|
icon: StrokeWidthBoldIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 4,
|
value: 4,
|
||||||
text: t("labels.extraBold"),
|
text: t("labels.extraBold"),
|
||||||
icon: <StrokeWidthIcon theme={appState.theme} strokeWidth={10} />,
|
icon: StrokeWidthExtraBoldIcon,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
value={getFormValue(
|
value={getFormValue(
|
||||||
@ -407,17 +411,17 @@ export const actionChangeSloppiness = register({
|
|||||||
{
|
{
|
||||||
value: 0,
|
value: 0,
|
||||||
text: t("labels.architect"),
|
text: t("labels.architect"),
|
||||||
icon: <SloppinessArchitectIcon theme={appState.theme} />,
|
icon: SloppinessArchitectIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 1,
|
value: 1,
|
||||||
text: t("labels.artist"),
|
text: t("labels.artist"),
|
||||||
icon: <SloppinessArtistIcon theme={appState.theme} />,
|
icon: SloppinessArtistIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 2,
|
value: 2,
|
||||||
text: t("labels.cartoonist"),
|
text: t("labels.cartoonist"),
|
||||||
icon: <SloppinessCartoonistIcon theme={appState.theme} />,
|
icon: SloppinessCartoonistIcon,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
value={getFormValue(
|
value={getFormValue(
|
||||||
@ -455,17 +459,17 @@ export const actionChangeStrokeStyle = register({
|
|||||||
{
|
{
|
||||||
value: "solid",
|
value: "solid",
|
||||||
text: t("labels.strokeStyle_solid"),
|
text: t("labels.strokeStyle_solid"),
|
||||||
icon: <StrokeStyleSolidIcon theme={appState.theme} />,
|
icon: StrokeWidthBaseIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "dashed",
|
value: "dashed",
|
||||||
text: t("labels.strokeStyle_dashed"),
|
text: t("labels.strokeStyle_dashed"),
|
||||||
icon: <StrokeStyleDashedIcon theme={appState.theme} />,
|
icon: StrokeStyleDashedIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "dotted",
|
value: "dotted",
|
||||||
text: t("labels.strokeStyle_dotted"),
|
text: t("labels.strokeStyle_dotted"),
|
||||||
icon: <StrokeStyleDottedIcon theme={appState.theme} />,
|
icon: StrokeStyleDottedIcon,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
value={getFormValue(
|
value={getFormValue(
|
||||||
@ -535,25 +539,25 @@ export const actionChangeFontSize = register({
|
|||||||
{
|
{
|
||||||
value: 16,
|
value: 16,
|
||||||
text: t("labels.small"),
|
text: t("labels.small"),
|
||||||
icon: <FontSizeSmallIcon theme={appState.theme} />,
|
icon: FontSizeSmallIcon,
|
||||||
testId: "fontSize-small",
|
testId: "fontSize-small",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 20,
|
value: 20,
|
||||||
text: t("labels.medium"),
|
text: t("labels.medium"),
|
||||||
icon: <FontSizeMediumIcon theme={appState.theme} />,
|
icon: FontSizeMediumIcon,
|
||||||
testId: "fontSize-medium",
|
testId: "fontSize-medium",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 28,
|
value: 28,
|
||||||
text: t("labels.large"),
|
text: t("labels.large"),
|
||||||
icon: <FontSizeLargeIcon theme={appState.theme} />,
|
icon: FontSizeLargeIcon,
|
||||||
testId: "fontSize-large",
|
testId: "fontSize-large",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 36,
|
value: 36,
|
||||||
text: t("labels.veryLarge"),
|
text: t("labels.veryLarge"),
|
||||||
icon: <FontSizeExtraLargeIcon theme={appState.theme} />,
|
icon: FontSizeExtraLargeIcon,
|
||||||
testId: "fontSize-veryLarge",
|
testId: "fontSize-veryLarge",
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
@ -658,17 +662,17 @@ export const actionChangeFontFamily = register({
|
|||||||
{
|
{
|
||||||
value: FONT_FAMILY.Virgil,
|
value: FONT_FAMILY.Virgil,
|
||||||
text: t("labels.handDrawn"),
|
text: t("labels.handDrawn"),
|
||||||
icon: <FontFamilyHandDrawnIcon theme={appState.theme} />,
|
icon: FreedrawIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: FONT_FAMILY.Helvetica,
|
value: FONT_FAMILY.Helvetica,
|
||||||
text: t("labels.normal"),
|
text: t("labels.normal"),
|
||||||
icon: <FontFamilyNormalIcon theme={appState.theme} />,
|
icon: FontFamilyNormalIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: FONT_FAMILY.Cascadia,
|
value: FONT_FAMILY.Cascadia,
|
||||||
text: t("labels.code"),
|
text: t("labels.code"),
|
||||||
icon: <FontFamilyCodeIcon theme={appState.theme} />,
|
icon: FontFamilyCodeIcon,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -739,17 +743,17 @@ export const actionChangeTextAlign = register({
|
|||||||
{
|
{
|
||||||
value: "left",
|
value: "left",
|
||||||
text: t("labels.left"),
|
text: t("labels.left"),
|
||||||
icon: <TextAlignLeftIcon theme={appState.theme} />,
|
icon: TextAlignLeftIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "center",
|
value: "center",
|
||||||
text: t("labels.center"),
|
text: t("labels.center"),
|
||||||
icon: <TextAlignCenterIcon theme={appState.theme} />,
|
icon: TextAlignCenterIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "right",
|
value: "right",
|
||||||
text: t("labels.right"),
|
text: t("labels.right"),
|
||||||
icon: <TextAlignRightIcon theme={appState.theme} />,
|
icon: TextAlignRightIcon,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
value={getFormValue(
|
value={getFormValue(
|
||||||
@ -882,12 +886,12 @@ export const actionChangeSharpness = register({
|
|||||||
{
|
{
|
||||||
value: "sharp",
|
value: "sharp",
|
||||||
text: t("labels.sharp"),
|
text: t("labels.sharp"),
|
||||||
icon: <EdgeSharpIcon theme={appState.theme} />,
|
icon: EdgeSharpIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "round",
|
value: "round",
|
||||||
text: t("labels.round"),
|
text: t("labels.round"),
|
||||||
icon: <EdgeRoundIcon theme={appState.theme} />,
|
icon: EdgeRoundIcon,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
value={getFormValue(
|
value={getFormValue(
|
||||||
@ -949,42 +953,38 @@ export const actionChangeArrowhead = register({
|
|||||||
return (
|
return (
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{t("labels.arrowheads")}</legend>
|
<legend>{t("labels.arrowheads")}</legend>
|
||||||
<div className="iconSelectList">
|
<div className="iconSelectList buttonList">
|
||||||
<IconPicker
|
<IconPicker
|
||||||
label="arrowhead_start"
|
label="arrowhead_start"
|
||||||
options={[
|
options={[
|
||||||
{
|
{
|
||||||
value: null,
|
value: null,
|
||||||
text: t("labels.arrowhead_none"),
|
text: t("labels.arrowhead_none"),
|
||||||
icon: <ArrowheadNoneIcon theme={appState.theme} />,
|
icon: ArrowheadNoneIcon,
|
||||||
keyBinding: "q",
|
keyBinding: "q",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "arrow",
|
value: "arrow",
|
||||||
text: t("labels.arrowhead_arrow"),
|
text: t("labels.arrowhead_arrow"),
|
||||||
icon: (
|
icon: <ArrowheadArrowIcon flip={!isRTL} />,
|
||||||
<ArrowheadArrowIcon theme={appState.theme} flip={!isRTL} />
|
|
||||||
),
|
|
||||||
keyBinding: "w",
|
keyBinding: "w",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "bar",
|
value: "bar",
|
||||||
text: t("labels.arrowhead_bar"),
|
text: t("labels.arrowhead_bar"),
|
||||||
icon: <ArrowheadBarIcon theme={appState.theme} flip={!isRTL} />,
|
icon: <ArrowheadBarIcon flip={!isRTL} />,
|
||||||
keyBinding: "e",
|
keyBinding: "e",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "dot",
|
value: "dot",
|
||||||
text: t("labels.arrowhead_dot"),
|
text: t("labels.arrowhead_dot"),
|
||||||
icon: <ArrowheadDotIcon theme={appState.theme} flip={!isRTL} />,
|
icon: <ArrowheadDotIcon flip={!isRTL} />,
|
||||||
keyBinding: "r",
|
keyBinding: "r",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "triangle",
|
value: "triangle",
|
||||||
text: t("labels.arrowhead_triangle"),
|
text: t("labels.arrowhead_triangle"),
|
||||||
icon: (
|
icon: <ArrowheadTriangleIcon flip={!isRTL} />,
|
||||||
<ArrowheadTriangleIcon theme={appState.theme} flip={!isRTL} />
|
|
||||||
),
|
|
||||||
keyBinding: "t",
|
keyBinding: "t",
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
@ -1007,34 +1007,30 @@ export const actionChangeArrowhead = register({
|
|||||||
value: null,
|
value: null,
|
||||||
text: t("labels.arrowhead_none"),
|
text: t("labels.arrowhead_none"),
|
||||||
keyBinding: "q",
|
keyBinding: "q",
|
||||||
icon: <ArrowheadNoneIcon theme={appState.theme} />,
|
icon: ArrowheadNoneIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "arrow",
|
value: "arrow",
|
||||||
text: t("labels.arrowhead_arrow"),
|
text: t("labels.arrowhead_arrow"),
|
||||||
keyBinding: "w",
|
keyBinding: "w",
|
||||||
icon: (
|
icon: <ArrowheadArrowIcon flip={isRTL} />,
|
||||||
<ArrowheadArrowIcon theme={appState.theme} flip={isRTL} />
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "bar",
|
value: "bar",
|
||||||
text: t("labels.arrowhead_bar"),
|
text: t("labels.arrowhead_bar"),
|
||||||
keyBinding: "e",
|
keyBinding: "e",
|
||||||
icon: <ArrowheadBarIcon theme={appState.theme} flip={isRTL} />,
|
icon: <ArrowheadBarIcon flip={isRTL} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "dot",
|
value: "dot",
|
||||||
text: t("labels.arrowhead_dot"),
|
text: t("labels.arrowhead_dot"),
|
||||||
keyBinding: "r",
|
keyBinding: "r",
|
||||||
icon: <ArrowheadDotIcon theme={appState.theme} flip={isRTL} />,
|
icon: <ArrowheadDotIcon flip={isRTL} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "triangle",
|
value: "triangle",
|
||||||
text: t("labels.arrowhead_triangle"),
|
text: t("labels.arrowhead_triangle"),
|
||||||
icon: (
|
icon: <ArrowheadTriangleIcon flip={isRTL} />,
|
||||||
<ArrowheadTriangleIcon theme={appState.theme} flip={isRTL} />
|
|
||||||
),
|
|
||||||
keyBinding: "t",
|
keyBinding: "t",
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
@ -10,10 +10,10 @@ import { t } from "../i18n";
|
|||||||
import { getShortcutKey } from "../utils";
|
import { getShortcutKey } from "../utils";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import {
|
import {
|
||||||
SendBackwardIcon,
|
|
||||||
BringToFrontIcon,
|
|
||||||
SendToBackIcon,
|
|
||||||
BringForwardIcon,
|
BringForwardIcon,
|
||||||
|
BringToFrontIcon,
|
||||||
|
SendBackwardIcon,
|
||||||
|
SendToBackIcon,
|
||||||
} from "../components/icons";
|
} from "../components/icons";
|
||||||
|
|
||||||
export const actionSendBackward = register({
|
export const actionSendBackward = register({
|
||||||
@ -39,7 +39,7 @@ export const actionSendBackward = register({
|
|||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
title={`${t("labels.sendBackward")} — ${getShortcutKey("CtrlOrCmd+[")}`}
|
title={`${t("labels.sendBackward")} — ${getShortcutKey("CtrlOrCmd+[")}`}
|
||||||
>
|
>
|
||||||
<SendBackwardIcon theme={appState.theme} />
|
{SendBackwardIcon}
|
||||||
</button>
|
</button>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
@ -67,7 +67,7 @@ export const actionBringForward = register({
|
|||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
title={`${t("labels.bringForward")} — ${getShortcutKey("CtrlOrCmd+]")}`}
|
title={`${t("labels.bringForward")} — ${getShortcutKey("CtrlOrCmd+]")}`}
|
||||||
>
|
>
|
||||||
<BringForwardIcon theme={appState.theme} />
|
{BringForwardIcon}
|
||||||
</button>
|
</button>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
@ -102,7 +102,7 @@ export const actionSendToBack = register({
|
|||||||
: getShortcutKey("CtrlOrCmd+Shift+[")
|
: getShortcutKey("CtrlOrCmd+Shift+[")
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<SendToBackIcon theme={appState.theme} />
|
{SendToBackIcon}
|
||||||
</button>
|
</button>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
@ -138,7 +138,7 @@ export const actionBringToFront = register({
|
|||||||
: getShortcutKey("CtrlOrCmd+Shift+]")
|
: getShortcutKey("CtrlOrCmd+Shift+]")
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<BringToFrontIcon theme={appState.theme} />
|
{BringToFrontIcon}
|
||||||
</button>
|
</button>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
@ -135,8 +135,13 @@ export class ActionManager {
|
|||||||
/**
|
/**
|
||||||
* @param data additional data sent to the PanelComponent
|
* @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;
|
const canvasActions = this.app.props.UIOptions.canvasActions;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.actions[name] &&
|
this.actions[name] &&
|
||||||
"PanelComponent" in this.actions[name] &&
|
"PanelComponent" in this.actions[name] &&
|
||||||
@ -169,6 +174,7 @@ export class ActionManager {
|
|||||||
updateData={updateData}
|
updateData={updateData}
|
||||||
appProps={this.app.props}
|
appProps={this.app.props}
|
||||||
data={data}
|
data={data}
|
||||||
|
isInHamburgerMenu={isInHamburgerMenu}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,11 @@ import { isDarwin } from "../keys";
|
|||||||
import { getShortcutKey } from "../utils";
|
import { getShortcutKey } from "../utils";
|
||||||
import { ActionName } from "./types";
|
import { ActionName } from "./types";
|
||||||
|
|
||||||
export type ShortcutName = SubtypeOf<
|
export type ShortcutName =
|
||||||
|
| SubtypeOf<
|
||||||
ActionName,
|
ActionName,
|
||||||
|
| "toggleTheme"
|
||||||
|
| "loadScene"
|
||||||
| "cut"
|
| "cut"
|
||||||
| "copy"
|
| "copy"
|
||||||
| "paste"
|
| "paste"
|
||||||
@ -30,9 +33,15 @@ export type ShortcutName = SubtypeOf<
|
|||||||
| "flipVertical"
|
| "flipVertical"
|
||||||
| "hyperlink"
|
| "hyperlink"
|
||||||
| "toggleLock"
|
| "toggleLock"
|
||||||
>;
|
>
|
||||||
|
| "saveScene"
|
||||||
|
| "imageExport";
|
||||||
|
|
||||||
const shortcutMap: Record<ShortcutName, string[]> = {
|
const shortcutMap: Record<ShortcutName, string[]> = {
|
||||||
|
toggleTheme: [getShortcutKey("Shit+Alt+D")],
|
||||||
|
saveScene: [getShortcutKey("CtrlOrCmd+S")],
|
||||||
|
loadScene: [getShortcutKey("CtrlOrCmd+O")],
|
||||||
|
imageExport: [getShortcutKey("CtrlOrCmd+Shift+E")],
|
||||||
cut: [getShortcutKey("CtrlOrCmd+X")],
|
cut: [getShortcutKey("CtrlOrCmd+X")],
|
||||||
copy: [getShortcutKey("CtrlOrCmd+C")],
|
copy: [getShortcutKey("CtrlOrCmd+C")],
|
||||||
paste: [getShortcutKey("CtrlOrCmd+V")],
|
paste: [getShortcutKey("CtrlOrCmd+V")],
|
||||||
|
@ -124,7 +124,9 @@ export type PanelComponentProps = {
|
|||||||
|
|
||||||
export interface Action {
|
export interface Action {
|
||||||
name: ActionName;
|
name: ActionName;
|
||||||
PanelComponent?: React.FC<PanelComponentProps>;
|
PanelComponent?: React.FC<
|
||||||
|
PanelComponentProps & { isInHamburgerMenu: boolean }
|
||||||
|
>;
|
||||||
perform: ActionFn;
|
perform: ActionFn;
|
||||||
keyPriority?: number;
|
keyPriority?: number;
|
||||||
keyTest?: (
|
keyTest?: (
|
||||||
|
@ -19,6 +19,7 @@ export const getDefaultAppState = (): Omit<
|
|||||||
"offsetTop" | "offsetLeft" | "width" | "height"
|
"offsetTop" | "offsetLeft" | "width" | "height"
|
||||||
> => {
|
> => {
|
||||||
return {
|
return {
|
||||||
|
showWelcomeScreen: false,
|
||||||
theme: THEME.LIGHT,
|
theme: THEME.LIGHT,
|
||||||
collaborators: new Map(),
|
collaborators: new Map(),
|
||||||
currentChartType: "bar",
|
currentChartType: "bar",
|
||||||
@ -67,6 +68,7 @@ export const getDefaultAppState = (): Omit<
|
|||||||
openMenu: null,
|
openMenu: null,
|
||||||
openPopup: null,
|
openPopup: null,
|
||||||
openSidebar: null,
|
openSidebar: null,
|
||||||
|
openDialog: null,
|
||||||
pasteDialog: { shown: false, data: null },
|
pasteDialog: { shown: false, data: null },
|
||||||
previousSelectedElementIds: {},
|
previousSelectedElementIds: {},
|
||||||
resizingElement: null,
|
resizingElement: null,
|
||||||
@ -77,7 +79,6 @@ export const getDefaultAppState = (): Omit<
|
|||||||
selectedGroupIds: {},
|
selectedGroupIds: {},
|
||||||
selectionElement: null,
|
selectionElement: null,
|
||||||
shouldCacheIgnoreZoom: false,
|
shouldCacheIgnoreZoom: false,
|
||||||
showHelpDialog: false,
|
|
||||||
showStats: false,
|
showStats: false,
|
||||||
startBoundElement: null,
|
startBoundElement: null,
|
||||||
suggestedBindings: [],
|
suggestedBindings: [],
|
||||||
@ -110,6 +111,7 @@ const APP_STATE_STORAGE_CONF = (<
|
|||||||
T extends Record<keyof AppState, Values>,
|
T extends Record<keyof AppState, Values>,
|
||||||
>(config: { [K in keyof T]: K extends keyof AppState ? T[K] : never }) =>
|
>(config: { [K in keyof T]: K extends keyof AppState ? T[K] : never }) =>
|
||||||
config)({
|
config)({
|
||||||
|
showWelcomeScreen: { browser: true, export: false, server: false },
|
||||||
theme: { browser: true, export: false, server: false },
|
theme: { browser: true, export: false, server: false },
|
||||||
collaborators: { browser: false, export: false, server: false },
|
collaborators: { browser: false, export: false, server: false },
|
||||||
currentChartType: { browser: true, 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 },
|
openMenu: { browser: true, export: false, server: false },
|
||||||
openPopup: { browser: false, export: false, server: false },
|
openPopup: { browser: false, export: false, server: false },
|
||||||
openSidebar: { browser: true, 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 },
|
pasteDialog: { browser: false, export: false, server: false },
|
||||||
previousSelectedElementIds: { browser: true, export: false, server: false },
|
previousSelectedElementIds: { browser: true, export: false, server: false },
|
||||||
resizingElement: { browser: false, 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 },
|
selectedGroupIds: { browser: true, export: false, server: false },
|
||||||
selectionElement: { browser: false, export: false, server: false },
|
selectionElement: { browser: false, export: false, server: false },
|
||||||
shouldCacheIgnoreZoom: { browser: true, 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 },
|
showStats: { browser: true, export: false, server: false },
|
||||||
startBoundElement: { browser: false, export: false, server: false },
|
startBoundElement: { browser: false, export: false, server: false },
|
||||||
suggestedBindings: { 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
|
// Naive way of getting an integer out of the clientId
|
||||||
const sum = clientId.split("").reduce((a, str) => a + str.charCodeAt(0), 0);
|
const sum = clientId.split("").reduce((a, str) => a + str.charCodeAt(0), 0);
|
||||||
|
|
||||||
// Skip transparent background.
|
// Skip transparent & gray colors
|
||||||
const backgrounds = colors.elementBackground.slice(1);
|
const backgrounds = colors.elementBackground.slice(3);
|
||||||
const strokes = colors.elementStroke.slice(1);
|
const strokes = colors.elementStroke.slice(3);
|
||||||
return {
|
return {
|
||||||
background: backgrounds[sum % backgrounds.length],
|
background: backgrounds[sum % backgrounds.length],
|
||||||
stroke: strokes[sum % strokes.length],
|
stroke: strokes[sum % strokes.length],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getClientInitials = (username?: string | null) => {
|
export const getClientInitials = (userName?: string | null) => {
|
||||||
if (!username) {
|
if (!userName) {
|
||||||
return "?";
|
return "?";
|
||||||
}
|
}
|
||||||
const names = username.trim().split(" ");
|
return userName.trim()[0].toUpperCase();
|
||||||
|
|
||||||
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();
|
|
||||||
};
|
};
|
||||||
|
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 { hasBoundTextElement, isBoundToContainer } from "../element/typeChecks";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { actionToggleZenMode } from "../actions";
|
import { actionToggleZenMode } from "../actions";
|
||||||
|
import "./Actions.scss";
|
||||||
|
import { Tooltip } from "./Tooltip";
|
||||||
|
|
||||||
export const SelectedShapeActions = ({
|
export const SelectedShapeActions = ({
|
||||||
appState,
|
appState,
|
||||||
@ -79,12 +81,16 @@ export const SelectedShapeActions = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="panelColumn">
|
<div className="panelColumn">
|
||||||
|
<div>
|
||||||
{((hasStrokeColor(appState.activeTool.type) &&
|
{((hasStrokeColor(appState.activeTool.type) &&
|
||||||
appState.activeTool.type !== "image" &&
|
appState.activeTool.type !== "image" &&
|
||||||
commonSelectedType !== "image") ||
|
commonSelectedType !== "image") ||
|
||||||
targetElements.some((element) => hasStrokeColor(element.type))) &&
|
targetElements.some((element) => hasStrokeColor(element.type))) &&
|
||||||
renderAction("changeStrokeColor")}
|
renderAction("changeStrokeColor")}
|
||||||
{showChangeBackgroundIcons && renderAction("changeBackgroundColor")}
|
</div>
|
||||||
|
{showChangeBackgroundIcons && (
|
||||||
|
<div>{renderAction("changeBackgroundColor")}</div>
|
||||||
|
)}
|
||||||
{showFillIcons && renderAction("changeFillStyle")}
|
{showFillIcons && renderAction("changeFillStyle")}
|
||||||
|
|
||||||
{(hasStrokeWidth(appState.activeTool.type) ||
|
{(hasStrokeWidth(appState.activeTool.type) ||
|
||||||
@ -163,7 +169,16 @@ export const SelectedShapeActions = ({
|
|||||||
)}
|
)}
|
||||||
{targetElements.length > 2 &&
|
{targetElements.length > 2 &&
|
||||||
renderAction("distributeHorizontally")}
|
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("alignTop")}
|
||||||
{renderAction("alignVerticallyCentered")}
|
{renderAction("alignVerticallyCentered")}
|
||||||
{renderAction("alignBottom")}
|
{renderAction("alignBottom")}
|
||||||
@ -203,22 +218,23 @@ export const ShapesSwitcher = ({
|
|||||||
appState: AppState;
|
appState: AppState;
|
||||||
}) => (
|
}) => (
|
||||||
<>
|
<>
|
||||||
{SHAPES.map(({ value, icon, key }, index) => {
|
{SHAPES.map(({ value, icon, key, fillable }, index) => {
|
||||||
|
const numberKey = value === "eraser" ? 0 : index + 1;
|
||||||
const label = t(`toolBar.${value}`);
|
const label = t(`toolBar.${value}`);
|
||||||
const letter = key && (typeof key === "string" ? key : key[0]);
|
const letter = key && (typeof key === "string" ? key : key[0]);
|
||||||
const shortcut = letter
|
const shortcut = letter
|
||||||
? `${capitalizeString(letter)} ${t("helpDialog.or")} ${index + 1}`
|
? `${capitalizeString(letter)} ${t("helpDialog.or")} ${numberKey}`
|
||||||
: `${index + 1}`;
|
: `${numberKey}`;
|
||||||
return (
|
return (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
className="Shape"
|
className={clsx("Shape", { fillable })}
|
||||||
key={value}
|
key={value}
|
||||||
type="radio"
|
type="radio"
|
||||||
icon={icon}
|
icon={icon}
|
||||||
checked={activeTool.type === value}
|
checked={activeTool.type === value}
|
||||||
name="editor-current-shape"
|
name="editor-current-shape"
|
||||||
title={`${capitalizeString(label)} — ${shortcut}`}
|
title={`${capitalizeString(label)} — ${shortcut}`}
|
||||||
keyBindingLabel={`${index + 1}`}
|
keyBindingLabel={`${numberKey}`}
|
||||||
aria-label={capitalizeString(label)}
|
aria-label={capitalizeString(label)}
|
||||||
aria-keyshortcuts={shortcut}
|
aria-keyshortcuts={shortcut}
|
||||||
data-testid={value}
|
data-testid={value}
|
||||||
@ -263,11 +279,11 @@ export const ZoomActions = ({
|
|||||||
renderAction: ActionManager["renderAction"];
|
renderAction: ActionManager["renderAction"];
|
||||||
zoom: Zoom;
|
zoom: Zoom;
|
||||||
}) => (
|
}) => (
|
||||||
<Stack.Col gap={1}>
|
<Stack.Col gap={1} className="zoom-actions">
|
||||||
<Stack.Row gap={1} align="center">
|
<Stack.Row align="center">
|
||||||
{renderAction("zoomOut")}
|
{renderAction("zoomOut")}
|
||||||
{renderAction("zoomIn")}
|
|
||||||
{renderAction("resetZoom")}
|
{renderAction("resetZoom")}
|
||||||
|
{renderAction("zoomIn")}
|
||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
</Stack.Col>
|
</Stack.Col>
|
||||||
);
|
);
|
||||||
@ -280,8 +296,12 @@ export const UndoRedoActions = ({
|
|||||||
className?: string;
|
className?: string;
|
||||||
}) => (
|
}) => (
|
||||||
<div className={`undo-redo-buttons ${className}`}>
|
<div className={`undo-redo-buttons ${className}`}>
|
||||||
{renderAction("undo", { size: "small" })}
|
<div className="undo-button-container">
|
||||||
{renderAction("redo", { size: "small" })}
|
<Tooltip label={t("buttons.undo")}>{renderAction("undo")}</Tooltip>
|
||||||
|
</div>
|
||||||
|
<div className="redo-button-container">
|
||||||
|
<Tooltip label={t("buttons.redo")}> {renderAction("redo")}</Tooltip>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import Stack from "../components/Stack";
|
// TODO barnabasmolnar/editor-redesign
|
||||||
import { ToolButton } from "../components/ToolButton";
|
// this icon is not great
|
||||||
import { save, file } from "../components/icons";
|
import { getShortcutFromShortcutName } from "../actions/shortcuts";
|
||||||
|
import { save } from "../components/icons";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
|
|
||||||
import "./ActiveFile.scss";
|
import "./ActiveFile.scss";
|
||||||
|
import MenuItem from "./MenuItem";
|
||||||
|
|
||||||
type ActiveFileProps = {
|
type ActiveFileProps = {
|
||||||
fileName?: string;
|
fileName?: string;
|
||||||
@ -11,18 +13,11 @@ type ActiveFileProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const ActiveFile = ({ fileName, onSave }: ActiveFileProps) => (
|
export const ActiveFile = ({ fileName, onSave }: ActiveFileProps) => (
|
||||||
<Stack.Row className="ActiveFile" gap={1} align="center">
|
<MenuItem
|
||||||
<span className="ActiveFile__fileName">
|
label={`${t("buttons.save")}`}
|
||||||
{file}
|
shortcut={getShortcutFromShortcutName("saveScene")}
|
||||||
<span>{fileName}</span>
|
dataTestId="save-button"
|
||||||
</span>
|
|
||||||
<ToolButton
|
|
||||||
type="icon"
|
|
||||||
icon={save}
|
|
||||||
title={t("buttons.save")}
|
|
||||||
aria-label={t("buttons.save")}
|
|
||||||
onClick={onSave}
|
onClick={onSave}
|
||||||
data-testid="save-button"
|
icon={save}
|
||||||
/>
|
/>
|
||||||
</Stack.Row>
|
|
||||||
);
|
);
|
||||||
|
@ -266,6 +266,10 @@ import {
|
|||||||
isLocalLink,
|
isLocalLink,
|
||||||
} from "../element/Hyperlink";
|
} from "../element/Hyperlink";
|
||||||
import { shouldShowBoundingBox } from "../element/transformHandles";
|
import { shouldShowBoundingBox } from "../element/transformHandles";
|
||||||
|
import { atom } from "jotai";
|
||||||
|
|
||||||
|
export const isMenuOpenAtom = atom(false);
|
||||||
|
export const isDropdownOpenAtom = atom(false);
|
||||||
|
|
||||||
const deviceContextInitialValue = {
|
const deviceContextInitialValue = {
|
||||||
isSmScreen: false,
|
isSmScreen: false,
|
||||||
@ -571,6 +575,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
library={this.library}
|
library={this.library}
|
||||||
id={this.id}
|
id={this.id}
|
||||||
onImageAction={this.onImageAction}
|
onImageAction={this.onImageAction}
|
||||||
|
renderWelcomeScreen={
|
||||||
|
this.state.showWelcomeScreen &&
|
||||||
|
this.state.activeTool.type === "selection" &&
|
||||||
|
!this.scene.getElementsIncludingDeleted().length
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<div className="excalidraw-textEditorContainer" />
|
<div className="excalidraw-textEditorContainer" />
|
||||||
<div className="excalidraw-contextMenuContainer" />
|
<div className="excalidraw-contextMenuContainer" />
|
||||||
@ -1085,6 +1094,13 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps: AppProps, prevState: AppState) {
|
componentDidUpdate(prevProps: AppProps, prevState: AppState) {
|
||||||
|
if (
|
||||||
|
!this.state.showWelcomeScreen &&
|
||||||
|
!this.scene.getElementsIncludingDeleted().length
|
||||||
|
) {
|
||||||
|
this.setState({ showWelcomeScreen: true });
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.excalidrawContainerRef.current &&
|
this.excalidrawContainerRef.current &&
|
||||||
prevProps.UIOptions.dockedSidebarBreakpoint !==
|
prevProps.UIOptions.dockedSidebarBreakpoint !==
|
||||||
@ -1276,6 +1292,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const selectionColor = getComputedStyle(
|
||||||
|
document.querySelector(".excalidraw")!,
|
||||||
|
).getPropertyValue("--color-selection");
|
||||||
|
|
||||||
renderScene(
|
renderScene(
|
||||||
{
|
{
|
||||||
elements: renderingElements,
|
elements: renderingElements,
|
||||||
@ -1284,6 +1304,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
rc: this.rc!,
|
rc: this.rc!,
|
||||||
canvas: this.canvas!,
|
canvas: this.canvas!,
|
||||||
renderConfig: {
|
renderConfig: {
|
||||||
|
selectionColor,
|
||||||
scrollX: this.state.scrollX,
|
scrollX: this.state.scrollX,
|
||||||
scrollY: this.state.scrollY,
|
scrollY: this.state.scrollY,
|
||||||
viewBackgroundColor: this.state.viewBackgroundColor,
|
viewBackgroundColor: this.state.viewBackgroundColor,
|
||||||
@ -1867,8 +1888,16 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
if (event.key === KEYS.QUESTION_MARK) {
|
if (event.key === KEYS.QUESTION_MARK) {
|
||||||
this.setState({
|
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)) {
|
if (this.actionManager.handleKeyDown(event)) {
|
||||||
|
@ -2,16 +2,19 @@
|
|||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
.Avatar {
|
.Avatar {
|
||||||
width: 2.5rem;
|
width: 1.25rem;
|
||||||
height: 2.5rem;
|
height: 1.25rem;
|
||||||
border-radius: 1.25rem;
|
border-radius: 100%;
|
||||||
|
outline: 2px solid var(--avatar-border-color);
|
||||||
|
outline-offset: 2px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: $oc-white;
|
color: $oc-white;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.8rem;
|
font-size: 0.625rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
&-img {
|
&-img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -11,13 +11,11 @@ type AvatarProps = {
|
|||||||
src?: string;
|
src?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Avatar = ({ color, border, onClick, name, src }: AvatarProps) => {
|
export const Avatar = ({ color, onClick, name, src }: AvatarProps) => {
|
||||||
const shortName = getClientInitials(name);
|
const shortName = getClientInitials(name);
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
const loadImg = !error && src;
|
const loadImg = !error && src;
|
||||||
const style = loadImg
|
const style = loadImg ? undefined : { background: color };
|
||||||
? undefined
|
|
||||||
: { background: color, border: `1px solid ${border}` };
|
|
||||||
return (
|
return (
|
||||||
<div className="Avatar" style={style} onClick={onClick}>
|
<div className="Avatar" style={style} onClick={onClick}>
|
||||||
{loadImg ? (
|
{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};
|
color: #{$oc-blue-7};
|
||||||
|
|
||||||
|
border: 0;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
box-shadow: 0 0 0 3px #{$oc-blue-7};
|
box-shadow: 0 0 0 3px #{$oc-blue-7};
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { useDevice } from "./App";
|
import { TrashIcon } from "./icons";
|
||||||
import { trash } from "./icons";
|
|
||||||
import { ToolButton } from "./ToolButton";
|
|
||||||
|
|
||||||
import ConfirmDialog from "./ConfirmDialog";
|
import ConfirmDialog from "./ConfirmDialog";
|
||||||
|
import MenuItem from "./MenuItem";
|
||||||
|
|
||||||
const ClearCanvas = ({ onConfirm }: { onConfirm: () => void }) => {
|
const ClearCanvas = ({ onConfirm }: { onConfirm: () => void }) => {
|
||||||
const [showDialog, setShowDialog] = useState(false);
|
const [showDialog, setShowDialog] = useState(false);
|
||||||
@ -14,14 +13,11 @@ const ClearCanvas = ({ onConfirm }: { onConfirm: () => void }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ToolButton
|
<MenuItem
|
||||||
type="button"
|
label={t("buttons.clearReset")}
|
||||||
icon={trash}
|
icon={TrashIcon}
|
||||||
title={t("buttons.clearReset")}
|
|
||||||
aria-label={t("buttons.clearReset")}
|
|
||||||
showAriaLabel={useDevice().isMobile}
|
|
||||||
onClick={toggleDialog}
|
onClick={toggleDialog}
|
||||||
data-testid="clear-canvas-button"
|
dataTestId="clear-canvas-button"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{showDialog && (
|
{showDialog && (
|
||||||
|
@ -1,6 +1,51 @@
|
|||||||
@import "../css/variables.module";
|
@import "../css/variables.module";
|
||||||
|
|
||||||
.excalidraw {
|
.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 {
|
.CollabButton.is-collaborating {
|
||||||
background-color: var(--button-special-active-bg-color);
|
background-color: var(--button-special-active-bg-color);
|
||||||
|
|
||||||
@ -24,9 +69,9 @@
|
|||||||
bottom: -5px;
|
bottom: -5px;
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background-color: $oc-green-6;
|
background-color: $oc-green-2;
|
||||||
color: $oc-white;
|
color: $oc-green-9;
|
||||||
font-size: 0.6em;
|
font-size: 0.6rem;
|
||||||
font-family: "Cascadia";
|
font-family: "Cascadia";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,37 +1,47 @@
|
|||||||
import clsx from "clsx";
|
|
||||||
import { ToolButton } from "./ToolButton";
|
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { useDevice } from "../components/App";
|
import { UsersIcon } from "./icons";
|
||||||
import { users } from "./icons";
|
|
||||||
|
|
||||||
import "./CollabButton.scss";
|
import "./CollabButton.scss";
|
||||||
|
import MenuItem from "./MenuItem";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
const CollabButton = ({
|
const CollabButton = ({
|
||||||
isCollaborating,
|
isCollaborating,
|
||||||
collaboratorCount,
|
collaboratorCount,
|
||||||
onClick,
|
onClick,
|
||||||
|
isInHamburgerMenu = true,
|
||||||
}: {
|
}: {
|
||||||
isCollaborating: boolean;
|
isCollaborating: boolean;
|
||||||
collaboratorCount: number;
|
collaboratorCount: number;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
|
isInHamburgerMenu?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ToolButton
|
{isInHamburgerMenu ? (
|
||||||
className={clsx("CollabButton", {
|
<MenuItem
|
||||||
"is-collaborating": isCollaborating,
|
label={t("labels.liveCollaboration")}
|
||||||
})}
|
dataTestId="collab-button"
|
||||||
|
icon={UsersIcon}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
icon={users}
|
isCollaborating={isCollaborating}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
className={clsx("collab-button", { active: isCollaborating })}
|
||||||
type="button"
|
type="button"
|
||||||
|
onClick={onClick}
|
||||||
|
style={{ position: "relative" }}
|
||||||
title={t("labels.liveCollaboration")}
|
title={t("labels.liveCollaboration")}
|
||||||
aria-label={t("labels.liveCollaboration")}
|
|
||||||
showAriaLabel={useDevice().isMobile}
|
|
||||||
>
|
>
|
||||||
{isCollaborating && (
|
{UsersIcon}
|
||||||
<div className="CollabButton-collaborators">{collaboratorCount}</div>
|
{collaboratorCount > 0 && (
|
||||||
|
<div className="CollabButton-collaborators">
|
||||||
|
{collaboratorCount}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
</ToolButton>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -21,6 +21,23 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto 1fr;
|
grid-template-columns: auto 1fr;
|
||||||
align-items: center;
|
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 {
|
.color-picker-triangle {
|
||||||
@ -30,20 +47,29 @@
|
|||||||
border-width: 0 9px 10px;
|
border-width: 0 9px 10px;
|
||||||
border-color: transparent transparent var(--popup-bg-color);
|
border-color: transparent transparent var(--popup-bg-color);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -10px;
|
top: 10px;
|
||||||
|
|
||||||
:root[dir="ltr"] & {
|
:root[dir="ltr"] & {
|
||||||
left: 12px;
|
transform: rotate(270deg);
|
||||||
|
left: -14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[dir="rtl"] & {
|
:root[dir="rtl"] & {
|
||||||
right: 12px;
|
transform: rotate(90deg);
|
||||||
|
right: -14px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.color-picker-triangle-shadow {
|
.color-picker-triangle-shadow {
|
||||||
border-color: transparent transparent transparentize($oc-black, 0.9);
|
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 {
|
.color-picker-content--default {
|
||||||
@ -119,16 +145,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.color-picker-hash {
|
.color-picker-hash {
|
||||||
background: var(--input-border-color);
|
height: var(--default-button-size);
|
||||||
height: 1.875rem;
|
flex-shrink: 0;
|
||||||
width: 1.875rem;
|
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"] & {
|
:root[dir="ltr"] & {
|
||||||
border-radius: 4px 0 0 4px;
|
border-radius: var(--border-radius-lg) 0 0 var(--border-radius-lg);
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[dir="rtl"] & {
|
: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);
|
color: var(--input-label-color);
|
||||||
@ -138,81 +169,64 @@
|
|||||||
position: relative;
|
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 {
|
.color-input-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
box-shadow: 0 0 0 1px var(--color-primary-darkest);
|
||||||
|
border-radius: var(--border-radius-lg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.color-picker-input {
|
.color-picker-input {
|
||||||
width: 11ch; /* length of `transparent` */
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1rem;
|
font-size: 0.875rem;
|
||||||
background-color: var(--input-bg-color);
|
background-color: transparent;
|
||||||
color: var(--text-primary-color);
|
color: var(--text-primary-color);
|
||||||
border: 0;
|
border: 0;
|
||||||
outline: none;
|
outline: none;
|
||||||
height: 1.75em;
|
height: var(--default-button-size);
|
||||||
box-shadow: var(--input-border-color) 0 0 0 1px inset;
|
border: 1px solid var(--default-border-color);
|
||||||
|
border-left: 0;
|
||||||
|
letter-spacing: 0.4px;
|
||||||
|
|
||||||
:root[dir="ltr"] & {
|
:root[dir="ltr"] & {
|
||||||
border-radius: 0 4px 4px 0;
|
border-radius: 0 var(--border-radius-lg) var(--border-radius-lg) 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[dir="rtl"] & {
|
: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: 0.5rem;
|
||||||
padding: 1px;
|
padding-left: 0.25rem;
|
||||||
padding-inline-start: 0.5em;
|
|
||||||
appearance: none;
|
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 {
|
.color-picker-label-swatch {
|
||||||
height: 1.875rem;
|
@include outlineButtonStyles;
|
||||||
width: 1.875rem;
|
background-color: var(--swatch-color) !important;
|
||||||
margin-inline-end: 0.25rem;
|
|
||||||
border: 1px solid $oc-gray-3;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background-color: transparent !important;
|
position: relative;
|
||||||
filter: var(--theme-filter);
|
filter: var(--theme-filter);
|
||||||
|
border: 0 !important;
|
||||||
|
|
||||||
&:after {
|
&:after {
|
||||||
content: "";
|
content: "";
|
||||||
|
@ -365,10 +365,12 @@ export const ColorPicker = ({
|
|||||||
appState: AppState;
|
appState: AppState;
|
||||||
}) => {
|
}) => {
|
||||||
const pickerButton = React.useRef<HTMLButtonElement>(null);
|
const pickerButton = React.useRef<HTMLButtonElement>(null);
|
||||||
|
const coords = pickerButton.current?.getBoundingClientRect();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="color-picker-control-container">
|
<div className="color-picker-control-container">
|
||||||
|
<div className="color-picker-label-swatch-container">
|
||||||
<button
|
<button
|
||||||
className="color-picker-label-swatch"
|
className="color-picker-label-swatch"
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
@ -376,6 +378,7 @@ export const ColorPicker = ({
|
|||||||
onClick={() => setActive(!isActive)}
|
onClick={() => setActive(!isActive)}
|
||||||
ref={pickerButton}
|
ref={pickerButton}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
<ColorInput
|
<ColorInput
|
||||||
color={color}
|
color={color}
|
||||||
label={label}
|
label={label}
|
||||||
@ -386,6 +389,15 @@ export const ColorPicker = ({
|
|||||||
</div>
|
</div>
|
||||||
<React.Suspense fallback="">
|
<React.Suspense fallback="">
|
||||||
{isActive ? (
|
{isActive ? (
|
||||||
|
<div
|
||||||
|
className="color-picker-popover-container"
|
||||||
|
style={{
|
||||||
|
position: "fixed",
|
||||||
|
top: coords?.top,
|
||||||
|
left: coords?.right,
|
||||||
|
zIndex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Popover
|
<Popover
|
||||||
onCloseRequest={(event) =>
|
onCloseRequest={(event) =>
|
||||||
event.target !== pickerButton.current && setActive(false)
|
event.target !== pickerButton.current && setActive(false)
|
||||||
@ -407,6 +419,7 @@ export const ColorPicker = ({
|
|||||||
elements={elements}
|
elements={elements}
|
||||||
/>
|
/>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,34 +4,8 @@
|
|||||||
.confirm-dialog {
|
.confirm-dialog {
|
||||||
&-buttons {
|
&-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 0.2rem 0;
|
column-gap: 0.5rem;
|
||||||
justify-content: flex-end;
|
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 { t } from "../i18n";
|
||||||
import { Dialog, DialogProps } from "./Dialog";
|
import { Dialog, DialogProps } from "./Dialog";
|
||||||
import { ToolButton } from "./ToolButton";
|
|
||||||
|
|
||||||
import "./ConfirmDialog.scss";
|
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"> {
|
interface Props extends Omit<DialogProps, "onCloseRequest"> {
|
||||||
onConfirm: () => void;
|
onConfirm: () => void;
|
||||||
@ -20,6 +23,10 @@ const ConfirmDialog = (props: Props) => {
|
|||||||
className = "",
|
className = "",
|
||||||
...rest
|
...rest
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
const setIsMenuOpen = useSetAtom(isMenuOpenAtom);
|
||||||
|
const setIsDropdownOpen = useSetAtom(isDropdownOpenAtom);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
onCloseRequest={onCancel}
|
onCloseRequest={onCancel}
|
||||||
@ -29,21 +36,22 @@ const ConfirmDialog = (props: Props) => {
|
|||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<div className="confirm-dialog-buttons">
|
<div className="confirm-dialog-buttons">
|
||||||
<ToolButton
|
<DialogActionButton
|
||||||
type="button"
|
|
||||||
title={cancelText}
|
|
||||||
aria-label={cancelText}
|
|
||||||
label={cancelText}
|
label={cancelText}
|
||||||
onClick={onCancel}
|
onClick={() => {
|
||||||
className="confirm-dialog--cancel"
|
setIsMenuOpen(false);
|
||||||
|
setIsDropdownOpen(false);
|
||||||
|
onCancel();
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<ToolButton
|
<DialogActionButton
|
||||||
type="button"
|
|
||||||
title={confirmText}
|
|
||||||
aria-label={confirmText}
|
|
||||||
label={confirmText}
|
label={confirmText}
|
||||||
onClick={onConfirm}
|
onClick={() => {
|
||||||
className="confirm-dialog--confirm"
|
setIsMenuOpen(false);
|
||||||
|
setIsDropdownOpen(false);
|
||||||
|
onConfirm();
|
||||||
|
}}
|
||||||
|
actionType="danger"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
@ -7,68 +7,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.Dialog__title {
|
.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;
|
margin: 0;
|
||||||
}
|
text-align: left;
|
||||||
|
font-size: 1.25rem;
|
||||||
.Dialog__content {
|
border-bottom: 1px solid var(--dialog-border-color);
|
||||||
padding: 0 16px 16px;
|
padding: 0 0 0.75rem;
|
||||||
}
|
margin-bottom: 1.5rem;
|
||||||
|
|
||||||
@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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,11 +5,13 @@ import { t } from "../i18n";
|
|||||||
import { useExcalidrawContainer, useDevice } from "../components/App";
|
import { useExcalidrawContainer, useDevice } from "../components/App";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import "./Dialog.scss";
|
import "./Dialog.scss";
|
||||||
import { back, close } from "./icons";
|
import { back, CloseIcon } from "./icons";
|
||||||
import { Island } from "./Island";
|
import { Island } from "./Island";
|
||||||
import { Modal } from "./Modal";
|
import { Modal } from "./Modal";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { queryFocusableElements } from "../utils";
|
import { queryFocusableElements } from "../utils";
|
||||||
|
import { isMenuOpenAtom, isDropdownOpenAtom } from "./App";
|
||||||
|
import { useSetAtom } from "jotai";
|
||||||
|
|
||||||
export interface DialogProps {
|
export interface DialogProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@ -65,7 +67,12 @@ export const Dialog = (props: DialogProps) => {
|
|||||||
return () => islandNode.removeEventListener("keydown", handleKeyDown);
|
return () => islandNode.removeEventListener("keydown", handleKeyDown);
|
||||||
}, [islandNode, props.autofocus]);
|
}, [islandNode, props.autofocus]);
|
||||||
|
|
||||||
|
const setIsMenuOpen = useSetAtom(isMenuOpenAtom);
|
||||||
|
const setIsDropdownOpen = useSetAtom(isDropdownOpenAtom);
|
||||||
|
|
||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
|
setIsMenuOpen(false);
|
||||||
|
setIsDropdownOpen(false);
|
||||||
(lastActiveElement as HTMLElement).focus();
|
(lastActiveElement as HTMLElement).focus();
|
||||||
props.onCloseRequest();
|
props.onCloseRequest();
|
||||||
};
|
};
|
||||||
@ -88,7 +95,7 @@ export const Dialog = (props: DialogProps) => {
|
|||||||
title={t("buttons.close")}
|
title={t("buttons.close")}
|
||||||
aria-label={t("buttons.close")}
|
aria-label={t("buttons.close")}
|
||||||
>
|
>
|
||||||
{useDevice().isMobile ? back : close}
|
{useDevice().isMobile ? back : CloseIcon}
|
||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
<div className="Dialog__content">{props.children}</div>
|
<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 {
|
button.ExportDialog-imageExportButton {
|
||||||
|
border: 0;
|
||||||
|
|
||||||
width: 5rem;
|
width: 5rem;
|
||||||
height: 5rem;
|
height: 5rem;
|
||||||
margin: 0 0.2em;
|
margin: 0 0.2em;
|
||||||
|
@ -9,9 +9,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.FixedSideContainer_side_top {
|
.FixedSideContainer_side_top {
|
||||||
left: var(--space-factor);
|
left: 1rem;
|
||||||
top: var(--space-factor);
|
top: 1rem;
|
||||||
right: var(--space-factor);
|
right: 1rem;
|
||||||
|
bottom: 1rem;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { ActionManager } from "../actions/manager";
|
import { ActionManager } from "../actions/manager";
|
||||||
|
import { t } from "../i18n";
|
||||||
import { AppState, ExcalidrawProps } from "../types";
|
import { AppState, ExcalidrawProps } from "../types";
|
||||||
import {
|
import {
|
||||||
ExitZenModeAction,
|
ExitZenModeAction,
|
||||||
@ -8,20 +9,23 @@ import {
|
|||||||
ZoomActions,
|
ZoomActions,
|
||||||
} from "./Actions";
|
} from "./Actions";
|
||||||
import { useDevice } from "./App";
|
import { useDevice } from "./App";
|
||||||
import { Island } from "./Island";
|
import { WelcomeScreenHelpArrow } from "./icons";
|
||||||
import { Section } from "./Section";
|
import { Section } from "./Section";
|
||||||
import Stack from "./Stack";
|
import Stack from "./Stack";
|
||||||
|
import WelcomeScreenDecor from "./WelcomeScreenDecor";
|
||||||
|
|
||||||
const Footer = ({
|
const Footer = ({
|
||||||
appState,
|
appState,
|
||||||
actionManager,
|
actionManager,
|
||||||
renderCustomFooter,
|
renderCustomFooter,
|
||||||
showExitZenModeBtn,
|
showExitZenModeBtn,
|
||||||
|
renderWelcomeScreen,
|
||||||
}: {
|
}: {
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
actionManager: ActionManager;
|
actionManager: ActionManager;
|
||||||
renderCustomFooter?: ExcalidrawProps["renderFooter"];
|
renderCustomFooter?: ExcalidrawProps["renderFooter"];
|
||||||
showExitZenModeBtn: boolean;
|
showExitZenModeBtn: boolean;
|
||||||
|
renderWelcomeScreen: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const device = useDevice();
|
const device = useDevice();
|
||||||
const showFinalize =
|
const showFinalize =
|
||||||
@ -39,14 +43,12 @@ const Footer = ({
|
|||||||
>
|
>
|
||||||
<Stack.Col gap={2}>
|
<Stack.Col gap={2}>
|
||||||
<Section heading="canvasActions">
|
<Section heading="canvasActions">
|
||||||
<Island padding={1}>
|
|
||||||
<ZoomActions
|
<ZoomActions
|
||||||
renderAction={actionManager.renderAction}
|
renderAction={actionManager.renderAction}
|
||||||
zoom={appState.zoom}
|
zoom={appState.zoom}
|
||||||
/>
|
/>
|
||||||
</Island>
|
|
||||||
{!appState.viewModeEnabled && (
|
{!appState.viewModeEnabled && (
|
||||||
<>
|
|
||||||
<UndoRedoActions
|
<UndoRedoActions
|
||||||
renderAction={actionManager.renderAction}
|
renderAction={actionManager.renderAction}
|
||||||
className={clsx("zen-mode-transition", {
|
className={clsx("zen-mode-transition", {
|
||||||
@ -54,16 +56,6 @@ const Footer = ({
|
|||||||
appState.zenModeEnabled,
|
appState.zenModeEnabled,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
|
||||||
className={clsx("eraser-buttons zen-mode-transition", {
|
|
||||||
"layer-ui__wrapper__footer-left--transition-left":
|
|
||||||
appState.zenModeEnabled,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{actionManager.renderAction("eraser", { size: "small" })}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
{showFinalize && (
|
{showFinalize && (
|
||||||
<FinalizeAction
|
<FinalizeAction
|
||||||
@ -93,8 +85,19 @@ const Footer = ({
|
|||||||
"transition-right disable-pointerEvents": appState.zenModeEnabled,
|
"transition-right disable-pointerEvents": appState.zenModeEnabled,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
<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")}
|
{actionManager.renderAction("toggleShortcuts")}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<ExitZenModeAction
|
<ExitZenModeAction
|
||||||
actionManager={actionManager}
|
actionManager={actionManager}
|
||||||
showExitZenModeBtn={showExitZenModeBtn}
|
showExitZenModeBtn={showExitZenModeBtn}
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { questionCircle } from "../components/icons";
|
import { HelpIcon } from "./icons";
|
||||||
|
|
||||||
type HelpIconProps = {
|
type HelpButtonProps = {
|
||||||
title?: string;
|
title?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
onClick?(): void;
|
onClick?(): void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HelpIcon = (props: HelpIconProps) => (
|
export const HelpButton = (props: HelpButtonProps) => (
|
||||||
<button
|
<button
|
||||||
className="help-icon"
|
className="help-icon"
|
||||||
onClick={props.onClick}
|
onClick={props.onClick}
|
||||||
@ -15,6 +15,6 @@ export const HelpIcon = (props: HelpIconProps) => (
|
|||||||
title={`${props.title} — ?`}
|
title={`${props.title} — ?`}
|
||||||
aria-label={props.title}
|
aria-label={props.title}
|
||||||
>
|
>
|
||||||
{questionCircle}
|
{HelpIcon}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
@ -1,56 +1,115 @@
|
|||||||
@import "../css/variables.module";
|
@import "../css/variables.module";
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
.HelpDialog h3 {
|
.HelpDialog {
|
||||||
border-bottom: 1px solid var(--button-gray-2);
|
.Modal__content {
|
||||||
padding-bottom: 4px;
|
max-width: 960px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.HelpDialog--island {
|
h3 {
|
||||||
border: 1px solid var(--button-gray-2);
|
margin: 1.5rem 0;
|
||||||
margin-bottom: 16px;
|
font-weight: bold;
|
||||||
|
font-size: 1.125rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.HelpDialog--island-title {
|
&__header {
|
||||||
margin: 0;
|
|
||||||
padding: 4px;
|
|
||||||
background-color: var(--button-gray-1);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.HelpDialog--shortcut {
|
|
||||||
border-top: 1px solid var(--button-gray-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__btn {
|
||||||
|
display: flex;
|
||||||
|
column-gap: 0.5rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-family: inherit;
|
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--header {
|
&:hover {
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-evenly;
|
|
||||||
margin-bottom: 32px;
|
|
||||||
padding-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.HelpDialog--btn {
|
|
||||||
border: 1px solid var(--link-color);
|
|
||||||
padding: 8px 32px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
.HelpDialog--btn:hover {
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__link-icon {
|
||||||
|
line-height: 0;
|
||||||
|
svg {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,32 +4,36 @@ import { isDarwin, isWindows } from "../keys";
|
|||||||
import { Dialog } from "./Dialog";
|
import { Dialog } from "./Dialog";
|
||||||
import { getShortcutKey } from "../utils";
|
import { getShortcutKey } from "../utils";
|
||||||
import "./HelpDialog.scss";
|
import "./HelpDialog.scss";
|
||||||
|
import { ExternalLinkIcon } from "./icons";
|
||||||
|
|
||||||
const Header = () => (
|
const Header = () => (
|
||||||
<div className="HelpDialog--header">
|
<div className="HelpDialog__header">
|
||||||
<a
|
<a
|
||||||
className="HelpDialog--btn"
|
className="HelpDialog__btn"
|
||||||
href="https://github.com/excalidraw/excalidraw#documentation"
|
href="https://github.com/excalidraw/excalidraw#documentation"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
{t("helpDialog.documentation")}
|
{t("helpDialog.documentation")}
|
||||||
|
<div className="HelpDialog__link-icon">{ExternalLinkIcon}</div>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
className="HelpDialog--btn"
|
className="HelpDialog__btn"
|
||||||
href="https://blog.excalidraw.com"
|
href="https://blog.excalidraw.com"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
{t("helpDialog.blog")}
|
{t("helpDialog.blog")}
|
||||||
|
<div className="HelpDialog__link-icon">{ExternalLinkIcon}</div>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
className="HelpDialog--btn"
|
className="HelpDialog__btn"
|
||||||
href="https://github.com/excalidraw/excalidraw/issues"
|
href="https://github.com/excalidraw/excalidraw/issues"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
{t("helpDialog.github")}
|
{t("helpDialog.github")}
|
||||||
|
<div className="HelpDialog__link-icon">{ExternalLinkIcon}</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -37,88 +41,61 @@ const Header = () => (
|
|||||||
const Section = (props: { title: string; children: React.ReactNode }) => (
|
const Section = (props: { title: string; children: React.ReactNode }) => (
|
||||||
<>
|
<>
|
||||||
<h3>{props.title}</h3>
|
<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: {
|
const ShortcutIsland = (props: {
|
||||||
caption: string;
|
caption: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
}) => (
|
}) => (
|
||||||
<div className="HelpDialog--island">
|
<div className={`HelpDialog__island ${props.className}`}>
|
||||||
<h3 className="HelpDialog--island-title">{props.caption}</h3>
|
<h4 className="HelpDialog__island-title">{props.caption}</h4>
|
||||||
{props.children}
|
<div className="HelpDialog__island-content">{props.children}</div>
|
||||||
</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;
|
label: string;
|
||||||
shortcuts: 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 (
|
return (
|
||||||
<div className="HelpDialog--shortcut">
|
<div className="HelpDialog__shortcut">
|
||||||
<div
|
<div>{label}</div>
|
||||||
style={{
|
<div className="HelpDialog__key-container">
|
||||||
display: "flex",
|
{[...intersperse(splitShortcutKeys, isOr ? t("helpDialog.or") : null)]}
|
||||||
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
Shortcut.defaultProps = {
|
|
||||||
isOr: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
const ShortcutKey = (props: { children: React.ReactNode }) => (
|
const ShortcutKey = (props: { children: React.ReactNode }) => (
|
||||||
<kbd className="HelpDialog--key" {...props} />
|
<kbd className="HelpDialog__key" {...props} />
|
||||||
);
|
);
|
||||||
|
|
||||||
export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
||||||
@ -137,17 +114,12 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||||||
>
|
>
|
||||||
<Header />
|
<Header />
|
||||||
<Section title={t("helpDialog.shortcuts")}>
|
<Section title={t("helpDialog.shortcuts")}>
|
||||||
<Columns>
|
<ShortcutIsland
|
||||||
<Column>
|
className="HelpDialog__island--tools"
|
||||||
<ShortcutIsland caption={t("helpDialog.tools")}>
|
caption={t("helpDialog.tools")}
|
||||||
<Shortcut
|
>
|
||||||
label={t("toolBar.selection")}
|
<Shortcut label={t("toolBar.selection")} shortcuts={["V", "1"]} />
|
||||||
shortcuts={["V", "1"]}
|
<Shortcut label={t("toolBar.rectangle")} shortcuts={["R", "2"]} />
|
||||||
/>
|
|
||||||
<Shortcut
|
|
||||||
label={t("toolBar.rectangle")}
|
|
||||||
shortcuts={["R", "2"]}
|
|
||||||
/>
|
|
||||||
<Shortcut label={t("toolBar.diamond")} shortcuts={["D", "3"]} />
|
<Shortcut label={t("toolBar.diamond")} shortcuts={["D", "3"]} />
|
||||||
<Shortcut label={t("toolBar.ellipse")} shortcuts={["O", "4"]} />
|
<Shortcut label={t("toolBar.ellipse")} shortcuts={["O", "4"]} />
|
||||||
<Shortcut label={t("toolBar.arrow")} shortcuts={["A", "5"]} />
|
<Shortcut label={t("toolBar.arrow")} shortcuts={["A", "5"]} />
|
||||||
@ -165,10 +137,7 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||||||
/>
|
/>
|
||||||
<Shortcut
|
<Shortcut
|
||||||
label={t("helpDialog.editSelectedShape")}
|
label={t("helpDialog.editSelectedShape")}
|
||||||
shortcuts={[
|
shortcuts={[getShortcutKey("Enter"), t("helpDialog.doubleClick")]}
|
||||||
getShortcutKey("Enter"),
|
|
||||||
t("helpDialog.doubleClick"),
|
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
<Shortcut
|
<Shortcut
|
||||||
label={t("helpDialog.textNewLine")}
|
label={t("helpDialog.textNewLine")}
|
||||||
@ -214,7 +183,10 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||||||
shortcuts={[getShortcutKey("CtrlOrCmd+K")]}
|
shortcuts={[getShortcutKey("CtrlOrCmd+K")]}
|
||||||
/>
|
/>
|
||||||
</ShortcutIsland>
|
</ShortcutIsland>
|
||||||
<ShortcutIsland caption={t("helpDialog.view")}>
|
<ShortcutIsland
|
||||||
|
className="HelpDialog__island--view"
|
||||||
|
caption={t("helpDialog.view")}
|
||||||
|
>
|
||||||
<Shortcut
|
<Shortcut
|
||||||
label={t("buttons.zoomIn")}
|
label={t("buttons.zoomIn")}
|
||||||
shortcuts={[getShortcutKey("CtrlOrCmd++")]}
|
shortcuts={[getShortcutKey("CtrlOrCmd++")]}
|
||||||
@ -257,9 +229,10 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||||||
shortcuts={[getShortcutKey("Alt+/")]}
|
shortcuts={[getShortcutKey("Alt+/")]}
|
||||||
/>
|
/>
|
||||||
</ShortcutIsland>
|
</ShortcutIsland>
|
||||||
</Column>
|
<ShortcutIsland
|
||||||
<Column>
|
className="HelpDialog__island--editor"
|
||||||
<ShortcutIsland caption={t("helpDialog.editor")}>
|
caption={t("helpDialog.editor")}
|
||||||
|
>
|
||||||
<Shortcut
|
<Shortcut
|
||||||
label={t("labels.selectAll")}
|
label={t("labels.selectAll")}
|
||||||
shortcuts={[getShortcutKey("CtrlOrCmd+A")]}
|
shortcuts={[getShortcutKey("CtrlOrCmd+A")]}
|
||||||
@ -270,15 +243,11 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||||||
/>
|
/>
|
||||||
<Shortcut
|
<Shortcut
|
||||||
label={t("helpDialog.deepSelect")}
|
label={t("helpDialog.deepSelect")}
|
||||||
shortcuts={[
|
shortcuts={[getShortcutKey(`CtrlOrCmd+${t("helpDialog.click")}`)]}
|
||||||
getShortcutKey(`CtrlOrCmd+${t("helpDialog.click")}`),
|
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
<Shortcut
|
<Shortcut
|
||||||
label={t("helpDialog.deepBoxSelect")}
|
label={t("helpDialog.deepBoxSelect")}
|
||||||
shortcuts={[
|
shortcuts={[getShortcutKey(`CtrlOrCmd+${t("helpDialog.drag")}`)]}
|
||||||
getShortcutKey(`CtrlOrCmd+${t("helpDialog.drag")}`),
|
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
<Shortcut
|
<Shortcut
|
||||||
label={t("labels.moveCanvas")}
|
label={t("labels.moveCanvas")}
|
||||||
@ -415,8 +384,6 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||||||
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+>")]}
|
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+>")]}
|
||||||
/>
|
/>
|
||||||
</ShortcutIsland>
|
</ShortcutIsland>
|
||||||
</Column>
|
|
||||||
</Columns>
|
|
||||||
</Section>
|
</Section>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</>
|
</>
|
||||||
|
@ -14,20 +14,24 @@ $wide-viewport-width: 1000px;
|
|||||||
top: 100%;
|
top: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 6px;
|
margin-top: 0.5rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: $oc-gray-6;
|
color: var(--color-gray-40);
|
||||||
font-size: 0.8rem;
|
font-size: 0.75rem;
|
||||||
|
|
||||||
@include isMobile {
|
@include isMobile {
|
||||||
position: static;
|
position: static;
|
||||||
padding-right: 2em;
|
padding-right: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
> span {
|
> span {
|
||||||
padding: 0.2rem 0.4rem;
|
padding: 0.25rem;
|
||||||
background-color: var(--overlay-bg-color);
|
}
|
||||||
border-radius: 4px;
|
}
|
||||||
|
|
||||||
|
&.theme--dark {
|
||||||
|
.HintViewer {
|
||||||
|
color: var(--color-gray-60);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,8 @@
|
|||||||
.picker {
|
.picker {
|
||||||
background: var(--popup-bg-color);
|
background: var(--popup-bg-color);
|
||||||
border: 0 solid transparentize($oc-white, 0.75);
|
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;
|
border-radius: 4px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
@ -46,7 +47,6 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
width: 36px;
|
width: 36px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
opacity: 0.6;
|
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import { Popover } from "./Popover";
|
|||||||
import "./IconPicker.scss";
|
import "./IconPicker.scss";
|
||||||
import { isArrowKey, KEYS } from "../keys";
|
import { isArrowKey, KEYS } from "../keys";
|
||||||
import { getLanguage } from "../i18n";
|
import { getLanguage } from "../i18n";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
function Picker<T>({
|
function Picker<T>({
|
||||||
options,
|
options,
|
||||||
@ -102,7 +103,9 @@ function Picker<T>({
|
|||||||
<div className="picker-content" ref={rGallery}>
|
<div className="picker-content" ref={rGallery}>
|
||||||
{options.map((option, i) => (
|
{options.map((option, i) => (
|
||||||
<button
|
<button
|
||||||
className="picker-option"
|
className={clsx("picker-option", {
|
||||||
|
active: value === option.value,
|
||||||
|
})}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
(event.currentTarget as HTMLButtonElement).focus();
|
(event.currentTarget as HTMLButtonElement).focus();
|
||||||
onChange(option.value);
|
onChange(option.value);
|
||||||
@ -150,7 +153,7 @@ export function IconPicker<T>({
|
|||||||
const isRTL = getLanguage().rtl;
|
const isRTL = getLanguage().rtl;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label className={"picker-container"}>
|
<div>
|
||||||
<button
|
<button
|
||||||
name={group}
|
name={group}
|
||||||
className={isActive ? "active" : ""}
|
className={isActive ? "active" : ""}
|
||||||
@ -184,6 +187,6 @@ export function IconPicker<T>({
|
|||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
</label>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -5,14 +5,12 @@ import { canvasToBlob } from "../data/blob";
|
|||||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
import { CanvasError } from "../errors";
|
import { CanvasError } from "../errors";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { useDevice } from "./App";
|
|
||||||
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
||||||
import { exportToCanvas } from "../scene/export";
|
import { exportToCanvas } from "../scene/export";
|
||||||
import { AppState, BinaryFiles } from "../types";
|
import { AppState, BinaryFiles } from "../types";
|
||||||
import { Dialog } from "./Dialog";
|
import { Dialog } from "./Dialog";
|
||||||
import { clipboard, exportImage } from "./icons";
|
import { clipboard } from "./icons";
|
||||||
import Stack from "./Stack";
|
import Stack from "./Stack";
|
||||||
import { ToolButton } from "./ToolButton";
|
|
||||||
import "./ExportDialog.scss";
|
import "./ExportDialog.scss";
|
||||||
import OpenColor from "open-color";
|
import OpenColor from "open-color";
|
||||||
import { CheckboxItem } from "./CheckboxItem";
|
import { CheckboxItem } from "./CheckboxItem";
|
||||||
@ -221,6 +219,7 @@ const ImageExportModal = ({
|
|||||||
export const ImageExportDialog = ({
|
export const ImageExportDialog = ({
|
||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
|
setAppState,
|
||||||
files,
|
files,
|
||||||
exportPadding = DEFAULT_EXPORT_PADDING,
|
exportPadding = DEFAULT_EXPORT_PADDING,
|
||||||
actionManager,
|
actionManager,
|
||||||
@ -229,6 +228,7 @@ export const ImageExportDialog = ({
|
|||||||
onExportToClipboard,
|
onExportToClipboard,
|
||||||
}: {
|
}: {
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
elements: readonly NonDeletedExcalidrawElement[];
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
files: BinaryFiles;
|
files: BinaryFiles;
|
||||||
exportPadding?: number;
|
exportPadding?: number;
|
||||||
@ -237,26 +237,13 @@ export const ImageExportDialog = ({
|
|||||||
onExportToSvg: ExportCB;
|
onExportToSvg: ExportCB;
|
||||||
onExportToClipboard: ExportCB;
|
onExportToClipboard: ExportCB;
|
||||||
}) => {
|
}) => {
|
||||||
const [modalIsShown, setModalIsShown] = useState(false);
|
|
||||||
|
|
||||||
const handleClose = React.useCallback(() => {
|
const handleClose = React.useCallback(() => {
|
||||||
setModalIsShown(false);
|
setAppState({ openDialog: null });
|
||||||
}, []);
|
}, [setAppState]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ToolButton
|
{appState.openDialog === "imageExport" && (
|
||||||
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 && (
|
|
||||||
<Dialog onCloseRequest={handleClose} title={t("buttons.exportImage")}>
|
<Dialog onCloseRequest={handleClose} title={t("buttons.exportImage")}>
|
||||||
<ImageExportModal
|
<ImageExportModal
|
||||||
elements={elements}
|
elements={elements}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
.excalidraw {
|
.excalidraw {
|
||||||
.Island {
|
.Island {
|
||||||
--padding: 0;
|
--padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
background-color: var(--island-bg-color);
|
background-color: var(--island-bg-color);
|
||||||
box-shadow: var(--shadow-island);
|
box-shadow: var(--shadow-island);
|
||||||
border-radius: var(--border-radius-lg);
|
border-radius: var(--border-radius-lg);
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { useDevice } from "./App";
|
|
||||||
import { AppState, ExportOpts, BinaryFiles } from "../types";
|
import { AppState, ExportOpts, BinaryFiles } from "../types";
|
||||||
import { Dialog } from "./Dialog";
|
import { Dialog } from "./Dialog";
|
||||||
import { exportFile, exportToFileIcon, link } from "./icons";
|
import { ExportIcon, exportToFileIcon, LinkIcon } from "./icons";
|
||||||
import { ToolButton } from "./ToolButton";
|
import { ToolButton } from "./ToolButton";
|
||||||
import { actionSaveFileToDisk } from "../actions/actionExport";
|
import { actionSaveFileToDisk } from "../actions/actionExport";
|
||||||
import { Card } from "./Card";
|
import { Card } from "./Card";
|
||||||
@ -14,6 +14,7 @@ import { nativeFileSystemSupported } from "../data/filesystem";
|
|||||||
import { trackEvent } from "../analytics";
|
import { trackEvent } from "../analytics";
|
||||||
import { ActionManager } from "../actions/manager";
|
import { ActionManager } from "../actions/manager";
|
||||||
import { getFrame } from "../utils";
|
import { getFrame } from "../utils";
|
||||||
|
import MenuItem from "./MenuItem";
|
||||||
|
|
||||||
export type ExportCB = (
|
export type ExportCB = (
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
@ -63,7 +64,7 @@ const JSONExportModal = ({
|
|||||||
)}
|
)}
|
||||||
{onExportToBackend && (
|
{onExportToBackend && (
|
||||||
<Card color="pink">
|
<Card color="pink">
|
||||||
<div className="Card-icon">{link}</div>
|
<div className="Card-icon">{LinkIcon}</div>
|
||||||
<h2>{t("exportDialog.link_title")}</h2>
|
<h2>{t("exportDialog.link_title")}</h2>
|
||||||
<div className="Card-details">{t("exportDialog.link_details")}</div>
|
<div className="Card-details">{t("exportDialog.link_details")}</div>
|
||||||
<ToolButton
|
<ToolButton
|
||||||
@ -109,16 +110,13 @@ export const JSONExportDialog = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ToolButton
|
<MenuItem
|
||||||
|
icon={ExportIcon}
|
||||||
|
label={t("buttons.export")}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setModalIsShown(true);
|
setModalIsShown(true);
|
||||||
}}
|
}}
|
||||||
data-testid="json-export-button"
|
dataTestId="json-export-button"
|
||||||
icon={exportFile}
|
|
||||||
type="button"
|
|
||||||
aria-label={t("buttons.export")}
|
|
||||||
showAriaLabel={useDevice().isMobile}
|
|
||||||
title={t("buttons.export")}
|
|
||||||
/>
|
/>
|
||||||
{modalIsShown && (
|
{modalIsShown && (
|
||||||
<Dialog onCloseRequest={handleClose} title={t("buttons.export")}>
|
<Dialog onCloseRequest={handleClose} title={t("buttons.export")}>
|
||||||
|
@ -16,8 +16,10 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: var(--zIndex-layerUI);
|
z-index: var(--zIndex-layerUI);
|
||||||
|
|
||||||
&__top-right {
|
&__top-right {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__footer {
|
&__footer {
|
||||||
@ -48,13 +50,6 @@
|
|||||||
transform: translate(-999px, 0);
|
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 {
|
&.layer-ui__wrapper__footer-left--transition-bottom {
|
||||||
transform: translate(0, 92px);
|
transform: translate(0, 92px);
|
||||||
}
|
}
|
||||||
@ -97,14 +92,9 @@
|
|||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.layer-ui__wrapper__footer-left {
|
|
||||||
margin-bottom: 0.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.layer-ui__wrapper__footer-right {
|
.layer-ui__wrapper__footer-right {
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
margin-bottom: 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 { AppProps, AppState, ExcalidrawProps, BinaryFiles } from "../types";
|
||||||
import { muteFSAbortError } from "../utils";
|
import { muteFSAbortError } from "../utils";
|
||||||
import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
|
import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
|
||||||
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
|
|
||||||
import CollabButton from "./CollabButton";
|
import CollabButton from "./CollabButton";
|
||||||
import { ErrorDialog } from "./ErrorDialog";
|
import { ErrorDialog } from "./ErrorDialog";
|
||||||
import { ExportCB, ImageExportDialog } from "./ImageExportDialog";
|
import { ExportCB, ImageExportDialog } from "./ImageExportDialog";
|
||||||
@ -36,13 +35,26 @@ import "./LayerUI.scss";
|
|||||||
import "./Toolbar.scss";
|
import "./Toolbar.scss";
|
||||||
import { PenModeButton } from "./PenModeButton";
|
import { PenModeButton } from "./PenModeButton";
|
||||||
import { trackEvent } from "../analytics";
|
import { trackEvent } from "../analytics";
|
||||||
import { useDevice } from "../components/App";
|
import { isMenuOpenAtom, useDevice } from "../components/App";
|
||||||
import { Stats } from "./Stats";
|
import { Stats } from "./Stats";
|
||||||
import { actionToggleStats } from "../actions/actionToggleStats";
|
import { actionToggleStats } from "../actions/actionToggleStats";
|
||||||
import Footer from "./Footer";
|
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 { hostSidebarCountersAtom } from "./Sidebar/Sidebar";
|
||||||
import { jotaiScope } from "../jotai";
|
import { jotaiScope } from "../jotai";
|
||||||
import { useAtom } 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 {
|
interface LayerUIProps {
|
||||||
actionManager: ActionManager;
|
actionManager: ActionManager;
|
||||||
@ -68,6 +80,7 @@ interface LayerUIProps {
|
|||||||
library: Library;
|
library: Library;
|
||||||
id: string;
|
id: string;
|
||||||
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
|
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
|
||||||
|
renderWelcomeScreen: boolean;
|
||||||
}
|
}
|
||||||
const LayerUI = ({
|
const LayerUI = ({
|
||||||
actionManager,
|
actionManager,
|
||||||
@ -92,6 +105,7 @@ const LayerUI = ({
|
|||||||
library,
|
library,
|
||||||
id,
|
id,
|
||||||
onImageAction,
|
onImageAction,
|
||||||
|
renderWelcomeScreen,
|
||||||
}: LayerUIProps) => {
|
}: LayerUIProps) => {
|
||||||
const device = useDevice();
|
const device = useDevice();
|
||||||
|
|
||||||
@ -151,6 +165,7 @@ const LayerUI = ({
|
|||||||
<ImageExportDialog
|
<ImageExportDialog
|
||||||
elements={elements}
|
elements={elements}
|
||||||
appState={appState}
|
appState={appState}
|
||||||
|
setAppState={setAppState}
|
||||||
files={files}
|
files={files}
|
||||||
actionManager={actionManager}
|
actionManager={actionManager}
|
||||||
onExportToPng={createExporter("png")}
|
onExportToPng={createExporter("png")}
|
||||||
@ -160,50 +175,57 @@ const LayerUI = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Separator = () => {
|
const [isMenuOpen, setIsMenuOpen] = useAtom(isMenuOpenAtom);
|
||||||
return <div style={{ width: ".625em" }} />;
|
const menuRef = useOutsideClickHook(() => setIsMenuOpen(false));
|
||||||
};
|
|
||||||
|
|
||||||
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 renderCanvasActions = () => (
|
const renderCanvasActions = () => (
|
||||||
<Section
|
<div style={{ position: "relative" }}>
|
||||||
heading="canvasActions"
|
<WelcomeScreenDecor
|
||||||
className={clsx("zen-mode-transition", {
|
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,
|
"transition-left": appState.zenModeEnabled,
|
||||||
})}
|
})}
|
||||||
|
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
||||||
|
type="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,
|
{/* the zIndex ensures this menu has higher stacking order,
|
||||||
see https://github.com/excalidraw/excalidraw/pull/1445 */}
|
see https://github.com/excalidraw/excalidraw/pull/1445 */}
|
||||||
<Island padding={2} style={{ zIndex: 1 }}>
|
<Island
|
||||||
<Stack.Col gap={4}>
|
className="menu-container"
|
||||||
<Stack.Row gap={1} justifyContent="space-between">
|
padding={2}
|
||||||
{actionManager.renderAction("clearCanvas")}
|
style={{ zIndex: 1 }}
|
||||||
<Separator />
|
>
|
||||||
{actionManager.renderAction("loadScene")}
|
{actionManager.renderAction("loadScene")}
|
||||||
|
{/* // TODO barnabasmolnar/editor-redesign */}
|
||||||
|
{/* is this fine here? */}
|
||||||
|
{appState.fileHandle &&
|
||||||
|
actionManager.renderAction("saveToActiveFile")}
|
||||||
{renderJSONExportDialog()}
|
{renderJSONExportDialog()}
|
||||||
{renderImageExportDialog()}
|
<MenuItem
|
||||||
<Separator />
|
label={t("buttons.exportImage")}
|
||||||
|
icon={ExportImageIcon}
|
||||||
|
dataTestId="image-export-button"
|
||||||
|
onClick={() => setAppState({ openDialog: "imageExport" })}
|
||||||
|
shortcut={getShortcutFromShortcutName("imageExport")}
|
||||||
|
/>
|
||||||
{onCollabButtonClick && (
|
{onCollabButtonClick && (
|
||||||
<CollabButton
|
<CollabButton
|
||||||
isCollaborating={isCollaborating}
|
isCollaborating={isCollaborating}
|
||||||
@ -211,20 +233,42 @@ const LayerUI = ({
|
|||||||
onClick={onCollabButtonClick}
|
onClick={onCollabButtonClick}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Stack.Row>
|
{actionManager.renderAction("toggleShortcuts", undefined, true)}
|
||||||
<BackgroundPickerAndDarkModeToggle actionManager={actionManager} />
|
{actionManager.renderAction("clearCanvas")}
|
||||||
{appState.fileHandle && (
|
<Separator />
|
||||||
<>{actionManager.renderAction("saveToActiveFile")}</>
|
<MenuLinks />
|
||||||
)}
|
<Separator />
|
||||||
</Stack.Col>
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
rowGap: ".5rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>{actionManager.renderAction("toggleTheme")}</div>
|
||||||
|
<div style={{ padding: "0 0.625rem" }}>
|
||||||
|
<LanguageList style={{ width: "100%" }} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: ".75rem", marginBottom: ".5rem" }}>
|
||||||
|
{t("labels.canvasBackground")}
|
||||||
|
</div>
|
||||||
|
<div style={{ padding: "0 0.625rem" }}>
|
||||||
|
{actionManager.renderAction("changeViewBackgroundColor")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Island>
|
</Island>
|
||||||
</Section>
|
</Section>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderSelectedShapeActions = () => (
|
const renderSelectedShapeActions = () => (
|
||||||
<Section
|
<Section
|
||||||
heading="selectedShapeActions"
|
heading="selectedShapeActions"
|
||||||
className={clsx("zen-mode-transition", {
|
className={clsx("selected-shape-actions zen-mode-transition", {
|
||||||
"transition-left": appState.zenModeEnabled,
|
"transition-left": appState.zenModeEnabled,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
@ -232,10 +276,9 @@ const LayerUI = ({
|
|||||||
className={CLASSES.SHAPE_ACTIONS_MENU}
|
className={CLASSES.SHAPE_ACTIONS_MENU}
|
||||||
padding={2}
|
padding={2}
|
||||||
style={{
|
style={{
|
||||||
// we want to make sure this doesn't overflow so subtracting 200
|
// we want to make sure this doesn't overflow so subtracting the
|
||||||
// which is approximately height of zoom footer and top left menu items with some buffer
|
// approximate height of hamburgerMenu + footer
|
||||||
// if active file name is displayed, subtracting 248 to account for its height
|
maxHeight: `${appState.height - 166}px`,
|
||||||
maxHeight: `${appState.height - (appState.fileHandle ? 248 : 200)}px`,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectedShapeActions
|
<SelectedShapeActions
|
||||||
@ -255,21 +298,34 @@ const LayerUI = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<FixedSideContainer side="top">
|
<FixedSideContainer side="top">
|
||||||
|
{renderWelcomeScreen && !appState.isLoading && (
|
||||||
|
<WelcomeScreen actionManager={actionManager} />
|
||||||
|
)}
|
||||||
<div className="App-menu App-menu_top">
|
<div className="App-menu App-menu_top">
|
||||||
<Stack.Col
|
<Stack.Col
|
||||||
gap={4}
|
gap={6}
|
||||||
className={clsx({
|
className={clsx({
|
||||||
"disable-pointerEvents": appState.zenModeEnabled,
|
"disable-pointerEvents": appState.zenModeEnabled,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{appState.viewModeEnabled
|
{renderCanvasActions()}
|
||||||
? renderViewModeCanvasActions()
|
|
||||||
: renderCanvasActions()}
|
|
||||||
{shouldRenderSelectedShapeActions && renderSelectedShapeActions()}
|
{shouldRenderSelectedShapeActions && renderSelectedShapeActions()}
|
||||||
</Stack.Col>
|
</Stack.Col>
|
||||||
{!appState.viewModeEnabled && (
|
{!appState.viewModeEnabled && (
|
||||||
<Section heading="shapes">
|
<Section heading="shapes" className="shapes-section">
|
||||||
{(heading: React.ReactNode) => (
|
{(heading: React.ReactNode) => (
|
||||||
|
<div style={{ position: "relative" }}>
|
||||||
|
<WelcomeScreenDecor
|
||||||
|
shouldRender={renderWelcomeScreen && !appState.isLoading}
|
||||||
|
>
|
||||||
|
<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.Col gap={4} align="start">
|
||||||
<Stack.Row
|
<Stack.Row
|
||||||
gap={1}
|
gap={1}
|
||||||
@ -277,19 +333,6 @@ const LayerUI = ({
|
|||||||
"zen-mode": appState.zenModeEnabled,
|
"zen-mode": appState.zenModeEnabled,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<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
|
<Island
|
||||||
padding={1}
|
padding={1}
|
||||||
className={clsx("App-toolbar", {
|
className={clsx("App-toolbar", {
|
||||||
@ -304,6 +347,21 @@ const LayerUI = ({
|
|||||||
/>
|
/>
|
||||||
{heading}
|
{heading}
|
||||||
<Stack.Row gap={1}>
|
<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
|
<ShapesSwitcher
|
||||||
appState={appState}
|
appState={appState}
|
||||||
canvas={canvas}
|
canvas={canvas}
|
||||||
@ -315,14 +373,14 @@ const LayerUI = ({
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{/* {actionManager.renderAction("eraser", {
|
||||||
|
// size: "small",
|
||||||
|
})} */}
|
||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
</Island>
|
</Island>
|
||||||
<LibraryButton
|
|
||||||
appState={appState}
|
|
||||||
setAppState={setAppState}
|
|
||||||
/>
|
|
||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
</Stack.Col>
|
</Stack.Col>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</Section>
|
</Section>
|
||||||
)}
|
)}
|
||||||
@ -338,7 +396,16 @@ const LayerUI = ({
|
|||||||
collaborators={appState.collaborators}
|
collaborators={appState.collaborators}
|
||||||
actionManager={actionManager}
|
actionManager={actionManager}
|
||||||
/>
|
/>
|
||||||
|
{onCollabButtonClick && (
|
||||||
|
<CollabButton
|
||||||
|
isInHamburgerMenu={false}
|
||||||
|
isCollaborating={isCollaborating}
|
||||||
|
collaboratorCount={appState.collaborators.size}
|
||||||
|
onClick={onCollabButtonClick}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{renderTopRightUI?.(device.isMobile, appState)}
|
{renderTopRightUI?.(device.isMobile, appState)}
|
||||||
|
<LibraryButton appState={appState} setAppState={setAppState} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</FixedSideContainer>
|
</FixedSideContainer>
|
||||||
@ -371,13 +438,14 @@ const LayerUI = ({
|
|||||||
onClose={() => setAppState({ errorMessage: null })}
|
onClose={() => setAppState({ errorMessage: null })}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{appState.showHelpDialog && (
|
{appState.openDialog === "help" && (
|
||||||
<HelpDialog
|
<HelpDialog
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setAppState({ showHelpDialog: false });
|
setAppState({ openDialog: null });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{renderImageExportDialog()}
|
||||||
{appState.pasteDialog.shown && (
|
{appState.pasteDialog.shown && (
|
||||||
<PasteChartDialog
|
<PasteChartDialog
|
||||||
setAppState={setAppState}
|
setAppState={setAppState}
|
||||||
@ -392,6 +460,7 @@ const LayerUI = ({
|
|||||||
)}
|
)}
|
||||||
{device.isMobile && (
|
{device.isMobile && (
|
||||||
<MobileMenu
|
<MobileMenu
|
||||||
|
renderWelcomeScreen={renderWelcomeScreen}
|
||||||
appState={appState}
|
appState={appState}
|
||||||
elements={elements}
|
elements={elements}
|
||||||
actionManager={actionManager}
|
actionManager={actionManager}
|
||||||
@ -433,6 +502,7 @@ const LayerUI = ({
|
|||||||
>
|
>
|
||||||
{renderFixedSideContainer()}
|
{renderFixedSideContainer()}
|
||||||
<Footer
|
<Footer
|
||||||
|
renderWelcomeScreen={renderWelcomeScreen}
|
||||||
appState={appState}
|
appState={appState}
|
||||||
actionManager={actionManager}
|
actionManager={actionManager}
|
||||||
renderCustomFooter={renderCustomFooter}
|
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 React from "react";
|
||||||
import clsx from "clsx";
|
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { capitalizeString } from "../utils";
|
import { capitalizeString } from "../utils";
|
||||||
import { trackEvent } from "../analytics";
|
import { trackEvent } from "../analytics";
|
||||||
import { useDevice } from "./App";
|
import { useDevice } from "./App";
|
||||||
|
import "./LibraryButton.scss";
|
||||||
const LIBRARY_ICON = (
|
import { LibraryIcon } from "./icons";
|
||||||
<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>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const LibraryButton: React.FC<{
|
export const LibraryButton: React.FC<{
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
@ -21,17 +13,16 @@ export const LibraryButton: React.FC<{
|
|||||||
isMobile?: boolean;
|
isMobile?: boolean;
|
||||||
}> = ({ appState, setAppState, isMobile }) => {
|
}> = ({ appState, setAppState, isMobile }) => {
|
||||||
const device = useDevice();
|
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 (
|
return (
|
||||||
<label
|
<label title={`${capitalizeString(t("toolBar.library"))} — 0`}>
|
||||||
className={clsx(
|
|
||||||
"ToolIcon ToolIcon_type_floating ToolIcon__library",
|
|
||||||
`ToolIcon_size_medium`,
|
|
||||||
{
|
|
||||||
"is-mobile": isMobile,
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
title={`${capitalizeString(t("toolBar.library"))} — 0`}
|
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
className="ToolIcon_type_checkbox"
|
className="ToolIcon_type_checkbox"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@ -55,7 +46,12 @@ export const LibraryButton: React.FC<{
|
|||||||
aria-label={capitalizeString(t("toolBar.library"))}
|
aria-label={capitalizeString(t("toolBar.library"))}
|
||||||
aria-keyshortcuts="0"
|
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>
|
</label>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -35,103 +35,32 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.library-actions {
|
.library-actions-counter {
|
||||||
width: 100%;
|
background-color: var(--color-primary);
|
||||||
|
color: var(--color-primary-light);
|
||||||
|
font-weight: bold;
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-right: auto;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
button .library-actions-counter {
|
|
||||||
position: absolute;
|
|
||||||
right: 2px;
|
|
||||||
bottom: 2px;
|
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
width: 1em;
|
width: 1rem;
|
||||||
height: 1em;
|
height: 1rem;
|
||||||
padding: 1px;
|
position: absolute;
|
||||||
font-size: 0.7rem;
|
bottom: -0.25rem;
|
||||||
background: #fff;
|
right: -0.25rem;
|
||||||
}
|
font-size: 0.625rem;
|
||||||
|
pointer-events: none;
|
||||||
&--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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.layer-ui__library-message {
|
.layer-ui__library-message {
|
||||||
padding: 2em 4em;
|
padding: 2rem;
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
.Spinner {
|
flex-grow: 1;
|
||||||
margin-bottom: 1em;
|
justify-content: center;
|
||||||
}
|
|
||||||
span {
|
span {
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
}
|
}
|
||||||
@ -159,11 +88,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.library-menu-browse-button {
|
.library-menu-browse-button {
|
||||||
width: 80%;
|
margin: 1rem auto;
|
||||||
min-height: 22px;
|
|
||||||
margin: 0 auto;
|
padding: 0.875rem 1rem;
|
||||||
margin-top: 1rem;
|
|
||||||
padding: 10px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@ -176,6 +104,10 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-decoration: none !important;
|
text-decoration: none !important;
|
||||||
|
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--color-primary-darker);
|
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 {
|
.library-menu-browse-button--mobile {
|
||||||
min-height: 22px;
|
min-height: 22px;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
|
@ -16,7 +16,7 @@ import { LibraryItems, LibraryItem, AppState, ExcalidrawProps } from "../types";
|
|||||||
|
|
||||||
import "./LibraryMenu.scss";
|
import "./LibraryMenu.scss";
|
||||||
import LibraryMenuItems from "./LibraryMenuItems";
|
import LibraryMenuItems from "./LibraryMenuItems";
|
||||||
import { EVENT, VERSIONS } from "../constants";
|
import { EVENT } from "../constants";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { trackEvent } from "../analytics";
|
import { trackEvent } from "../analytics";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
@ -31,6 +31,7 @@ import { Sidebar } from "./Sidebar/Sidebar";
|
|||||||
import { getSelectedElements } from "../scene";
|
import { getSelectedElements } from "../scene";
|
||||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
import { LibraryMenuHeader } from "./LibraryMenuHeaderContent";
|
import { LibraryMenuHeader } from "./LibraryMenuHeaderContent";
|
||||||
|
import LibraryMenuBrowseButton from "./LibraryMenuBrowseButton";
|
||||||
|
|
||||||
const useOnClickOutside = (
|
const useOnClickOutside = (
|
||||||
ref: RefObject<HTMLElement>,
|
ref: RefObject<HTMLElement>,
|
||||||
@ -94,9 +95,6 @@ export const LibraryMenuContent = ({
|
|||||||
selectedItems: LibraryItem["id"][];
|
selectedItems: LibraryItem["id"][];
|
||||||
onSelectItems: (id: LibraryItem["id"][]) => void;
|
onSelectItems: (id: LibraryItem["id"][]) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const referrer =
|
|
||||||
libraryReturnUrl || window.location.origin + window.location.pathname;
|
|
||||||
|
|
||||||
const [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope);
|
const [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope);
|
||||||
|
|
||||||
const addToLibrary = useCallback(
|
const addToLibrary = useCallback(
|
||||||
@ -131,13 +129,18 @@ export const LibraryMenuContent = ({
|
|||||||
return (
|
return (
|
||||||
<LibraryMenuWrapper>
|
<LibraryMenuWrapper>
|
||||||
<div className="layer-ui__library-message">
|
<div className="layer-ui__library-message">
|
||||||
|
<div>
|
||||||
<Spinner size="2em" />
|
<Spinner size="2em" />
|
||||||
<span>{t("labels.libraryLoadingMessage")}</span>
|
<span>{t("labels.libraryLoadingMessage")}</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</LibraryMenuWrapper>
|
</LibraryMenuWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showBtn =
|
||||||
|
libraryItemsData.libraryItems.length > 0 || pendingElements.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LibraryMenuWrapper>
|
<LibraryMenuWrapper>
|
||||||
<LibraryMenuItems
|
<LibraryMenuItems
|
||||||
@ -150,18 +153,17 @@ export const LibraryMenuContent = ({
|
|||||||
pendingElements={pendingElements}
|
pendingElements={pendingElements}
|
||||||
selectedItems={selectedItems}
|
selectedItems={selectedItems}
|
||||||
onSelectItems={onSelectItems}
|
onSelectItems={onSelectItems}
|
||||||
|
id={id}
|
||||||
|
libraryReturnUrl={libraryReturnUrl}
|
||||||
|
theme={appState.theme}
|
||||||
/>
|
/>
|
||||||
<a
|
{showBtn && (
|
||||||
className="library-menu-browse-button"
|
<LibraryMenuBrowseButton
|
||||||
href={`${process.env.REACT_APP_LIBRARY_URL}?target=${
|
id={id}
|
||||||
window.name || "_blank"
|
libraryReturnUrl={libraryReturnUrl}
|
||||||
}&referrer=${referrer}&useHash=true&token=${id}&theme=${
|
theme={appState.theme}
|
||||||
appState.theme
|
/>
|
||||||
}&version=${VERSIONS.excalidrawLibrary}`}
|
)}
|
||||||
target="_excalidraw_libraries"
|
|
||||||
>
|
|
||||||
{t("labels.libraries")}
|
|
||||||
</a>
|
|
||||||
</LibraryMenuWrapper>
|
</LibraryMenuWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -265,6 +267,7 @@ export const LibraryMenu: React.FC<{
|
|||||||
// is colled correctly
|
// is colled correctly
|
||||||
key="library"
|
key="library"
|
||||||
className="layer-ui__library-sidebar"
|
className="layer-ui__library-sidebar"
|
||||||
|
initialDockedState={appState.isSidebarDocked}
|
||||||
onDock={(docked) => {
|
onDock={(docked) => {
|
||||||
trackEvent(
|
trackEvent(
|
||||||
"library",
|
"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 Library, { libraryItemsAtom } from "../data/library";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { AppState, LibraryItem, LibraryItems } from "../types";
|
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 { ToolButton } from "./ToolButton";
|
||||||
import { Tooltip } from "./Tooltip";
|
|
||||||
import { fileOpen } from "../data/filesystem";
|
import { fileOpen } from "../data/filesystem";
|
||||||
import { muteFSAbortError } from "../utils";
|
import { muteFSAbortError } from "../utils";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
@ -13,6 +18,9 @@ import { jotaiScope } from "../jotai";
|
|||||||
import ConfirmDialog from "./ConfirmDialog";
|
import ConfirmDialog from "./ConfirmDialog";
|
||||||
import PublishLibrary from "./PublishLibrary";
|
import PublishLibrary from "./PublishLibrary";
|
||||||
import { Dialog } from "./Dialog";
|
import { Dialog } from "./Dialog";
|
||||||
|
import { useOutsideClickHook } from "../hooks/useOutsideClick";
|
||||||
|
import MenuItem from "./MenuItem";
|
||||||
|
import { isDropdownOpenAtom } from "./App";
|
||||||
|
|
||||||
const getSelectedItems = (
|
const getSelectedItems = (
|
||||||
libraryItems: LibraryItems,
|
libraryItems: LibraryItems,
|
||||||
@ -165,8 +173,37 @@ export const LibraryMenuHeader: React.FC<{
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [isDropdownOpen, setIsDropdownOpen] = useAtom(isDropdownOpenAtom);
|
||||||
|
const dropdownRef = useOutsideClickHook(() => setIsDropdownOpen(false));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="library-actions">
|
<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}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{showRemoveLibAlert && renderRemoveLibAlert()}
|
{showRemoveLibAlert && renderRemoveLibAlert()}
|
||||||
{showPublishLibraryDialog && (
|
{showPublishLibraryDialog && (
|
||||||
<PublishLibrary
|
<PublishLibrary
|
||||||
@ -189,69 +226,31 @@ export const LibraryMenuHeader: React.FC<{
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{publishLibSuccess && renderPublishSuccess()}
|
{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 && (
|
{!!items.length && (
|
||||||
<>
|
<>
|
||||||
<ToolButton
|
<MenuItem
|
||||||
key="export"
|
label={t("buttons.export")}
|
||||||
type="button"
|
icon={ExportIcon}
|
||||||
title={t("buttons.export")}
|
|
||||||
aria-label={t("buttons.export")}
|
|
||||||
icon={exportToFileIcon}
|
|
||||||
onClick={onLibraryExport}
|
onClick={onLibraryExport}
|
||||||
className="library-actions--export"
|
dataTestId="lib-dropdown--export"
|
||||||
>
|
/>
|
||||||
{selectedItems.length > 0 && (
|
<MenuItem
|
||||||
<span className="library-actions-counter">
|
label={resetLabel}
|
||||||
{selectedItems.length}
|
icon={TrashIcon}
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</ToolButton>
|
|
||||||
<ToolButton
|
|
||||||
key="reset"
|
|
||||||
type="button"
|
|
||||||
title={resetLabel}
|
|
||||||
aria-label={resetLabel}
|
|
||||||
icon={trash}
|
|
||||||
onClick={() => setShowRemoveLibAlert(true)}
|
onClick={() => setShowRemoveLibAlert(true)}
|
||||||
className="library-actions--remove"
|
dataTestId="lib-dropdown--remove"
|
||||||
>
|
/>
|
||||||
{selectedItems.length > 0 && (
|
|
||||||
<span className="library-actions-counter">
|
|
||||||
{selectedItems.length}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</ToolButton>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{itemsSelected && (
|
{itemsSelected && (
|
||||||
<Tooltip label={t("hints.publishLibrary")}>
|
<MenuItem
|
||||||
<ToolButton
|
|
||||||
type="button"
|
|
||||||
aria-label={t("buttons.publishLibrary")}
|
|
||||||
label={t("buttons.publishLibrary")}
|
label={t("buttons.publishLibrary")}
|
||||||
icon={publishIcon}
|
icon={publishIcon}
|
||||||
className="library-actions--publish"
|
dataTestId="lib-dropdown--publish"
|
||||||
onClick={() => setShowPublishLibraryDialog(true)}
|
onClick={() => setShowPublishLibraryDialog(true)}
|
||||||
>
|
/>
|
||||||
<label>{t("buttons.publishLibrary")}</label>
|
|
||||||
{selectedItems.length > 0 && (
|
|
||||||
<span className="library-actions-counter">
|
|
||||||
{selectedItems.length}
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</ToolButton>
|
</div>
|
||||||
</Tooltip>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,18 +1,70 @@
|
|||||||
@import "open-color/open-color";
|
@import "open-color/open-color";
|
||||||
|
|
||||||
.excalidraw {
|
.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 {
|
.library-menu-items-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
flex-basis: 0;
|
||||||
|
overflow-y: auto;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
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 {
|
&__items {
|
||||||
|
row-gap: 0.5rem;
|
||||||
|
padding: var(--container-padding-y) var(--container-padding-x);
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
margin-bottom: 1rem;
|
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 {
|
.separator {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -2,7 +2,7 @@ import React, { useState } from "react";
|
|||||||
import { serializeLibraryAsJSON } from "../data/json";
|
import { serializeLibraryAsJSON } from "../data/json";
|
||||||
import { ExcalidrawElement, NonDeleted } from "../element/types";
|
import { ExcalidrawElement, NonDeleted } from "../element/types";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { LibraryItem, LibraryItems } from "../types";
|
import { AppState, ExcalidrawProps, LibraryItem, LibraryItems } from "../types";
|
||||||
import { arrayToMap, chunk } from "../utils";
|
import { arrayToMap, chunk } from "../utils";
|
||||||
import { LibraryUnit } from "./LibraryUnit";
|
import { LibraryUnit } from "./LibraryUnit";
|
||||||
import Stack from "./Stack";
|
import Stack from "./Stack";
|
||||||
@ -10,6 +10,8 @@ import Stack from "./Stack";
|
|||||||
import "./LibraryMenuItems.scss";
|
import "./LibraryMenuItems.scss";
|
||||||
import { MIME_TYPES } from "../constants";
|
import { MIME_TYPES } from "../constants";
|
||||||
import Spinner from "./Spinner";
|
import Spinner from "./Spinner";
|
||||||
|
import LibraryMenuBrowseButton from "./LibraryMenuBrowseButton";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
const CELLS_PER_ROW = 4;
|
const CELLS_PER_ROW = 4;
|
||||||
|
|
||||||
@ -21,6 +23,9 @@ const LibraryMenuItems = ({
|
|||||||
pendingElements,
|
pendingElements,
|
||||||
selectedItems,
|
selectedItems,
|
||||||
onSelectItems,
|
onSelectItems,
|
||||||
|
theme,
|
||||||
|
id,
|
||||||
|
libraryReturnUrl,
|
||||||
}: {
|
}: {
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
libraryItems: LibraryItems;
|
libraryItems: LibraryItems;
|
||||||
@ -29,6 +34,9 @@ const LibraryMenuItems = ({
|
|||||||
onAddToLibrary: (elements: LibraryItem["elements"]) => void;
|
onAddToLibrary: (elements: LibraryItem["elements"]) => void;
|
||||||
selectedItems: LibraryItem["id"][];
|
selectedItems: LibraryItem["id"][];
|
||||||
onSelectItems: (id: LibraryItem["id"][]) => void;
|
onSelectItems: (id: LibraryItem["id"][]) => void;
|
||||||
|
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
||||||
|
theme: AppState["theme"];
|
||||||
|
id: string;
|
||||||
}) => {
|
}) => {
|
||||||
const [lastSelectedItem, setLastSelectedItem] = useState<
|
const [lastSelectedItem, setLastSelectedItem] = useState<
|
||||||
LibraryItem["id"] | null
|
LibraryItem["id"] | null
|
||||||
@ -167,7 +175,11 @@ const LibraryMenuItems = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Stack.Row align="center" gap={1} key={index}>
|
<Stack.Row
|
||||||
|
align="center"
|
||||||
|
key={index}
|
||||||
|
className="library-menu-items-container__row"
|
||||||
|
>
|
||||||
{rowItems}
|
{rowItems}
|
||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
);
|
);
|
||||||
@ -181,19 +193,21 @@ const LibraryMenuItems = ({
|
|||||||
(item) => item.status === "published",
|
(item) => item.status === "published",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const showBtn =
|
||||||
|
!libraryItems.length &&
|
||||||
|
!unpublishedItems.length &&
|
||||||
|
!publishedItems.length &&
|
||||||
|
!pendingElements.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="library-menu-items-container"
|
className="library-menu-items-container"
|
||||||
style={
|
style={
|
||||||
publishedItems.length || unpublishedItems.length
|
pendingElements.length ||
|
||||||
? {
|
unpublishedItems.length ||
|
||||||
flex: "1 1 0",
|
publishedItems.length
|
||||||
overflowY: "auto",
|
? { justifyContent: "flex-start" }
|
||||||
}
|
: {}
|
||||||
: {
|
|
||||||
marginBottom: "2rem",
|
|
||||||
flex: 0,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Stack.Col
|
<Stack.Col
|
||||||
@ -206,49 +220,37 @@ const LibraryMenuItems = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
<div className="separator">
|
<div>
|
||||||
{(pendingElements.length > 0 ||
|
{(pendingElements.length > 0 ||
|
||||||
unpublishedItems.length > 0 ||
|
unpublishedItems.length > 0 ||
|
||||||
publishedItems.length > 0) && (
|
publishedItems.length > 0) && (
|
||||||
<div>{t("labels.personalLib")}</div>
|
<div className="library-menu-items-container__header">
|
||||||
|
{t("labels.personalLib")}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
marginLeft: "auto",
|
position: "absolute",
|
||||||
marginRight: "1rem",
|
top: "var(--container-padding-y)",
|
||||||
display: "flex",
|
right: "var(--container-padding-x)",
|
||||||
alignItems: "center",
|
transform: "translateY(50%)",
|
||||||
fontWeight: "normal",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ transform: "translateY(2px)" }}>
|
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!pendingElements.length && !unpublishedItems.length ? (
|
{!pendingElements.length && !unpublishedItems.length ? (
|
||||||
|
<div className="library-menu-items__no-items">
|
||||||
<div
|
<div
|
||||||
style={{
|
className={clsx({
|
||||||
height: 65,
|
"library-menu-items__no-items__label": showBtn,
|
||||||
display: "flex",
|
})}
|
||||||
flexDirection: "column",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
width: "100%",
|
|
||||||
fontSize: ".9rem",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{t("library.noItems")}
|
{t("library.noItems")}
|
||||||
<div
|
</div>
|
||||||
style={{
|
<div className="library-menu-items__no-items__hint">
|
||||||
margin: ".6rem 0",
|
|
||||||
fontSize: ".8em",
|
|
||||||
width: "70%",
|
|
||||||
textAlign: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{publishedItems.length > 0
|
{publishedItems.length > 0
|
||||||
? t("library.hint_emptyPrivateLibrary")
|
? t("library.hint_emptyPrivateLibrary")
|
||||||
: t("library.hint_emptyLibrary")}
|
: t("library.hint_emptyLibrary")}
|
||||||
@ -269,7 +271,9 @@ const LibraryMenuItems = ({
|
|||||||
{(publishedItems.length > 0 ||
|
{(publishedItems.length > 0 ||
|
||||||
pendingElements.length > 0 ||
|
pendingElements.length > 0 ||
|
||||||
unpublishedItems.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 ? (
|
{publishedItems.length > 0 ? (
|
||||||
renderLibrarySection(publishedItems)
|
renderLibrarySection(publishedItems)
|
||||||
@ -289,6 +293,14 @@ const LibraryMenuItems = ({
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
|
|
||||||
|
{showBtn && (
|
||||||
|
<LibraryMenuBrowseButton
|
||||||
|
id={id}
|
||||||
|
libraryReturnUrl={libraryReturnUrl}
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Stack.Col>
|
</Stack.Col>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -7,17 +7,18 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 63px;
|
width: 55px;
|
||||||
height: 63px; // match width
|
height: 55px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: var(--border-radius-lg);
|
||||||
|
|
||||||
&--hover {
|
&--hover {
|
||||||
box-shadow: inset 0px 0px 0px 2px $oc-blue-5;
|
border-color: var(--color-primary);
|
||||||
border-color: $oc-blue-5;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&--selected {
|
&--selected {
|
||||||
box-shadow: inset 0px 0px 0px 2px $oc-blue-8;
|
border-color: var(--color-primary);
|
||||||
border-color: $oc-blue-8;
|
border-width: 1px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,20 +60,34 @@
|
|||||||
|
|
||||||
.library-unit__checkbox {
|
.library-unit__checkbox {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 2.3rem;
|
top: 0.125rem;
|
||||||
bottom: 2.3rem;
|
right: 0.125rem;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
.Checkbox-box {
|
.Checkbox-box {
|
||||||
width: 13px;
|
margin: 0;
|
||||||
height: 13px;
|
width: 1rem;
|
||||||
border-radius: 2px;
|
height: 1rem;
|
||||||
margin: 0.5em 0.5em 0.2em 0.2em;
|
border-radius: 4px;
|
||||||
background-color: $oc-blue-1;
|
background-color: var(--color-primary-light);
|
||||||
|
border: 1px solid var(--color-primary);
|
||||||
|
box-shadow: none !important;
|
||||||
|
padding: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.Checkbox:hover {
|
&.Checkbox:hover {
|
||||||
.Checkbox-box {
|
.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 {
|
.library-unit__adder {
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
animation: library-unit__adder-animation 1s ease-in infinite;
|
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 {
|
.library-unit:active .library-unit__adder {
|
||||||
animation: none;
|
animation: none;
|
||||||
transform: scale(0.8);
|
transform: scale(0.8);
|
||||||
fill: $oc-black;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.library-unit__active {
|
.library-unit__active {
|
||||||
|
@ -6,19 +6,7 @@ import { exportToSvg } from "../scene/export";
|
|||||||
import { LibraryItem } from "../types";
|
import { LibraryItem } from "../types";
|
||||||
import "./LibraryUnit.scss";
|
import "./LibraryUnit.scss";
|
||||||
import { CheckboxItem } from "./CheckboxItem";
|
import { CheckboxItem } from "./CheckboxItem";
|
||||||
|
import { PlusIcon } from "./icons";
|
||||||
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>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const LibraryUnit = ({
|
export const LibraryUnit = ({
|
||||||
id,
|
id,
|
||||||
@ -67,7 +55,7 @@ export const LibraryUnit = ({
|
|||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
const isMobile = useDevice().isMobile;
|
const isMobile = useDevice().isMobile;
|
||||||
const adder = isPending && (
|
const adder = isPending && (
|
||||||
<div className="library-unit__adder">{PLUS_ICON}</div>
|
<div className="library-unit__adder">{PlusIcon}</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import "./ToolIcon.scss";
|
import "./ToolIcon.scss";
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { ToolButtonSize } from "./ToolButton";
|
import { ToolButtonSize } from "./ToolButton";
|
||||||
|
import { LockedIcon, UnlockedIcon } from "./icons";
|
||||||
|
|
||||||
type LockIconProps = {
|
type LockIconProps = {
|
||||||
title?: string;
|
title?: string;
|
||||||
@ -16,34 +16,15 @@ type LockIconProps = {
|
|||||||
const DEFAULT_SIZE: ToolButtonSize = "medium";
|
const DEFAULT_SIZE: ToolButtonSize = "medium";
|
||||||
|
|
||||||
const ICONS = {
|
const ICONS = {
|
||||||
CHECKED: (
|
CHECKED: LockedIcon,
|
||||||
<svg
|
UNCHECKED: UnlockedIcon,
|
||||||
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>
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LockButton = (props: LockIconProps) => {
|
export const LockButton = (props: LockIconProps) => {
|
||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"ToolIcon ToolIcon__lock ToolIcon_type_floating",
|
"ToolIcon ToolIcon__lock",
|
||||||
`ToolIcon_size_${DEFAULT_SIZE}`,
|
`ToolIcon_size_${DEFAULT_SIZE}`,
|
||||||
{
|
{
|
||||||
"is-mobile": props.isMobile,
|
"is-mobile": props.isMobile,
|
||||||
|
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 { FixedSideContainer } from "./FixedSideContainer";
|
||||||
import { Island } from "./Island";
|
import { Island } from "./Island";
|
||||||
import { HintViewer } from "./HintViewer";
|
import { HintViewer } from "./HintViewer";
|
||||||
import { calculateScrollCenter, getSelectedElements } from "../scene";
|
import { calculateScrollCenter } from "../scene";
|
||||||
import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
|
import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
|
||||||
import { Section } from "./Section";
|
import { Section } from "./Section";
|
||||||
import CollabButton from "./CollabButton";
|
import CollabButton from "./CollabButton";
|
||||||
import { SCROLLBAR_WIDTH, SCROLLBAR_MARGIN } from "../scene/scrollbars";
|
import { SCROLLBAR_WIDTH, SCROLLBAR_MARGIN } from "../scene/scrollbars";
|
||||||
import { LockButton } from "./LockButton";
|
import { LockButton } from "./LockButton";
|
||||||
import { UserList } from "./UserList";
|
import { UserList } from "./UserList";
|
||||||
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
|
|
||||||
import { LibraryButton } from "./LibraryButton";
|
import { LibraryButton } from "./LibraryButton";
|
||||||
import { PenModeButton } from "./PenModeButton";
|
import { PenModeButton } from "./PenModeButton";
|
||||||
import { Stats } from "./Stats";
|
import { Stats } from "./Stats";
|
||||||
import { actionToggleStats } from "../actions";
|
import { actionToggleStats } from "../actions";
|
||||||
|
import { MenuLinks, Separator } from "./MenuUtils";
|
||||||
|
import WelcomeScreen from "./WelcomeScreen";
|
||||||
|
import MenuItem from "./MenuItem";
|
||||||
|
import { ExportImageIcon } from "./icons";
|
||||||
|
|
||||||
type MobileMenuProps = {
|
type MobileMenuProps = {
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
@ -45,6 +48,7 @@ type MobileMenuProps = {
|
|||||||
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
|
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
|
||||||
renderSidebars: () => JSX.Element | null;
|
renderSidebars: () => JSX.Element | null;
|
||||||
device: Device;
|
device: Device;
|
||||||
|
renderWelcomeScreen?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MobileMenu = ({
|
export const MobileMenu = ({
|
||||||
@ -65,17 +69,35 @@ export const MobileMenu = ({
|
|||||||
renderCustomStats,
|
renderCustomStats,
|
||||||
renderSidebars,
|
renderSidebars,
|
||||||
device,
|
device,
|
||||||
|
renderWelcomeScreen,
|
||||||
}: MobileMenuProps) => {
|
}: MobileMenuProps) => {
|
||||||
const renderToolbar = () => {
|
const renderToolbar = () => {
|
||||||
return (
|
return (
|
||||||
<FixedSideContainer side="top" className="App-top-bar">
|
<FixedSideContainer side="top" className="App-top-bar">
|
||||||
|
{renderWelcomeScreen && !appState.isLoading && (
|
||||||
|
<WelcomeScreen actionManager={actionManager} />
|
||||||
|
)}
|
||||||
<Section heading="shapes">
|
<Section heading="shapes">
|
||||||
{(heading: React.ReactNode) => (
|
{(heading: React.ReactNode) => (
|
||||||
<Stack.Col gap={4} align="center">
|
<Stack.Col gap={4} align="center">
|
||||||
<Stack.Row gap={1} className="App-toolbar-container">
|
<Stack.Row gap={1} className="App-toolbar-container">
|
||||||
<Island padding={1} className="App-toolbar">
|
<Island padding={1} className="App-toolbar App-toolbar--mobile">
|
||||||
{heading}
|
{heading}
|
||||||
<Stack.Row gap={1}>
|
<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
|
<ShapesSwitcher
|
||||||
appState={appState}
|
appState={appState}
|
||||||
canvas={canvas}
|
canvas={canvas}
|
||||||
@ -90,6 +112,15 @@ export const MobileMenu = ({
|
|||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
</Island>
|
</Island>
|
||||||
{renderTopRightUI && renderTopRightUI(true, appState)}
|
{renderTopRightUI && renderTopRightUI(true, appState)}
|
||||||
|
<div className="mobile-misc-tools-container">
|
||||||
|
<PenModeButton
|
||||||
|
checked={appState.penMode}
|
||||||
|
onChange={onPenModeToggle}
|
||||||
|
title={t("toolBar.penMode")}
|
||||||
|
isMobile
|
||||||
|
penDetected={appState.penDetected}
|
||||||
|
// penDetected={true}
|
||||||
|
/>
|
||||||
<LockButton
|
<LockButton
|
||||||
checked={appState.activeTool.locked}
|
checked={appState.activeTool.locked}
|
||||||
onChange={onLockToggle}
|
onChange={onLockToggle}
|
||||||
@ -101,13 +132,7 @@ export const MobileMenu = ({
|
|||||||
setAppState={setAppState}
|
setAppState={setAppState}
|
||||||
isMobile
|
isMobile
|
||||||
/>
|
/>
|
||||||
<PenModeButton
|
</div>
|
||||||
checked={appState.penMode}
|
|
||||||
onChange={onPenModeToggle}
|
|
||||||
title={t("toolBar.penMode")}
|
|
||||||
isMobile
|
|
||||||
penDetected={appState.penDetected}
|
|
||||||
/>
|
|
||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
</Stack.Col>
|
</Stack.Col>
|
||||||
)}
|
)}
|
||||||
@ -123,11 +148,6 @@ export const MobileMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderAppToolbar = () => {
|
const renderAppToolbar = () => {
|
||||||
// Render eraser conditionally in mobile
|
|
||||||
const showEraser =
|
|
||||||
!appState.editingElement &&
|
|
||||||
getSelectedElements(elements, appState).length === 0;
|
|
||||||
|
|
||||||
if (appState.viewModeEnabled) {
|
if (appState.viewModeEnabled) {
|
||||||
return (
|
return (
|
||||||
<div className="App-toolbar-content">
|
<div className="App-toolbar-content">
|
||||||
@ -140,12 +160,9 @@ export const MobileMenu = ({
|
|||||||
<div className="App-toolbar-content">
|
<div className="App-toolbar-content">
|
||||||
{actionManager.renderAction("toggleCanvasMenu")}
|
{actionManager.renderAction("toggleCanvasMenu")}
|
||||||
{actionManager.renderAction("toggleEditMenu")}
|
{actionManager.renderAction("toggleEditMenu")}
|
||||||
|
|
||||||
{actionManager.renderAction("undo")}
|
{actionManager.renderAction("undo")}
|
||||||
{actionManager.renderAction("redo")}
|
{actionManager.renderAction("redo")}
|
||||||
{showEraser
|
{actionManager.renderAction(
|
||||||
? actionManager.renderAction("eraser")
|
|
||||||
: actionManager.renderAction(
|
|
||||||
appState.multiElement ? "finalize" : "duplicateSelection",
|
appState.multiElement ? "finalize" : "duplicateSelection",
|
||||||
)}
|
)}
|
||||||
{actionManager.renderAction("deleteSelectedElements")}
|
{actionManager.renderAction("deleteSelectedElements")}
|
||||||
@ -158,16 +175,27 @@ export const MobileMenu = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{renderJSONExportDialog()}
|
{renderJSONExportDialog()}
|
||||||
|
<MenuItem
|
||||||
|
label={t("buttons.exportImage")}
|
||||||
|
icon={ExportImageIcon}
|
||||||
|
dataTestId="image-export-button"
|
||||||
|
onClick={() => setAppState({ openDialog: "imageExport" })}
|
||||||
|
/>
|
||||||
{renderImageExportDialog()}
|
{renderImageExportDialog()}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{actionManager.renderAction("clearCanvas")}
|
|
||||||
{actionManager.renderAction("loadScene")}
|
{actionManager.renderAction("loadScene")}
|
||||||
{renderJSONExportDialog()}
|
{renderJSONExportDialog()}
|
||||||
{renderImageExportDialog()}
|
{renderImageExportDialog()}
|
||||||
|
<MenuItem
|
||||||
|
label={t("buttons.exportImage")}
|
||||||
|
icon={ExportImageIcon}
|
||||||
|
dataTestId="image-export-button"
|
||||||
|
onClick={() => setAppState({ openDialog: "imageExport" })}
|
||||||
|
/>
|
||||||
{onCollabButtonClick && (
|
{onCollabButtonClick && (
|
||||||
<CollabButton
|
<CollabButton
|
||||||
isCollaborating={isCollaborating}
|
isCollaborating={isCollaborating}
|
||||||
@ -175,7 +203,20 @@ export const MobileMenu = ({
|
|||||||
onClick={onCollabButtonClick}
|
onClick={onCollabButtonClick}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{<BackgroundPickerAndDarkModeToggle actionManager={actionManager} />}
|
{actionManager.renderAction("toggleShortcuts", undefined, true)}
|
||||||
|
{actionManager.renderAction("clearCanvas")}
|
||||||
|
<Separator />
|
||||||
|
<MenuLinks />
|
||||||
|
<Separator />
|
||||||
|
<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 +247,7 @@ export const MobileMenu = ({
|
|||||||
{appState.openMenu === "canvas" ? (
|
{appState.openMenu === "canvas" ? (
|
||||||
<Section className="App-mobile-menu" heading="canvasActions">
|
<Section className="App-mobile-menu" heading="canvasActions">
|
||||||
<div className="panelColumn">
|
<div className="panelColumn">
|
||||||
<Stack.Col gap={4}>
|
<Stack.Col gap={2}>
|
||||||
{renderCanvasActions()}
|
{renderCanvasActions()}
|
||||||
{renderCustomFooter?.(true, appState)}
|
{renderCustomFooter?.(true, appState)}
|
||||||
{appState.collaborators.size > 0 && (
|
{appState.collaborators.size > 0 && (
|
||||||
|
@ -17,6 +17,10 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: calc(var(--space-factor) * 10);
|
padding: calc(var(--space-factor) * 10);
|
||||||
|
|
||||||
|
.Island {
|
||||||
|
padding: 2.5rem !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.Modal__background {
|
.Modal__background {
|
||||||
@ -26,7 +30,7 @@
|
|||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
background-color: transparentize($oc-black, 0.3);
|
background-color: rgba(#121212, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.Modal__content {
|
.Modal__content {
|
||||||
@ -46,7 +50,7 @@
|
|||||||
background: var(--island-bg-color);
|
background: var(--island-bg-color);
|
||||||
|
|
||||||
border: 1px solid var(--dialog-border-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;
|
border-radius: 6px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
@ -73,14 +77,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.Modal__close {
|
.Modal__close {
|
||||||
width: calc(var(--space-factor) * 7);
|
color: var(--icon-fill-color);
|
||||||
height: calc(var(--space-factor) * 7);
|
margin: 0;
|
||||||
display: flex;
|
padding: 0.375rem;
|
||||||
align-items: center;
|
position: absolute;
|
||||||
justify-content: center;
|
top: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
border: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
line-height: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
svg {
|
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"
|
aria-modal="true"
|
||||||
onKeyDown={handleKeydown}
|
onKeyDown={handleKeydown}
|
||||||
aria-labelledby={props.labelledBy}
|
aria-labelledby={props.labelledBy}
|
||||||
|
data-prevent-outside-click
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="Modal__background"
|
className="Modal__background"
|
||||||
|
@ -2,6 +2,7 @@ import "./ToolIcon.scss";
|
|||||||
|
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { ToolButtonSize } from "./ToolButton";
|
import { ToolButtonSize } from "./ToolButton";
|
||||||
|
import { PenModeIcon } from "./icons";
|
||||||
|
|
||||||
type PenModeIconProps = {
|
type PenModeIconProps = {
|
||||||
title?: string;
|
title?: string;
|
||||||
@ -15,59 +16,15 @@ type PenModeIconProps = {
|
|||||||
|
|
||||||
const DEFAULT_SIZE: ToolButtonSize = "medium";
|
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) => {
|
export const PenModeButton = (props: PenModeIconProps) => {
|
||||||
if (!props.penDetected) {
|
if (!props.penDetected) {
|
||||||
if (props.isMobile) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"ToolIcon ToolIcon__penMode ToolIcon_type_floating",
|
"ToolIcon ToolIcon__penMode",
|
||||||
`ToolIcon_size_${DEFAULT_SIZE}`,
|
|
||||||
{
|
|
||||||
"is-mobile": props.isMobile,
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="ToolIcon__icon ToolIcon__hidden" />
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<label
|
|
||||||
className={clsx(
|
|
||||||
"ToolIcon ToolIcon__penMode ToolIcon_type_floating",
|
|
||||||
`ToolIcon_size_${DEFAULT_SIZE}`,
|
`ToolIcon_size_${DEFAULT_SIZE}`,
|
||||||
{
|
{
|
||||||
"is-mobile": props.isMobile,
|
"is-mobile": props.isMobile,
|
||||||
@ -83,9 +40,7 @@ export const PenModeButton = (props: PenModeIconProps) => {
|
|||||||
checked={props.checked}
|
checked={props.checked}
|
||||||
aria-label={props.title}
|
aria-label={props.title}
|
||||||
/>
|
/>
|
||||||
<div className="ToolIcon__icon">
|
<div className="ToolIcon__icon">{PenModeIcon}</div>
|
||||||
{props.checked ? ICONS.CHECKED : ICONS.UNCHECKED}
|
|
||||||
</div>
|
|
||||||
</label>
|
</label>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
label {
|
label {
|
||||||
padding: 1em;
|
padding: 1em 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -34,6 +34,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
padding: 0.2rem 0;
|
padding: 0.2rem 0;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
gap: 0.5rem;
|
||||||
|
|
||||||
.ToolIcon__icon {
|
.ToolIcon__icon {
|
||||||
min-width: 2.5rem;
|
min-width: 2.5rem;
|
||||||
@ -74,7 +75,6 @@
|
|||||||
|
|
||||||
.selected-library-items {
|
.selected-library-items {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 0 0.8rem;
|
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
||||||
.single-library-item-wrapper {
|
.single-library-item-wrapper {
|
||||||
@ -87,7 +87,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&-note {
|
&-note {
|
||||||
padding: 1em;
|
padding: 1em 0;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -4,8 +4,6 @@ import OpenColor from "open-color";
|
|||||||
import { Dialog } from "./Dialog";
|
import { Dialog } from "./Dialog";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
|
|
||||||
import { ToolButton } from "./ToolButton";
|
|
||||||
|
|
||||||
import { AppState, LibraryItems, LibraryItem } from "../types";
|
import { AppState, LibraryItems, LibraryItem } from "../types";
|
||||||
import { exportToCanvas } from "../packages/utils";
|
import { exportToCanvas } from "../packages/utils";
|
||||||
import {
|
import {
|
||||||
@ -20,6 +18,7 @@ import "./PublishLibrary.scss";
|
|||||||
import SingleLibraryItem from "./SingleLibraryItem";
|
import SingleLibraryItem from "./SingleLibraryItem";
|
||||||
import { canvasToBlob, resizeImageFile } from "../data/blob";
|
import { canvasToBlob, resizeImageFile } from "../data/blob";
|
||||||
import { chunk } from "../utils";
|
import { chunk } from "../utils";
|
||||||
|
import DialogActionButton from "./DialogActionButton";
|
||||||
|
|
||||||
interface PublishLibraryDataParams {
|
interface PublishLibraryDataParams {
|
||||||
authorName: string;
|
authorName: string;
|
||||||
@ -434,21 +433,15 @@ const PublishLibrary = ({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="publish-library__buttons">
|
<div className="publish-library__buttons">
|
||||||
<ToolButton
|
<DialogActionButton
|
||||||
type="button"
|
|
||||||
title={t("buttons.cancel")}
|
|
||||||
aria-label={t("buttons.cancel")}
|
|
||||||
label={t("buttons.cancel")}
|
label={t("buttons.cancel")}
|
||||||
onClick={onDialogClose}
|
onClick={onDialogClose}
|
||||||
data-testid="cancel-clear-canvas-button"
|
data-testid="cancel-clear-canvas-button"
|
||||||
className="publish-library__buttons--cancel"
|
|
||||||
/>
|
/>
|
||||||
<ToolButton
|
<DialogActionButton
|
||||||
type="submit"
|
type="submit"
|
||||||
title={t("buttons.submit")}
|
|
||||||
aria-label={t("buttons.submit")}
|
|
||||||
label={t("buttons.submit")}
|
label={t("buttons.submit")}
|
||||||
className="publish-library__buttons--confirm"
|
actionType="primary"
|
||||||
isLoading={isSubmitting}
|
isLoading={isSubmitting}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,20 +2,101 @@
|
|||||||
@import "../../css/variables.module";
|
@import "../../css/variables.module";
|
||||||
|
|
||||||
.excalidraw {
|
.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 {
|
.layer-ui__sidebar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: var(--sat);
|
top: 0;
|
||||||
bottom: var(--sab);
|
bottom: 0;
|
||||||
right: var(--sar);
|
right: 0;
|
||||||
z-index: 5;
|
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;
|
overflow: hidden;
|
||||||
border-radius: var(--border-radius-lg);
|
border-radius: 0;
|
||||||
margin: var(--space-factor);
|
|
||||||
width: calc(#{$right-sidebar-width} - var(--space-factor) * 2);
|
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;
|
box-sizing: border-box;
|
||||||
|
|
||||||
.Island {
|
.Island {
|
||||||
@ -48,42 +129,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.layer-ui__sidebar__header {
|
.layer-ui__sidebar__header {
|
||||||
|
box-sizing: border-box;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 2px 0 15px 0;
|
padding: 1rem;
|
||||||
&:empty {
|
border-bottom: 1px solid var(--sidebar-border-color);
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
// 2px from the left to account for focus border of left-most button
|
|
||||||
margin: 0 2px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.layer-ui__sidebar__header__buttons {
|
.layer-ui__sidebar__header__buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-left: auto;
|
gap: 0.625rem;
|
||||||
}
|
|
||||||
|
|
||||||
.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,6 +33,13 @@ export const Sidebar = Object.assign(
|
|||||||
onClose,
|
onClose,
|
||||||
onDock,
|
onDock,
|
||||||
docked,
|
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,
|
dockable = true,
|
||||||
className,
|
className,
|
||||||
__isInternal,
|
__isInternal,
|
||||||
@ -52,7 +59,9 @@ export const Sidebar = Object.assign(
|
|||||||
|
|
||||||
const setAppState = useExcalidrawSetAppState();
|
const setAppState = useExcalidrawSetAppState();
|
||||||
|
|
||||||
const [isDockedFallback, setIsDockedFallback] = useState(docked ?? false);
|
const [isDockedFallback, setIsDockedFallback] = useState(
|
||||||
|
docked ?? initialDockedState ?? false,
|
||||||
|
);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (docked === undefined) {
|
if (docked === undefined) {
|
||||||
@ -119,8 +128,11 @@ export const Sidebar = Object.assign(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Island
|
<Island
|
||||||
padding={2}
|
className={clsx(
|
||||||
className={clsx("layer-ui__sidebar", className)}
|
"layer-ui__sidebar",
|
||||||
|
{ "layer-ui__sidebar--docked": isDockedFallback },
|
||||||
|
className,
|
||||||
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
>
|
>
|
||||||
<SidebarPropsContext.Provider value={headerPropsRef.current}>
|
<SidebarPropsContext.Provider value={headerPropsRef.current}>
|
||||||
|
@ -3,16 +3,10 @@ import { useContext } from "react";
|
|||||||
import { t } from "../../i18n";
|
import { t } from "../../i18n";
|
||||||
import { useDevice } from "../App";
|
import { useDevice } from "../App";
|
||||||
import { SidebarPropsContext } from "./common";
|
import { SidebarPropsContext } from "./common";
|
||||||
import { close } from "../icons";
|
import { CloseIcon, PinIcon } from "../icons";
|
||||||
import { withUpstreamOverride } from "../hoc/withUpstreamOverride";
|
import { withUpstreamOverride } from "../hoc/withUpstreamOverride";
|
||||||
import { Tooltip } from "../Tooltip";
|
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: {
|
export const SidebarDockButton = (props: {
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
onChange?(): void;
|
onChange?(): void;
|
||||||
@ -33,8 +27,13 @@ export const SidebarDockButton = (props: {
|
|||||||
checked={props.checked}
|
checked={props.checked}
|
||||||
aria-label={t("labels.sidebarLock")}
|
aria-label={t("labels.sidebarLock")}
|
||||||
/>{" "}
|
/>{" "}
|
||||||
<div className="ToolIcon__icon" tabIndex={0}>
|
<div
|
||||||
{SIDE_LIBRARY_TOGGLE_ICON}
|
className={clsx("Sidebar__pin-btn", {
|
||||||
|
"Sidebar__pin-btn--pinned": props.checked,
|
||||||
|
})}
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
{PinIcon}
|
||||||
</div>{" "}
|
</div>{" "}
|
||||||
</label>{" "}
|
</label>{" "}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@ -64,24 +63,19 @@ const _SidebarHeader: React.FC<{
|
|||||||
<SidebarDockButton
|
<SidebarDockButton
|
||||||
checked={!!props.docked}
|
checked={!!props.docked}
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
document
|
|
||||||
.querySelector(".layer-ui__wrapper")
|
|
||||||
?.classList.add("animate");
|
|
||||||
|
|
||||||
props.onDock?.(!props.docked);
|
props.onDock?.(!props.docked);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{renderCloseButton && (
|
{renderCloseButton && (
|
||||||
<div className="ToolIcon__icon__close" data-testid="sidebar-close">
|
|
||||||
<button
|
<button
|
||||||
className="Modal__close"
|
data-testid="sidebar-close"
|
||||||
|
className="Sidebar__close-btn"
|
||||||
onClick={props.onClose}
|
onClick={props.onClose}
|
||||||
aria-label={t("buttons.close")}
|
aria-label={t("buttons.close")}
|
||||||
>
|
>
|
||||||
{close}
|
{CloseIcon}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -9,6 +9,7 @@ export type SidebarProps<P = {}> = {
|
|||||||
/** if not supplied, sidebar won't be dockable */
|
/** if not supplied, sidebar won't be dockable */
|
||||||
onDock?: (docked: boolean) => void;
|
onDock?: (docked: boolean) => void;
|
||||||
docked?: boolean;
|
docked?: boolean;
|
||||||
|
initialDockedState?: boolean;
|
||||||
dockable?: boolean;
|
dockable?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
} & P;
|
} & P;
|
||||||
|
@ -3,7 +3,7 @@ import { useEffect, useRef } from "react";
|
|||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { exportToSvg } from "../packages/utils";
|
import { exportToSvg } from "../packages/utils";
|
||||||
import { AppState, LibraryItem } from "../types";
|
import { AppState, LibraryItem } from "../types";
|
||||||
import { close } from "./icons";
|
import { CloseIcon } from "./icons";
|
||||||
|
|
||||||
import "./SingleLibraryItem.scss";
|
import "./SingleLibraryItem.scss";
|
||||||
import { ToolButton } from "./ToolButton";
|
import { ToolButton } from "./ToolButton";
|
||||||
@ -54,7 +54,7 @@ const SingleLibraryItem = ({
|
|||||||
<ToolButton
|
<ToolButton
|
||||||
aria-label={t("buttons.remove")}
|
aria-label={t("buttons.remove")}
|
||||||
type="button"
|
type="button"
|
||||||
icon={close}
|
icon={CloseIcon}
|
||||||
className="single-library-item--remove"
|
className="single-library-item--remove"
|
||||||
onClick={onRemove.bind(null, libItem.id)}
|
onClick={onRemove.bind(null, libItem.id)}
|
||||||
title={t("buttons.remove")}
|
title={t("buttons.remove")}
|
||||||
@ -62,7 +62,7 @@ const SingleLibraryItem = ({
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
margin: "0.8rem 0.3rem",
|
margin: "0.8rem 0",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
fontSize: "14px",
|
fontSize: "14px",
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
|
@ -4,7 +4,7 @@ import { NonDeletedExcalidrawElement } from "../element/types";
|
|||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { getTargetElements } from "../scene";
|
import { getTargetElements } from "../scene";
|
||||||
import { AppState, ExcalidrawProps } from "../types";
|
import { AppState, ExcalidrawProps } from "../types";
|
||||||
import { close } from "./icons";
|
import { CloseIcon } from "./icons";
|
||||||
import { Island } from "./Island";
|
import { Island } from "./Island";
|
||||||
import "./Stats.scss";
|
import "./Stats.scss";
|
||||||
|
|
||||||
@ -23,7 +23,7 @@ export const Stats = (props: {
|
|||||||
<div className="Stats">
|
<div className="Stats">
|
||||||
<Island padding={2}>
|
<Island padding={2}>
|
||||||
<div className="close" onClick={props.onClose}>
|
<div className="close" onClick={props.onClose}>
|
||||||
{close}
|
{CloseIcon}
|
||||||
</div>
|
</div>
|
||||||
<h3>{t("stats.title")}</h3>
|
<h3>{t("stats.title")}</h3>
|
||||||
<table>
|
<table>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useEffect, useRef } from "react";
|
import { useCallback, useEffect, useRef } from "react";
|
||||||
import { close } from "./icons";
|
import { CloseIcon } from "./icons";
|
||||||
import "./Toast.scss";
|
import "./Toast.scss";
|
||||||
import { ToolButton } from "./ToolButton";
|
import { ToolButton } from "./ToolButton";
|
||||||
|
|
||||||
@ -47,7 +47,7 @@ export const Toast = ({
|
|||||||
<p className="Toast__message">{message}</p>
|
<p className="Toast__message">{message}</p>
|
||||||
{closable && (
|
{closable && (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
icon={close}
|
icon={CloseIcon}
|
||||||
aria-label="close"
|
aria-label="close"
|
||||||
type="icon"
|
type="icon"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
|
@ -3,12 +3,19 @@
|
|||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
.ToolIcon {
|
.ToolIcon {
|
||||||
|
border-radius: var(--border-radius-lg);
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
|
&__hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include toolbarButtonColorStates;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ToolIcon--plain {
|
.ToolIcon--plain {
|
||||||
@ -21,21 +28,15 @@
|
|||||||
|
|
||||||
.ToolIcon_type_radio,
|
.ToolIcon_type_radio,
|
||||||
.ToolIcon_type_checkbox {
|
.ToolIcon_type_checkbox {
|
||||||
& + .ToolIcon__icon {
|
position: absolute;
|
||||||
background-color: var(--button-gray-1);
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
&:hover {
|
|
||||||
background-color: var(--button-gray-2);
|
|
||||||
}
|
|
||||||
&:active {
|
|
||||||
background-color: var(--button-gray-3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ToolIcon__icon {
|
.ToolIcon__icon {
|
||||||
width: 2.5rem;
|
box-sizing: border-box;
|
||||||
height: 2.5rem;
|
width: var(--default-button-size);
|
||||||
|
height: var(--default-button-size);
|
||||||
color: var(--icon-fill-color);
|
color: var(--icon-fill-color);
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -50,8 +51,8 @@
|
|||||||
|
|
||||||
svg {
|
svg {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 1em;
|
width: var(--default-icon-size);
|
||||||
fill: var(--icon-fill-color);
|
height: var(--default-icon-size);
|
||||||
color: var(--icon-fill-color);
|
color: var(--icon-fill-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -75,13 +76,14 @@
|
|||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.excalidraw .ToolIcon_type_button,
|
.ToolIcon_type_button,
|
||||||
.Modal .ToolIcon_type_button,
|
.Modal .ToolIcon_type_button,
|
||||||
.ToolIcon_type_button {
|
.ToolIcon_type_button {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: none;
|
border: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
|
background-color: initial;
|
||||||
|
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
box-shadow: 0 0 0 2px var(--focus-highlight-color);
|
box-shadow: 0 0 0 2px var(--focus-highlight-color);
|
||||||
@ -95,9 +97,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
// &:hover {
|
||||||
background-color: var(--button-gray-2);
|
// background-color: var(--button-gray-2);
|
||||||
}
|
// }
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
background-color: var(--button-gray-3);
|
background-color: var(--button-gray-3);
|
||||||
@ -108,29 +110,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&--hide {
|
&--hide {
|
||||||
visibility: hidden;
|
// visibility: hidden;
|
||||||
}
|
display: none !important;
|
||||||
}
|
|
||||||
|
|
||||||
.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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,66 +144,12 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 2px;
|
bottom: 2px;
|
||||||
right: 3px;
|
right: 3px;
|
||||||
font-size: 0.5em;
|
font-size: 0.625rem;
|
||||||
color: var(--keybinding-color);
|
color: var(--keybinding-color);
|
||||||
font-family: var(--ui-font);
|
font-family: var(--ui-font);
|
||||||
user-select: none;
|
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 {
|
.unlocked-icon {
|
||||||
:root[dir="ltr"] & {
|
:root[dir="ltr"] & {
|
||||||
left: 2px;
|
left: 2px;
|
||||||
@ -232,4 +159,16 @@
|
|||||||
right: 2px;
|
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";
|
@import "../css/variables.module";
|
||||||
|
|
||||||
.excalidraw {
|
.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 {
|
.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 {
|
&.zen-mode {
|
||||||
.ToolIcon__keybinding,
|
.ToolIcon__keybinding,
|
||||||
.HintViewer {
|
.HintViewer {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
&.theme--dark .App-toolbar .ToolIcon:active {
|
&__divider {
|
||||||
--icon-fill-color: #{$oc-gray-3};
|
width: 1px;
|
||||||
--keybinding-color: #{$oc-gray-3};
|
height: 1.5rem;
|
||||||
|
align-self: center;
|
||||||
|
background-color: var(--default-border-color);
|
||||||
|
margin: 0 0.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,23 +7,30 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
gap: 0.625rem;
|
||||||
|
|
||||||
&:empty {
|
&:empty {
|
||||||
display: none;
|
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 > * {
|
.UserList > * {
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
margin: 0 0 var(--space-factor) var(--space-factor);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.UserList_mobile {
|
.UserList_mobile {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
justify-content: normal;
|
justify-content: normal;
|
||||||
}
|
margin: 0.5rem 0;
|
||||||
|
|
||||||
.UserList_mobile > * {
|
|
||||||
margin: 0 var(--space-factor) var(--space-factor) 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 (
|
return (
|
||||||
<div className={clsx("UserList", className, { UserList_mobile: mobile })}>
|
<div className={clsx("UserList", className, { UserList_mobile: mobile })}>
|
||||||
{avatars}
|
{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;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
131
src/components/WelcomeScreen.tsx
Normal file
131
src/components/WelcomeScreen.tsx
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
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 {
|
||||||
|
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 = ({ actionManager }: { actionManager: ActionManager }) => {
|
||||||
|
const [, setCollabDialogShown] = useAtom(collabDialogShownAtom);
|
||||||
|
|
||||||
|
let subheadingJSX;
|
||||||
|
|
||||||
|
if (isExcalidrawPlusSignedUser) {
|
||||||
|
subheadingJSX = t("welcomeScreen.switchToPlusApp")
|
||||||
|
.split(/(Excalidraw\+)/)
|
||||||
|
.map((bit) => {
|
||||||
|
if (bit === "Excalidraw+") {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
style={{ pointerEvents: "all" }}
|
||||||
|
href={`${process.env.REACT_APP_PLUS_APP}?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenSignedInUser`}
|
||||||
|
>
|
||||||
|
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">
|
||||||
|
<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;
|
File diff suppressed because it is too large
Load Diff
@ -19,6 +19,10 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
@ -85,15 +89,16 @@
|
|||||||
.panelColumn {
|
.panelColumn {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
row-gap: 0.75rem;
|
||||||
|
|
||||||
h3,
|
h3,
|
||||||
legend,
|
legend,
|
||||||
.control-label {
|
.control-label {
|
||||||
margin-top: 0.333rem;
|
margin: 0;
|
||||||
margin-bottom: 0.333rem;
|
margin-bottom: 0.25rem;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: var(--text-primary-color);
|
color: var(--text-primary-color);
|
||||||
font-weight: bold;
|
font-weight: normal;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,12 +107,6 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
h3:first-child,
|
|
||||||
legend:first-child,
|
|
||||||
.control-label:first-child {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
legend {
|
legend {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
@ -119,11 +118,12 @@
|
|||||||
|
|
||||||
.buttonList {
|
.buttonList {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
display: flex;
|
||||||
|
column-gap: 0.5rem;
|
||||||
|
row-gap: 0.5rem;
|
||||||
|
|
||||||
label {
|
label {
|
||||||
margin-right: 0.25rem;
|
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
display: inline-block;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="radio"],
|
input[type="radio"],
|
||||||
@ -136,38 +136,10 @@
|
|||||||
.iconRow {
|
.iconRow {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ToolIcon {
|
|
||||||
margin: 0;
|
|
||||||
margin-inline-end: 8px;
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline: transparent;
|
|
||||||
box-shadow: 0 0 0 2px var(--focus-highlight-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--button-gray-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background-color: var(--button-gray-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ToolIcon__icon {
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldset {
|
fieldset {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
margin-top: 0.333rem;
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
@ -185,64 +157,26 @@
|
|||||||
box-shadow: 0 0 0 2px var(--focus-highlight-color);
|
box-shadow: 0 0 0 2px var(--focus-highlight-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.buttonList {
|
||||||
|
.ToolIcon__icon {
|
||||||
|
all: unset !important;
|
||||||
|
display: flex !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
label,
|
||||||
button,
|
button,
|
||||||
.buttonList label {
|
.zIndexButton {
|
||||||
user-select: none;
|
@include outlineButtonStyles;
|
||||||
background-color: var(--button-gray-1);
|
|
||||||
border: 0;
|
|
||||||
border-radius: var(--border-radius-md);
|
|
||||||
margin: 0.125rem 0;
|
|
||||||
padding: 0.25rem;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
cursor: pointer;
|
padding: 0;
|
||||||
|
|
||||||
&:focus-visible {
|
|
||||||
outline: transparent;
|
|
||||||
box-shadow: 0 0 0 2px var(--focus-highlight-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--button-gray-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background-color: var(--button-gray-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.active,
|
|
||||||
.buttonList label.active {
|
|
||||||
background-color: var(--color-primary);
|
|
||||||
|
|
||||||
--icon-fill-color: #{$oc-white};
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--color-primary-darker);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background-color: var(--color-primary-darkest);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttonList.buttonListIcon {
|
|
||||||
label {
|
|
||||||
display: inline-flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
svg {
|
svg {
|
||||||
width: 35px;
|
width: var(--default-icon-size);
|
||||||
height: 14px;
|
height: var(--default-icon-size);
|
||||||
padding: 2px;
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
&.active svg {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -289,8 +223,6 @@
|
|||||||
.App-toolbar {
|
.App-toolbar {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
.eraser {
|
.eraser {
|
||||||
&.ToolIcon:hover {
|
&.ToolIcon:hover {
|
||||||
--icon-fill-color: #fff;
|
--icon-fill-color: #fff;
|
||||||
@ -322,12 +254,27 @@
|
|||||||
color: var(--icon-fill-color);
|
color: var(--icon-fill-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.shapes-section {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
pointer-events: none !important;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.App-menu_top {
|
.App-menu_top {
|
||||||
grid-template-columns: auto max-content auto;
|
grid-template-columns: 1fr 2fr 1fr;
|
||||||
grid-gap: 4px;
|
grid-gap: 2rem;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
pointer-events: none !important;
|
pointer-events: none !important;
|
||||||
|
|
||||||
|
@media (min-width: 1536px) {
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
grid-gap: 3rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.layer-ui__wrapper:not(.disable-pointerEvents) .App-menu_top > * {
|
.layer-ui__wrapper:not(.disable-pointerEvents) .App-menu_top > * {
|
||||||
@ -344,20 +291,14 @@
|
|||||||
|
|
||||||
.App-menu_bottom {
|
.App-menu_bottom {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
bottom: 1rem;
|
||||||
grid-template-columns: min-content auto min-content;
|
display: flex;
|
||||||
grid-gap: 15px;
|
justify-content: space-between;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
pointer-events: none !important;
|
pointer-events: none !important;
|
||||||
|
box-sizing: border-box;
|
||||||
:root[dir="ltr"] & {
|
padding: 0 1rem;
|
||||||
left: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[dir="rtl"] & {
|
|
||||||
right: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--transition-left {
|
&--transition-left {
|
||||||
section {
|
section {
|
||||||
@ -390,7 +331,10 @@
|
|||||||
|
|
||||||
.App-menu__left {
|
.App-menu__left {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
box-shadow: var(--shadow-island);
|
padding: 0.75rem;
|
||||||
|
width: 202px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-select {
|
.dropdown-select {
|
||||||
@ -426,55 +370,65 @@
|
|||||||
&:active {
|
&:active {
|
||||||
background-color: var(--button-gray-2);
|
background-color: var(--button-gray-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__language {
|
||||||
|
height: 2rem;
|
||||||
|
background-color: var(--island-bg-color);
|
||||||
|
border-color: var(--default-border-color) !important;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--island-bg-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.zIndexButton {
|
.disable-zen-mode {
|
||||||
margin: 0;
|
border-radius: var(--border-radius-lg);
|
||||||
margin-inline-end: 8px;
|
background-color: var(--color-gray-20);
|
||||||
padding: 5px;
|
border: 1px solid var(--color-gray-30);
|
||||||
display: inline-flex;
|
padding: 10px 20px;
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
svg {
|
&:hover {
|
||||||
width: 18px;
|
background-color: var(--color-gray-30);
|
||||||
height: 18px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.scroll-back-to-content {
|
.scroll-back-to-content {
|
||||||
color: var(--popup-text-color);
|
border-radius: var(--border-radius-lg);
|
||||||
|
background-color: var(--island-bg-color);
|
||||||
|
color: var(--icon-fill-color);
|
||||||
|
|
||||||
|
border: 1px solid var(--default-border-color);
|
||||||
|
padding: 10px 20px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
bottom: 30px;
|
bottom: 30px;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
padding: 10px 20px;
|
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--button-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
border: 1px solid var(--color-primary-darkest);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.help-icon {
|
.help-icon {
|
||||||
display: flex;
|
@include outlineButtonStyles;
|
||||||
cursor: pointer;
|
background-color: var(--island-bg-color);
|
||||||
fill: $oc-gray-6;
|
width: var(--lg-button-size);
|
||||||
padding: 0;
|
height: var(--lg-button-size);
|
||||||
margin: 0;
|
|
||||||
background: none;
|
|
||||||
color: var(--icon-fill-color);
|
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
width: 1.5rem;
|
width: var(--lg-icon-size);
|
||||||
height: 1.5rem;
|
height: var(--lg-icon-size);
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: none;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.reset-zoom-button {
|
.reset-zoom-button {
|
||||||
padding: 0.2em;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--text-primary-color);
|
|
||||||
font-family: var(--ui-font);
|
font-family: var(--ui-font);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -491,7 +445,6 @@
|
|||||||
.eraser-buttons {
|
.eraser-buttons {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-auto-flow: column;
|
grid-auto-flow: column;
|
||||||
gap: 0.4em;
|
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
margin-bottom: auto;
|
margin-bottom: auto;
|
||||||
margin-inline-start: 0.6em;
|
margin-inline-start: 0.6em;
|
||||||
@ -572,17 +525,49 @@
|
|||||||
// use custom, minimalistic scrollbar
|
// use custom, minimalistic scrollbar
|
||||||
// (doesn't work in Firefox)
|
// (doesn't work in Firefox)
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 5px;
|
width: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: var(--button-gray-2);
|
background: var(--scrollbar-thumb);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: var(--button-gray-3);
|
background: var(--scrollbar-thumb-hover);
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar-thumb:active {
|
::-webkit-scrollbar-thumb:active {
|
||||||
background: var(--button-gray-2);
|
background: var(--scrollbar-thumb);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-misc-tools-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 5rem;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: 1px solid var(--sidebar-border-color);
|
||||||
|
border-top-left-radius: var(--border-radius-lg);
|
||||||
|
border-bottom-left-radius: var(--border-radius-lg);
|
||||||
|
border-right: 0;
|
||||||
|
|
||||||
|
background-color: var(--island-bg-color);
|
||||||
|
|
||||||
|
.ToolIcon__icon {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-button {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.App-toolbar--mobile {
|
||||||
|
overflow-x: hidden;
|
||||||
|
max-width: 100vw;
|
||||||
|
|
||||||
|
.ToolIcon__keybinding {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,10 +9,10 @@
|
|||||||
--button-gray-2: #{$oc-gray-4};
|
--button-gray-2: #{$oc-gray-4};
|
||||||
--button-gray-3: #{$oc-gray-5};
|
--button-gray-3: #{$oc-gray-5};
|
||||||
--button-special-active-bg-color: #{$oc-green-0};
|
--button-special-active-bg-color: #{$oc-green-0};
|
||||||
--dialog-border-color: #{$oc-gray-6};
|
--dialog-border-color: var(--color-gray-20);
|
||||||
--dropdown-icon: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="292.4" height="292.4" viewBox="0 0 292 292"><path d="M287 197L159 69c-4-3-8-5-13-5s-9 2-13 5L5 197c-3 4-5 8-5 13s2 9 5 13c4 4 8 5 13 5h256c5 0 9-1 13-5s5-8 5-13-1-9-5-13z"/></svg>');
|
--dropdown-icon: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="292.4" height="292.4" viewBox="0 0 292 292"><path d="M287 197L159 69c-4-3-8-5-13-5s-9 2-13 5L5 197c-3 4-5 8-5 13s2 9 5 13c4 4 8 5 13 5h256c5 0 9-1 13-5s5-8 5-13-1-9-5-13z"/></svg>');
|
||||||
--focus-highlight-color: #{$oc-blue-2};
|
--focus-highlight-color: #{$oc-blue-2};
|
||||||
--icon-fill-color: #{$oc-gray-9};
|
--icon-fill-color: var(--color-gray-80);
|
||||||
--icon-green-fill-color: #{$oc-green-9};
|
--icon-green-fill-color: #{$oc-green-9};
|
||||||
--default-bg-color: #{$oc-white};
|
--default-bg-color: #{$oc-white};
|
||||||
--input-bg-color: #{$oc-white};
|
--input-bg-color: #{$oc-white};
|
||||||
@ -20,7 +20,7 @@
|
|||||||
--input-hover-bg-color: #{$oc-gray-1};
|
--input-hover-bg-color: #{$oc-gray-1};
|
||||||
--input-label-color: #{$oc-gray-7};
|
--input-label-color: #{$oc-gray-7};
|
||||||
--island-bg-color: rgba(255, 255, 255, 0.96);
|
--island-bg-color: rgba(255, 255, 255, 0.96);
|
||||||
--keybinding-color: #{$oc-gray-5};
|
--keybinding-color: var(--color-gray-40);
|
||||||
--link-color: #{$oc-blue-7};
|
--link-color: #{$oc-blue-7};
|
||||||
--overlay-bg-color: #{transparentize($oc-white, 0.12)};
|
--overlay-bg-color: #{transparentize($oc-white, 0.12)};
|
||||||
--popup-bg-color: #{$oc-white};
|
--popup-bg-color: #{$oc-white};
|
||||||
@ -32,22 +32,75 @@
|
|||||||
--sar: env(safe-area-inset-right);
|
--sar: env(safe-area-inset-right);
|
||||||
--sat: env(safe-area-inset-top);
|
--sat: env(safe-area-inset-top);
|
||||||
--select-highlight-color: #{$oc-blue-5};
|
--select-highlight-color: #{$oc-blue-5};
|
||||||
--shadow-island: 0 0 0 1px rgba(0, 0, 0, 0.01), 1px 1px 5px rgb(0 0 0 / 12%);
|
--shadow-island: 0px 7px 14px rgba(0, 0, 0, 0.05),
|
||||||
|
0px 0px 3.12708px rgba(0, 0, 0, 0.0798),
|
||||||
|
0px 0px 0.931014px rgba(0, 0, 0, 0.1702);
|
||||||
|
--button-hover: var(--color-gray-10);
|
||||||
|
--default-border-color: var(--color-gray-30);
|
||||||
|
|
||||||
|
--default-button-size: 2rem;
|
||||||
|
--default-icon-size: 1rem;
|
||||||
|
--lg-button-size: 2.25rem;
|
||||||
|
--lg-icon-size: 1rem;
|
||||||
|
|
||||||
|
@media screen and (min-device-width: 1921px) {
|
||||||
|
--lg-button-size: 2.5rem;
|
||||||
|
--lg-icon-size: 1.25rem;
|
||||||
|
--default-button-size: 2.25rem;
|
||||||
|
--default-icon-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
--scrollbar-thumb: var(--button-gray-2);
|
||||||
|
--scrollbar-thumb-hover: var(--button-gray-3);
|
||||||
|
|
||||||
|
--modal-shadow: 0px 100px 80px rgba(0, 0, 0, 0.07),
|
||||||
|
0px 41.7776px 33.4221px rgba(0, 0, 0, 0.0503198),
|
||||||
|
0px 22.3363px 17.869px rgba(0, 0, 0, 0.0417275),
|
||||||
|
0px 12.5216px 10.0172px rgba(0, 0, 0, 0.035),
|
||||||
|
0px 6.6501px 5.32008px rgba(0, 0, 0, 0.0282725),
|
||||||
|
0px 2.76726px 2.21381px rgba(0, 0, 0, 0.0196802);
|
||||||
|
--avatar-border-color: var(--color-gray-20);
|
||||||
|
--sidebar-shadow: 0px 100px 80px rgba(0, 0, 0, 0.07),
|
||||||
|
0px 41.7776px 33.4221px rgba(0, 0, 0, 0.0503198),
|
||||||
|
0px 22.3363px 17.869px rgba(0, 0, 0, 0.0417275),
|
||||||
|
0px 12.5216px 10.0172px rgba(0, 0, 0, 0.035),
|
||||||
|
0px 6.6501px 5.32008px rgba(0, 0, 0, 0.0282725),
|
||||||
|
0px 2.76726px 2.21381px rgba(0, 0, 0, 0.0196802);
|
||||||
|
--sidebar-border-color: var(--color-gray-20);
|
||||||
|
--sidebar-bg-color: #fff;
|
||||||
|
--library-dropdown-shadow: 0px 15px 6px rgba(0, 0, 0, 0.01),
|
||||||
|
0px 8px 5px rgba(0, 0, 0, 0.05), 0px 4px 4px rgba(0, 0, 0, 0.09),
|
||||||
|
0px 1px 2px rgba(0, 0, 0, 0.1), 0px 0px 0px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
--space-factor: 0.25rem;
|
--space-factor: 0.25rem;
|
||||||
--text-primary-color: #{$oc-gray-8};
|
--text-primary-color: var(--color-gray-80);
|
||||||
|
|
||||||
|
--color-selection: #6965db;
|
||||||
|
|
||||||
--color-primary: #6965db;
|
--color-primary: #6965db;
|
||||||
--color-primary-darker: #5b57d1;
|
--color-primary-darker: #5b57d1;
|
||||||
--color-primary-darkest: #4a47b1;
|
--color-primary-darkest: #4a47b1;
|
||||||
--color-primary-light: #e2e1fc;
|
--color-primary-light: #e3e2fe;
|
||||||
|
|
||||||
|
--color-gray-10: #f5f5f5;
|
||||||
|
--color-gray-20: #ebebeb;
|
||||||
|
--color-gray-30: #d6d6d6;
|
||||||
|
--color-gray-40: #b8b8b8;
|
||||||
|
--color-gray-50: #999999;
|
||||||
|
--color-gray-60: #7a7a7a;
|
||||||
|
--color-gray-70: #5c5c5c;
|
||||||
|
--color-gray-80: #3d3d3d;
|
||||||
|
--color-gray-85: #242424;
|
||||||
|
--color-gray-90: #1e1e1e;
|
||||||
|
--color-gray-100: #121212;
|
||||||
|
|
||||||
|
--color-danger: #db6965;
|
||||||
|
--color-promo: #e70078;
|
||||||
|
|
||||||
--border-radius-md: 0.375rem;
|
--border-radius-md: 0.375rem;
|
||||||
--border-radius-lg: 0.5rem;
|
--border-radius-lg: 0.5rem;
|
||||||
|
|
||||||
&.theme--dark {
|
&.theme--dark {
|
||||||
background: $oc-black;
|
|
||||||
|
|
||||||
&.theme--dark-background-none {
|
&.theme--dark-background-none {
|
||||||
background: none;
|
background: none;
|
||||||
}
|
}
|
||||||
@ -57,22 +110,23 @@
|
|||||||
--theme-filter: #{$theme-filter};
|
--theme-filter: #{$theme-filter};
|
||||||
--button-destructive-bg-color: #5a0000;
|
--button-destructive-bg-color: #5a0000;
|
||||||
--button-destructive-color: #{$oc-red-3};
|
--button-destructive-color: #{$oc-red-3};
|
||||||
|
|
||||||
--button-gray-1: #363636;
|
--button-gray-1: #363636;
|
||||||
--button-gray-2: #272727;
|
--button-gray-2: #272727;
|
||||||
--button-gray-3: #222;
|
--button-gray-3: #222;
|
||||||
--button-special-active-bg-color: #204624;
|
--button-special-active-bg-color: #204624;
|
||||||
--dialog-border-color: #{$oc-gray-9};
|
--dialog-border-color: var(--color-gray-80);
|
||||||
--dropdown-icon: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="292.4" height="292.4" viewBox="0 0 292 292"><path fill="%23ced4da" d="M287 197L159 69c-4-3-8-5-13-5s-9 2-13 5L5 197c-3 4-5 8-5 13s2 9 5 13c4 4 8 5 13 5h256c5 0 9-1 13-5s5-8 5-13-1-9-5-13z"/></svg>');
|
--dropdown-icon: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="292.4" height="292.4" viewBox="0 0 292 292"><path fill="%23ced4da" d="M287 197L159 69c-4-3-8-5-13-5s-9 2-13 5L5 197c-3 4-5 8-5 13s2 9 5 13c4 4 8 5 13 5h256c5 0 9-1 13-5s5-8 5-13-1-9-5-13z"/></svg>');
|
||||||
--focus-highlight-color: #{$oc-blue-6};
|
--focus-highlight-color: #{$oc-blue-6};
|
||||||
--icon-fill-color: #{$oc-gray-4};
|
--icon-fill-color: var(--color-gray-40);
|
||||||
--icon-green-fill-color: #{$oc-green-4};
|
--icon-green-fill-color: #{$oc-green-4};
|
||||||
--default-bg-color: #121212;
|
--default-bg-color: #121212;
|
||||||
--input-bg-color: #121212;
|
--input-bg-color: #121212;
|
||||||
--input-border-color: #2e2e2e;
|
--input-border-color: #2e2e2e;
|
||||||
--input-hover-bg-color: #181818;
|
--input-hover-bg-color: #181818;
|
||||||
--input-label-color: #{$oc-gray-2};
|
--input-label-color: #{$oc-gray-2};
|
||||||
--island-bg-color: rgba(30, 30, 30, 0.98);
|
--island-bg-color: #262627;
|
||||||
--keybinding-color: #{$oc-gray-6};
|
--keybinding-color: var(--color-gray-60);
|
||||||
--link-color: #{$oc-blue-4};
|
--link-color: #{$oc-blue-4};
|
||||||
--overlay-bg-color: #{transparentize($oc-gray-8, 0.88)};
|
--overlay-bg-color: #{transparentize($oc-gray-8, 0.88)};
|
||||||
--popup-bg-color: #2c2c2c;
|
--popup-bg-color: #2c2c2c;
|
||||||
@ -80,12 +134,35 @@
|
|||||||
--popup-text-color: #{$oc-gray-4};
|
--popup-text-color: #{$oc-gray-4};
|
||||||
--popup-text-inverted-color: #2c2c2c;
|
--popup-text-inverted-color: #2c2c2c;
|
||||||
--select-highlight-color: #{$oc-blue-4};
|
--select-highlight-color: #{$oc-blue-4};
|
||||||
--shadow-island: 1px 1px 5px #{transparentize($oc-black, 0.7)};
|
--text-primary-color: var(--color-gray-40);
|
||||||
--text-primary-color: #{$oc-gray-4};
|
--button-hover: var(--color-gray-80);
|
||||||
|
--default-border-color: var(--color-gray-80);
|
||||||
|
--shadow-island: 0px 13px 33px rgba(0, 0, 0, 0.07),
|
||||||
|
0px 4.13px 9.94853px rgba(0, 0, 0, 0.0456112),
|
||||||
|
0px 1.13px 4.13211px rgba(0, 0, 0, 0.035),
|
||||||
|
0px 0.769896px 1.4945px rgba(0, 0, 0, 0.0243888);
|
||||||
|
--modal-shadow: 0px 100px 80px rgba(0, 0, 0, 0.07),
|
||||||
|
0px 41.7776px 33.4221px rgba(0, 0, 0, 0.0503198),
|
||||||
|
0px 22.3363px 17.869px rgba(0, 0, 0, 0.0417275),
|
||||||
|
0px 12.5216px 10.0172px rgba(0, 0, 0, 0.035),
|
||||||
|
0px 6.6501px 5.32008px rgba(0, 0, 0, 0.0282725),
|
||||||
|
0px 2.76726px 2.21381px rgba(0, 0, 0, 0.0196802);
|
||||||
|
--avatar-border-color: var(--color-gray-85);
|
||||||
|
--sidebar-border-color: var(--color-gray-85);
|
||||||
|
--sidebar-bg-color: #191919;
|
||||||
|
|
||||||
--color-primary: #5650f0;
|
--scrollbar-thumb: #{$oc-gray-8};
|
||||||
--color-primary-darker: #4b46d8;
|
--scrollbar-thumb-hover: #{$oc-gray-7};
|
||||||
--color-primary-darkest: #3e39be;
|
|
||||||
--color-primary-light: #3f3d64;
|
// will be inverted to a lighter color.
|
||||||
|
--color-selection: #3530c4;
|
||||||
|
|
||||||
|
--color-primary: #a8a5ff;
|
||||||
|
--color-primary-darker: #b2aeff;
|
||||||
|
--color-primary-darkest: #beb9ff;
|
||||||
|
--color-primary-light: #4f4d6f;
|
||||||
|
|
||||||
|
--color-danger: #ffa8a5;
|
||||||
|
--color-promo: #d297ff;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,18 +7,28 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@mixin toolbarButtonColorStates {
|
@mixin toolbarButtonColorStates {
|
||||||
|
&.fillable {
|
||||||
.ToolIcon_type_radio,
|
.ToolIcon_type_radio,
|
||||||
.ToolIcon_type_checkbox {
|
.ToolIcon_type_checkbox {
|
||||||
& + .ToolIcon__icon:active {
|
|
||||||
background: var(--color-primary-light);
|
|
||||||
}
|
|
||||||
&:checked + .ToolIcon__icon {
|
&:checked + .ToolIcon__icon {
|
||||||
background: var(--color-primary);
|
--icon-fill-color: var(--color-primary-darker);
|
||||||
--icon-fill-color: #{$oc-white};
|
|
||||||
--keybinding-color: #{$oc-white};
|
svg {
|
||||||
|
fill: var(--icon-fill-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ToolIcon_type_radio,
|
||||||
|
.ToolIcon_type_checkbox {
|
||||||
|
&:checked + .ToolIcon__icon {
|
||||||
|
background: var(--color-primary-light);
|
||||||
|
--keybinding-color: var(--color-gray-60);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
color: var(--color-primary-darker);
|
||||||
}
|
}
|
||||||
&:checked + .ToolIcon__icon:active {
|
|
||||||
background: var(--color-primary-darker);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -26,6 +36,56 @@
|
|||||||
bottom: 4px;
|
bottom: 4px;
|
||||||
right: 4px;
|
right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ToolIcon__icon {
|
||||||
|
&:hover {
|
||||||
|
background: var(--button-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: var(--button-hover);
|
||||||
|
border: 1px solid var(--color-primary-darkest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin outlineButtonStyles {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.625rem;
|
||||||
|
width: var(--default-button-size);
|
||||||
|
height: var(--default-button-size);
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-width: 1px;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: var(--default-border-color);
|
||||||
|
border-radius: var(--border-radius-lg);
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--text-primary-color);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--button-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: var(--button-hover);
|
||||||
|
border-color: var(--color-primary-darkest);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: var(--color-primary-light);
|
||||||
|
border-color: var(--color-primary-light);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-primary-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
color: var(--color-primary-darker);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$theme-filter: "invert(93%) hue-rotate(180deg)";
|
$theme-filter: "invert(93%) hue-rotate(180deg)";
|
||||||
|
@ -10,7 +10,7 @@ import { NonDeletedExcalidrawElement } from "./types";
|
|||||||
|
|
||||||
import { register } from "../actions/register";
|
import { register } from "../actions/register";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import { editIcon, link, trash } from "../components/icons";
|
import { FreedrawIcon, LinkIcon, TrashIcon } from "../components/icons";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import {
|
import {
|
||||||
useCallback,
|
useCallback,
|
||||||
@ -197,7 +197,7 @@ export const Hyperlink = ({
|
|||||||
label={t("buttons.edit")}
|
label={t("buttons.edit")}
|
||||||
onClick={onEdit}
|
onClick={onEdit}
|
||||||
className="excalidraw-hyperlinkContainer--edit"
|
className="excalidraw-hyperlinkContainer--edit"
|
||||||
icon={editIcon}
|
icon={FreedrawIcon}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -209,7 +209,7 @@ export const Hyperlink = ({
|
|||||||
label={t("buttons.remove")}
|
label={t("buttons.remove")}
|
||||||
onClick={handleRemove}
|
onClick={handleRemove}
|
||||||
className="excalidraw-hyperlinkContainer--remove"
|
className="excalidraw-hyperlinkContainer--remove"
|
||||||
icon={trash}
|
icon={TrashIcon}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -277,7 +277,7 @@ export const actionLink = register({
|
|||||||
return (
|
return (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
type="button"
|
type="button"
|
||||||
icon={link}
|
icon={LinkIcon}
|
||||||
aria-label={t(getContextMenuLabel(elements, appState))}
|
aria-label={t(getContextMenuLabel(elements, appState))}
|
||||||
title={`${t("labels.link.label")} - ${getShortcutKey("CtrlOrCmd+K")}`}
|
title={`${t("labels.link.label")} - ${getShortcutKey("CtrlOrCmd+K")}`}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
|
@ -100,7 +100,7 @@ export const getTransformHandlesFromCoords = (
|
|||||||
const cx = (x1 + x2) / 2;
|
const cx = (x1 + x2) / 2;
|
||||||
const cy = (y1 + y2) / 2;
|
const cy = (y1 + y2) / 2;
|
||||||
const dashedLineMargin = margin / zoom.value;
|
const dashedLineMargin = margin / zoom.value;
|
||||||
const centeringOffset = (size - 8) / (2 * zoom.value);
|
const centeringOffset = (size - DEFAULT_SPACING * 2) / (2 * zoom.value);
|
||||||
|
|
||||||
const transformHandles: TransformHandles = {
|
const transformHandles: TransformHandles = {
|
||||||
nw: omitSides.nw
|
nw: omitSides.nw
|
||||||
@ -253,7 +253,7 @@ export const getTransformHandles = (
|
|||||||
omitSides = OMIT_SIDES_FOR_TEXT_ELEMENT;
|
omitSides = OMIT_SIDES_FOR_TEXT_ELEMENT;
|
||||||
}
|
}
|
||||||
const dashedLineMargin = isLinearElement(element)
|
const dashedLineMargin = isLinearElement(element)
|
||||||
? DEFAULT_SPACING * 3
|
? DEFAULT_SPACING + 8
|
||||||
: DEFAULT_SPACING;
|
: DEFAULT_SPACING;
|
||||||
return getTransformHandlesFromCoords(
|
return getTransformHandlesFromCoords(
|
||||||
getElementAbsoluteCoords(element),
|
getElementAbsoluteCoords(element),
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
@import "../../css/variables.module";
|
@import "../../css/variables.module";
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
|
.RoomDialog__button {
|
||||||
|
border: 1px solid var(--default-border-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
.RoomDialog-linkContainer {
|
.RoomDialog-linkContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin: 1.5em 0;
|
margin: 1.5em 0;
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user