feat: add sidebar for libraries panel (#5274)

Co-authored-by: dwelle <luzar.david@gmail.com>
Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
This commit is contained in:
Ishtiaq Bhatti 2022-06-21 20:03:23 +05:00 committed by GitHub
parent 4712393b62
commit cdf352d4c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 782 additions and 241 deletions

View File

@ -7,7 +7,7 @@ import { DarkModeToggle } from "../components/DarkModeToggle";
import { loadFromJSON, saveAsJSON } from "../data";
import { resaveAsImageWithScene } from "../data/resave";
import { t } from "../i18n";
import { useDeviceType } from "../components/App";
import { useDevice } from "../components/App";
import { KEYS } from "../keys";
import { register } from "./register";
import { CheckboxItem } from "../components/CheckboxItem";
@ -204,7 +204,7 @@ export const actionSaveFileToDisk = register({
icon={saveAs}
title={t("buttons.saveAs")}
aria-label={t("buttons.saveAs")}
showAriaLabel={useDeviceType().isMobile}
showAriaLabel={useDevice().isMobile}
hidden={!nativeFileSystemSupported}
onClick={() => updateData(null)}
data-testid="save-as-button"
@ -248,7 +248,7 @@ export const actionLoadScene = register({
icon={load}
title={t("buttons.load")}
aria-label={t("buttons.load")}
showAriaLabel={useDeviceType().isMobile}
showAriaLabel={useDevice().isMobile}
onClick={updateData}
data-testid="load-button"
/>

View File

@ -30,7 +30,7 @@ const trackAction = (
trackEvent(
action.trackEvent.category,
action.trackEvent.action || action.name,
`${source} (${app.deviceType.isMobile ? "mobile" : "desktop"})`,
`${source} (${app.device.isMobile ? "mobile" : "desktop"})`,
);
}
}

View File

@ -58,6 +58,7 @@ export const getDefaultAppState = (): Omit<
gridSize: null,
isBindingEnabled: true,
isLibraryOpen: false,
isLibraryMenuDocked: false,
isLoading: false,
isResizing: false,
isRotating: false,
@ -146,7 +147,8 @@ const APP_STATE_STORAGE_CONF = (<
gridSize: { browser: true, export: true, server: true },
height: { browser: false, export: false, server: false },
isBindingEnabled: { browser: false, export: false, server: false },
isLibraryOpen: { browser: false, export: false, server: false },
isLibraryOpen: { browser: true, export: false, server: false },
isLibraryMenuDocked: { browser: true, export: false, server: false },
isLoading: { browser: false, export: false, server: false },
isResizing: { browser: false, export: false, server: false },
isRotating: { browser: false, export: false, server: false },

View File

@ -3,7 +3,7 @@ import { ActionManager } from "../actions/manager";
import { getNonDeletedElements } from "../element";
import { ExcalidrawElement, PointerType } from "../element/types";
import { t } from "../i18n";
import { useDeviceType } from "../components/App";
import { useDevice } from "../components/App";
import {
canChangeSharpness,
canHaveArrowheads,
@ -52,7 +52,7 @@ export const SelectedShapeActions = ({
isSingleElementBoundContainer = true;
}
const isEditing = Boolean(appState.editingElement);
const deviceType = useDeviceType();
const device = useDevice();
const isRTL = document.documentElement.getAttribute("dir") === "rtl";
const showFillIcons =
@ -177,8 +177,8 @@ export const SelectedShapeActions = ({
<fieldset>
<legend>{t("labels.actions")}</legend>
<div className="buttonList">
{!deviceType.isMobile && renderAction("duplicateSelection")}
{!deviceType.isMobile && renderAction("deleteSelectedElements")}
{!device.isMobile && renderAction("duplicateSelection")}
{!device.isMobile && renderAction("deleteSelectedElements")}
{renderAction("group")}
{renderAction("ungroup")}
{showLinkIcon && renderAction("hyperlink")}

View File

@ -64,6 +64,8 @@ import {
MQ_MAX_HEIGHT_LANDSCAPE,
MQ_MAX_WIDTH_LANDSCAPE,
MQ_MAX_WIDTH_PORTRAIT,
MQ_RIGHT_SIDEBAR_MIN_WIDTH,
MQ_SM_MAX_WIDTH,
POINTER_BUTTON,
SCROLL_TIMEOUT,
TAP_TWICE_TIMEOUT,
@ -194,7 +196,7 @@ import {
LibraryItems,
PointerDownState,
SceneData,
DeviceType,
Device,
} from "../types";
import {
debounce,
@ -220,7 +222,6 @@ import {
} from "../utils";
import ContextMenu, { ContextMenuOption } from "./ContextMenu";
import LayerUI from "./LayerUI";
import { Stats } from "./Stats";
import { Toast } from "./Toast";
import { actionToggleViewMode } from "../actions/actionToggleViewMode";
import {
@ -259,12 +260,14 @@ import {
isLocalLink,
} from "../element/Hyperlink";
const defaultDeviceTypeContext: DeviceType = {
const deviceContextInitialValue = {
isSmScreen: false,
isMobile: false,
isTouchScreen: false,
canDeviceFitSidebar: false,
};
const DeviceTypeContext = React.createContext(defaultDeviceTypeContext);
export const useDeviceType = () => useContext(DeviceTypeContext);
const DeviceContext = React.createContext<Device>(deviceContextInitialValue);
export const useDevice = () => useContext<Device>(DeviceContext);
const ExcalidrawContainerContext = React.createContext<{
container: HTMLDivElement | null;
id: string | null;
@ -296,10 +299,7 @@ class App extends React.Component<AppProps, AppState> {
rc: RoughCanvas | null = null;
unmounted: boolean = false;
actionManager: ActionManager;
deviceType: DeviceType = {
isMobile: false,
isTouchScreen: false,
};
device: Device = deviceContextInitialValue;
detachIsMobileMqHandler?: () => void;
private excalidrawContainerRef = React.createRef<HTMLDivElement>();
@ -353,12 +353,12 @@ class App extends React.Component<AppProps, AppState> {
width: window.innerWidth,
height: window.innerHeight,
showHyperlinkPopup: false,
isLibraryMenuDocked: false,
};
this.id = nanoid();
this.library = new Library(this);
if (excalidrawRef) {
const readyPromise =
("current" in excalidrawRef && excalidrawRef.current?.readyPromise) ||
@ -485,7 +485,7 @@ class App extends React.Component<AppProps, AppState> {
<div
className={clsx("excalidraw excalidraw-container", {
"excalidraw--view-mode": viewModeEnabled,
"excalidraw--mobile": this.deviceType.isMobile,
"excalidraw--mobile": this.device.isMobile,
})}
ref={this.excalidrawContainerRef}
onDrop={this.handleAppOnDrop}
@ -497,7 +497,7 @@ class App extends React.Component<AppProps, AppState> {
<ExcalidrawContainerContext.Provider
value={this.excalidrawContainerValue}
>
<DeviceTypeContext.Provider value={this.deviceType}>
<DeviceContext.Provider value={this.device}>
<LayerUI
canvas={this.canvas}
appState={this.state}
@ -521,6 +521,7 @@ class App extends React.Component<AppProps, AppState> {
isCollaborating={this.props.isCollaborating}
renderTopRightUI={renderTopRightUI}
renderCustomFooter={renderFooter}
renderCustomStats={renderCustomStats}
viewModeEnabled={viewModeEnabled}
showExitZenModeBtn={
typeof this.props?.zenModeEnabled === "undefined" &&
@ -548,15 +549,6 @@ class App extends React.Component<AppProps, AppState> {
onLinkOpen={this.props.onLinkOpen}
/>
)}
{this.state.showStats && (
<Stats
appState={this.state}
setAppState={this.setAppState}
elements={this.scene.getNonDeletedElements()}
onClose={this.toggleStats}
renderCustomStats={renderCustomStats}
/>
)}
{this.state.toastMessage !== null && (
<Toast
message={this.state.toastMessage}
@ -564,7 +556,7 @@ class App extends React.Component<AppProps, AppState> {
/>
)}
<main>{this.renderCanvas()}</main>
</DeviceTypeContext.Provider>
</DeviceContext.Provider>
</ExcalidrawContainerContext.Provider>
</div>
);
@ -763,7 +755,12 @@ class App extends React.Component<AppProps, AppState> {
const scene = restore(initialData, null, null);
scene.appState = {
...scene.appState,
isLibraryOpen: this.state.isLibraryOpen,
// we're falling back to current (pre-init) state when deciding
// whether to open the library, to handle a case where we
// update the state outside of initialData (e.g. when loading the app
// with a library install link, which should auto-open the library)
isLibraryOpen:
initialData?.appState?.isLibraryOpen || this.state.isLibraryOpen,
activeTool:
scene.appState.activeTool.type === "image"
? { ...scene.appState.activeTool, type: "selection" }
@ -794,6 +791,21 @@ class App extends React.Component<AppProps, AppState> {
});
};
private refreshDeviceState = (container: HTMLDivElement) => {
const { width, height } = container.getBoundingClientRect();
const sidebarBreakpoint =
this.props.UIOptions.dockedSidebarBreakpoint != null
? this.props.UIOptions.dockedSidebarBreakpoint
: MQ_RIGHT_SIDEBAR_MIN_WIDTH;
this.device = updateObject(this.device, {
isSmScreen: width < MQ_SM_MAX_WIDTH,
isMobile:
width < MQ_MAX_WIDTH_PORTRAIT ||
(height < MQ_MAX_HEIGHT_LANDSCAPE && width < MQ_MAX_WIDTH_LANDSCAPE),
canDeviceFitSidebar: width > sidebarBreakpoint,
});
};
public async componentDidMount() {
this.unmounted = false;
this.excalidrawContainerValue.container =
@ -835,34 +847,53 @@ class App extends React.Component<AppProps, AppState> {
this.focusContainer();
}
if (
this.excalidrawContainerRef.current &&
// bounding rects don't work in tests so updating
// the state on init would result in making the test enviro run
// in mobile breakpoint (0 width/height), making everything fail
process.env.NODE_ENV !== "test"
) {
this.refreshDeviceState(this.excalidrawContainerRef.current);
}
if ("ResizeObserver" in window && this.excalidrawContainerRef?.current) {
this.resizeObserver = new ResizeObserver(() => {
// compute isMobile state
// recompute device dimensions state
// ---------------------------------------------------------------------
const { width, height } =
this.excalidrawContainerRef.current!.getBoundingClientRect();
this.deviceType = updateObject(this.deviceType, {
isMobile:
width < MQ_MAX_WIDTH_PORTRAIT ||
(height < MQ_MAX_HEIGHT_LANDSCAPE &&
width < MQ_MAX_WIDTH_LANDSCAPE),
});
this.refreshDeviceState(this.excalidrawContainerRef.current!);
// refresh offsets
// ---------------------------------------------------------------------
this.updateDOMRect();
});
this.resizeObserver?.observe(this.excalidrawContainerRef.current);
} else if (window.matchMedia) {
const mediaQuery = window.matchMedia(
const mdScreenQuery = window.matchMedia(
`(max-width: ${MQ_MAX_WIDTH_PORTRAIT}px), (max-height: ${MQ_MAX_HEIGHT_LANDSCAPE}px) and (max-width: ${MQ_MAX_WIDTH_LANDSCAPE}px)`,
);
const smScreenQuery = window.matchMedia(
`(max-width: ${MQ_SM_MAX_WIDTH}px)`,
);
const canDeviceFitSidebarMediaQuery = window.matchMedia(
`(min-width: ${
// NOTE this won't update if a different breakpoint is supplied
// after mount
this.props.UIOptions.dockedSidebarBreakpoint != null
? this.props.UIOptions.dockedSidebarBreakpoint
: MQ_RIGHT_SIDEBAR_MIN_WIDTH
}px)`,
);
const handler = () => {
this.deviceType = updateObject(this.deviceType, {
isMobile: mediaQuery.matches,
this.excalidrawContainerRef.current!.getBoundingClientRect();
this.device = updateObject(this.device, {
isSmScreen: smScreenQuery.matches,
isMobile: mdScreenQuery.matches,
canDeviceFitSidebar: canDeviceFitSidebarMediaQuery.matches,
});
};
mediaQuery.addListener(handler);
this.detachIsMobileMqHandler = () => mediaQuery.removeListener(handler);
mdScreenQuery.addListener(handler);
this.detachIsMobileMqHandler = () =>
mdScreenQuery.removeListener(handler);
}
const searchParams = new URLSearchParams(window.location.search.slice(1));
@ -1003,6 +1034,14 @@ class App extends React.Component<AppProps, AppState> {
}
componentDidUpdate(prevProps: AppProps, prevState: AppState) {
if (
this.excalidrawContainerRef.current &&
prevProps.UIOptions.dockedSidebarBreakpoint !==
this.props.UIOptions.dockedSidebarBreakpoint
) {
this.refreshDeviceState(this.excalidrawContainerRef.current);
}
if (
prevState.scrollX !== this.state.scrollX ||
prevState.scrollY !== this.state.scrollY
@ -1175,7 +1214,7 @@ class App extends React.Component<AppProps, AppState> {
theme: this.state.theme,
imageCache: this.imageCache,
isExporting: false,
renderScrollbars: !this.deviceType.isMobile,
renderScrollbars: !this.device.isMobile,
},
);
@ -1453,11 +1492,15 @@ class App extends React.Component<AppProps, AppState> {
this.scene.replaceAllElements(nextElements);
this.history.resumeRecording();
this.setState(
selectGroupsForSelectedElements(
{
...this.state,
isLibraryOpen: false,
isLibraryOpen:
this.state.isLibraryOpen && this.device.canDeviceFitSidebar
? this.state.isLibraryMenuDocked
: false,
selectedElementIds: newElements.reduce((map, element) => {
if (!isBoundToContainer(element)) {
map[element.id] = true;
@ -1529,7 +1572,7 @@ class App extends React.Component<AppProps, AppState> {
trackEvent(
"toolbar",
"toggleLock",
`${source} (${this.deviceType.isMobile ? "mobile" : "desktop"})`,
`${source} (${this.device.isMobile ? "mobile" : "desktop"})`,
);
}
this.setState((prevState) => {
@ -1560,10 +1603,6 @@ class App extends React.Component<AppProps, AppState> {
this.actionManager.executeAction(actionToggleZenMode);
};
toggleStats = () => {
this.actionManager.executeAction(actionToggleStats);
};
scrollToContent = (
target:
| ExcalidrawElement
@ -1721,7 +1760,16 @@ class App extends React.Component<AppProps, AppState> {
}
if (event.code === CODES.ZERO) {
this.setState({ isLibraryOpen: !this.state.isLibraryOpen });
const nextState = !this.state.isLibraryOpen;
this.setState({ isLibraryOpen: nextState });
// track only openings
if (nextState) {
trackEvent(
"library",
"toggleLibrary (open)",
`keyboard (${this.device.isMobile ? "mobile" : "desktop"})`,
);
}
}
if (isArrowKey(event.key)) {
@ -1815,7 +1863,7 @@ class App extends React.Component<AppProps, AppState> {
trackEvent(
"toolbar",
shape,
`keyboard (${this.deviceType.isMobile ? "mobile" : "desktop"})`,
`keyboard (${this.device.isMobile ? "mobile" : "desktop"})`,
);
}
this.setActiveTool({ type: shape });
@ -2440,7 +2488,7 @@ class App extends React.Component<AppProps, AppState> {
element,
this.state,
[scenePointer.x, scenePointer.y],
this.deviceType.isMobile,
this.device.isMobile,
)
);
});
@ -2472,7 +2520,7 @@ class App extends React.Component<AppProps, AppState> {
this.hitLinkElement,
this.state,
[lastPointerDownCoords.x, lastPointerDownCoords.y],
this.deviceType.isMobile,
this.device.isMobile,
);
const lastPointerUpCoords = viewportCoordsToSceneCoords(
this.lastPointerUp!,
@ -2482,7 +2530,7 @@ class App extends React.Component<AppProps, AppState> {
this.hitLinkElement,
this.state,
[lastPointerUpCoords.x, lastPointerUpCoords.y],
this.deviceType.isMobile,
this.device.isMobile,
);
if (lastPointerDownHittingLinkIcon && lastPointerUpHittingLinkIcon) {
const url = this.hitLinkElement.link;
@ -2921,10 +2969,10 @@ class App extends React.Component<AppProps, AppState> {
}
if (
!this.deviceType.isTouchScreen &&
!this.device.isTouchScreen &&
["pen", "touch"].includes(event.pointerType)
) {
this.deviceType = updateObject(this.deviceType, { isTouchScreen: true });
this.device = updateObject(this.device, { isTouchScreen: true });
}
if (isPanning) {
@ -3066,7 +3114,7 @@ class App extends React.Component<AppProps, AppState> {
event: React.PointerEvent<HTMLCanvasElement>,
) => {
this.lastPointerUp = event;
if (this.deviceType.isTouchScreen) {
if (this.device.isTouchScreen) {
const scenePointer = viewportCoordsToSceneCoords(
{ clientX: event.clientX, clientY: event.clientY },
this.state,
@ -3084,7 +3132,7 @@ class App extends React.Component<AppProps, AppState> {
this.hitLinkElement &&
!this.state.selectedElementIds[this.hitLinkElement.id]
) {
this.redirectToLink(event, this.deviceType.isTouchScreen);
this.redirectToLink(event, this.device.isTouchScreen);
}
this.removePointer(event);
@ -3456,7 +3504,7 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState.hit.element,
this.state,
[pointerDownState.origin.x, pointerDownState.origin.y],
this.deviceType.isMobile,
this.device.isMobile,
)
) {
return false;
@ -5563,7 +5611,7 @@ class App extends React.Component<AppProps, AppState> {
} else {
ContextMenu.push({
options: [
this.deviceType.isMobile &&
this.device.isMobile &&
navigator.clipboard && {
trackEvent: false,
name: "paste",
@ -5575,7 +5623,7 @@ class App extends React.Component<AppProps, AppState> {
},
contextItemLabel: "labels.paste",
},
this.deviceType.isMobile && navigator.clipboard && separator,
this.device.isMobile && navigator.clipboard && separator,
probablySupportsClipboardBlob &&
elements.length > 0 &&
actionCopyAsPng,
@ -5620,9 +5668,9 @@ class App extends React.Component<AppProps, AppState> {
} else {
ContextMenu.push({
options: [
this.deviceType.isMobile && actionCut,
this.deviceType.isMobile && navigator.clipboard && actionCopy,
this.deviceType.isMobile &&
this.device.isMobile && actionCut,
this.device.isMobile && navigator.clipboard && actionCopy,
this.device.isMobile &&
navigator.clipboard && {
name: "paste",
trackEvent: false,
@ -5634,7 +5682,7 @@ class App extends React.Component<AppProps, AppState> {
},
contextItemLabel: "labels.paste",
},
this.deviceType.isMobile && separator,
this.device.isMobile && separator,
...options,
separator,
actionCopyStyles,

View File

@ -1,6 +1,6 @@
import { useState } from "react";
import { t } from "../i18n";
import { useDeviceType } from "./App";
import { useDevice } from "./App";
import { trash } from "./icons";
import { ToolButton } from "./ToolButton";
@ -19,7 +19,7 @@ const ClearCanvas = ({ onConfirm }: { onConfirm: () => void }) => {
icon={trash}
title={t("buttons.clearReset")}
aria-label={t("buttons.clearReset")}
showAriaLabel={useDeviceType().isMobile}
showAriaLabel={useDevice().isMobile}
onClick={toggleDialog}
data-testid="clear-canvas-button"
/>

View File

@ -1,7 +1,7 @@
import clsx from "clsx";
import { ToolButton } from "./ToolButton";
import { t } from "../i18n";
import { useDeviceType } from "../components/App";
import { useDevice } from "../components/App";
import { users } from "./icons";
import "./CollabButton.scss";
@ -26,7 +26,7 @@ const CollabButton = ({
type="button"
title={t("labels.liveCollaboration")}
aria-label={t("labels.liveCollaboration")}
showAriaLabel={useDeviceType().isMobile}
showAriaLabel={useDevice().isMobile}
>
{collaboratorCount > 0 && (
<div className="CollabButton-collaborators">{collaboratorCount}</div>

View File

@ -2,7 +2,7 @@ import clsx from "clsx";
import React, { useEffect, useState } from "react";
import { useCallbackRefState } from "../hooks/useCallbackRefState";
import { t } from "../i18n";
import { useExcalidrawContainer, useDeviceType } from "../components/App";
import { useExcalidrawContainer, useDevice } from "../components/App";
import { KEYS } from "../keys";
import "./Dialog.scss";
import { back, close } from "./icons";
@ -94,7 +94,7 @@ export const Dialog = (props: DialogProps) => {
onClick={onClose}
aria-label={t("buttons.close")}
>
{useDeviceType().isMobile ? back : close}
{useDevice().isMobile ? back : close}
</button>
</h2>
<div className="Dialog__content">{props.children}</div>

View File

@ -5,7 +5,7 @@ import { canvasToBlob } from "../data/blob";
import { NonDeletedExcalidrawElement } from "../element/types";
import { CanvasError } from "../errors";
import { t } from "../i18n";
import { useDeviceType } from "./App";
import { useDevice } from "./App";
import { getSelectedElements, isSomeElementSelected } from "../scene";
import { exportToCanvas } from "../scene/export";
import { AppState, BinaryFiles } from "../types";
@ -250,7 +250,7 @@ export const ImageExportDialog = ({
icon={exportImage}
type="button"
aria-label={t("buttons.exportImage")}
showAriaLabel={useDeviceType().isMobile}
showAriaLabel={useDevice().isMobile}
title={t("buttons.exportImage")}
/>
{modalIsShown && (

View File

@ -1,7 +1,7 @@
import React, { useState } from "react";
import { NonDeletedExcalidrawElement } from "../element/types";
import { t } from "../i18n";
import { useDeviceType } from "./App";
import { useDevice } from "./App";
import { AppState, ExportOpts, BinaryFiles } from "../types";
import { Dialog } from "./Dialog";
import { exportFile, exportToFileIcon, link } from "./icons";
@ -117,7 +117,7 @@ export const JSONExportDialog = ({
icon={exportFile}
type="button"
aria-label={t("buttons.export")}
showAriaLabel={useDeviceType().isMobile}
showAriaLabel={useDevice().isMobile}
title={t("buttons.export")}
/>
{modalIsShown && (

View File

@ -1,9 +1,63 @@
@import "open-color/open-color";
@import "../css/variables.module";
.layer-ui__sidebar {
position: absolute;
top: var(--sat);
bottom: var(--sab);
right: var(--sar);
z-index: 5;
box-shadow: var(--shadow-island);
overflow: hidden;
border-radius: var(--border-radius-lg);
margin: var(--space-factor);
width: calc(#{$right-sidebar-width} - var(--space-factor) * 2);
.Island {
box-shadow: none;
}
.ToolIcon__icon {
border-radius: var(--border-radius-md);
}
.ToolIcon__icon__close {
.Modal__close {
width: calc(var(--space-factor) * 7);
height: calc(var(--space-factor) * 7);
display: flex;
justify-content: center;
align-items: center;
color: var(--color-text);
}
}
.Island {
--padding: 0;
background-color: var(--island-bg-color);
border-radius: var(--border-radius-lg);
padding: calc(var(--padding) * var(--space-factor));
position: relative;
transition: box-shadow 0.5s ease-in-out;
}
}
.excalidraw {
.layer-ui__wrapper.animate {
transition: width 0.1s ease-in-out;
}
.layer-ui__wrapper {
// when the rightside sidebar is docked, we need to resize the UI by its
// width, making the nested UI content shift to the left. To do this,
// we need the UI container to actually have dimensions set, but
// then we also need to disable pointer events else the canvas below
// wouldn't be interactive.
position: absolute;
width: 100%;
height: 100%;
pointer-events: none;
z-index: var(--zIndex-layerUI);
&__top-right {
display: flex;
}

View File

@ -1,7 +1,7 @@
import clsx from "clsx";
import React, { useCallback } from "react";
import { ActionManager } from "../actions/manager";
import { CLASSES } from "../constants";
import { CLASSES, LIBRARY_SIDEBAR_WIDTH } from "../constants";
import { exportCanvas } from "../data";
import { isTextElement, showSelectedShapeActions } from "../element";
import { NonDeletedExcalidrawElement } from "../element/types";
@ -36,7 +36,9 @@ import "./LayerUI.scss";
import "./Toolbar.scss";
import { PenModeButton } from "./PenModeButton";
import { trackEvent } from "../analytics";
import { useDeviceType } from "../components/App";
import { useDevice } from "../components/App";
import { Stats } from "./Stats";
import { actionToggleStats } from "../actions/actionToggleStats";
interface LayerUIProps {
actionManager: ActionManager;
@ -55,14 +57,9 @@ interface LayerUIProps {
toggleZenMode: () => void;
langCode: Language["code"];
isCollaborating: boolean;
renderTopRightUI?: (
isMobile: boolean,
appState: AppState,
) => JSX.Element | null;
renderCustomFooter?: (
isMobile: boolean,
appState: AppState,
) => JSX.Element | null;
renderTopRightUI?: ExcalidrawProps["renderTopRightUI"];
renderCustomFooter?: ExcalidrawProps["renderFooter"];
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
viewModeEnabled: boolean;
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
UIOptions: AppProps["UIOptions"];
@ -71,7 +68,6 @@ interface LayerUIProps {
id: string;
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
}
const LayerUI = ({
actionManager,
appState,
@ -90,6 +86,7 @@ const LayerUI = ({
isCollaborating,
renderTopRightUI,
renderCustomFooter,
renderCustomStats,
viewModeEnabled,
libraryReturnUrl,
UIOptions,
@ -98,7 +95,7 @@ const LayerUI = ({
id,
onImageAction,
}: LayerUIProps) => {
const deviceType = useDeviceType();
const device = useDevice();
const renderJSONExportDialog = () => {
if (!UIOptions.canvasActions.export) {
@ -344,7 +341,7 @@ const LayerUI = ({
<HintViewer
appState={appState}
elements={elements}
isMobile={deviceType.isMobile}
isMobile={device.isMobile}
/>
{heading}
<Stack.Row gap={1}>
@ -366,7 +363,6 @@ const LayerUI = ({
setAppState={setAppState}
/>
</Stack.Row>
{libraryMenu}
</Stack.Col>
)}
</Section>
@ -383,7 +379,7 @@ const LayerUI = ({
collaborators={appState.collaborators}
actionManager={actionManager}
/>
{renderTopRightUI?.(deviceType.isMobile, appState)}
{renderTopRightUI?.(device.isMobile, appState)}
</div>
</div>
</FixedSideContainer>
@ -436,7 +432,7 @@ const LayerUI = ({
)}
{!viewModeEnabled &&
appState.multiElement &&
deviceType.isTouchScreen && (
device.isTouchScreen && (
<div
className={clsx("finalize-button zen-mode-transition", {
"layer-ui__wrapper__footer-left--transition-left":
@ -513,7 +509,24 @@ const LayerUI = ({
</>
);
return deviceType.isMobile ? (
const renderStats = () => {
if (!appState.showStats) {
return null;
}
return (
<Stats
appState={appState}
setAppState={setAppState}
elements={elements}
onClose={() => {
actionManager.executeAction(actionToggleStats);
}}
renderCustomStats={renderCustomStats}
/>
);
};
return device.isMobile ? (
<>
{dialogs}
<MobileMenu
@ -534,33 +547,48 @@ const LayerUI = ({
showThemeBtn={showThemeBtn}
onImageAction={onImageAction}
renderTopRightUI={renderTopRightUI}
renderStats={renderStats}
/>
</>
) : (
<div
className={clsx("layer-ui__wrapper", {
"disable-pointerEvents":
appState.draggingElement ||
appState.resizingElement ||
(appState.editingElement && !isTextElement(appState.editingElement)),
})}
>
{dialogs}
{renderFixedSideContainer()}
{renderBottomAppMenu()}
{appState.scrolledOutside && (
<button
className="scroll-back-to-content"
onClick={() => {
setAppState({
...calculateScrollCenter(elements, appState, canvas),
});
}}
>
{t("buttons.scrollBackToContent")}
</button>
<>
<div
className={clsx("layer-ui__wrapper", {
"disable-pointerEvents":
appState.draggingElement ||
appState.resizingElement ||
(appState.editingElement &&
!isTextElement(appState.editingElement)),
})}
style={
appState.isLibraryOpen &&
appState.isLibraryMenuDocked &&
device.canDeviceFitSidebar
? { width: `calc(100% - ${LIBRARY_SIDEBAR_WIDTH}px)` }
: {}
}
>
{dialogs}
{renderFixedSideContainer()}
{renderBottomAppMenu()}
{renderStats()}
{appState.scrolledOutside && (
<button
className="scroll-back-to-content"
onClick={() => {
setAppState({
...calculateScrollCenter(elements, appState, canvas),
});
}}
>
{t("buttons.scrollBackToContent")}
</button>
)}
</div>
{appState.isLibraryOpen && (
<div className="layer-ui__sidebar">{libraryMenu}</div>
)}
</div>
</>
);
};

View File

@ -3,6 +3,8 @@ import clsx from "clsx";
import { t } from "../i18n";
import { AppState } from "../types";
import { capitalizeString } from "../utils";
import { trackEvent } from "../analytics";
import { useDevice } from "./App";
const LIBRARY_ICON = (
<svg viewBox="0 0 576 512">
@ -18,6 +20,7 @@ export const LibraryButton: React.FC<{
setAppState: React.Component<any, AppState>["setState"];
isMobile?: boolean;
}> = ({ appState, setAppState, isMobile }) => {
const device = useDevice();
return (
<label
className={clsx(
@ -34,7 +37,19 @@ export const LibraryButton: React.FC<{
type="checkbox"
name="editor-library"
onChange={(event) => {
setAppState({ isLibraryOpen: event.target.checked });
document
.querySelector(".layer-ui__wrapper")
?.classList.remove("animate");
const nextState = event.target.checked;
setAppState({ isLibraryOpen: nextState });
// track only openings
if (nextState) {
trackEvent(
"library",
"toggleLibrary (open)",
`toolbar (${device.isMobile ? "mobile" : "desktop"})`,
);
}
}}
checked={appState.isLibraryOpen}
aria-label={capitalizeString(t("toolBar.library"))}

View File

@ -2,7 +2,6 @@
.excalidraw {
.layer-ui__library {
margin: auto;
display: flex;
align-items: center;
justify-content: center;
@ -11,8 +10,7 @@
display: flex;
align-items: center;
width: 100%;
margin: 2px 0;
margin: 2px 0 15px 0;
.Spinner {
margin-right: 1rem;
}
@ -21,13 +19,17 @@
// 2px from the left to account for focus border of left-most button
margin: 0 2px;
}
}
}
a {
margin-inline-start: auto;
// 17px for scrollbar (needed for overlay scrollbars on Big Sur?) + 1px extra
padding-inline-end: 18px;
white-space: nowrap;
}
.layer-ui__sidebar {
.layer-ui__library {
padding: 0;
height: 100%;
}
.library-menu-items-container {
height: 100%;
width: 100%;
}
}
@ -65,4 +67,38 @@
}
}
}
.library-menu-browse-button {
width: 80%;
min-height: 22px;
margin: 0 auto;
margin-top: 1rem;
padding: 10px;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
position: relative;
border-radius: var(--border-radius-lg);
background-color: var(--color-primary);
color: $oc-white;
text-align: center;
white-space: nowrap;
text-decoration: none !important;
&:hover {
background-color: var(--color-primary-darker);
}
&:active {
background-color: var(--color-primary-darkest);
}
}
.library-menu-browse-button--mobile {
min-height: 22px;
margin-left: auto;
a {
padding-right: 0;
}
}
}

View File

@ -29,6 +29,7 @@ import { trackEvent } from "../analytics";
import { useAtom } from "jotai";
import { jotaiScope } from "../jotai";
import Spinner from "./Spinner";
import { useDevice } from "./App";
const useOnClickOutside = (
ref: RefObject<HTMLElement>,
@ -103,17 +104,30 @@ export const LibraryMenu = ({
}) => {
const ref = useRef<HTMLDivElement | null>(null);
useOnClickOutside(ref, (event) => {
// If click on the library icon, do nothing.
if ((event.target as Element).closest(".ToolIcon__library")) {
return;
}
onClose();
});
const device = useDevice();
useOnClickOutside(
ref,
useCallback(
(event) => {
// If click on the library icon, do nothing.
if ((event.target as Element).closest(".ToolIcon__library")) {
return;
}
if (!appState.isLibraryMenuDocked || !device.canDeviceFitSidebar) {
onClose();
}
},
[onClose, appState.isLibraryMenuDocked, device.canDeviceFitSidebar],
),
);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === KEYS.ESCAPE) {
if (
event.key === KEYS.ESCAPE &&
(!appState.isLibraryMenuDocked || !device.canDeviceFitSidebar)
) {
onClose();
}
};
@ -121,7 +135,7 @@ export const LibraryMenu = ({
return () => {
document.removeEventListener(EVENT.KEYDOWN, handleKeyDown);
};
}, [onClose]);
}, [onClose, appState.isLibraryMenuDocked, device.canDeviceFitSidebar]);
const [selectedItems, setSelectedItems] = useState<LibraryItem["id"][]>([]);
const [showPublishLibraryDialog, setShowPublishLibraryDialog] =
@ -273,6 +287,7 @@ export const LibraryMenu = ({
onInsertLibraryItems={onInsertLibraryItems}
pendingElements={pendingElements}
setAppState={setAppState}
appState={appState}
libraryReturnUrl={libraryReturnUrl}
library={library}
theme={theme}

View File

@ -2,8 +2,17 @@
.excalidraw {
.library-menu-items-container {
display: flex;
flex-direction: column;
height: 100%;
padding: 0.5rem;
box-sizing: border-box;
.library-actions {
width: 100%;
display: flex;
margin-right: auto;
align-items: center;
button .library-actions-counter {
position: absolute;
@ -87,12 +96,16 @@
}
}
&__items {
max-height: 50vh;
overflow: auto;
margin-top: 0.5rem;
flex: 1;
overflow-y: auto;
overflow-x: hidden;
margin-bottom: 1rem;
}
.separator {
width: 100%;
display: flex;
align-items: center;
font-weight: 500;
font-size: 0.9rem;
margin: 0.6em 0.2em;

View File

@ -12,9 +12,9 @@ import {
LibraryItems,
} from "../types";
import { arrayToMap, muteFSAbortError } from "../utils";
import { useDeviceType } from "./App";
import { useDevice } from "./App";
import ConfirmDialog from "./ConfirmDialog";
import { exportToFileIcon, load, publishIcon, trash } from "./icons";
import { close, exportToFileIcon, load, publishIcon, trash } from "./icons";
import { LibraryUnit } from "./LibraryUnit";
import Stack from "./Stack";
import { ToolButton } from "./ToolButton";
@ -25,6 +25,9 @@ import { MIME_TYPES, VERSIONS } from "../constants";
import Spinner from "./Spinner";
import { fileOpen } from "../data/filesystem";
import { SidebarLockButton } from "./SidebarLockButton";
import { trackEvent } from "../analytics";
const LibraryMenuItems = ({
isLoading,
libraryItems,
@ -34,6 +37,7 @@ const LibraryMenuItems = ({
pendingElements,
theme,
setAppState,
appState,
libraryReturnUrl,
library,
files,
@ -52,6 +56,7 @@ const LibraryMenuItems = ({
theme: AppState["theme"];
files: BinaryFiles;
setAppState: React.Component<any, AppState>["setState"];
appState: AppState;
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
library: Library;
id: string;
@ -88,9 +93,7 @@ const LibraryMenuItems = ({
}, [selectedItems, onRemoveFromLibrary, resetLibrary]);
const [showRemoveLibAlert, setShowRemoveLibAlert] = useState(false);
const isMobile = useDeviceType().isMobile;
const device = useDevice();
const renderLibraryActions = () => {
const itemsSelected = !!selectedItems.length;
const items = itemsSelected
@ -101,7 +104,7 @@ const LibraryMenuItems = ({
: t("buttons.resetLibrary");
return (
<div className="library-actions">
{(!itemsSelected || !isMobile) && (
{!itemsSelected && (
<ToolButton
key="import"
type="button"
@ -186,7 +189,7 @@ const LibraryMenuItems = ({
className="library-actions--publish"
onClick={onPublish}
>
{!isMobile && <label>{t("buttons.publishLibrary")}</label>}
{!device.isMobile && <label>{t("buttons.publishLibrary")}</label>}
{selectedItems.length > 0 && (
<span className="library-actions-counter">
{selectedItems.length}
@ -195,11 +198,25 @@ const LibraryMenuItems = ({
</ToolButton>
</Tooltip>
)}
{device.isMobile && (
<div className="library-menu-browse-button--mobile">
<a
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>
</div>
)}
</div>
);
};
const CELLS_PER_ROW = isMobile ? 4 : 6;
const CELLS_PER_ROW = device.isMobile && !device.isSmScreen ? 6 : 4;
const referrer =
libraryReturnUrl || window.location.origin + window.location.pathname;
@ -356,48 +373,169 @@ const LibraryMenuItems = ({
(item) => item.status === "published",
);
return (
<div className="library-menu-items-container">
{showRemoveLibAlert && renderRemoveLibAlert()}
<div className="layer-ui__library-header" key="library-header">
{renderLibraryActions()}
{isLoading ? (
<Spinner />
) : (
<a
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>
)}
</div>
const renderLibraryHeader = () => {
return (
<>
<div className="layer-ui__library-header" key="library-header">
{renderLibraryActions()}
{device.canDeviceFitSidebar && (
<>
<div className="layer-ui__sidebar-lock-button">
<SidebarLockButton
checked={appState.isLibraryMenuDocked}
onChange={() => {
document
.querySelector(".layer-ui__wrapper")
?.classList.add("animate");
const nextState = !appState.isLibraryMenuDocked;
setAppState({
isLibraryMenuDocked: nextState,
});
trackEvent(
"library",
`toggleLibraryDock (${nextState ? "dock" : "undock"})`,
`sidebar (${device.isMobile ? "mobile" : "desktop"})`,
);
}}
/>
</div>
</>
)}
{!device.isMobile && (
<div className="ToolIcon__icon__close">
<button
className="Modal__close"
onClick={() =>
setAppState({
isLibraryOpen: false,
})
}
aria-label={t("buttons.close")}
>
{close}
</button>
</div>
)}
</div>
</>
);
};
const renderLibraryMenuItems = () => {
return (
<Stack.Col
className="library-menu-items-container__items"
align="start"
gap={1}
style={{
flex: publishedItems.length > 0 ? 1 : "0 0 auto",
marginBottom: 0,
}}
>
<>
<div className="separator">{t("labels.personalLib")}</div>
{renderLibrarySection([
// append pending library item
...(pendingElements.length
? [{ id: null, elements: pendingElements }]
: []),
...unpublishedItems,
])}
<div className="separator">
{(pendingElements.length > 0 ||
unpublishedItems.length > 0 ||
publishedItems.length > 0) && (
<div>{t("labels.personalLib")}</div>
)}
{isLoading && (
<div
style={{
marginLeft: "auto",
marginRight: "1rem",
display: "flex",
alignItems: "center",
fontWeight: "normal",
}}
>
<div style={{ transform: "translateY(2px)" }}>
<Spinner />
</div>
</div>
)}
</div>
{!pendingElements.length && !unpublishedItems.length ? (
<div
style={{
height: 65,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
width: "100%",
fontSize: ".9rem",
}}
>
No items yet!
<div
style={{
margin: ".6rem 0",
fontSize: ".8em",
width: "70%",
textAlign: "center",
}}
>
{publishedItems.length > 0
? t("library.hint_emptyPrivateLibrary")
: t("library.hint_emptyLibrary")}
</div>
</div>
) : (
renderLibrarySection([
// append pending library item
...(pendingElements.length
? [{ id: null, elements: pendingElements }]
: []),
...unpublishedItems,
])
)}
</>
<>
<div className="separator">{t("labels.excalidrawLib")} </div>
{renderLibrarySection(publishedItems)}
{(publishedItems.length > 0 ||
(!device.isMobile &&
(pendingElements.length > 0 || unpublishedItems.length > 0))) && (
<div className="separator">{t("labels.excalidrawLib")}</div>
)}
{publishedItems.length > 0 && renderLibrarySection(publishedItems)}
</>
</Stack.Col>
);
};
const renderLibraryFooter = () => {
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>
);
};
return (
<div
className="library-menu-items-container"
style={
device.isMobile
? {
minHeight: "200px",
maxHeight: "70vh",
}
: undefined
}
>
{showRemoveLibAlert && renderRemoveLibAlert()}
{renderLibraryHeader()}
{renderLibraryMenuItems()}
{!device.isMobile && renderLibraryFooter()}
</div>
);
};

View File

@ -3,7 +3,7 @@
.excalidraw {
.library-unit {
align-items: center;
border: 1px solid var(--button-gray-2);
border: 1px solid transparent;
display: flex;
justify-content: center;
position: relative;
@ -21,10 +21,6 @@
}
}
&.theme--dark .library-unit {
border-color: rgb(48, 48, 48);
}
.library-unit__dragger {
display: flex;
align-items: center;

View File

@ -1,7 +1,7 @@
import clsx from "clsx";
import oc from "open-color";
import { useEffect, useRef, useState } from "react";
import { useDeviceType } from "../components/App";
import { useDevice } from "../components/App";
import { exportToSvg } from "../scene/export";
import { BinaryFiles, LibraryItem } from "../types";
import "./LibraryUnit.scss";
@ -67,7 +67,7 @@ export const LibraryUnit = ({
}, [elements, files]);
const [isHovered, setIsHovered] = useState(false);
const isMobile = useDeviceType().isMobile;
const isMobile = useDevice().isMobile;
const adder = isPending && (
<div className="library-unit__adder">{PLUS_ICON}</div>
);

View File

@ -43,6 +43,7 @@ type MobileMenuProps = {
isMobile: boolean,
appState: AppState,
) => JSX.Element | null;
renderStats: () => JSX.Element | null;
};
export const MobileMenu = ({
@ -63,6 +64,7 @@ export const MobileMenu = ({
showThemeBtn,
onImageAction,
renderTopRightUI,
renderStats,
}: MobileMenuProps) => {
const renderToolbar = () => {
return (
@ -184,6 +186,7 @@ export const MobileMenu = ({
return (
<>
{!viewModeEnabled && renderToolbar()}
{renderStats()}
<div
className="App-bottom-bar"
style={{

View File

@ -4,7 +4,7 @@ import React, { useState, useLayoutEffect, useRef } from "react";
import { createPortal } from "react-dom";
import clsx from "clsx";
import { KEYS } from "../keys";
import { useExcalidrawContainer, useDeviceType } from "./App";
import { useExcalidrawContainer, useDevice } from "./App";
import { AppState } from "../types";
import { THEME } from "../constants";
@ -59,17 +59,17 @@ export const Modal = (props: {
const useBodyRoot = (theme: AppState["theme"]) => {
const [div, setDiv] = useState<HTMLDivElement | null>(null);
const deviceType = useDeviceType();
const isMobileRef = useRef(deviceType.isMobile);
isMobileRef.current = deviceType.isMobile;
const device = useDevice();
const isMobileRef = useRef(device.isMobile);
isMobileRef.current = device.isMobile;
const { container: excalidrawContainer } = useExcalidrawContainer();
useLayoutEffect(() => {
if (div) {
div.classList.toggle("excalidraw--mobile", deviceType.isMobile);
div.classList.toggle("excalidraw--mobile", device.isMobile);
}
}, [div, deviceType.isMobile]);
}, [div, device.isMobile]);
useLayoutEffect(() => {
const isDarkTheme =

View File

@ -0,0 +1,22 @@
@import "../css/variables.module";
.excalidraw {
.layer-ui__sidebar-lock-button {
@include toolbarButtonColorStates;
margin-right: 0.2rem;
}
.ToolIcon_type_floating .side_lock_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 + .side_lock_icon {
background-color: var(--color-primary);
}
}
}

View File

@ -0,0 +1,46 @@
import "./ToolIcon.scss";
import React from "react";
import clsx from "clsx";
import { ToolButtonSize } from "./ToolButton";
import { t } from "../i18n";
import { Tooltip } from "./Tooltip";
import "./SidebarLockButton.scss";
type SidebarLockIconProps = {
checked: boolean;
onChange?(): void;
};
const DEFAULT_SIZE: ToolButtonSize = "medium";
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 SidebarLockButton = (props: SidebarLockIconProps) => {
return (
<Tooltip label={t("labels.sidebarLock")}>
<label
className={clsx(
"ToolIcon ToolIcon__lock ToolIcon_type_floating",
`ToolIcon_size_${DEFAULT_SIZE}`,
)}
>
<input
className="ToolIcon_type_checkbox"
type="checkbox"
onChange={props.onChange}
checked={props.checked}
aria-label={t("labels.sidebarLock")}
/>{" "}
<div className="ToolIcon__icon side_lock_icon" tabIndex={0}>
{SIDE_LIBRARY_TOGGLE_ICON}
</div>{" "}
</label>{" "}
</Tooltip>
);
};

View File

@ -41,6 +41,7 @@ const ColStack = ({
align,
justifyContent,
className,
style,
}: StackProps) => {
return (
<div
@ -49,6 +50,7 @@ const ColStack = ({
"--gap": gap,
justifyItems: align,
justifyContent,
...style,
}}
>
{children}

View File

@ -7,6 +7,7 @@
right: 12px;
font-size: 12px;
z-index: 10;
pointer-events: all;
h3 {
margin: 0 24px 8px 0;

View File

@ -2,7 +2,7 @@ import React from "react";
import { getCommonBounds } from "../element/bounds";
import { NonDeletedExcalidrawElement } from "../element/types";
import { t } from "../i18n";
import { useDeviceType } from "../components/App";
import { useDevice } from "../components/App";
import { getTargetElements } from "../scene";
import { AppState, ExcalidrawProps } from "../types";
import { close } from "./icons";
@ -16,16 +16,13 @@ export const Stats = (props: {
onClose: () => void;
renderCustomStats: ExcalidrawProps["renderCustomStats"];
}) => {
const deviceType = useDeviceType();
const device = useDevice();
const boundingBox = getCommonBounds(props.elements);
const selectedElements = getTargetElements(props.elements, props.appState);
const selectedBoundingBox = getCommonBounds(selectedElements);
if (deviceType.isMobile && props.appState.openMenu) {
if (device.isMobile && props.appState.openMenu) {
return null;
}
return (
<div className="Stats">
<Island padding={2}>

View File

@ -1,26 +1,5 @@
@import "open-color/open-color.scss";
@mixin toolbarButtonColorStates {
.ToolIcon_type_radio,
.ToolIcon_type_checkbox {
& + .ToolIcon__icon:active {
background: var(--color-primary-light);
}
&:checked + .ToolIcon__icon {
background: var(--color-primary);
--icon-fill-color: #{$oc-white};
--keybinding-color: #{$oc-white};
}
&:checked + .ToolIcon__icon:active {
background: var(--color-primary-darker);
}
}
.ToolIcon__keybinding {
bottom: 4px;
right: 4px;
}
}
@import "../css/variables.module";
.excalidraw {
.App-toolbar-container {

View File

@ -155,9 +155,19 @@ export const DEFAULT_UI_OPTIONS: AppProps["UIOptions"] = {
},
};
// breakpoints
// -----------------------------------------------------------------------------
// sm screen
export const MQ_SM_MAX_WIDTH = 640;
// md screen
export const MQ_MAX_WIDTH_PORTRAIT = 730;
export const MQ_MAX_WIDTH_LANDSCAPE = 1000;
export const MQ_MAX_HEIGHT_LANDSCAPE = 500;
// sidebar
export const MQ_RIGHT_SIDEBAR_MIN_WIDTH = 1229;
// -----------------------------------------------------------------------------
export const LIBRARY_SIDEBAR_WIDTH = parseInt(cssVariables.rightSidebarWidth);
export const MAX_DECIMALS_FOR_SVG_EXPORT = 2;

View File

@ -350,7 +350,6 @@
align-items: flex-start;
cursor: default;
pointer-events: none !important;
z-index: 100;
:root[dir="ltr"] & {
left: 0.25rem;
@ -391,6 +390,7 @@
.App-menu__left {
overflow-y: auto;
box-shadow: var(--shadow-island);
}
.dropdown-select {
@ -449,6 +449,7 @@
bottom: 30px;
transform: translateX(-50%);
padding: 10px 20px;
pointer-events: all;
}
.help-icon {
@ -567,6 +568,22 @@
display: none;
}
}
// use custom, minimalistic scrollbar
// (doesn't work in Firefox)
::-webkit-scrollbar {
width: 5px;
}
::-webkit-scrollbar-thumb {
background: var(--button-gray-2);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--button-gray-3);
}
::-webkit-scrollbar-thumb:active {
background: var(--button-gray-2);
}
}
.ErrorSplash.excalidraw {

View File

@ -6,8 +6,32 @@
}
}
@mixin toolbarButtonColorStates {
.ToolIcon_type_radio,
.ToolIcon_type_checkbox {
& + .ToolIcon__icon:active {
background: var(--color-primary-light);
}
&:checked + .ToolIcon__icon {
background: var(--color-primary);
--icon-fill-color: #{$oc-white};
--keybinding-color: #{$oc-white};
}
&:checked + .ToolIcon__icon:active {
background: var(--color-primary-darker);
}
}
.ToolIcon__keybinding {
bottom: 4px;
right: 4px;
}
}
$theme-filter: "invert(93%) hue-rotate(180deg)";
$right-sidebar-width: "302px";
:export {
themeFilter: unquote($theme-filter);
rightSidebarWidth: unquote($right-sidebar-width);
}

View File

@ -283,6 +283,11 @@ export const restoreAppState = (
value: appState.zoom as NormalizedZoomValue,
}
: appState.zoom || defaultAppState.zoom,
// when sidebar docked and user left it open in last session,
// keep it open. If not docked, keep it closed irrespective of last state.
isLibraryOpen: nextAppState.isLibraryMenuDocked
? nextAppState.isLibraryOpen
: false,
};
};

View File

@ -120,7 +120,12 @@
"lockAll": "Lock all",
"unlockAll": "Unlock all"
},
"statusPublished": "Published"
"statusPublished": "Published",
"sidebarLock": "Keep sidebar open"
},
"library": {
"hint_emptyLibrary": "Select an item on canvas to add it here, or install a library from the public repository, below.",
"hint_emptyPrivateLibrary": "Select an item on canvas to add it here."
},
"buttons": {
"clearReset": "Reset the canvas",

View File

@ -17,6 +17,8 @@ Please add the latest change on the top under the correct section.
#### Features
- Add `[UIOptions.dockedSidebarBreakpoint]`(https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#dockedSidebarBreakpoint) to customize at which point to break from the docked sidebar [#5274](https://github.com/excalidraw/excalidraw/pull/5274).
- Added support for supplying user `id` in the Collaborator object (see `collaborators` in [`updateScene()`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#updateScene)), which will be used to deduplicate users when rendering collaborator avatar list. Cursors will still be rendered for every user. [#5309](https://github.com/excalidraw/excalidraw/pull/5309)
- Export API to [set](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#setCursor) and [reset](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#resetCursor) mouse cursor on the canvas [#5215](https://github.com/excalidraw/excalidraw/pull/5215).

View File

@ -639,7 +639,7 @@ This prop sets the name of the drawing which will be used when exporting the dra
#### `UIOptions`
This prop can be used to customise UI of Excalidraw. Currently we support customising only [`canvasActions`](#canvasActions). It accepts the below parameters
This prop can be used to customise UI of Excalidraw. Currently we support customising [`canvasActions`](#canvasActions) and [`dockedSidebarBreakpoint`](dockedSidebarBreakpoint). It accepts the below parameters
<pre>
{ canvasActions: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L208"> CanvasActions<a/> }
@ -657,6 +657,12 @@ This prop can be used to customise UI of Excalidraw. Currently we support custom
| `theme` | boolean | true | Implies whether to show `Theme toggle` |
| `saveAsImage` | boolean | true | Implies whether to show `Save as image button` |
##### `dockedSidebarBreakpoint`
This prop indicates at what point should we break to a docked, permanent sidebar. If not passed it defaults to [`MQ_RIGHT_SIDEBAR_MAX_WIDTH_PORTRAIT`](https://github.com/excalidraw/excalidraw/blob/master/src/constants.ts#L167). If the `width` of the `excalidraw` container exceeds `dockedSidebarBreakpoint`, the sidebar will be dockable. If user choses to dock the sidebar, it will push the right part of the UI towards the left, making space for the sidebar as shown below.
![image](https://user-images.githubusercontent.com/11256141/174664866-c698c3fa-197b-43ff-956c-d79852c7b326.png)
#### `exportOpts`
The below attributes can be set in `UIOptions.canvasActions.export` to customize the export dialog. If `UIOptions.canvasActions.export` is `false` the export button will not be rendered.

View File

@ -44,6 +44,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
const canvasActions = props.UIOptions?.canvasActions;
const UIOptions: AppProps["UIOptions"] = {
...props.UIOptions,
canvasActions: {
...DEFAULT_UI_OPTIONS.canvasActions,
...canvasActions,

View File

@ -38,6 +38,7 @@ Object {
"gridSize": null,
"height": 100,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -211,6 +212,7 @@ Object {
"gridSize": null,
"height": 100,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -388,6 +390,7 @@ Object {
"gridSize": null,
"height": 100,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -726,6 +729,7 @@ Object {
"gridSize": null,
"height": 100,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -1064,6 +1068,7 @@ Object {
"gridSize": null,
"height": 100,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -1241,6 +1246,7 @@ Object {
"gridSize": null,
"height": 100,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -1454,6 +1460,7 @@ Object {
"gridSize": null,
"height": 100,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -1726,6 +1733,7 @@ Object {
"gridSize": null,
"height": 100,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -2082,6 +2090,7 @@ Object {
"gridSize": null,
"height": 100,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -2882,6 +2891,7 @@ Object {
"gridSize": null,
"height": 100,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -3220,6 +3230,7 @@ Object {
"gridSize": null,
"height": 100,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -3558,6 +3569,7 @@ Object {
"gridSize": null,
"height": 100,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -3976,6 +3988,7 @@ Object {
"gridSize": null,
"height": 100,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -4254,6 +4267,7 @@ Object {
"gridSize": null,
"height": 100,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -4613,6 +4627,7 @@ Object {
"gridSize": null,
"height": 100,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -4719,6 +4734,7 @@ Object {
"gridSize": null,
"height": 100,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -4803,6 +4819,7 @@ Object {
"gridSize": null,
"height": 100,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,

View File

@ -38,6 +38,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -547,6 +548,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -1062,6 +1064,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": false,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -1922,6 +1925,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -2143,6 +2147,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -2649,6 +2654,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -2925,6 +2931,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -3102,6 +3109,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -3591,6 +3599,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -3848,6 +3857,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -4069,6 +4079,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -4334,6 +4345,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -4609,6 +4621,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -5031,6 +5044,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -5355,6 +5369,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -5654,6 +5669,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -5881,6 +5897,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -6058,6 +6075,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -6559,6 +6577,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -6907,6 +6926,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -9146,6 +9166,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -9544,6 +9565,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -9820,6 +9842,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -10057,6 +10080,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -10363,6 +10387,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -10540,6 +10565,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -10717,6 +10743,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -10894,6 +10921,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -11101,6 +11129,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -11308,6 +11337,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -11533,6 +11563,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -11740,6 +11771,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -11917,6 +11949,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -12124,6 +12157,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -12301,6 +12335,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -12478,6 +12513,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -12703,6 +12739,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -13497,6 +13534,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -13773,6 +13811,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -13881,6 +13920,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -13987,6 +14027,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -14167,6 +14208,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -14518,6 +14560,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -14735,6 +14778,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -15647,6 +15691,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -15753,6 +15798,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -16592,6 +16638,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -17039,6 +17086,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -17338,6 +17386,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -17446,6 +17495,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -17990,6 +18040,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,
@ -18096,6 +18147,7 @@ Object {
"gridSize": null,
"height": 768,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,

View File

@ -38,6 +38,7 @@ Object {
"fileHandle": null,
"gridSize": null,
"isBindingEnabled": true,
"isLibraryMenuDocked": false,
"isLibraryOpen": false,
"isLoading": false,
"isResizing": false,

View File

@ -162,6 +162,7 @@ export type AppState = {
offsetLeft: number;
isLibraryOpen: boolean;
isLibraryMenuDocked: boolean;
fileHandle: FileSystemHandle | null;
collaborators: Map<string, Collaborator>;
showStats: boolean;
@ -291,7 +292,10 @@ export interface ExcalidrawProps {
elements: readonly NonDeletedExcalidrawElement[],
appState: AppState,
) => JSX.Element;
UIOptions?: UIOptions;
UIOptions?: {
dockedSidebarBreakpoint?: number;
canvasActions?: CanvasActions;
};
detectScroll?: boolean;
handleKeyboardGlobally?: boolean;
onLibraryChange?: (libraryItems: LibraryItems) => void | Promise<any>;
@ -349,18 +353,18 @@ type CanvasActions = {
saveAsImage?: boolean;
};
export type UIOptions = {
canvasActions?: CanvasActions;
};
export type AppProps = ExcalidrawProps & {
UIOptions: {
canvasActions: Required<CanvasActions> & { export: ExportOpts };
};
detectScroll: boolean;
handleKeyboardGlobally: boolean;
isCollaborating: boolean;
};
export type AppProps = Merge<
ExcalidrawProps,
{
UIOptions: {
canvasActions: Required<CanvasActions> & { export: ExportOpts };
dockedSidebarBreakpoint?: number;
};
detectScroll: boolean;
handleKeyboardGlobally: boolean;
isCollaborating: boolean;
}
>;
/** A subset of App class properties that we need to use elsewhere
* in the app, eg Manager. Factored out into a separate type to keep DRY. */
@ -377,7 +381,7 @@ export type AppClassProperties = {
}
>;
files: BinaryFiles;
deviceType: App["deviceType"];
device: App["device"];
scene: App["scene"];
};
@ -473,7 +477,9 @@ export type ExcalidrawImperativeAPI = {
resetCursor: InstanceType<typeof App>["resetCursor"];
};
export type DeviceType = {
export type Device = Readonly<{
isSmScreen: boolean;
isMobile: boolean;
isTouchScreen: boolean;
};
canDeviceFitSidebar: boolean;
}>;