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

View File

@ -30,7 +30,7 @@ const trackAction = (
trackEvent( trackEvent(
action.trackEvent.category, action.trackEvent.category,
action.trackEvent.action || action.name, 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, gridSize: null,
isBindingEnabled: true, isBindingEnabled: true,
isLibraryOpen: false, isLibraryOpen: false,
isLibraryMenuDocked: false,
isLoading: false, isLoading: false,
isResizing: false, isResizing: false,
isRotating: false, isRotating: false,
@ -146,7 +147,8 @@ const APP_STATE_STORAGE_CONF = (<
gridSize: { browser: true, export: true, server: true }, gridSize: { browser: true, export: true, server: true },
height: { browser: false, export: false, server: false }, height: { browser: false, export: false, server: false },
isBindingEnabled: { 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 }, isLoading: { browser: false, export: false, server: false },
isResizing: { browser: false, export: false, server: false }, isResizing: { browser: false, export: false, server: false },
isRotating: { 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 { getNonDeletedElements } from "../element";
import { ExcalidrawElement, PointerType } from "../element/types"; import { ExcalidrawElement, PointerType } from "../element/types";
import { t } from "../i18n"; import { t } from "../i18n";
import { useDeviceType } from "../components/App"; import { useDevice } from "../components/App";
import { import {
canChangeSharpness, canChangeSharpness,
canHaveArrowheads, canHaveArrowheads,
@ -52,7 +52,7 @@ export const SelectedShapeActions = ({
isSingleElementBoundContainer = true; isSingleElementBoundContainer = true;
} }
const isEditing = Boolean(appState.editingElement); const isEditing = Boolean(appState.editingElement);
const deviceType = useDeviceType(); const device = useDevice();
const isRTL = document.documentElement.getAttribute("dir") === "rtl"; const isRTL = document.documentElement.getAttribute("dir") === "rtl";
const showFillIcons = const showFillIcons =
@ -177,8 +177,8 @@ export const SelectedShapeActions = ({
<fieldset> <fieldset>
<legend>{t("labels.actions")}</legend> <legend>{t("labels.actions")}</legend>
<div className="buttonList"> <div className="buttonList">
{!deviceType.isMobile && renderAction("duplicateSelection")} {!device.isMobile && renderAction("duplicateSelection")}
{!deviceType.isMobile && renderAction("deleteSelectedElements")} {!device.isMobile && renderAction("deleteSelectedElements")}
{renderAction("group")} {renderAction("group")}
{renderAction("ungroup")} {renderAction("ungroup")}
{showLinkIcon && renderAction("hyperlink")} {showLinkIcon && renderAction("hyperlink")}

View File

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

View File

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

View File

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

View File

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

View File

@ -5,7 +5,7 @@ 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 { useDeviceType } from "./App"; 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";
@ -250,7 +250,7 @@ export const ImageExportDialog = ({
icon={exportImage} icon={exportImage}
type="button" type="button"
aria-label={t("buttons.exportImage")} aria-label={t("buttons.exportImage")}
showAriaLabel={useDeviceType().isMobile} showAriaLabel={useDevice().isMobile}
title={t("buttons.exportImage")} title={t("buttons.exportImage")}
/> />
{modalIsShown && ( {modalIsShown && (

View File

@ -1,7 +1,7 @@
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 { useDeviceType } from "./App"; 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 { exportFile, exportToFileIcon, link } from "./icons";
@ -117,7 +117,7 @@ export const JSONExportDialog = ({
icon={exportFile} icon={exportFile}
type="button" type="button"
aria-label={t("buttons.export")} aria-label={t("buttons.export")}
showAriaLabel={useDeviceType().isMobile} showAriaLabel={useDevice().isMobile}
title={t("buttons.export")} title={t("buttons.export")}
/> />
{modalIsShown && ( {modalIsShown && (

View File

@ -1,9 +1,63 @@
@import "open-color/open-color"; @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 { .excalidraw {
.layer-ui__wrapper.animate {
transition: width 0.1s ease-in-out;
}
.layer-ui__wrapper { .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); z-index: var(--zIndex-layerUI);
&__top-right { &__top-right {
display: flex; display: flex;
} }

View File

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

View File

@ -3,6 +3,8 @@ 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 { useDevice } from "./App";
const LIBRARY_ICON = ( const LIBRARY_ICON = (
<svg viewBox="0 0 576 512"> <svg viewBox="0 0 576 512">
@ -18,6 +20,7 @@ export const LibraryButton: React.FC<{
setAppState: React.Component<any, AppState>["setState"]; setAppState: React.Component<any, AppState>["setState"];
isMobile?: boolean; isMobile?: boolean;
}> = ({ appState, setAppState, isMobile }) => { }> = ({ appState, setAppState, isMobile }) => {
const device = useDevice();
return ( return (
<label <label
className={clsx( className={clsx(
@ -34,7 +37,19 @@ export const LibraryButton: React.FC<{
type="checkbox" type="checkbox"
name="editor-library" name="editor-library"
onChange={(event) => { 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} checked={appState.isLibraryOpen}
aria-label={capitalizeString(t("toolBar.library"))} aria-label={capitalizeString(t("toolBar.library"))}

View File

@ -2,7 +2,6 @@
.excalidraw { .excalidraw {
.layer-ui__library { .layer-ui__library {
margin: auto;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -11,8 +10,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
width: 100%; width: 100%;
margin: 2px 0; margin: 2px 0 15px 0;
.Spinner { .Spinner {
margin-right: 1rem; margin-right: 1rem;
} }
@ -21,14 +19,18 @@
// 2px from the left to account for focus border of left-most button // 2px from the left to account for focus border of left-most button
margin: 0 2px; 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%;
}
} }
.layer-ui__library-message { .layer-ui__library-message {
@ -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 { useAtom } from "jotai";
import { jotaiScope } from "../jotai"; import { jotaiScope } from "../jotai";
import Spinner from "./Spinner"; import Spinner from "./Spinner";
import { useDevice } from "./App";
const useOnClickOutside = ( const useOnClickOutside = (
ref: RefObject<HTMLElement>, ref: RefObject<HTMLElement>,
@ -103,17 +104,30 @@ export const LibraryMenu = ({
}) => { }) => {
const ref = useRef<HTMLDivElement | null>(null); const ref = useRef<HTMLDivElement | null>(null);
useOnClickOutside(ref, (event) => { const device = useDevice();
useOnClickOutside(
ref,
useCallback(
(event) => {
// If click on the library icon, do nothing. // If click on the library icon, do nothing.
if ((event.target as Element).closest(".ToolIcon__library")) { if ((event.target as Element).closest(".ToolIcon__library")) {
return; return;
} }
if (!appState.isLibraryMenuDocked || !device.canDeviceFitSidebar) {
onClose(); onClose();
}); }
},
[onClose, appState.isLibraryMenuDocked, device.canDeviceFitSidebar],
),
);
useEffect(() => { useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => { const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === KEYS.ESCAPE) { if (
event.key === KEYS.ESCAPE &&
(!appState.isLibraryMenuDocked || !device.canDeviceFitSidebar)
) {
onClose(); onClose();
} }
}; };
@ -121,7 +135,7 @@ export const LibraryMenu = ({
return () => { return () => {
document.removeEventListener(EVENT.KEYDOWN, handleKeyDown); document.removeEventListener(EVENT.KEYDOWN, handleKeyDown);
}; };
}, [onClose]); }, [onClose, appState.isLibraryMenuDocked, device.canDeviceFitSidebar]);
const [selectedItems, setSelectedItems] = useState<LibraryItem["id"][]>([]); const [selectedItems, setSelectedItems] = useState<LibraryItem["id"][]>([]);
const [showPublishLibraryDialog, setShowPublishLibraryDialog] = const [showPublishLibraryDialog, setShowPublishLibraryDialog] =
@ -273,6 +287,7 @@ export const LibraryMenu = ({
onInsertLibraryItems={onInsertLibraryItems} onInsertLibraryItems={onInsertLibraryItems}
pendingElements={pendingElements} pendingElements={pendingElements}
setAppState={setAppState} setAppState={setAppState}
appState={appState}
libraryReturnUrl={libraryReturnUrl} libraryReturnUrl={libraryReturnUrl}
library={library} library={library}
theme={theme} theme={theme}

View File

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

View File

@ -12,9 +12,9 @@ import {
LibraryItems, LibraryItems,
} from "../types"; } from "../types";
import { arrayToMap, muteFSAbortError } from "../utils"; import { arrayToMap, muteFSAbortError } from "../utils";
import { useDeviceType } from "./App"; import { useDevice } from "./App";
import ConfirmDialog from "./ConfirmDialog"; import ConfirmDialog from "./ConfirmDialog";
import { exportToFileIcon, load, publishIcon, trash } from "./icons"; import { close, exportToFileIcon, load, publishIcon, trash } from "./icons";
import { LibraryUnit } from "./LibraryUnit"; import { LibraryUnit } from "./LibraryUnit";
import Stack from "./Stack"; import Stack from "./Stack";
import { ToolButton } from "./ToolButton"; import { ToolButton } from "./ToolButton";
@ -25,6 +25,9 @@ import { MIME_TYPES, VERSIONS } from "../constants";
import Spinner from "./Spinner"; import Spinner from "./Spinner";
import { fileOpen } from "../data/filesystem"; import { fileOpen } from "../data/filesystem";
import { SidebarLockButton } from "./SidebarLockButton";
import { trackEvent } from "../analytics";
const LibraryMenuItems = ({ const LibraryMenuItems = ({
isLoading, isLoading,
libraryItems, libraryItems,
@ -34,6 +37,7 @@ const LibraryMenuItems = ({
pendingElements, pendingElements,
theme, theme,
setAppState, setAppState,
appState,
libraryReturnUrl, libraryReturnUrl,
library, library,
files, files,
@ -52,6 +56,7 @@ const LibraryMenuItems = ({
theme: AppState["theme"]; theme: AppState["theme"];
files: BinaryFiles; files: BinaryFiles;
setAppState: React.Component<any, AppState>["setState"]; setAppState: React.Component<any, AppState>["setState"];
appState: AppState;
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"]; libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
library: Library; library: Library;
id: string; id: string;
@ -88,9 +93,7 @@ const LibraryMenuItems = ({
}, [selectedItems, onRemoveFromLibrary, resetLibrary]); }, [selectedItems, onRemoveFromLibrary, resetLibrary]);
const [showRemoveLibAlert, setShowRemoveLibAlert] = useState(false); const [showRemoveLibAlert, setShowRemoveLibAlert] = useState(false);
const device = useDevice();
const isMobile = useDeviceType().isMobile;
const renderLibraryActions = () => { const renderLibraryActions = () => {
const itemsSelected = !!selectedItems.length; const itemsSelected = !!selectedItems.length;
const items = itemsSelected const items = itemsSelected
@ -101,7 +104,7 @@ const LibraryMenuItems = ({
: t("buttons.resetLibrary"); : t("buttons.resetLibrary");
return ( return (
<div className="library-actions"> <div className="library-actions">
{(!itemsSelected || !isMobile) && ( {!itemsSelected && (
<ToolButton <ToolButton
key="import" key="import"
type="button" type="button"
@ -186,7 +189,7 @@ const LibraryMenuItems = ({
className="library-actions--publish" className="library-actions--publish"
onClick={onPublish} onClick={onPublish}
> >
{!isMobile && <label>{t("buttons.publishLibrary")}</label>} {!device.isMobile && <label>{t("buttons.publishLibrary")}</label>}
{selectedItems.length > 0 && ( {selectedItems.length > 0 && (
<span className="library-actions-counter"> <span className="library-actions-counter">
{selectedItems.length} {selectedItems.length}
@ -195,11 +198,25 @@ const LibraryMenuItems = ({
</ToolButton> </ToolButton>
</Tooltip> </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> </div>
); );
}; };
const CELLS_PER_ROW = isMobile ? 4 : 6; const CELLS_PER_ROW = device.isMobile && !device.isSmScreen ? 6 : 4;
const referrer = const referrer =
libraryReturnUrl || window.location.origin + window.location.pathname; libraryReturnUrl || window.location.origin + window.location.pathname;
@ -356,15 +373,141 @@ const LibraryMenuItems = ({
(item) => item.status === "published", (item) => item.status === "published",
); );
const renderLibraryHeader = () => {
return ( return (
<div className="library-menu-items-container"> <>
{showRemoveLibAlert && renderRemoveLibAlert()}
<div className="layer-ui__library-header" key="library-header"> <div className="layer-ui__library-header" key="library-header">
{renderLibraryActions()} {renderLibraryActions()}
{isLoading ? ( {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">
{(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 /> <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,
])
)}
</>
<>
{(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 <a
className="library-menu-browse-button"
href={`${process.env.REACT_APP_LIBRARY_URL}?target=${ href={`${process.env.REACT_APP_LIBRARY_URL}?target=${
window.name || "_blank" window.name || "_blank"
}&referrer=${referrer}&useHash=true&token=${id}&theme=${theme}&version=${ }&referrer=${referrer}&useHash=true&token=${id}&theme=${theme}&version=${
@ -374,30 +517,25 @@ const LibraryMenuItems = ({
> >
{t("labels.libraries")} {t("labels.libraries")}
</a> </a>
)} );
</div> };
<Stack.Col
className="library-menu-items-container__items" return (
align="start" <div
gap={1} className="library-menu-items-container"
style={
device.isMobile
? {
minHeight: "200px",
maxHeight: "70vh",
}
: undefined
}
> >
<> {showRemoveLibAlert && renderRemoveLibAlert()}
<div className="separator">{t("labels.personalLib")}</div> {renderLibraryHeader()}
{renderLibrarySection([ {renderLibraryMenuItems()}
// append pending library item {!device.isMobile && renderLibraryFooter()}
...(pendingElements.length
? [{ id: null, elements: pendingElements }]
: []),
...unpublishedItems,
])}
</>
<>
<div className="separator">{t("labels.excalidrawLib")} </div>
{renderLibrarySection(publishedItems)}
</>
</Stack.Col>
</div> </div>
); );
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,26 +1,5 @@
@import "open-color/open-color.scss"; @import "open-color/open-color.scss";
@import "../css/variables.module";
@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;
}
}
.excalidraw { .excalidraw {
.App-toolbar-container { .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_PORTRAIT = 730;
export const MQ_MAX_WIDTH_LANDSCAPE = 1000; export const MQ_MAX_WIDTH_LANDSCAPE = 1000;
export const MQ_MAX_HEIGHT_LANDSCAPE = 500; 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; export const MAX_DECIMALS_FOR_SVG_EXPORT = 2;

View File

@ -350,7 +350,6 @@
align-items: flex-start; align-items: flex-start;
cursor: default; cursor: default;
pointer-events: none !important; pointer-events: none !important;
z-index: 100;
:root[dir="ltr"] & { :root[dir="ltr"] & {
left: 0.25rem; left: 0.25rem;
@ -391,6 +390,7 @@
.App-menu__left { .App-menu__left {
overflow-y: auto; overflow-y: auto;
box-shadow: var(--shadow-island);
} }
.dropdown-select { .dropdown-select {
@ -449,6 +449,7 @@
bottom: 30px; bottom: 30px;
transform: translateX(-50%); transform: translateX(-50%);
padding: 10px 20px; padding: 10px 20px;
pointer-events: all;
} }
.help-icon { .help-icon {
@ -567,6 +568,22 @@
display: none; 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 { .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)"; $theme-filter: "invert(93%) hue-rotate(180deg)";
$right-sidebar-width: "302px";
:export { :export {
themeFilter: unquote($theme-filter); themeFilter: unquote($theme-filter);
rightSidebarWidth: unquote($right-sidebar-width);
} }

View File

@ -283,6 +283,11 @@ export const restoreAppState = (
value: appState.zoom as NormalizedZoomValue, value: appState.zoom as NormalizedZoomValue,
} }
: appState.zoom || defaultAppState.zoom, : 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", "lockAll": "Lock all",
"unlockAll": "Unlock 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": { "buttons": {
"clearReset": "Reset the canvas", "clearReset": "Reset the canvas",

View File

@ -17,6 +17,8 @@ Please add the latest change on the top under the correct section.
#### Features #### 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) - 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). - 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` #### `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> <pre>
{ canvasActions: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L208"> CanvasActions<a/> } { 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` | | `theme` | boolean | true | Implies whether to show `Theme toggle` |
| `saveAsImage` | boolean | true | Implies whether to show `Save as image button` | | `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` #### `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. 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 canvasActions = props.UIOptions?.canvasActions;
const UIOptions: AppProps["UIOptions"] = { const UIOptions: AppProps["UIOptions"] = {
...props.UIOptions,
canvasActions: { canvasActions: {
...DEFAULT_UI_OPTIONS.canvasActions, ...DEFAULT_UI_OPTIONS.canvasActions,
...canvasActions, ...canvasActions,

View File

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

View File

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

View File

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

View File

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