feat: sidebar tabs support (#6213)

* feat: Sidebar tabs support [wip]

* tab trigger styling tweaks

* add `:hover` & `:active` states

* replace `@dwelle/tunnel-rat` with `tunnel-rat`

* make stuff more explicit

- remove `Sidebar.Header` fallback (host apps need to render manually), and stop tunneling it (render in place)
- make `docked` state explicit
- stop tunneling `Sidebar.TabTriggers` (render in place)

* redesign sidebar / library as per latest spec

* support no label on `Sidebar.Trigger`

* add Sidebar `props.onStateChange`

* style fixes

* make `appState.isSidebarDocked` into a soft user preference

* px -> rem & refactor

* remove `props.renderSidebar`

* update tests

* remove

* refactor

* rename constants

* tab triggers styling fixes

* factor out library-related logic from generic sidebar trigger

* change `props.onClose` to `onToggle`

* rename `props.value` -> `props.tab`

* add displayNames

* allow HTMLAttributes on applicable compos

* fix example App

* more styling tweaks and fixes

* fix not setting `dockable`

* more style fixes

* fix and align sidebar header button styling

* make DefaultSidebar dockable on if host apps supplies `onDock`

* stop `Sidebar.Trigger` hiding label on mobile

this should be only the default sidebar trigger behavior, and for that we don't need to use `device` hook as we handle in CSS

* fix `dockable` prop of defaultSidebar

* remove extra `typescript` dep

* remove `defaultTab` prop

in favor of explicit `tab` value in `<Sidebar.Trigger/>` and `toggleSidebar()`, to reduce API surface area and solve inconsistency of `appState.openSidebar.tab` not reflecting actual UI value if `defaultTab` was supported (without additional syncing logic which feels like the wrong solution).

* remove `onToggle` in favor of `onStateChange`

reducing API surface area

* fix restore

* comment no longer applies

* reuse `Button` component in sidebar buttons

* fix tests

* split Sidebar sub-components into files

* remove `props.dockable` in favor of `props.onDock` only

* split tests

* fix sidebar showing dock button if no `props.docked` supplied & add more tests

* reorder and group sidebar tests

* clarify

* rename classes & dedupe css

* refactor tests

* update changelog

* update changelog

---------

Co-authored-by: barnabasmolnar <barnabas@excalidraw.com>
This commit is contained in:
David Luzar 2023-05-04 19:33:31 +02:00 committed by GitHub
parent b1311a407a
commit e9cae918a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
61 changed files with 1972 additions and 1431 deletions

View File

@ -19,7 +19,7 @@
] ]
}, },
"dependencies": { "dependencies": {
"@dwelle/tunnel-rat": "0.1.1", "@radix-ui/react-tabs": "1.0.2",
"@sentry/browser": "6.2.5", "@sentry/browser": "6.2.5",
"@sentry/integrations": "6.2.5", "@sentry/integrations": "6.2.5",
"@testing-library/jest-dom": "5.16.2", "@testing-library/jest-dom": "5.16.2",
@ -51,7 +51,7 @@
"roughjs": "4.5.2", "roughjs": "4.5.2",
"sass": "1.51.0", "sass": "1.51.0",
"socket.io-client": "2.3.1", "socket.io-client": "2.3.1",
"tunnel-rat": "0.1.0", "tunnel-rat": "0.1.2",
"workbox-background-sync": "^6.5.4", "workbox-background-sync": "^6.5.4",
"workbox-broadcast-update": "^6.5.4", "workbox-broadcast-update": "^6.5.4",
"workbox-cacheable-response": "^6.5.4", "workbox-cacheable-response": "^6.5.4",

View File

@ -58,7 +58,7 @@ export const getDefaultAppState = (): Omit<
fileHandle: null, fileHandle: null,
gridSize: null, gridSize: null,
isBindingEnabled: true, isBindingEnabled: true,
isSidebarDocked: false, defaultSidebarDockedPreference: false,
isLoading: false, isLoading: false,
isResizing: false, isResizing: false,
isRotating: false, isRotating: false,
@ -150,7 +150,11 @@ 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 },
isSidebarDocked: { browser: true, export: false, server: false }, defaultSidebarDockedPreference: {
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

@ -210,6 +210,8 @@ import {
PointerDownState, PointerDownState,
SceneData, SceneData,
Device, Device,
SidebarName,
SidebarTabName,
} from "../types"; } from "../types";
import { import {
debounce, debounce,
@ -299,6 +301,9 @@ import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
import { actionWrapTextInContainer } from "../actions/actionBoundText"; import { actionWrapTextInContainer } from "../actions/actionBoundText";
import BraveMeasureTextError from "./BraveMeasureTextError"; import BraveMeasureTextError from "./BraveMeasureTextError";
const AppContext = React.createContext<AppClassProperties>(null!);
const AppPropsContext = React.createContext<AppProps>(null!);
const deviceContextInitialValue = { const deviceContextInitialValue = {
isSmScreen: false, isSmScreen: false,
isMobile: false, isMobile: false,
@ -340,6 +345,8 @@ const ExcalidrawActionManagerContext = React.createContext<ActionManager>(
); );
ExcalidrawActionManagerContext.displayName = "ExcalidrawActionManagerContext"; ExcalidrawActionManagerContext.displayName = "ExcalidrawActionManagerContext";
export const useApp = () => useContext(AppContext);
export const useAppProps = () => useContext(AppPropsContext);
export const useDevice = () => useContext<Device>(DeviceContext); export const useDevice = () => useContext<Device>(DeviceContext);
export const useExcalidrawContainer = () => export const useExcalidrawContainer = () =>
useContext(ExcalidrawContainerContext); useContext(ExcalidrawContainerContext);
@ -400,7 +407,7 @@ class App extends React.Component<AppProps, AppState> {
private nearestScrollableContainer: HTMLElement | Document | undefined; private nearestScrollableContainer: HTMLElement | Document | undefined;
public library: AppClassProperties["library"]; public library: AppClassProperties["library"];
public libraryItemsFromStorage: LibraryItems | undefined; public libraryItemsFromStorage: LibraryItems | undefined;
private id: string; public id: string;
private history: History; private history: History;
private excalidrawContainerValue: { private excalidrawContainerValue: {
container: HTMLDivElement | null; container: HTMLDivElement | null;
@ -438,7 +445,7 @@ class App extends React.Component<AppProps, AppState> {
width: window.innerWidth, width: window.innerWidth,
height: window.innerHeight, height: window.innerHeight,
showHyperlinkPopup: false, showHyperlinkPopup: false,
isSidebarDocked: false, defaultSidebarDockedPreference: false,
}; };
this.id = nanoid(); this.id = nanoid();
@ -469,7 +476,7 @@ class App extends React.Component<AppProps, AppState> {
setActiveTool: this.setActiveTool, setActiveTool: this.setActiveTool,
setCursor: this.setCursor, setCursor: this.setCursor,
resetCursor: this.resetCursor, resetCursor: this.resetCursor,
toggleMenu: this.toggleMenu, toggleSidebar: this.toggleSidebar,
} as const; } as const;
if (typeof excalidrawRef === "function") { if (typeof excalidrawRef === "function") {
excalidrawRef(api); excalidrawRef(api);
@ -577,101 +584,91 @@ class App extends React.Component<AppProps, AppState> {
this.props.handleKeyboardGlobally ? undefined : this.onKeyDown this.props.handleKeyboardGlobally ? undefined : this.onKeyDown
} }
> >
<ExcalidrawContainerContext.Provider <AppContext.Provider value={this}>
value={this.excalidrawContainerValue} <AppPropsContext.Provider value={this.props}>
> <ExcalidrawContainerContext.Provider
<DeviceContext.Provider value={this.device}> value={this.excalidrawContainerValue}
<ExcalidrawSetAppStateContext.Provider value={this.setAppState}> >
<ExcalidrawAppStateContext.Provider value={this.state}> <DeviceContext.Provider value={this.device}>
<ExcalidrawElementsContext.Provider <ExcalidrawSetAppStateContext.Provider value={this.setAppState}>
value={this.scene.getNonDeletedElements()} <ExcalidrawAppStateContext.Provider value={this.state}>
> <ExcalidrawElementsContext.Provider
<ExcalidrawActionManagerContext.Provider value={this.scene.getNonDeletedElements()}
value={this.actionManager}
>
<LayerUI
canvas={this.canvas}
appState={this.state}
files={this.files}
setAppState={this.setAppState}
actionManager={this.actionManager}
elements={this.scene.getNonDeletedElements()}
onLockToggle={this.toggleLock}
onPenModeToggle={this.togglePenMode}
onHandToolToggle={this.onHandToolToggle}
onInsertElements={(elements) =>
this.addElementsFromPasteOrLibrary({
elements,
position: "center",
files: null,
})
}
langCode={getLanguage().code}
renderTopRightUI={renderTopRightUI}
renderCustomStats={renderCustomStats}
renderCustomSidebar={this.props.renderSidebar}
showExitZenModeBtn={
typeof this.props?.zenModeEnabled === "undefined" &&
this.state.zenModeEnabled
}
libraryReturnUrl={this.props.libraryReturnUrl}
UIOptions={this.props.UIOptions}
focusContainer={this.focusContainer}
library={this.library}
id={this.id}
onImageAction={this.onImageAction}
renderWelcomeScreen={
!this.state.isLoading &&
this.state.showWelcomeScreen &&
this.state.activeTool.type === "selection" &&
!this.scene.getElementsIncludingDeleted().length
}
> >
{this.props.children} <ExcalidrawActionManagerContext.Provider
</LayerUI> value={this.actionManager}
<div className="excalidraw-textEditorContainer" /> >
<div className="excalidraw-contextMenuContainer" /> <LayerUI
{selectedElement.length === 1 && canvas={this.canvas}
!this.state.contextMenu && appState={this.state}
this.state.showHyperlinkPopup && ( files={this.files}
<Hyperlink
key={selectedElement[0].id}
element={selectedElement[0]}
setAppState={this.setAppState} setAppState={this.setAppState}
onLinkOpen={this.props.onLinkOpen} actionManager={this.actionManager}
/> elements={this.scene.getNonDeletedElements()}
)} onLockToggle={this.toggleLock}
{this.state.toast !== null && ( onPenModeToggle={this.togglePenMode}
<Toast onHandToolToggle={this.onHandToolToggle}
message={this.state.toast.message} langCode={getLanguage().code}
onClose={() => this.setToast(null)} renderTopRightUI={renderTopRightUI}
duration={this.state.toast.duration} renderCustomStats={renderCustomStats}
closable={this.state.toast.closable} showExitZenModeBtn={
/> typeof this.props?.zenModeEnabled === "undefined" &&
)} this.state.zenModeEnabled
{this.state.contextMenu && ( }
<ContextMenu UIOptions={this.props.UIOptions}
items={this.state.contextMenu.items} onImageAction={this.onImageAction}
top={this.state.contextMenu.top} renderWelcomeScreen={
left={this.state.contextMenu.left} !this.state.isLoading &&
actionManager={this.actionManager} this.state.showWelcomeScreen &&
/> this.state.activeTool.type === "selection" &&
)} !this.scene.getElementsIncludingDeleted().length
<main>{this.renderCanvas()}</main> }
</ExcalidrawActionManagerContext.Provider> >
</ExcalidrawElementsContext.Provider>{" "} {this.props.children}
</ExcalidrawAppStateContext.Provider> </LayerUI>
</ExcalidrawSetAppStateContext.Provider> <div className="excalidraw-textEditorContainer" />
</DeviceContext.Provider> <div className="excalidraw-contextMenuContainer" />
</ExcalidrawContainerContext.Provider> {selectedElement.length === 1 &&
!this.state.contextMenu &&
this.state.showHyperlinkPopup && (
<Hyperlink
key={selectedElement[0].id}
element={selectedElement[0]}
setAppState={this.setAppState}
onLinkOpen={this.props.onLinkOpen}
/>
)}
{this.state.toast !== null && (
<Toast
message={this.state.toast.message}
onClose={() => this.setToast(null)}
duration={this.state.toast.duration}
closable={this.state.toast.closable}
/>
)}
{this.state.contextMenu && (
<ContextMenu
items={this.state.contextMenu.items}
top={this.state.contextMenu.top}
left={this.state.contextMenu.left}
actionManager={this.actionManager}
/>
)}
<main>{this.renderCanvas()}</main>
</ExcalidrawActionManagerContext.Provider>
</ExcalidrawElementsContext.Provider>{" "}
</ExcalidrawAppStateContext.Provider>
</ExcalidrawSetAppStateContext.Provider>
</DeviceContext.Provider>
</ExcalidrawContainerContext.Provider>
</AppPropsContext.Provider>
</AppContext.Provider>
</div> </div>
); );
} }
public focusContainer: AppClassProperties["focusContainer"] = () => { public focusContainer: AppClassProperties["focusContainer"] = () => {
if (this.props.autoFocus) { this.excalidrawContainerRef.current?.focus();
this.excalidrawContainerRef.current?.focus();
}
}; };
public getSceneElementsIncludingDeleted = () => { public getSceneElementsIncludingDeleted = () => {
@ -682,6 +679,14 @@ class App extends React.Component<AppProps, AppState> {
return this.scene.getNonDeletedElements(); return this.scene.getNonDeletedElements();
}; };
public onInsertElements = (elements: readonly ExcalidrawElement[]) => {
this.addElementsFromPasteOrLibrary({
elements,
position: "center",
files: null,
});
};
private syncActionResult = withBatchedUpdates( private syncActionResult = withBatchedUpdates(
(actionResult: ActionResult) => { (actionResult: ActionResult) => {
if (this.unmounted || actionResult === false) { if (this.unmounted || actionResult === false) {
@ -951,7 +956,7 @@ class App extends React.Component<AppProps, AppState> {
this.scene.addCallback(this.onSceneUpdated); this.scene.addCallback(this.onSceneUpdated);
this.addEventListeners(); this.addEventListeners();
if (this.excalidrawContainerRef.current) { if (this.props.autoFocus && this.excalidrawContainerRef.current) {
this.focusContainer(); this.focusContainer();
} }
@ -1679,7 +1684,7 @@ class App extends React.Component<AppProps, AppState> {
openSidebar: openSidebar:
this.state.openSidebar && this.state.openSidebar &&
this.device.canDeviceFitSidebar && this.device.canDeviceFitSidebar &&
this.state.isSidebarDocked this.state.defaultSidebarDockedPreference
? this.state.openSidebar ? this.state.openSidebar
: null, : null,
selectedElementIds: newElements.reduce( selectedElementIds: newElements.reduce(
@ -2017,30 +2022,24 @@ class App extends React.Component<AppProps, AppState> {
/** /**
* @returns whether the menu was toggled on or off * @returns whether the menu was toggled on or off
*/ */
public toggleMenu = ( public toggleSidebar = ({
type: "library" | "customSidebar", name,
force?: boolean, tab,
): boolean => { force,
if (type === "customSidebar" && !this.props.renderSidebar) { }: {
console.warn( name: SidebarName;
`attempting to toggle "customSidebar", but no "props.renderSidebar" is defined`, tab?: SidebarTabName;
); force?: boolean;
return false; }): boolean => {
let nextName;
if (force === undefined) {
nextName = this.state.openSidebar?.name === name ? null : name;
} else {
nextName = force ? name : null;
} }
this.setState({ openSidebar: nextName ? { name: nextName, tab } : null });
if (type === "library" || type === "customSidebar") { return !!nextName;
let nextValue;
if (force === undefined) {
nextValue = this.state.openSidebar === type ? null : type;
} else {
nextValue = force ? type : null;
}
this.setState({ openSidebar: nextValue });
return !!nextValue;
}
return false;
}; };
private updateCurrentCursorPosition = withBatchedUpdates( private updateCurrentCursorPosition = withBatchedUpdates(

View File

@ -1,8 +1,12 @@
import clsx from "clsx";
import { composeEventHandlers } from "../utils";
import "./Button.scss"; import "./Button.scss";
interface ButtonProps extends React.HTMLAttributes<HTMLButtonElement> { interface ButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
type?: "button" | "submit" | "reset"; type?: "button" | "submit" | "reset";
onSelect: () => any; onSelect: () => any;
/** whether button is in active state */
selected?: boolean;
children: React.ReactNode; children: React.ReactNode;
className?: string; className?: string;
} }
@ -15,18 +19,18 @@ interface ButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
export const Button = ({ export const Button = ({
type = "button", type = "button",
onSelect, onSelect,
selected,
children, children,
className = "", className = "",
...rest ...rest
}: ButtonProps) => { }: ButtonProps) => {
return ( return (
<button <button
onClick={(event) => { onClick={composeEventHandlers(rest.onClick, (event) => {
onSelect(); onSelect();
rest.onClick?.(event); })}
}}
type={type} type={type}
className={`excalidraw-button ${className}`} className={clsx("excalidraw-button", className, { selected })}
{...rest} {...rest}
> >
{children} {children}

View File

@ -4,8 +4,8 @@ import { Dialog, DialogProps } from "./Dialog";
import "./ConfirmDialog.scss"; import "./ConfirmDialog.scss";
import DialogActionButton from "./DialogActionButton"; import DialogActionButton from "./DialogActionButton";
import { useSetAtom } from "jotai"; import { useSetAtom } from "jotai";
import { isLibraryMenuOpenAtom } from "./LibraryMenuHeaderContent"; import { isLibraryMenuOpenAtom } from "./LibraryMenu";
import { useExcalidrawSetAppState } from "./App"; import { useExcalidrawContainer, useExcalidrawSetAppState } from "./App";
import { jotaiScope } from "../jotai"; import { jotaiScope } from "../jotai";
interface Props extends Omit<DialogProps, "onCloseRequest"> { interface Props extends Omit<DialogProps, "onCloseRequest"> {
@ -26,6 +26,7 @@ const ConfirmDialog = (props: Props) => {
} = props; } = props;
const setAppState = useExcalidrawSetAppState(); const setAppState = useExcalidrawSetAppState();
const setIsLibraryMenuOpen = useSetAtom(isLibraryMenuOpenAtom, jotaiScope); const setIsLibraryMenuOpen = useSetAtom(isLibraryMenuOpenAtom, jotaiScope);
const { container } = useExcalidrawContainer();
return ( return (
<Dialog <Dialog
@ -42,6 +43,7 @@ const ConfirmDialog = (props: Props) => {
setAppState({ openMenu: null }); setAppState({ openMenu: null });
setIsLibraryMenuOpen(false); setIsLibraryMenuOpen(false);
onCancel(); onCancel();
container?.focus();
}} }}
/> />
<DialogActionButton <DialogActionButton
@ -50,6 +52,7 @@ const ConfirmDialog = (props: Props) => {
setAppState({ openMenu: null }); setAppState({ openMenu: null });
setIsLibraryMenuOpen(false); setIsLibraryMenuOpen(false);
onConfirm(); onConfirm();
container?.focus();
}} }}
actionType="danger" actionType="danger"
/> />

View File

@ -0,0 +1,144 @@
import React from "react";
import { DEFAULT_SIDEBAR } from "../constants";
import { DefaultSidebar } from "../packages/excalidraw/index";
import {
fireEvent,
waitFor,
withExcalidrawDimensions,
} from "../tests/test-utils";
import {
assertExcalidrawWithSidebar,
assertSidebarDockButton,
} from "./Sidebar/Sidebar.test";
const { h } = window;
describe("DefaultSidebar", () => {
it("when `docked={undefined}` & `onDock={undefined}`, should allow docking", async () => {
await assertExcalidrawWithSidebar(
<DefaultSidebar />,
DEFAULT_SIDEBAR.name,
async () => {
expect(h.state.defaultSidebarDockedPreference).toBe(false);
const { dockButton } = await assertSidebarDockButton(true);
fireEvent.click(dockButton);
await waitFor(() => {
expect(h.state.defaultSidebarDockedPreference).toBe(true);
expect(dockButton).toHaveClass("selected");
});
fireEvent.click(dockButton);
await waitFor(() => {
expect(h.state.defaultSidebarDockedPreference).toBe(false);
expect(dockButton).not.toHaveClass("selected");
});
},
);
});
it("when `docked={undefined}` & `onDock`, should allow docking", async () => {
await assertExcalidrawWithSidebar(
<DefaultSidebar onDock={() => {}} />,
DEFAULT_SIDEBAR.name,
async () => {
expect(h.state.defaultSidebarDockedPreference).toBe(false);
const { dockButton } = await assertSidebarDockButton(true);
fireEvent.click(dockButton);
await waitFor(() => {
expect(h.state.defaultSidebarDockedPreference).toBe(true);
expect(dockButton).toHaveClass("selected");
});
fireEvent.click(dockButton);
await waitFor(() => {
expect(h.state.defaultSidebarDockedPreference).toBe(false);
expect(dockButton).not.toHaveClass("selected");
});
},
);
});
it("when `docked={true}` & `onDock`, should allow docking", async () => {
await assertExcalidrawWithSidebar(
<DefaultSidebar onDock={() => {}} />,
DEFAULT_SIDEBAR.name,
async () => {
expect(h.state.defaultSidebarDockedPreference).toBe(false);
const { dockButton } = await assertSidebarDockButton(true);
fireEvent.click(dockButton);
await waitFor(() => {
expect(h.state.defaultSidebarDockedPreference).toBe(true);
expect(dockButton).toHaveClass("selected");
});
fireEvent.click(dockButton);
await waitFor(() => {
expect(h.state.defaultSidebarDockedPreference).toBe(false);
expect(dockButton).not.toHaveClass("selected");
});
},
);
});
it("when `onDock={false}`, should disable docking", async () => {
await assertExcalidrawWithSidebar(
<DefaultSidebar onDock={false} />,
DEFAULT_SIDEBAR.name,
async () => {
await withExcalidrawDimensions(
{ width: 1920, height: 1080 },
async () => {
expect(h.state.defaultSidebarDockedPreference).toBe(false);
await assertSidebarDockButton(false);
},
);
},
);
});
it("when `docked={true}` & `onDock={false}`, should force-dock sidebar", async () => {
await assertExcalidrawWithSidebar(
<DefaultSidebar docked onDock={false} />,
DEFAULT_SIDEBAR.name,
async () => {
expect(h.state.defaultSidebarDockedPreference).toBe(false);
const { sidebar } = await assertSidebarDockButton(false);
expect(sidebar).toHaveClass("sidebar--docked");
},
);
});
it("when `docked={true}` & `onDock={undefined}`, should force-dock sidebar", async () => {
await assertExcalidrawWithSidebar(
<DefaultSidebar docked />,
DEFAULT_SIDEBAR.name,
async () => {
expect(h.state.defaultSidebarDockedPreference).toBe(false);
const { sidebar } = await assertSidebarDockButton(false);
expect(sidebar).toHaveClass("sidebar--docked");
},
);
});
it("when `docked={false}` & `onDock={undefined}`, should force-undock sidebar", async () => {
await assertExcalidrawWithSidebar(
<DefaultSidebar docked={false} />,
DEFAULT_SIDEBAR.name,
async () => {
expect(h.state.defaultSidebarDockedPreference).toBe(false);
const { sidebar } = await assertSidebarDockButton(false);
expect(sidebar).not.toHaveClass("sidebar--docked");
},
);
});
});

View File

@ -0,0 +1,118 @@
import clsx from "clsx";
import { DEFAULT_SIDEBAR, LIBRARY_SIDEBAR_TAB } from "../constants";
import { useTunnels } from "../context/tunnels";
import { useUIAppState } from "../context/ui-appState";
import { t } from "../i18n";
import { MarkOptional, Merge } from "../utility-types";
import { composeEventHandlers } from "../utils";
import { useExcalidrawSetAppState } from "./App";
import { withInternalFallback } from "./hoc/withInternalFallback";
import { LibraryMenu } from "./LibraryMenu";
import { SidebarProps, SidebarTriggerProps } from "./Sidebar/common";
import { Sidebar } from "./Sidebar/Sidebar";
const DefaultSidebarTrigger = withInternalFallback(
"DefaultSidebarTrigger",
(
props: Omit<SidebarTriggerProps, "name"> &
React.HTMLAttributes<HTMLDivElement>,
) => {
const { DefaultSidebarTriggerTunnel } = useTunnels();
return (
<DefaultSidebarTriggerTunnel.In>
<Sidebar.Trigger
{...props}
className="default-sidebar-trigger"
name={DEFAULT_SIDEBAR.name}
/>
</DefaultSidebarTriggerTunnel.In>
);
},
);
DefaultSidebarTrigger.displayName = "DefaultSidebarTrigger";
const DefaultTabTriggers = ({
children,
...rest
}: { children: React.ReactNode } & React.HTMLAttributes<HTMLDivElement>) => {
const { DefaultSidebarTabTriggersTunnel } = useTunnels();
return (
<DefaultSidebarTabTriggersTunnel.In>
<Sidebar.TabTriggers {...rest}>{children}</Sidebar.TabTriggers>
</DefaultSidebarTabTriggersTunnel.In>
);
};
DefaultTabTriggers.displayName = "DefaultTabTriggers";
export const DefaultSidebar = Object.assign(
withInternalFallback(
"DefaultSidebar",
({
children,
className,
onDock,
docked,
...rest
}: Merge<
MarkOptional<Omit<SidebarProps, "name">, "children">,
{
/** pass `false` to disable docking */
onDock?: SidebarProps["onDock"] | false;
}
>) => {
const appState = useUIAppState();
const setAppState = useExcalidrawSetAppState();
const { DefaultSidebarTabTriggersTunnel } = useTunnels();
return (
<Sidebar
{...rest}
name="default"
key="default"
className={clsx("default-sidebar", className)}
docked={docked ?? appState.defaultSidebarDockedPreference}
onDock={
// `onDock=false` disables docking.
// if `docked` passed, but no onDock passed, disable manual docking.
onDock === false || (!onDock && docked != null)
? undefined
: // compose to allow the host app to listen on default behavior
composeEventHandlers(onDock, (docked) => {
setAppState({ defaultSidebarDockedPreference: docked });
})
}
>
<Sidebar.Tabs>
<Sidebar.Header>
{rest.__fallback && (
<div
style={{
color: "var(--color-primary)",
fontSize: "1.2em",
fontWeight: "bold",
textOverflow: "ellipsis",
overflow: "hidden",
whiteSpace: "nowrap",
paddingRight: "1em",
}}
>
{t("toolBar.library")}
</div>
)}
<DefaultSidebarTabTriggersTunnel.Out />
</Sidebar.Header>
<Sidebar.Tab tab={LIBRARY_SIDEBAR_TAB}>
<LibraryMenu />
</Sidebar.Tab>
{children}
</Sidebar.Tabs>
</Sidebar>
);
},
),
{
Trigger: DefaultSidebarTrigger,
TabTriggers: DefaultTabTriggers,
},
);

View File

@ -15,7 +15,7 @@ import { Modal } from "./Modal";
import { AppState } from "../types"; import { AppState } from "../types";
import { queryFocusableElements } from "../utils"; import { queryFocusableElements } from "../utils";
import { useSetAtom } from "jotai"; import { useSetAtom } from "jotai";
import { isLibraryMenuOpenAtom } from "./LibraryMenuHeaderContent"; import { isLibraryMenuOpenAtom } from "./LibraryMenu";
import { jotaiScope } from "../jotai"; import { jotaiScope } from "../jotai";
export interface DialogProps { export interface DialogProps {

View File

@ -29,7 +29,7 @@ const getHints = ({
const { activeTool, isResizing, isRotating, lastPointerDownWith } = appState; const { activeTool, isResizing, isRotating, lastPointerDownWith } = appState;
const multiMode = appState.multiElement !== null; const multiMode = appState.multiElement !== null;
if (appState.openSidebar === "library" && !device.canDeviceFitSidebar) { if (appState.openSidebar && !device.canDeviceFitSidebar) {
return null; return null;
} }

View File

@ -1,7 +1,7 @@
import clsx from "clsx"; import clsx from "clsx";
import React from "react"; import React from "react";
import { ActionManager } from "../actions/manager"; import { ActionManager } from "../actions/manager";
import { CLASSES, LIBRARY_SIDEBAR_WIDTH } from "../constants"; import { CLASSES, DEFAULT_SIDEBAR, 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";
@ -9,7 +9,7 @@ import { Language, t } from "../i18n";
import { calculateScrollCenter } from "../scene"; import { calculateScrollCenter } from "../scene";
import { ExportType } from "../scene/types"; import { ExportType } from "../scene/types";
import { AppProps, AppState, ExcalidrawProps, BinaryFiles } from "../types"; import { AppProps, AppState, ExcalidrawProps, BinaryFiles } from "../types";
import { isShallowEqual, muteFSAbortError } from "../utils"; import { capitalizeString, isShallowEqual, muteFSAbortError } from "../utils";
import { SelectedShapeActions, ShapesSwitcher } from "./Actions"; import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
import { ErrorDialog } from "./ErrorDialog"; import { ErrorDialog } from "./ErrorDialog";
import { ExportCB, ImageExportDialog } from "./ImageExportDialog"; import { ExportCB, ImageExportDialog } from "./ImageExportDialog";
@ -24,28 +24,28 @@ import { Section } from "./Section";
import { HelpDialog } from "./HelpDialog"; import { HelpDialog } from "./HelpDialog";
import Stack from "./Stack"; import Stack from "./Stack";
import { UserList } from "./UserList"; import { UserList } from "./UserList";
import Library from "../data/library";
import { JSONExportDialog } from "./JSONExportDialog"; import { JSONExportDialog } from "./JSONExportDialog";
import { LibraryButton } from "./LibraryButton";
import { isImageFileHandle } from "../data/blob"; import { isImageFileHandle } from "../data/blob";
import { LibraryMenu } from "./LibraryMenu";
import "./LayerUI.scss";
import "./Toolbar.scss";
import { PenModeButton } from "./PenModeButton"; import { PenModeButton } from "./PenModeButton";
import { trackEvent } from "../analytics"; import { trackEvent } from "../analytics";
import { useDevice } from "../components/App"; import { useDevice } from "../components/App";
import { Stats } from "./Stats"; import { Stats } from "./Stats";
import { actionToggleStats } from "../actions/actionToggleStats"; import { actionToggleStats } from "../actions/actionToggleStats";
import Footer from "./footer/Footer"; import Footer from "./footer/Footer";
import { hostSidebarCountersAtom } from "./Sidebar/Sidebar"; import { isSidebarDockedAtom } from "./Sidebar/Sidebar";
import { jotaiScope } from "../jotai"; import { jotaiScope } from "../jotai";
import { Provider, useAtom } from "jotai"; import { Provider, useAtomValue } from "jotai";
import MainMenu from "./main-menu/MainMenu"; import MainMenu from "./main-menu/MainMenu";
import { ActiveConfirmDialog } from "./ActiveConfirmDialog"; import { ActiveConfirmDialog } from "./ActiveConfirmDialog";
import { HandButton } from "./HandButton"; import { HandButton } from "./HandButton";
import { isHandToolActive } from "../appState"; import { isHandToolActive } from "../appState";
import { TunnelsContext, useInitializeTunnels } from "./context/tunnels"; import { TunnelsContext, useInitializeTunnels } from "../context/tunnels";
import { LibraryIcon } from "./icons";
import { UIAppStateContext } from "../context/ui-appState";
import { DefaultSidebar } from "./DefaultSidebar";
import "./LayerUI.scss";
import "./Toolbar.scss";
interface LayerUIProps { interface LayerUIProps {
actionManager: ActionManager; actionManager: ActionManager;
@ -57,17 +57,11 @@ interface LayerUIProps {
onLockToggle: () => void; onLockToggle: () => void;
onHandToolToggle: () => void; onHandToolToggle: () => void;
onPenModeToggle: () => void; onPenModeToggle: () => void;
onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void;
showExitZenModeBtn: boolean; showExitZenModeBtn: boolean;
langCode: Language["code"]; langCode: Language["code"];
renderTopRightUI?: ExcalidrawProps["renderTopRightUI"]; renderTopRightUI?: ExcalidrawProps["renderTopRightUI"];
renderCustomStats?: ExcalidrawProps["renderCustomStats"]; renderCustomStats?: ExcalidrawProps["renderCustomStats"];
renderCustomSidebar?: ExcalidrawProps["renderSidebar"];
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
UIOptions: AppProps["UIOptions"]; UIOptions: AppProps["UIOptions"];
focusContainer: () => void;
library: Library;
id: string;
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void; onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
renderWelcomeScreen: boolean; renderWelcomeScreen: boolean;
children?: React.ReactNode; children?: React.ReactNode;
@ -109,16 +103,10 @@ const LayerUI = ({
onLockToggle, onLockToggle,
onHandToolToggle, onHandToolToggle,
onPenModeToggle, onPenModeToggle,
onInsertElements,
showExitZenModeBtn, showExitZenModeBtn,
renderTopRightUI, renderTopRightUI,
renderCustomStats, renderCustomStats,
renderCustomSidebar,
libraryReturnUrl,
UIOptions, UIOptions,
focusContainer,
library,
id,
onImageAction, onImageAction,
renderWelcomeScreen, renderWelcomeScreen,
children, children,
@ -197,8 +185,8 @@ const LayerUI = ({
<div style={{ position: "relative" }}> <div style={{ position: "relative" }}>
{/* wrapping to Fragment stops React from occasionally complaining {/* wrapping to Fragment stops React from occasionally complaining
about identical Keys */} about identical Keys */}
<tunnels.mainMenuTunnel.Out /> <tunnels.MainMenuTunnel.Out />
{renderWelcomeScreen && <tunnels.welcomeScreenMenuHintTunnel.Out />} {renderWelcomeScreen && <tunnels.WelcomeScreenMenuHintTunnel.Out />}
</div> </div>
); );
@ -250,7 +238,7 @@ const LayerUI = ({
{(heading: React.ReactNode) => ( {(heading: React.ReactNode) => (
<div style={{ position: "relative" }}> <div style={{ position: "relative" }}>
{renderWelcomeScreen && ( {renderWelcomeScreen && (
<tunnels.welcomeScreenToolbarHintTunnel.Out /> <tunnels.WelcomeScreenToolbarHintTunnel.Out />
)} )}
<Stack.Col gap={4} align="start"> <Stack.Col gap={4} align="start">
<Stack.Row <Stack.Row
@ -324,9 +312,12 @@ const LayerUI = ({
> >
<UserList collaborators={appState.collaborators} /> <UserList collaborators={appState.collaborators} />
{renderTopRightUI?.(device.isMobile, appState)} {renderTopRightUI?.(device.isMobile, appState)}
{!appState.viewModeEnabled && ( {!appState.viewModeEnabled &&
<LibraryButton appState={appState} setAppState={setAppState} /> // hide button when sidebar docked
)} (!isSidebarDocked ||
appState.openSidebar?.name !== DEFAULT_SIDEBAR.name) && (
<tunnels.DefaultSidebarTriggerTunnel.Out />
)}
</div> </div>
</div> </div>
</FixedSideContainer> </FixedSideContainer>
@ -334,21 +325,21 @@ const LayerUI = ({
}; };
const renderSidebars = () => { const renderSidebars = () => {
return appState.openSidebar === "customSidebar" ? ( return (
renderCustomSidebar?.() || null <DefaultSidebar
) : appState.openSidebar === "library" ? ( __fallback
<LibraryMenu onDock={(docked) => {
appState={appState} trackEvent(
onInsertElements={onInsertElements} "sidebar",
libraryReturnUrl={libraryReturnUrl} `toggleDock (${docked ? "dock" : "undock"})`,
focusContainer={focusContainer} `(${device.isMobile ? "mobile" : "desktop"})`,
library={library} );
id={id} }}
/> />
) : null; );
}; };
const [hostSidebarCounters] = useAtom(hostSidebarCountersAtom, jotaiScope); const isSidebarDocked = useAtomValue(isSidebarDockedAtom, jotaiScope);
const layerUIJSX = ( const layerUIJSX = (
<> <>
@ -358,8 +349,25 @@ const LayerUI = ({
{children} {children}
{/* render component fallbacks. Can be rendered anywhere as they'll be {/* render component fallbacks. Can be rendered anywhere as they'll be
tunneled away. We only render tunneled components that actually tunneled away. We only render tunneled components that actually
have defaults when host do not render anything. */} have defaults when host do not render anything. */}
<DefaultMainMenu UIOptions={UIOptions} /> <DefaultMainMenu UIOptions={UIOptions} />
<DefaultSidebar.Trigger
__fallback
icon={LibraryIcon}
title={capitalizeString(t("toolBar.library"))}
onToggle={(open) => {
if (open) {
trackEvent(
"sidebar",
`${DEFAULT_SIDEBAR.name} (open)`,
`button (${device.isMobile ? "mobile" : "desktop"})`,
);
}
}}
tab={DEFAULT_SIDEBAR.defaultTab}
>
{t("toolBar.library")}
</DefaultSidebar.Trigger>
{/* ------------------------------------------------------------------ */} {/* ------------------------------------------------------------------ */}
{appState.isLoading && <LoadingMessage delay={250} />} {appState.isLoading && <LoadingMessage delay={250} />}
@ -382,7 +390,6 @@ const LayerUI = ({
<PasteChartDialog <PasteChartDialog
setAppState={setAppState} setAppState={setAppState}
appState={appState} appState={appState}
onInsertChart={onInsertElements}
onClose={() => onClose={() =>
setAppState({ setAppState({
pasteDialog: { shown: false, data: null }, pasteDialog: { shown: false, data: null },
@ -410,7 +417,6 @@ const LayerUI = ({
renderWelcomeScreen={renderWelcomeScreen} renderWelcomeScreen={renderWelcomeScreen}
/> />
)} )}
{!device.isMobile && ( {!device.isMobile && (
<> <>
<div <div
@ -422,15 +428,14 @@ const LayerUI = ({
!isTextElement(appState.editingElement)), !isTextElement(appState.editingElement)),
})} })}
style={ style={
((appState.openSidebar === "library" && appState.openSidebar &&
appState.isSidebarDocked) || isSidebarDocked &&
hostSidebarCounters.docked) &&
device.canDeviceFitSidebar device.canDeviceFitSidebar
? { width: `calc(100% - ${LIBRARY_SIDEBAR_WIDTH}px)` } ? { width: `calc(100% - ${LIBRARY_SIDEBAR_WIDTH}px)` }
: {} : {}
} }
> >
{renderWelcomeScreen && <tunnels.welcomeScreenCenterTunnel.Out />} {renderWelcomeScreen && <tunnels.WelcomeScreenCenterTunnel.Out />}
{renderFixedSideContainer()} {renderFixedSideContainer()}
<Footer <Footer
appState={appState} appState={appState}
@ -469,17 +474,22 @@ const LayerUI = ({
); );
return ( return (
<Provider scope={tunnels.jotaiScope}> <UIAppStateContext.Provider value={appState}>
<TunnelsContext.Provider value={tunnels}> <Provider scope={tunnels.jotaiScope}>
{layerUIJSX} <TunnelsContext.Provider value={tunnels}>
</TunnelsContext.Provider> {layerUIJSX}
</Provider> </TunnelsContext.Provider>
</Provider>
</UIAppStateContext.Provider>
); );
}; };
const stripIrrelevantAppStateProps = ( const stripIrrelevantAppStateProps = (
appState: AppState, appState: AppState,
): Partial<AppState> => { ): Omit<
AppState,
"suggestedBindings" | "startBoundElement" | "cursorButton"
> => {
const { suggestedBindings, startBoundElement, cursorButton, ...ret } = const { suggestedBindings, startBoundElement, cursorButton, ...ret } =
appState; appState;
return ret; return ret;
@ -491,24 +501,17 @@ const areEqual = (prevProps: LayerUIProps, nextProps: LayerUIProps) => {
return false; return false;
} }
const { const { canvas: _prevCanvas, appState: prevAppState, ...prev } = prevProps;
canvas: _prevCanvas, const { canvas: _nextCanvas, appState: nextAppState, ...next } = nextProps;
// not stable, but shouldn't matter in our case
onInsertElements: _prevOnInsertElements,
appState: prevAppState,
...prev
} = prevProps;
const {
canvas: _nextCanvas,
onInsertElements: _nextOnInsertElements,
appState: nextAppState,
...next
} = nextProps;
return ( return (
isShallowEqual( isShallowEqual(
stripIrrelevantAppStateProps(prevAppState), stripIrrelevantAppStateProps(prevAppState),
stripIrrelevantAppStateProps(nextAppState), stripIrrelevantAppStateProps(nextAppState),
{
selectedElementIds: isShallowEqual,
selectedGroupIds: isShallowEqual,
},
) && isShallowEqual(prev, next) ) && isShallowEqual(prev, next)
); );
}; };

View File

@ -1,32 +0,0 @@
@import "../css/variables.module";
.library-button {
@include outlineButtonStyles;
background-color: var(--island-bg-color);
width: auto;
height: var(--lg-button-size);
display: flex;
align-items: center;
gap: 0.5rem;
line-height: 0;
font-size: 0.75rem;
letter-spacing: 0.4px;
svg {
width: var(--lg-icon-size);
height: var(--lg-icon-size);
}
&__label {
display: none;
@media screen and (min-width: 1024px) {
display: block;
}
}
}

View File

@ -1,57 +0,0 @@
import React from "react";
import { t } from "../i18n";
import { AppState } from "../types";
import { capitalizeString } from "../utils";
import { trackEvent } from "../analytics";
import { useDevice } from "./App";
import "./LibraryButton.scss";
import { LibraryIcon } from "./icons";
export const LibraryButton: React.FC<{
appState: AppState;
setAppState: React.Component<any, AppState>["setState"];
isMobile?: boolean;
}> = ({ appState, setAppState, isMobile }) => {
const device = useDevice();
const showLabel = !isMobile;
// TODO barnabasmolnar/redesign
// not great, toolbar jumps in a jarring manner
if (appState.isSidebarDocked && appState.openSidebar === "library") {
return null;
}
return (
<label title={`${capitalizeString(t("toolBar.library"))}`}>
<input
className="ToolIcon_type_checkbox"
type="checkbox"
name="editor-library"
onChange={(event) => {
document
.querySelector(".layer-ui__wrapper")
?.classList.remove("animate");
const isOpen = event.target.checked;
setAppState({ openSidebar: isOpen ? "library" : null });
// track only openings
if (isOpen) {
trackEvent(
"library",
"toggleLibrary (open)",
`toolbar (${device.isMobile ? "mobile" : "desktop"})`,
);
}
}}
checked={appState.openSidebar === "library"}
aria-label={capitalizeString(t("toolBar.library"))}
aria-keyshortcuts="0"
/>
<div className="library-button">
<div>{LibraryIcon}</div>
{showLabel && (
<div className="library-button__label">{t("toolBar.library")}</div>
)}
</div>
</label>
);
};

View File

@ -1,9 +1,9 @@
@import "open-color/open-color"; @import "open-color/open-color";
.excalidraw { .excalidraw {
.layer-ui__library-sidebar { .library-menu-items-container {
display: flex; height: 100%;
flex-direction: column; width: 100%;
} }
.layer-ui__library { .layer-ui__library {
@ -11,28 +11,6 @@
flex-direction: column; flex-direction: column;
flex: 1 1 auto; flex: 1 1 auto;
.layer-ui__library-header {
display: flex;
align-items: center;
width: 100%;
margin: 2px 0 15px 0;
.Spinner {
margin-right: 1rem;
}
button {
// 2px from the left to account for focus border of left-most button
margin: 0 2px;
}
}
}
.layer-ui__sidebar {
.library-menu-items-container {
height: 100%;
width: 100%;
}
} }
.library-actions-counter { .library-actions-counter {
@ -87,10 +65,17 @@
} }
} }
.library-menu-browse-button { .library-menu-control-buttons {
margin: 1rem auto; display: flex;
align-items: center;
justify-content: center;
gap: 0.625rem;
}
padding: 0.875rem 1rem; .library-menu-browse-button {
flex: 1;
height: var(--lg-button-size);
display: flex; display: flex;
align-items: center; align-items: center;
@ -122,30 +107,19 @@
} }
} }
.library-menu-browse-button--mobile { &.excalidraw--mobile .library-menu-browse-button {
min-height: 22px; height: var(--default-button-size);
margin-left: auto;
a {
padding-right: 0;
}
} }
.layer-ui__sidebar__header .dropdown-menu { .layer-ui__library .dropdown-menu {
&.dropdown-menu--mobile { width: auto;
top: 100%; top: initial;
} right: 0;
left: initial;
bottom: 100%;
margin-bottom: 0.625rem;
.dropdown-menu-container { .dropdown-menu-container {
--gap: 0;
z-index: 1;
position: absolute;
top: 100%;
left: 0;
:root[dir="rtl"] & {
right: 0;
left: auto;
}
width: 196px; width: 196px;
box-shadow: var(--library-dropdown-shadow); box-shadow: var(--library-dropdown-shadow);
border-radius: var(--border-radius-lg); border-radius: var(--border-radius-lg);

View File

@ -1,11 +1,4 @@
import { import React, { useState, useCallback } from "react";
useRef,
useState,
useEffect,
useCallback,
RefObject,
forwardRef,
} from "react";
import Library, { import Library, {
distributeLibraryItemsOnSquareGrid, distributeLibraryItemsOnSquareGrid,
libraryItemsAtom, libraryItemsAtom,
@ -13,65 +6,29 @@ import Library, {
import { t } from "../i18n"; import { t } from "../i18n";
import { randomId } from "../random"; import { randomId } from "../random";
import { LibraryItems, LibraryItem, AppState, ExcalidrawProps } from "../types"; import { LibraryItems, LibraryItem, AppState, ExcalidrawProps } from "../types";
import "./LibraryMenu.scss";
import LibraryMenuItems from "./LibraryMenuItems"; import LibraryMenuItems from "./LibraryMenuItems";
import { EVENT } from "../constants";
import { KEYS } from "../keys";
import { trackEvent } from "../analytics"; import { trackEvent } from "../analytics";
import { useAtom } from "jotai"; import { atom, useAtom } from "jotai";
import { jotaiScope } from "../jotai"; import { jotaiScope } from "../jotai";
import Spinner from "./Spinner"; import Spinner from "./Spinner";
import { import {
useDevice, useApp,
useAppProps,
useExcalidrawElements, useExcalidrawElements,
useExcalidrawSetAppState, useExcalidrawSetAppState,
} from "./App"; } from "./App";
import { Sidebar } from "./Sidebar/Sidebar";
import { getSelectedElements } from "../scene"; import { getSelectedElements } from "../scene";
import { NonDeletedExcalidrawElement } from "../element/types"; import { useUIAppState } from "../context/ui-appState";
import { LibraryMenuHeader } from "./LibraryMenuHeaderContent";
import LibraryMenuBrowseButton from "./LibraryMenuBrowseButton";
const useOnClickOutside = ( import "./LibraryMenu.scss";
ref: RefObject<HTMLElement>, import { LibraryMenuControlButtons } from "./LibraryMenuControlButtons";
cb: (event: MouseEvent) => void,
) => {
useEffect(() => {
const listener = (event: MouseEvent) => {
if (!ref.current) {
return;
}
if ( export const isLibraryMenuOpenAtom = atom(false);
event.target instanceof Element &&
(ref.current.contains(event.target) ||
!document.body.contains(event.target))
) {
return;
}
cb(event); const LibraryMenuWrapper = ({ children }: { children: React.ReactNode }) => {
}; return <div className="layer-ui__library">{children}</div>;
document.addEventListener("pointerdown", listener, false);
return () => {
document.removeEventListener("pointerdown", listener);
};
}, [ref, cb]);
}; };
const LibraryMenuWrapper = forwardRef<
HTMLDivElement,
{ children: React.ReactNode }
>(({ children }, ref) => {
return (
<div ref={ref} className="layer-ui__library">
{children}
</div>
);
});
export const LibraryMenuContent = ({ export const LibraryMenuContent = ({
onInsertLibraryItems, onInsertLibraryItems,
pendingElements, pendingElements,
@ -158,81 +115,31 @@ export const LibraryMenuContent = ({
theme={appState.theme} theme={appState.theme}
/> />
{showBtn && ( {showBtn && (
<LibraryMenuBrowseButton <LibraryMenuControlButtons
style={{ padding: "16px 12px 0 12px" }}
id={id} id={id}
libraryReturnUrl={libraryReturnUrl} libraryReturnUrl={libraryReturnUrl}
theme={appState.theme} theme={appState.theme}
selectedItems={selectedItems}
onSelectItems={onSelectItems}
/> />
)} )}
</LibraryMenuWrapper> </LibraryMenuWrapper>
); );
}; };
export const LibraryMenu: React.FC<{ /**
appState: AppState; * This component is meant to be rendered inside <Sidebar.Tab/> inside our
onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void; * <DefaultSidebar/> or host apps Sidebar components.
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"]; */
focusContainer: () => void; export const LibraryMenu = () => {
library: Library; const { library, id, onInsertElements } = useApp();
id: string; const appProps = useAppProps();
}> = ({ const appState = useUIAppState();
appState,
onInsertElements,
libraryReturnUrl,
focusContainer,
library,
id,
}) => {
const setAppState = useExcalidrawSetAppState(); const setAppState = useExcalidrawSetAppState();
const elements = useExcalidrawElements(); const elements = useExcalidrawElements();
const device = useDevice();
const [selectedItems, setSelectedItems] = useState<LibraryItem["id"][]>([]); const [selectedItems, setSelectedItems] = useState<LibraryItem["id"][]>([]);
const [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope);
const ref = useRef<HTMLDivElement | null>(null);
const closeLibrary = useCallback(() => {
const isDialogOpen = !!document.querySelector(".Dialog");
// Prevent closing if any dialog is open
if (isDialogOpen) {
return;
}
setAppState({ openSidebar: null });
}, [setAppState]);
useOnClickOutside(
ref,
useCallback(
(event) => {
// If click on the library icon, do nothing so that LibraryButton
// can toggle library menu
if ((event.target as Element).closest(".ToolIcon__library")) {
return;
}
if (!appState.isSidebarDocked || !device.canDeviceFitSidebar) {
closeLibrary();
}
},
[closeLibrary, appState.isSidebarDocked, device.canDeviceFitSidebar],
),
);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (
event.key === KEYS.ESCAPE &&
(!appState.isSidebarDocked || !device.canDeviceFitSidebar)
) {
closeLibrary();
}
};
document.addEventListener(EVENT.KEYDOWN, handleKeyDown);
return () => {
document.removeEventListener(EVENT.KEYDOWN, handleKeyDown);
};
}, [closeLibrary, appState.isSidebarDocked, device.canDeviceFitSidebar]);
const deselectItems = useCallback(() => { const deselectItems = useCallback(() => {
setAppState({ setAppState({
@ -241,69 +148,20 @@ export const LibraryMenu: React.FC<{
}); });
}, [setAppState]); }, [setAppState]);
const removeFromLibrary = useCallback(
async (libraryItems: LibraryItems) => {
const nextItems = libraryItems.filter(
(item) => !selectedItems.includes(item.id),
);
library.setLibrary(nextItems).catch(() => {
setAppState({ errorMessage: t("alerts.errorRemovingFromLibrary") });
});
setSelectedItems([]);
},
[library, setAppState, selectedItems, setSelectedItems],
);
const resetLibrary = useCallback(() => {
library.resetLibrary();
focusContainer();
}, [library, focusContainer]);
return ( return (
<Sidebar <LibraryMenuContent
__isInternal pendingElements={getSelectedElements(elements, appState, true)}
// necessary to remount when switching between internal onInsertLibraryItems={(libraryItems) => {
// and custom (host app) sidebar, so that the `props.onClose` onInsertElements(distributeLibraryItemsOnSquareGrid(libraryItems));
// is colled correctly
key="library"
className="layer-ui__library-sidebar"
initialDockedState={appState.isSidebarDocked}
onDock={(docked) => {
trackEvent(
"library",
`toggleLibraryDock (${docked ? "dock" : "undock"})`,
`sidebar (${device.isMobile ? "mobile" : "desktop"})`,
);
}} }}
ref={ref} onAddToLibrary={deselectItems}
> setAppState={setAppState}
<Sidebar.Header className="layer-ui__library-header"> libraryReturnUrl={appProps.libraryReturnUrl}
<LibraryMenuHeader library={library}
appState={appState} id={id}
setAppState={setAppState} appState={appState}
selectedItems={selectedItems} selectedItems={selectedItems}
onSelectItems={setSelectedItems} onSelectItems={setSelectedItems}
library={library} />
onRemoveFromLibrary={() =>
removeFromLibrary(libraryItemsData.libraryItems)
}
resetLibrary={resetLibrary}
/>
</Sidebar.Header>
<LibraryMenuContent
pendingElements={getSelectedElements(elements, appState, true)}
onInsertLibraryItems={(libraryItems) => {
onInsertElements(distributeLibraryItemsOnSquareGrid(libraryItems));
}}
onAddToLibrary={deselectItems}
setAppState={setAppState}
libraryReturnUrl={libraryReturnUrl}
library={library}
id={id}
appState={appState}
selectedItems={selectedItems}
onSelectItems={setSelectedItems}
/>
</Sidebar>
); );
}; };

View File

@ -0,0 +1,33 @@
import { LibraryItem, ExcalidrawProps, AppState } from "../types";
import LibraryMenuBrowseButton from "./LibraryMenuBrowseButton";
import { LibraryDropdownMenu } from "./LibraryMenuHeaderContent";
export const LibraryMenuControlButtons = ({
selectedItems,
onSelectItems,
libraryReturnUrl,
theme,
id,
style,
}: {
selectedItems: LibraryItem["id"][];
onSelectItems: (id: LibraryItem["id"][]) => void;
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
theme: AppState["theme"];
id: string;
style: React.CSSProperties;
}) => {
return (
<div className="library-menu-control-buttons" style={style}>
<LibraryMenuBrowseButton
id={id}
libraryReturnUrl={libraryReturnUrl}
theme={theme}
/>
<LibraryDropdownMenu
selectedItems={selectedItems}
onSelectItems={onSelectItems}
/>
</div>
);
};

View File

@ -1,8 +1,10 @@
import React, { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { t } from "../i18n";
import { jotaiScope } from "../jotai";
import { AppState, LibraryItem, LibraryItems } from "../types";
import { useApp, useExcalidrawAppState, useExcalidrawSetAppState } from "./App";
import { saveLibraryAsJSON } from "../data/json"; import { saveLibraryAsJSON } from "../data/json";
import Library, { libraryItemsAtom } from "../data/library"; import Library, { libraryItemsAtom } from "../data/library";
import { t } from "../i18n";
import { AppState, LibraryItem, LibraryItems } from "../types";
import { import {
DotsIcon, DotsIcon,
ExportIcon, ExportIcon,
@ -13,22 +15,19 @@ import {
import { ToolButton } from "./ToolButton"; import { ToolButton } from "./ToolButton";
import { fileOpen } from "../data/filesystem"; import { fileOpen } from "../data/filesystem";
import { muteFSAbortError } from "../utils"; import { muteFSAbortError } from "../utils";
import { atom, useAtom } from "jotai"; import { useAtom } from "jotai";
import { jotaiScope } from "../jotai";
import ConfirmDialog from "./ConfirmDialog"; import ConfirmDialog from "./ConfirmDialog";
import PublishLibrary from "./PublishLibrary"; import PublishLibrary from "./PublishLibrary";
import { Dialog } from "./Dialog"; import { Dialog } from "./Dialog";
import DropdownMenu from "./dropdownMenu/DropdownMenu"; import DropdownMenu from "./dropdownMenu/DropdownMenu";
import { isLibraryMenuOpenAtom } from "./LibraryMenu";
export const isLibraryMenuOpenAtom = atom(false);
const getSelectedItems = ( const getSelectedItems = (
libraryItems: LibraryItems, libraryItems: LibraryItems,
selectedItems: LibraryItem["id"][], selectedItems: LibraryItem["id"][],
) => libraryItems.filter((item) => selectedItems.includes(item.id)); ) => libraryItems.filter((item) => selectedItems.includes(item.id));
export const LibraryMenuHeader: React.FC<{ export const LibraryDropdownMenuButton: React.FC<{
setAppState: React.Component<any, AppState>["setState"]; setAppState: React.Component<any, AppState>["setState"];
selectedItems: LibraryItem["id"][]; selectedItems: LibraryItem["id"][];
library: Library; library: Library;
@ -50,6 +49,7 @@ export const LibraryMenuHeader: React.FC<{
isLibraryMenuOpenAtom, isLibraryMenuOpenAtom,
jotaiScope, jotaiScope,
); );
const renderRemoveLibAlert = useCallback(() => { const renderRemoveLibAlert = useCallback(() => {
const content = selectedItems.length const content = selectedItems.length
? t("alerts.removeItemsFromsLibrary", { count: selectedItems.length }) ? t("alerts.removeItemsFromsLibrary", { count: selectedItems.length })
@ -181,7 +181,6 @@ export const LibraryMenuHeader: React.FC<{
return ( return (
<DropdownMenu open={isLibraryMenuOpen}> <DropdownMenu open={isLibraryMenuOpen}>
<DropdownMenu.Trigger <DropdownMenu.Trigger
className="Sidebar__dropdown-btn"
onToggle={() => setIsLibraryMenuOpen(!isLibraryMenuOpen)} onToggle={() => setIsLibraryMenuOpen(!isLibraryMenuOpen)}
> >
{DotsIcon} {DotsIcon}
@ -230,6 +229,7 @@ export const LibraryMenuHeader: React.FC<{
</DropdownMenu> </DropdownMenu>
); );
}; };
return ( return (
<div style={{ position: "relative" }}> <div style={{ position: "relative" }}>
{renderLibraryMenu()} {renderLibraryMenu()}
@ -261,3 +261,48 @@ export const LibraryMenuHeader: React.FC<{
</div> </div>
); );
}; };
export const LibraryDropdownMenu = ({
selectedItems,
onSelectItems,
}: {
selectedItems: LibraryItem["id"][];
onSelectItems: (id: LibraryItem["id"][]) => void;
}) => {
const { library } = useApp();
const appState = useExcalidrawAppState();
const setAppState = useExcalidrawSetAppState();
const [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope);
const removeFromLibrary = useCallback(
async (libraryItems: LibraryItems) => {
const nextItems = libraryItems.filter(
(item) => !selectedItems.includes(item.id),
);
library.setLibrary(nextItems).catch(() => {
setAppState({ errorMessage: t("alerts.errorRemovingFromLibrary") });
});
onSelectItems([]);
},
[library, setAppState, selectedItems, onSelectItems],
);
const resetLibrary = useCallback(() => {
library.resetLibrary();
}, [library]);
return (
<LibraryDropdownMenuButton
appState={appState}
setAppState={setAppState}
selectedItems={selectedItems}
onSelectItems={onSelectItems}
library={library}
onRemoveFromLibrary={() =>
removeFromLibrary(libraryItemsData.libraryItems)
}
resetLibrary={resetLibrary}
/>
);
};

View File

@ -47,7 +47,7 @@
&__items { &__items {
row-gap: 0.5rem; row-gap: 0.5rem;
padding: var(--container-padding-y) var(--container-padding-x); padding: var(--container-padding-y) 0;
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
@ -61,7 +61,7 @@
margin-bottom: 0.75rem; margin-bottom: 0.75rem;
&--excal { &--excal {
margin-top: 2.5rem; margin-top: 2rem;
} }
} }

View File

@ -10,9 +10,8 @@ import Stack from "./Stack";
import "./LibraryMenuItems.scss"; import "./LibraryMenuItems.scss";
import { MIME_TYPES } from "../constants"; import { MIME_TYPES } from "../constants";
import Spinner from "./Spinner"; import Spinner from "./Spinner";
import LibraryMenuBrowseButton from "./LibraryMenuBrowseButton";
import clsx from "clsx";
import { duplicateElements } from "../element/newElement"; import { duplicateElements } from "../element/newElement";
import { LibraryMenuControlButtons } from "./LibraryMenuControlButtons";
const CELLS_PER_ROW = 4; const CELLS_PER_ROW = 4;
@ -201,11 +200,7 @@ const LibraryMenuItems = ({
(item) => item.status === "published", (item) => item.status === "published",
); );
const showBtn = const showBtn = !libraryItems.length && !pendingElements.length;
!libraryItems.length &&
!unpublishedItems.length &&
!publishedItems.length &&
!pendingElements.length;
return ( return (
<div <div
@ -215,7 +210,7 @@ const LibraryMenuItems = ({
unpublishedItems.length || unpublishedItems.length ||
publishedItems.length publishedItems.length
? { justifyContent: "flex-start" } ? { justifyContent: "flex-start" }
: {} : { borderBottom: 0 }
} }
> >
<Stack.Col <Stack.Col
@ -251,11 +246,7 @@ const LibraryMenuItems = ({
</div> </div>
{!pendingElements.length && !unpublishedItems.length ? ( {!pendingElements.length && !unpublishedItems.length ? (
<div className="library-menu-items__no-items"> <div className="library-menu-items__no-items">
<div <div className="library-menu-items__no-items__label">
className={clsx({
"library-menu-items__no-items__label": showBtn,
})}
>
{t("library.noItems")} {t("library.noItems")}
</div> </div>
<div className="library-menu-items__no-items__hint"> <div className="library-menu-items__no-items__hint">
@ -303,10 +294,13 @@ const LibraryMenuItems = ({
</> </>
{showBtn && ( {showBtn && (
<LibraryMenuBrowseButton <LibraryMenuControlButtons
style={{ padding: "16px 0", width: "100%" }}
id={id} id={id}
libraryReturnUrl={libraryReturnUrl} libraryReturnUrl={libraryReturnUrl}
theme={theme} theme={theme}
selectedItems={selectedItems}
onSelectItems={onSelectItems}
/> />
)} )}
</Stack.Col> </Stack.Col>

View File

@ -13,13 +13,12 @@ import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
import { Section } from "./Section"; import { Section } from "./Section";
import { SCROLLBAR_WIDTH, SCROLLBAR_MARGIN } from "../scene/scrollbars"; import { SCROLLBAR_WIDTH, SCROLLBAR_MARGIN } from "../scene/scrollbars";
import { LockButton } from "./LockButton"; import { LockButton } from "./LockButton";
import { LibraryButton } from "./LibraryButton";
import { PenModeButton } from "./PenModeButton"; import { PenModeButton } from "./PenModeButton";
import { Stats } from "./Stats"; import { Stats } from "./Stats";
import { actionToggleStats } from "../actions"; import { actionToggleStats } from "../actions";
import { HandButton } from "./HandButton"; import { HandButton } from "./HandButton";
import { isHandToolActive } from "../appState"; import { isHandToolActive } from "../appState";
import { useTunnels } from "./context/tunnels"; import { useTunnels } from "../context/tunnels";
type MobileMenuProps = { type MobileMenuProps = {
appState: AppState; appState: AppState;
@ -60,11 +59,15 @@ export const MobileMenu = ({
device, device,
renderWelcomeScreen, renderWelcomeScreen,
}: MobileMenuProps) => { }: MobileMenuProps) => {
const { welcomeScreenCenterTunnel, mainMenuTunnel } = useTunnels(); const {
WelcomeScreenCenterTunnel,
MainMenuTunnel,
DefaultSidebarTriggerTunnel,
} = useTunnels();
const renderToolbar = () => { const renderToolbar = () => {
return ( return (
<FixedSideContainer side="top" className="App-top-bar"> <FixedSideContainer side="top" className="App-top-bar">
{renderWelcomeScreen && <welcomeScreenCenterTunnel.Out />} {renderWelcomeScreen && <WelcomeScreenCenterTunnel.Out />}
<Section heading="shapes"> <Section heading="shapes">
{(heading: React.ReactNode) => ( {(heading: React.ReactNode) => (
<Stack.Col gap={4} align="center"> <Stack.Col gap={4} align="center">
@ -88,11 +91,7 @@ export const MobileMenu = ({
{renderTopRightUI && renderTopRightUI(true, appState)} {renderTopRightUI && renderTopRightUI(true, appState)}
<div className="mobile-misc-tools-container"> <div className="mobile-misc-tools-container">
{!appState.viewModeEnabled && ( {!appState.viewModeEnabled && (
<LibraryButton <DefaultSidebarTriggerTunnel.Out />
appState={appState}
setAppState={setAppState}
isMobile
/>
)} )}
<PenModeButton <PenModeButton
checked={appState.penMode} checked={appState.penMode}
@ -132,14 +131,14 @@ export const MobileMenu = ({
if (appState.viewModeEnabled) { if (appState.viewModeEnabled) {
return ( return (
<div className="App-toolbar-content"> <div className="App-toolbar-content">
<mainMenuTunnel.Out /> <MainMenuTunnel.Out />
</div> </div>
); );
} }
return ( return (
<div className="App-toolbar-content"> <div className="App-toolbar-content">
<mainMenuTunnel.Out /> <MainMenuTunnel.Out />
{actionManager.renderAction("toggleEditMenu")} {actionManager.renderAction("toggleEditMenu")}
{actionManager.renderAction("undo")} {actionManager.renderAction("undo")}
{actionManager.renderAction("redo")} {actionManager.renderAction("redo")}
@ -190,7 +189,7 @@ export const MobileMenu = ({
{renderAppToolbar()} {renderAppToolbar()}
{appState.scrolledOutside && {appState.scrolledOutside &&
!appState.openMenu && !appState.openMenu &&
appState.openSidebar !== "library" && ( !appState.openSidebar && (
<button <button
className="scroll-back-to-content" className="scroll-back-to-content"
onClick={() => { onClick={() => {

View File

@ -5,7 +5,8 @@ import { ChartElements, renderSpreadsheet, Spreadsheet } from "../charts";
import { ChartType } from "../element/types"; import { ChartType } from "../element/types";
import { t } from "../i18n"; import { t } from "../i18n";
import { exportToSvg } from "../scene/export"; import { exportToSvg } from "../scene/export";
import { AppState, LibraryItem } from "../types"; import { AppState } from "../types";
import { useApp } from "./App";
import { Dialog } from "./Dialog"; import { Dialog } from "./Dialog";
import "./PasteChartDialog.scss"; import "./PasteChartDialog.scss";
@ -78,13 +79,12 @@ export const PasteChartDialog = ({
setAppState, setAppState,
appState, appState,
onClose, onClose,
onInsertChart,
}: { }: {
appState: AppState; appState: AppState;
onClose: () => void; onClose: () => void;
setAppState: React.Component<any, AppState>["setState"]; setAppState: React.Component<any, AppState>["setState"];
onInsertChart: (elements: LibraryItem["elements"]) => void;
}) => { }) => {
const { onInsertElements } = useApp();
const handleClose = React.useCallback(() => { const handleClose = React.useCallback(() => {
if (onClose) { if (onClose) {
onClose(); onClose();
@ -92,7 +92,7 @@ export const PasteChartDialog = ({
}, [onClose]); }, [onClose]);
const handleChartClick = (chartType: ChartType, elements: ChartElements) => { const handleChartClick = (chartType: ChartType, elements: ChartElements) => {
onInsertChart(elements); onInsertElements(elements);
trackEvent("magic", "chart", chartType); trackEvent("magic", "chart", chartType);
setAppState({ setAppState({
currentChartType: chartType, currentChartType: chartType,

View File

@ -2,67 +2,26 @@
@import "../../css/variables.module"; @import "../../css/variables.module";
.excalidraw { .excalidraw {
.Sidebar { .sidebar {
&__close-btn, display: flex;
&__pin-btn, flex-direction: column;
&__dropdown-btn {
@include outlineButtonStyles;
width: var(--lg-button-size);
height: var(--lg-button-size);
padding: 0;
svg {
width: var(--lg-icon-size);
height: var(--lg-icon-size);
}
}
&__pin-btn {
&--pinned {
background-color: var(--color-primary);
border-color: var(--color-primary);
svg {
color: #fff;
}
&:hover,
&:active {
background-color: var(--color-primary-darker);
}
}
}
}
&.theme--dark {
.Sidebar {
&__pin-btn {
&--pinned {
svg {
color: var(--color-gray-90);
}
}
}
}
}
.layer-ui__sidebar {
position: absolute; position: absolute;
top: 0; top: 0;
bottom: 0; bottom: 0;
right: 0; right: 0;
z-index: 5; z-index: 5;
margin: 0; margin: 0;
padding: 0;
box-sizing: border-box;
background-color: var(--sidebar-bg-color);
box-shadow: var(--sidebar-shadow);
:root[dir="rtl"] & { :root[dir="rtl"] & {
left: 0; left: 0;
right: auto; right: auto;
} }
background-color: var(--sidebar-bg-color);
box-shadow: var(--sidebar-shadow);
&--docked { &--docked {
box-shadow: none; box-shadow: none;
} }
@ -77,52 +36,134 @@
border-right: 1px solid var(--sidebar-border-color); border-right: 1px solid var(--sidebar-border-color);
border-left: 0; border-left: 0;
} }
padding: 0;
box-sizing: border-box;
.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;
}
} }
.layer-ui__sidebar__header { // ---------------------------- sidebar header ------------------------------
.sidebar__header {
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
width: 100%; width: 100%;
padding: 1rem; padding-top: 1rem;
border-bottom: 1px solid var(--sidebar-border-color); padding-bottom: 1rem;
} }
.layer-ui__sidebar__header__buttons { .sidebar__header__buttons {
gap: 0;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.625rem; margin-left: auto;
button {
@include outlineButtonStyles;
--button-bg: transparent;
border: 0 !important;
width: var(--lg-button-size);
height: var(--lg-button-size);
padding: 0;
svg {
width: var(--lg-icon-size);
height: var(--lg-icon-size);
}
&:hover {
background: var(--button-hover-bg, var(--island-bg-color));
}
}
.sidebar__dock.selected {
svg {
stroke: var(--color-primary);
fill: var(--color-primary);
}
}
}
// ---------------------------- sidebar tabs ------------------------------
.sidebar-tabs-root {
display: flex;
flex-direction: column;
flex: 1 1 auto;
padding: 1rem 0.75rem;
[role="tabpanel"] {
flex: 1;
outline: none;
flex: 1 1 auto;
display: flex;
flex-direction: column;
outline: none;
}
[role="tabpanel"][data-state="inactive"] {
display: none !important;
}
[role="tablist"] {
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
}
}
.sidebar-tabs-root > .sidebar__header {
padding-top: 0;
padding-bottom: 1rem;
}
.sidebar-tab-trigger {
--button-width: auto;
--button-bg: transparent;
--button-hover-bg: transparent;
--button-active-bg: var(--color-primary);
--button-hover-color: var(--color-primary);
--button-hover-border: var(--color-primary);
&[data-state="active"] {
--button-bg: var(--color-primary);
--button-hover-bg: var(--color-primary-darker);
--button-hover-color: var(--color-icon-white);
--button-border: var(--color-primary);
color: var(--color-icon-white);
}
}
// ---------------------------- default sidebar ------------------------------
.default-sidebar {
display: flex;
flex-direction: column;
.sidebar-triggers {
$padding: 2px;
$border: 1px;
display: flex;
gap: 0;
padding: $padding;
// offset by padding + border to vertically center the list with sibling
// buttons (both from top and bototm, due to flex layout)
margin-top: -#{$padding + $border};
margin-bottom: -#{$padding + $border};
border: $border solid var(--sidebar-border-color);
background: var(--default-bg-color);
border-radius: 0.625rem;
.sidebar-tab-trigger {
height: var(--lg-button-size);
width: var(--lg-button-size);
border: none;
}
}
.sidebar__header {
border-bottom: 1px solid var(--sidebar-border-color);
}
} }
} }

View File

@ -1,8 +1,9 @@
import React from "react"; import React from "react";
import { DEFAULT_SIDEBAR } from "../../constants";
import { Excalidraw, Sidebar } from "../../packages/excalidraw/index"; import { Excalidraw, Sidebar } from "../../packages/excalidraw/index";
import { import {
act,
fireEvent, fireEvent,
GlobalTestState,
queryAllByTestId, queryAllByTestId,
queryByTestId, queryByTestId,
render, render,
@ -10,346 +11,321 @@ import {
withExcalidrawDimensions, withExcalidrawDimensions,
} from "../../tests/test-utils"; } from "../../tests/test-utils";
export const assertSidebarDockButton = async <T extends boolean>(
hasDockButton: T,
): Promise<
T extends false
? { dockButton: null; sidebar: HTMLElement }
: { dockButton: HTMLElement; sidebar: HTMLElement }
> => {
const sidebar =
GlobalTestState.renderResult.container.querySelector<HTMLElement>(
".sidebar",
);
expect(sidebar).not.toBe(null);
const dockButton = queryByTestId(sidebar!, "sidebar-dock");
if (hasDockButton) {
expect(dockButton).not.toBe(null);
return { dockButton: dockButton!, sidebar: sidebar! } as any;
}
expect(dockButton).toBe(null);
return { dockButton: null, sidebar: sidebar! } as any;
};
export const assertExcalidrawWithSidebar = async (
sidebar: React.ReactNode,
name: string,
test: () => void,
) => {
await render(
<Excalidraw initialData={{ appState: { openSidebar: { name } } }}>
{sidebar}
</Excalidraw>,
);
await withExcalidrawDimensions({ width: 1920, height: 1080 }, test);
};
describe("Sidebar", () => { describe("Sidebar", () => {
it("should render custom sidebar", async () => { describe("General behavior", () => {
const { container } = await render( it("should render custom sidebar", async () => {
<Excalidraw const { container } = await render(
initialData={{ appState: { openSidebar: "customSidebar" } }} <Excalidraw
renderSidebar={() => ( initialData={{ appState: { openSidebar: { name: "customSidebar" } } }}
<Sidebar> >
<Sidebar name="customSidebar">
<div id="test-sidebar-content">42</div> <div id="test-sidebar-content">42</div>
</Sidebar> </Sidebar>
)} </Excalidraw>,
/>, );
);
const node = container.querySelector("#test-sidebar-content"); const node = container.querySelector("#test-sidebar-content");
expect(node).not.toBe(null); expect(node).not.toBe(null);
});
it("should render only one sidebar and prefer the custom one", async () => {
const { container } = await render(
<Excalidraw
initialData={{ appState: { openSidebar: { name: "customSidebar" } } }}
>
<Sidebar name="customSidebar">
<div id="test-sidebar-content">42</div>
</Sidebar>
</Excalidraw>,
);
await waitFor(() => {
// make sure the custom sidebar is rendered
const node = container.querySelector("#test-sidebar-content");
expect(node).not.toBe(null);
// make sure only one sidebar is rendered
const sidebars = container.querySelectorAll(".sidebar");
expect(sidebars.length).toBe(1);
});
});
it("should toggle sidebar using props.toggleMenu()", async () => {
const { container } = await render(
<Excalidraw>
<Sidebar name="customSidebar">
<div id="test-sidebar-content">42</div>
</Sidebar>
</Excalidraw>,
);
// sidebar isn't rendered initially
// -------------------------------------------------------------------------
await waitFor(() => {
const node = container.querySelector("#test-sidebar-content");
expect(node).toBe(null);
});
// toggle sidebar on
// -------------------------------------------------------------------------
expect(window.h.app.toggleSidebar({ name: "customSidebar" })).toBe(true);
await waitFor(() => {
const node = container.querySelector("#test-sidebar-content");
expect(node).not.toBe(null);
});
// toggle sidebar off
// -------------------------------------------------------------------------
expect(window.h.app.toggleSidebar({ name: "customSidebar" })).toBe(false);
await waitFor(() => {
const node = container.querySelector("#test-sidebar-content");
expect(node).toBe(null);
});
// force-toggle sidebar off (=> still hidden)
// -------------------------------------------------------------------------
expect(
window.h.app.toggleSidebar({ name: "customSidebar", force: false }),
).toBe(false);
await waitFor(() => {
const node = container.querySelector("#test-sidebar-content");
expect(node).toBe(null);
});
// force-toggle sidebar on
// -------------------------------------------------------------------------
expect(
window.h.app.toggleSidebar({ name: "customSidebar", force: true }),
).toBe(true);
expect(
window.h.app.toggleSidebar({ name: "customSidebar", force: true }),
).toBe(true);
await waitFor(() => {
const node = container.querySelector("#test-sidebar-content");
expect(node).not.toBe(null);
});
// toggle library (= hide custom sidebar)
// -------------------------------------------------------------------------
expect(window.h.app.toggleSidebar({ name: DEFAULT_SIDEBAR.name })).toBe(
true,
);
await waitFor(() => {
const node = container.querySelector("#test-sidebar-content");
expect(node).toBe(null);
// make sure only one sidebar is rendered
const sidebars = container.querySelectorAll(".sidebar");
expect(sidebars.length).toBe(1);
});
});
}); });
it("should render custom sidebar header", async () => { describe("<Sidebar.Header/>", () => {
const { container } = await render( it("should render custom sidebar header", async () => {
<Excalidraw const { container } = await render(
initialData={{ appState: { openSidebar: "customSidebar" } }} <Excalidraw
renderSidebar={() => ( initialData={{ appState: { openSidebar: { name: "customSidebar" } } }}
<Sidebar> >
<Sidebar name="customSidebar">
<Sidebar.Header> <Sidebar.Header>
<div id="test-sidebar-header-content">42</div> <div id="test-sidebar-header-content">42</div>
</Sidebar.Header> </Sidebar.Header>
</Sidebar> </Sidebar>
)} </Excalidraw>,
/>, );
);
const node = container.querySelector("#test-sidebar-header-content"); const node = container.querySelector("#test-sidebar-header-content");
expect(node).not.toBe(null);
// make sure we don't render the default fallback header,
// just the custom one
expect(queryAllByTestId(container, "sidebar-header").length).toBe(1);
});
it("should render only one sidebar and prefer the custom one", async () => {
const { container } = await render(
<Excalidraw
initialData={{ appState: { openSidebar: "customSidebar" } }}
renderSidebar={() => (
<Sidebar>
<div id="test-sidebar-content">42</div>
</Sidebar>
)}
/>,
);
await waitFor(() => {
// make sure the custom sidebar is rendered
const node = container.querySelector("#test-sidebar-content");
expect(node).not.toBe(null); expect(node).not.toBe(null);
// make sure we don't render the default fallback header,
// make sure only one sidebar is rendered // just the custom one
const sidebars = container.querySelectorAll(".layer-ui__sidebar"); expect(queryAllByTestId(container, "sidebar-header").length).toBe(1);
expect(sidebars.length).toBe(1);
}); });
});
it("should always render custom sidebar with close button & close on click", async () => { it("should not render <Sidebar.Header> for custom sidebars by default", async () => {
const onClose = jest.fn(); const CustomExcalidraw = () => {
const CustomExcalidraw = () => { return (
return ( <Excalidraw
<Excalidraw initialData={{
initialData={{ appState: { openSidebar: "customSidebar" } }} appState: { openSidebar: { name: "customSidebar" } },
renderSidebar={() => ( }}
<Sidebar className="test-sidebar" onClose={onClose}> >
<Sidebar name="customSidebar" className="test-sidebar">
hello hello
</Sidebar> </Sidebar>
)} </Excalidraw>
/> );
); };
};
const { container } = await render(<CustomExcalidraw />); const { container } = await render(<CustomExcalidraw />);
const sidebar = container.querySelector<HTMLElement>(".test-sidebar");
expect(sidebar).not.toBe(null);
const closeButton = queryByTestId(sidebar!, "sidebar-close")!;
expect(closeButton).not.toBe(null);
fireEvent.click(closeButton);
await waitFor(() => {
expect(container.querySelector<HTMLElement>(".test-sidebar")).toBe(null);
expect(onClose).toHaveBeenCalled();
});
});
it("should render custom sidebar with dock (irrespective of onDock prop)", async () => {
const CustomExcalidraw = () => {
return (
<Excalidraw
initialData={{ appState: { openSidebar: "customSidebar" } }}
renderSidebar={() => (
<Sidebar className="test-sidebar">hello</Sidebar>
)}
/>
);
};
const { container } = await render(<CustomExcalidraw />);
// should show dock button when the sidebar fits to be docked
// -------------------------------------------------------------------------
await withExcalidrawDimensions({ width: 1920, height: 1080 }, () => {
const sidebar = container.querySelector<HTMLElement>(".test-sidebar"); const sidebar = container.querySelector<HTMLElement>(".test-sidebar");
expect(sidebar).not.toBe(null); expect(sidebar).not.toBe(null);
const closeButton = queryByTestId(sidebar!, "sidebar-dock"); const closeButton = queryByTestId(sidebar!, "sidebar-close");
expect(closeButton).not.toBe(null);
});
// should not show dock button when the sidebar does not fit to be docked
// -------------------------------------------------------------------------
await withExcalidrawDimensions({ width: 400, height: 1080 }, () => {
const sidebar = container.querySelector<HTMLElement>(".test-sidebar");
expect(sidebar).not.toBe(null);
const closeButton = queryByTestId(sidebar!, "sidebar-dock");
expect(closeButton).toBe(null); expect(closeButton).toBe(null);
}); });
});
it("should support controlled docking", async () => { it("<Sidebar.Header> should render close button", async () => {
let _setDockable: (dockable: boolean) => void = null!; const onStateChange = jest.fn();
const CustomExcalidraw = () => {
const CustomExcalidraw = () => { return (
const [dockable, setDockable] = React.useState(false); <Excalidraw
_setDockable = setDockable; initialData={{
return ( appState: { openSidebar: { name: "customSidebar" } },
<Excalidraw }}
initialData={{ appState: { openSidebar: "customSidebar" } }} >
renderSidebar={() => (
<Sidebar <Sidebar
name="customSidebar"
className="test-sidebar" className="test-sidebar"
docked={false} onStateChange={onStateChange}
dockable={dockable}
> >
hello <Sidebar.Header />
</Sidebar> </Sidebar>
)} </Excalidraw>
/> );
); };
};
const { container } = await render(<CustomExcalidraw />); const { container } = await render(<CustomExcalidraw />);
await withExcalidrawDimensions({ width: 1920, height: 1080 }, async () => { // initial open
// should not show dock button when `dockable` is `false` expect(onStateChange).toHaveBeenCalledWith({ name: "customSidebar" });
// -------------------------------------------------------------------------
act(() => { const sidebar = container.querySelector<HTMLElement>(".test-sidebar");
_setDockable(false); expect(sidebar).not.toBe(null);
}); const closeButton = queryByTestId(sidebar!, "sidebar-close")!;
expect(closeButton).not.toBe(null);
fireEvent.click(closeButton);
await waitFor(() => { await waitFor(() => {
const sidebar = container.querySelector<HTMLElement>(".test-sidebar"); expect(container.querySelector<HTMLElement>(".test-sidebar")).toBe(
expect(sidebar).not.toBe(null); null,
const closeButton = queryByTestId(sidebar!, "sidebar-dock"); );
expect(closeButton).toBe(null); expect(onStateChange).toHaveBeenCalledWith(null);
});
// should show dock button when `dockable` is `true`, even if `docked`
// prop is set
// -------------------------------------------------------------------------
act(() => {
_setDockable(true);
});
await waitFor(() => {
const sidebar = container.querySelector<HTMLElement>(".test-sidebar");
expect(sidebar).not.toBe(null);
const closeButton = queryByTestId(sidebar!, "sidebar-dock");
expect(closeButton).not.toBe(null);
}); });
}); });
}); });
it("should support controlled docking", async () => { describe("Docking behavior", () => {
let _setDocked: (docked?: boolean) => void = null!; it("shouldn't be user-dockable if `onDock` not supplied", async () => {
await assertExcalidrawWithSidebar(
<Sidebar name="customSidebar">
<Sidebar.Header />
</Sidebar>,
"customSidebar",
async () => {
await assertSidebarDockButton(false);
},
);
});
const CustomExcalidraw = () => { it("shouldn't be user-dockable if `onDock` not supplied & `docked={true}`", async () => {
const [docked, setDocked] = React.useState<boolean | undefined>(); await assertExcalidrawWithSidebar(
_setDocked = setDocked; <Sidebar name="customSidebar" docked={true}>
return ( <Sidebar.Header />
</Sidebar>,
"customSidebar",
async () => {
await assertSidebarDockButton(false);
},
);
});
it("shouldn't be user-dockable if `onDock` not supplied & docked={false}`", async () => {
await assertExcalidrawWithSidebar(
<Sidebar name="customSidebar" docked={false}>
<Sidebar.Header />
</Sidebar>,
"customSidebar",
async () => {
await assertSidebarDockButton(false);
},
);
});
it("should be user-dockable when both `onDock` and `docked` supplied", async () => {
await render(
<Excalidraw <Excalidraw
initialData={{ appState: { openSidebar: "customSidebar" } }} initialData={{ appState: { openSidebar: { name: "customSidebar" } } }}
renderSidebar={() => ( >
<Sidebar className="test-sidebar" docked={docked}> <Sidebar
hello name="customSidebar"
</Sidebar> className="test-sidebar"
)} onDock={() => {}}
/> docked
); >
}; <Sidebar.Header />
const { container } = await render(<CustomExcalidraw />);
const { h } = window;
await withExcalidrawDimensions({ width: 1920, height: 1080 }, async () => {
const dockButton = await waitFor(() => {
const sidebar = container.querySelector<HTMLElement>(".test-sidebar");
expect(sidebar).not.toBe(null);
const dockBotton = queryByTestId(sidebar!, "sidebar-dock");
expect(dockBotton).not.toBe(null);
return dockBotton!;
});
const dockButtonInput = dockButton.querySelector("input")!;
// should not show dock button when `dockable` is `false`
// -------------------------------------------------------------------------
expect(h.state.isSidebarDocked).toBe(false);
fireEvent.click(dockButtonInput);
await waitFor(() => {
expect(h.state.isSidebarDocked).toBe(true);
expect(dockButtonInput).toBeChecked();
});
fireEvent.click(dockButtonInput);
await waitFor(() => {
expect(h.state.isSidebarDocked).toBe(false);
expect(dockButtonInput).not.toBeChecked();
});
// shouldn't update `appState.isSidebarDocked` when the sidebar
// is controlled (`docked` prop is set), as host apps should handle
// the state themselves
// -------------------------------------------------------------------------
act(() => {
_setDocked(true);
});
await waitFor(() => {
expect(dockButtonInput).toBeChecked();
expect(h.state.isSidebarDocked).toBe(false);
expect(dockButtonInput).toBeChecked();
});
fireEvent.click(dockButtonInput);
await waitFor(() => {
expect(h.state.isSidebarDocked).toBe(false);
expect(dockButtonInput).toBeChecked();
});
// the `appState.isSidebarDocked` should remain untouched when
// `props.docked` is set to `false`, and user toggles
// -------------------------------------------------------------------------
act(() => {
_setDocked(false);
h.setState({ isSidebarDocked: true });
});
await waitFor(() => {
expect(h.state.isSidebarDocked).toBe(true);
expect(dockButtonInput).not.toBeChecked();
});
fireEvent.click(dockButtonInput);
await waitFor(() => {
expect(dockButtonInput).not.toBeChecked();
expect(h.state.isSidebarDocked).toBe(true);
});
});
});
it("should toggle sidebar using props.toggleMenu()", async () => {
const { container } = await render(
<Excalidraw
renderSidebar={() => (
<Sidebar>
<div id="test-sidebar-content">42</div>
</Sidebar> </Sidebar>
)} </Excalidraw>,
/>, );
);
// sidebar isn't rendered initially await withExcalidrawDimensions(
// ------------------------------------------------------------------------- { width: 1920, height: 1080 },
await waitFor(() => { async () => {
const node = container.querySelector("#test-sidebar-content"); await assertSidebarDockButton(true);
expect(node).toBe(null); },
);
}); });
// toggle sidebar on it("shouldn't be user-dockable when only `onDock` supplied w/o `docked`", async () => {
// ------------------------------------------------------------------------- await render(
expect(window.h.app.toggleMenu("customSidebar")).toBe(true); <Excalidraw
initialData={{ appState: { openSidebar: { name: "customSidebar" } } }}
>
<Sidebar
name="customSidebar"
className="test-sidebar"
onDock={() => {}}
>
<Sidebar.Header />
</Sidebar>
</Excalidraw>,
);
await waitFor(() => { await withExcalidrawDimensions(
const node = container.querySelector("#test-sidebar-content"); { width: 1920, height: 1080 },
expect(node).not.toBe(null); async () => {
}); await assertSidebarDockButton(false);
},
// toggle sidebar off );
// -------------------------------------------------------------------------
expect(window.h.app.toggleMenu("customSidebar")).toBe(false);
await waitFor(() => {
const node = container.querySelector("#test-sidebar-content");
expect(node).toBe(null);
});
// force-toggle sidebar off (=> still hidden)
// -------------------------------------------------------------------------
expect(window.h.app.toggleMenu("customSidebar", false)).toBe(false);
await waitFor(() => {
const node = container.querySelector("#test-sidebar-content");
expect(node).toBe(null);
});
// force-toggle sidebar on
// -------------------------------------------------------------------------
expect(window.h.app.toggleMenu("customSidebar", true)).toBe(true);
expect(window.h.app.toggleMenu("customSidebar", true)).toBe(true);
await waitFor(() => {
const node = container.querySelector("#test-sidebar-content");
expect(node).not.toBe(null);
});
// toggle library (= hide custom sidebar)
// -------------------------------------------------------------------------
expect(window.h.app.toggleMenu("library")).toBe(true);
await waitFor(() => {
const node = container.querySelector("#test-sidebar-content");
expect(node).toBe(null);
// make sure only one sidebar is rendered
const sidebars = container.querySelectorAll(".layer-ui__sidebar");
expect(sidebars.length).toBe(1);
}); });
}); });
}); });

View File

@ -1,151 +1,249 @@
import { import React, {
useEffect, useEffect,
useLayoutEffect, useLayoutEffect,
useRef, useRef,
useState, useState,
forwardRef, forwardRef,
useImperativeHandle,
useCallback,
RefObject,
} from "react"; } from "react";
import { Island } from ".././Island"; import { Island } from ".././Island";
import { atom, useAtom } from "jotai"; import { atom, useSetAtom } from "jotai";
import { jotaiScope } from "../../jotai"; import { jotaiScope } from "../../jotai";
import { import {
SidebarPropsContext, SidebarPropsContext,
SidebarProps, SidebarProps,
SidebarPropsContextValue, SidebarPropsContextValue,
} from "./common"; } from "./common";
import { SidebarHeader } from "./SidebarHeader";
import { SidebarHeaderComponents } from "./SidebarHeader"; import clsx from "clsx";
import {
useDevice,
useExcalidrawAppState,
useExcalidrawSetAppState,
} from "../App";
import { updateObject } from "../../utils";
import { KEYS } from "../../keys";
import { EVENT } from "../../constants";
import { SidebarTrigger } from "./SidebarTrigger";
import { SidebarTabTriggers } from "./SidebarTabTriggers";
import { SidebarTabTrigger } from "./SidebarTabTrigger";
import { SidebarTabs } from "./SidebarTabs";
import { SidebarTab } from "./SidebarTab";
import "./Sidebar.scss"; import "./Sidebar.scss";
import clsx from "clsx";
import { useExcalidrawSetAppState } from "../App";
import { updateObject } from "../../utils";
/** using a counter instead of boolean to handle race conditions where // FIXME replace this with the implem from ColorPicker once it's merged
* the host app may render (mount/unmount) multiple different sidebar */ const useOnClickOutside = (
export const hostSidebarCountersAtom = atom({ rendered: 0, docked: 0 }); ref: RefObject<HTMLElement>,
cb: (event: MouseEvent) => void,
export const Sidebar = Object.assign( ) => {
forwardRef( useEffect(() => {
( const listener = (event: MouseEvent) => {
{ if (!ref.current) {
children, return;
onClose,
onDock,
docked,
/** Undocumented, may be removed later. Generally should either be
* `props.docked` or `appState.isSidebarDocked`. Currently serves to
* prevent unwanted animation of the shadow if initially docked. */
//
// NOTE we'll want to remove this after we sort out how to subscribe to
// individual appState properties
initialDockedState = docked,
dockable = true,
className,
__isInternal,
}: SidebarProps<{
// NOTE sidebars we use internally inside the editor must have this flag set.
// It indicates that this sidebar should have lower precedence over host
// sidebars, if both are open.
/** @private internal */
__isInternal?: boolean;
}>,
ref: React.ForwardedRef<HTMLDivElement>,
) => {
const [hostSidebarCounters, setHostSidebarCounters] = useAtom(
hostSidebarCountersAtom,
jotaiScope,
);
const setAppState = useExcalidrawSetAppState();
const [isDockedFallback, setIsDockedFallback] = useState(
docked ?? initialDockedState ?? false,
);
useLayoutEffect(() => {
if (docked === undefined) {
// ugly hack to get initial state out of AppState without subscribing
// to it as a whole (once we have granular subscriptions, we'll move
// to that)
//
// NOTE this means that is updated `state.isSidebarDocked` changes outside
// of this compoent, it won't be reflected here. Currently doesn't happen.
setAppState((state) => {
setIsDockedFallback(state.isSidebarDocked);
// bail from update
return null;
});
}
}, [setAppState, docked]);
useLayoutEffect(() => {
if (!__isInternal) {
setHostSidebarCounters((s) => ({
rendered: s.rendered + 1,
docked: isDockedFallback ? s.docked + 1 : s.docked,
}));
return () => {
setHostSidebarCounters((s) => ({
rendered: s.rendered - 1,
docked: isDockedFallback ? s.docked - 1 : s.docked,
}));
};
}
}, [__isInternal, setHostSidebarCounters, isDockedFallback]);
const onCloseRef = useRef(onClose);
onCloseRef.current = onClose;
useEffect(() => {
return () => {
onCloseRef.current?.();
};
}, []);
const headerPropsRef = useRef<SidebarPropsContextValue>({});
headerPropsRef.current.onClose = () => {
setAppState({ openSidebar: null });
};
headerPropsRef.current.onDock = (isDocked) => {
if (docked === undefined) {
setAppState({ isSidebarDocked: isDocked });
setIsDockedFallback(isDocked);
}
onDock?.(isDocked);
};
// renew the ref object if the following props change since we want to
// rerender. We can't pass down as component props manually because
// the <Sidebar.Header/> can be rendered upsream.
headerPropsRef.current = updateObject(headerPropsRef.current, {
docked: docked ?? isDockedFallback,
dockable,
});
if (hostSidebarCounters.rendered > 0 && __isInternal) {
return null;
} }
return ( if (
<Island event.target instanceof Element &&
className={clsx( (ref.current.contains(event.target) ||
"layer-ui__sidebar", !document.body.contains(event.target))
{ "layer-ui__sidebar--docked": isDockedFallback }, ) {
className, return;
)} }
ref={ref}
> cb(event);
<SidebarPropsContext.Provider value={headerPropsRef.current}> };
<SidebarHeaderComponents.Context> document.addEventListener("pointerdown", listener, false);
<SidebarHeaderComponents.Component __isFallback />
{children} return () => {
</SidebarHeaderComponents.Context> document.removeEventListener("pointerdown", listener);
</SidebarPropsContext.Provider> };
</Island> }, [ref, cb]);
};
/**
* Flags whether the currently rendered Sidebar is docked or not, for use
* in upstream components that need to act on this (e.g. LayerUI to shift the
* UI). We use an atom because of potential host app sidebars (for the default
* sidebar we could just read from appState.defaultSidebarDockedPreference).
*
* Since we can only render one Sidebar at a time, we can use a simple flag.
*/
export const isSidebarDockedAtom = atom(false);
export const SidebarInner = forwardRef(
(
{
name,
children,
onDock,
docked,
className,
...rest
}: SidebarProps & Omit<React.RefAttributes<HTMLDivElement>, "onSelect">,
ref: React.ForwardedRef<HTMLDivElement>,
) => {
if (process.env.NODE_ENV === "development" && onDock && docked == null) {
console.warn(
"Sidebar: `docked` must be set when `onDock` is supplied for the sidebar to be user-dockable. To hide this message, either pass `docked` or remove `onDock`",
); );
}, }
),
{ const setAppState = useExcalidrawSetAppState();
Header: SidebarHeaderComponents.Component,
const setIsSidebarDockedAtom = useSetAtom(isSidebarDockedAtom, jotaiScope);
useLayoutEffect(() => {
setIsSidebarDockedAtom(!!docked);
return () => {
setIsSidebarDockedAtom(false);
};
}, [setIsSidebarDockedAtom, docked]);
const headerPropsRef = useRef<SidebarPropsContextValue>(
{} as SidebarPropsContextValue,
);
headerPropsRef.current.onCloseRequest = () => {
setAppState({ openSidebar: null });
};
headerPropsRef.current.onDock = (isDocked) => onDock?.(isDocked);
// renew the ref object if the following props change since we want to
// rerender. We can't pass down as component props manually because
// the <Sidebar.Header/> can be rendered upstream.
headerPropsRef.current = updateObject(headerPropsRef.current, {
docked,
// explicit prop to rerender on update
shouldRenderDockButton: !!onDock && docked != null,
});
const islandRef = useRef<HTMLDivElement>(null);
useImperativeHandle(ref, () => {
return islandRef.current!;
});
const device = useDevice();
const closeLibrary = useCallback(() => {
const isDialogOpen = !!document.querySelector(".Dialog");
// Prevent closing if any dialog is open
if (isDialogOpen) {
return;
}
setAppState({ openSidebar: null });
}, [setAppState]);
useOnClickOutside(
islandRef,
useCallback(
(event) => {
// If click on the library icon, do nothing so that LibraryButton
// can toggle library menu
if ((event.target as Element).closest(".sidebar-trigger")) {
return;
}
if (!docked || !device.canDeviceFitSidebar) {
closeLibrary();
}
},
[closeLibrary, docked, device.canDeviceFitSidebar],
),
);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (
event.key === KEYS.ESCAPE &&
(!docked || !device.canDeviceFitSidebar)
) {
closeLibrary();
}
};
document.addEventListener(EVENT.KEYDOWN, handleKeyDown);
return () => {
document.removeEventListener(EVENT.KEYDOWN, handleKeyDown);
};
}, [closeLibrary, docked, device.canDeviceFitSidebar]);
return (
<Island
{...rest}
className={clsx("sidebar", { "sidebar--docked": docked }, className)}
ref={islandRef}
>
<SidebarPropsContext.Provider value={headerPropsRef.current}>
{children}
</SidebarPropsContext.Provider>
</Island>
);
}, },
); );
SidebarInner.displayName = "SidebarInner";
export const Sidebar = Object.assign(
forwardRef((props: SidebarProps, ref: React.ForwardedRef<HTMLDivElement>) => {
const appState = useExcalidrawAppState();
const { onStateChange } = props;
const refPrevOpenSidebar = useRef(appState.openSidebar);
useEffect(() => {
if (
// closing sidebar
((!appState.openSidebar &&
refPrevOpenSidebar?.current?.name === props.name) ||
// opening current sidebar
(appState.openSidebar?.name === props.name &&
refPrevOpenSidebar?.current?.name !== props.name) ||
// switching tabs or switching to a different sidebar
refPrevOpenSidebar.current?.name === props.name) &&
appState.openSidebar !== refPrevOpenSidebar.current
) {
onStateChange?.(
appState.openSidebar?.name !== props.name
? null
: appState.openSidebar,
);
}
refPrevOpenSidebar.current = appState.openSidebar;
}, [appState.openSidebar, onStateChange, props.name]);
const [mounted, setMounted] = useState(false);
useLayoutEffect(() => {
setMounted(true);
return () => setMounted(false);
}, []);
// We want to render in the next tick (hence `mounted` flag) so that it's
// guaranteed to happen after unmount of the previous sidebar (in case the
// previous sidebar is mounted after the next one). This is necessary to
// prevent flicker of subcomponents that support fallbacks
// (e.g. SidebarHeader). This is because we're using flags to determine
// whether prefer the fallback component or not (otherwise both will render
// initially), and the flag won't be reset in time if the unmount order
// it not correct.
//
// Alternative, and more general solution would be to namespace the fallback
// HoC so that state is not shared between subcomponents when the wrapping
// component is of the same type (e.g. Sidebar -> SidebarHeader).
const shouldRender = mounted && appState.openSidebar?.name === props.name;
if (!shouldRender) {
return null;
}
return <SidebarInner {...props} ref={ref} key={props.name} />;
}),
{
Header: SidebarHeader,
TabTriggers: SidebarTabTriggers,
TabTrigger: SidebarTabTrigger,
Tabs: SidebarTabs,
Tab: SidebarTab,
Trigger: SidebarTrigger,
},
);
Sidebar.displayName = "Sidebar";

View File

@ -4,86 +4,54 @@ import { t } from "../../i18n";
import { useDevice } from "../App"; import { useDevice } from "../App";
import { SidebarPropsContext } from "./common"; import { SidebarPropsContext } from "./common";
import { CloseIcon, PinIcon } from "../icons"; import { CloseIcon, PinIcon } from "../icons";
import { withUpstreamOverride } from "../hoc/withUpstreamOverride";
import { Tooltip } from "../Tooltip"; import { Tooltip } from "../Tooltip";
import { Button } from "../Button";
export const SidebarDockButton = (props: { export const SidebarHeader = ({
checked: boolean; children,
onChange?(): void; className,
}) => { }: {
return (
<div className="layer-ui__sidebar-dock-button" data-testid="sidebar-dock">
<Tooltip label={t("labels.sidebarLock")}>
<label
className={clsx(
"ToolIcon ToolIcon__lock ToolIcon_type_floating",
`ToolIcon_size_medium`,
)}
>
<input
className="ToolIcon_type_checkbox"
type="checkbox"
onChange={props.onChange}
checked={props.checked}
aria-label={t("labels.sidebarLock")}
/>{" "}
<div
className={clsx("Sidebar__pin-btn", {
"Sidebar__pin-btn--pinned": props.checked,
})}
tabIndex={0}
>
{PinIcon}
</div>{" "}
</label>{" "}
</Tooltip>
</div>
);
};
const _SidebarHeader: React.FC<{
children?: React.ReactNode; children?: React.ReactNode;
className?: string; className?: string;
}> = ({ children, className }) => { }) => {
const device = useDevice(); const device = useDevice();
const props = useContext(SidebarPropsContext); const props = useContext(SidebarPropsContext);
const renderDockButton = !!(device.canDeviceFitSidebar && props.dockable); const renderDockButton = !!(
const renderCloseButton = !!props.onClose; device.canDeviceFitSidebar && props.shouldRenderDockButton
);
return ( return (
<div <div
className={clsx("layer-ui__sidebar__header", className)} className={clsx("sidebar__header", className)}
data-testid="sidebar-header" data-testid="sidebar-header"
> >
{children} {children}
{(renderDockButton || renderCloseButton) && ( <div className="sidebar__header__buttons">
<div className="layer-ui__sidebar__header__buttons"> {renderDockButton && (
{renderDockButton && ( <Tooltip label={t("labels.sidebarLock")}>
<SidebarDockButton <Button
checked={!!props.docked} onSelect={() => props.onDock?.(!props.docked)}
onChange={() => { selected={!!props.docked}
props.onDock?.(!props.docked); className="sidebar__dock"
}} data-testid="sidebar-dock"
/> aria-label={t("labels.sidebarLock")}
)}
{renderCloseButton && (
<button
data-testid="sidebar-close"
className="Sidebar__close-btn"
onClick={props.onClose}
aria-label={t("buttons.close")}
> >
{CloseIcon} {PinIcon}
</button> </Button>
)} </Tooltip>
</div> )}
)} <Button
data-testid="sidebar-close"
className="sidebar__close"
onSelect={props.onCloseRequest}
aria-label={t("buttons.close")}
>
{CloseIcon}
</Button>
</div>
</div> </div>
); );
}; };
const [Context, Component] = withUpstreamOverride(_SidebarHeader); SidebarHeader.displayName = "SidebarHeader";
/** @private */
export const SidebarHeaderComponents = { Context, Component };

View File

@ -0,0 +1,18 @@
import * as RadixTabs from "@radix-ui/react-tabs";
import { SidebarTabName } from "../../types";
export const SidebarTab = ({
tab,
children,
...rest
}: {
tab: SidebarTabName;
children: React.ReactNode;
} & React.HTMLAttributes<HTMLDivElement>) => {
return (
<RadixTabs.Content {...rest} value={tab}>
{children}
</RadixTabs.Content>
);
};
SidebarTab.displayName = "SidebarTab";

View File

@ -0,0 +1,26 @@
import * as RadixTabs from "@radix-ui/react-tabs";
import { SidebarTabName } from "../../types";
export const SidebarTabTrigger = ({
children,
tab,
onSelect,
...rest
}: {
children: React.ReactNode;
tab: SidebarTabName;
onSelect?: React.ReactEventHandler<HTMLButtonElement> | undefined;
} & Omit<React.HTMLAttributes<HTMLButtonElement>, "onSelect">) => {
return (
<RadixTabs.Trigger value={tab} asChild onSelect={onSelect}>
<button
type={"button"}
className={`excalidraw-button sidebar-tab-trigger`}
{...rest}
>
{children}
</button>
</RadixTabs.Trigger>
);
};
SidebarTabTrigger.displayName = "SidebarTabTrigger";

View File

@ -0,0 +1,16 @@
import * as RadixTabs from "@radix-ui/react-tabs";
export const SidebarTabTriggers = ({
children,
...rest
}: { children: React.ReactNode } & Omit<
React.RefAttributes<HTMLDivElement>,
"onSelect"
>) => {
return (
<RadixTabs.List className="sidebar-triggers" {...rest}>
{children}
</RadixTabs.List>
);
};
SidebarTabTriggers.displayName = "SidebarTabTriggers";

View File

@ -0,0 +1,36 @@
import * as RadixTabs from "@radix-ui/react-tabs";
import { useUIAppState } from "../../context/ui-appState";
import { useExcalidrawSetAppState } from "../App";
export const SidebarTabs = ({
children,
...rest
}: {
children: React.ReactNode;
} & Omit<React.RefAttributes<HTMLDivElement>, "onSelect">) => {
const appState = useUIAppState();
const setAppState = useExcalidrawSetAppState();
if (!appState.openSidebar) {
return null;
}
const { name } = appState.openSidebar;
return (
<RadixTabs.Root
className="sidebar-tabs-root"
value={appState.openSidebar.tab}
onValueChange={(tab) =>
setAppState((state) => ({
...state,
openSidebar: { ...state.openSidebar, name, tab },
}))
}
{...rest}
>
{children}
</RadixTabs.Root>
);
};
SidebarTabs.displayName = "SidebarTabs";

View File

@ -0,0 +1,34 @@
@import "../../css/variables.module";
.excalidraw {
.sidebar-trigger {
@include outlineButtonStyles;
background-color: var(--island-bg-color);
width: auto;
height: var(--lg-button-size);
display: flex;
align-items: center;
gap: 0.5rem;
line-height: 0;
font-size: 0.75rem;
letter-spacing: 0.4px;
svg {
width: var(--lg-icon-size);
height: var(--lg-icon-size);
}
}
.default-sidebar-trigger .sidebar-trigger__label {
display: none;
@media screen and (min-width: 1024px) {
display: block;
}
}
}

View File

@ -0,0 +1,45 @@
import { useExcalidrawSetAppState, useExcalidrawAppState } from "../App";
import { SidebarTriggerProps } from "./common";
import "./SidebarTrigger.scss";
import clsx from "clsx";
export const SidebarTrigger = ({
name,
tab,
icon,
title,
children,
onToggle,
className,
style,
}: SidebarTriggerProps) => {
const setAppState = useExcalidrawSetAppState();
// TODO replace with sidebar context
const appState = useExcalidrawAppState();
return (
<label title={title}>
<input
className="ToolIcon_type_checkbox"
type="checkbox"
onChange={(event) => {
document
.querySelector(".layer-ui__wrapper")
?.classList.remove("animate");
const isOpen = event.target.checked;
setAppState({ openSidebar: isOpen ? { name, tab } : null });
onToggle?.(isOpen);
}}
checked={appState.openSidebar?.name === name}
aria-label={title}
aria-keyshortcuts="0"
/>
<div className={clsx("sidebar-trigger", className)} style={style}>
{icon && <div>{icon}</div>}
{children && <div className="sidebar-trigger__label">{children}</div>}
</div>
</label>
);
};
SidebarTrigger.displayName = "SidebarTrigger";

View File

@ -1,23 +1,41 @@
import React from "react"; import React from "react";
import { AppState, SidebarName, SidebarTabName } from "../../types";
export type SidebarTriggerProps = {
name: SidebarName;
tab?: SidebarTabName;
icon?: JSX.Element;
children?: React.ReactNode;
title?: string;
className?: string;
onToggle?: (open: boolean) => void;
style?: React.CSSProperties;
};
export type SidebarProps<P = {}> = { export type SidebarProps<P = {}> = {
name: SidebarName;
children: React.ReactNode; children: React.ReactNode;
/** /**
* Called on sidebar close (either by user action or by the editor). * Called on sidebar open/close or tab change.
*/
onStateChange?: (state: AppState["openSidebar"]) => void;
/**
* supply alongside `docked` prop in order to make the Sidebar user-dockable
*/ */
onClose?: () => void | boolean;
/** if not supplied, sidebar won't be dockable */
onDock?: (docked: boolean) => void; onDock?: (docked: boolean) => void;
docked?: boolean; docked?: boolean;
initialDockedState?: boolean;
dockable?: boolean;
className?: string; className?: string;
// NOTE sidebars we use internally inside the editor must have this flag set.
// It indicates that this sidebar should have lower precedence over host
// sidebars, if both are open.
/** @private internal */
__fallback?: boolean;
} & P; } & P;
export type SidebarPropsContextValue = Pick< export type SidebarPropsContextValue = Pick<
SidebarProps, SidebarProps,
"onClose" | "onDock" | "docked" | "dockable" "onDock" | "docked"
>; > & { onCloseRequest: () => void; shouldRenderDockButton: boolean };
export const SidebarPropsContext = export const SidebarPropsContext =
React.createContext<SidebarPropsContextValue>({}); React.createContext<SidebarPropsContextValue>({} as SidebarPropsContextValue);

View File

@ -1,32 +0,0 @@
import React from "react";
import tunnel from "@dwelle/tunnel-rat";
type Tunnel = ReturnType<typeof tunnel>;
type TunnelsContextValue = {
mainMenuTunnel: Tunnel;
welcomeScreenMenuHintTunnel: Tunnel;
welcomeScreenToolbarHintTunnel: Tunnel;
welcomeScreenHelpHintTunnel: Tunnel;
welcomeScreenCenterTunnel: Tunnel;
footerCenterTunnel: Tunnel;
jotaiScope: symbol;
};
export const TunnelsContext = React.createContext<TunnelsContextValue>(null!);
export const useTunnels = () => React.useContext(TunnelsContext);
export const useInitializeTunnels = () => {
return React.useMemo((): TunnelsContextValue => {
return {
mainMenuTunnel: tunnel(),
welcomeScreenMenuHintTunnel: tunnel(),
welcomeScreenToolbarHintTunnel: tunnel(),
welcomeScreenHelpHintTunnel: tunnel(),
welcomeScreenCenterTunnel: tunnel(),
footerCenterTunnel: tunnel(),
jotaiScope: Symbol(),
};
}, []);
};

View File

@ -1,4 +1,4 @@
import { useOutsideClickHook } from "../../hooks/useOutsideClick"; import { useOutsideClick } from "../../hooks/useOutsideClick";
import { Island } from "../Island"; import { Island } from "../Island";
import { useDevice } from "../App"; import { useDevice } from "../App";
@ -24,7 +24,7 @@ const MenuContent = ({
style?: React.CSSProperties; style?: React.CSSProperties;
}) => { }) => {
const device = useDevice(); const device = useDevice();
const menuRef = useOutsideClickHook(() => { const menuRef = useOutsideClick(() => {
onClickOutside?.(); onClickOutside?.();
}); });

View File

@ -9,7 +9,7 @@ import {
ZoomActions, ZoomActions,
} from "../Actions"; } from "../Actions";
import { useDevice } from "../App"; import { useDevice } from "../App";
import { useTunnels } from "../context/tunnels"; import { useTunnels } from "../../context/tunnels";
import { HelpButton } from "../HelpButton"; import { HelpButton } from "../HelpButton";
import { Section } from "../Section"; import { Section } from "../Section";
import Stack from "../Stack"; import Stack from "../Stack";
@ -25,7 +25,7 @@ const Footer = ({
showExitZenModeBtn: boolean; showExitZenModeBtn: boolean;
renderWelcomeScreen: boolean; renderWelcomeScreen: boolean;
}) => { }) => {
const { footerCenterTunnel, welcomeScreenHelpHintTunnel } = useTunnels(); const { FooterCenterTunnel, WelcomeScreenHelpHintTunnel } = useTunnels();
const device = useDevice(); const device = useDevice();
const showFinalize = const showFinalize =
@ -70,14 +70,14 @@ const Footer = ({
</Section> </Section>
</Stack.Col> </Stack.Col>
</div> </div>
<footerCenterTunnel.Out /> <FooterCenterTunnel.Out />
<div <div
className={clsx("layer-ui__wrapper__footer-right zen-mode-transition", { className={clsx("layer-ui__wrapper__footer-right zen-mode-transition", {
"transition-right disable-pointerEvents": appState.zenModeEnabled, "transition-right disable-pointerEvents": appState.zenModeEnabled,
})} })}
> >
<div style={{ position: "relative" }}> <div style={{ position: "relative" }}>
{renderWelcomeScreen && <welcomeScreenHelpHintTunnel.Out />} {renderWelcomeScreen && <WelcomeScreenHelpHintTunnel.Out />}
<HelpButton <HelpButton
onClick={() => actionManager.executeAction(actionShortcuts)} onClick={() => actionManager.executeAction(actionShortcuts)}
/> />

View File

@ -1,13 +1,13 @@
import clsx from "clsx"; import clsx from "clsx";
import { useExcalidrawAppState } from "../App"; import { useExcalidrawAppState } from "../App";
import { useTunnels } from "../context/tunnels"; import { useTunnels } from "../../context/tunnels";
import "./FooterCenter.scss"; import "./FooterCenter.scss";
const FooterCenter = ({ children }: { children?: React.ReactNode }) => { const FooterCenter = ({ children }: { children?: React.ReactNode }) => {
const { footerCenterTunnel } = useTunnels(); const { FooterCenterTunnel } = useTunnels();
const appState = useExcalidrawAppState(); const appState = useExcalidrawAppState();
return ( return (
<footerCenterTunnel.In> <FooterCenterTunnel.In>
<div <div
className={clsx("footer-center zen-mode-transition", { className={clsx("footer-center zen-mode-transition", {
"layer-ui__wrapper__footer-left--transition-bottom": "layer-ui__wrapper__footer-left--transition-bottom":
@ -16,7 +16,7 @@ const FooterCenter = ({ children }: { children?: React.ReactNode }) => {
> >
{children} {children}
</div> </div>
</footerCenterTunnel.In> </FooterCenterTunnel.In>
); );
}; };

View File

@ -1,32 +1,46 @@
import { atom, useAtom } from "jotai"; import { atom, useAtom } from "jotai";
import React, { useLayoutEffect } from "react"; import React, { useLayoutEffect } from "react";
import { useTunnels } from "../context/tunnels"; import { useTunnels } from "../../context/tunnels";
export const withInternalFallback = <P,>( export const withInternalFallback = <P,>(
componentName: string, componentName: string,
Component: React.FC<P>, Component: React.FC<P>,
) => { ) => {
const counterAtom = atom(0); const renderAtom = atom(0);
// flag set on initial render to tell the fallback component to skip the // flag set on initial render to tell the fallback component to skip the
// render until mount counter are initialized. This is because the counter // render until mount counter are initialized. This is because the counter
// is initialized in an effect, and thus we could end rendering both // is initialized in an effect, and thus we could end rendering both
// components at the same time until counter is initialized. // components at the same time until counter is initialized.
let preferHost = false; let preferHost = false;
let counter = 0;
const WrapperComponent: React.FC< const WrapperComponent: React.FC<
P & { P & {
__fallback?: boolean; __fallback?: boolean;
} }
> = (props) => { > = (props) => {
const { jotaiScope } = useTunnels(); const { jotaiScope } = useTunnels();
const [counter, setCounter] = useAtom(counterAtom, jotaiScope); const [, setRender] = useAtom(renderAtom, jotaiScope);
useLayoutEffect(() => { useLayoutEffect(() => {
setCounter((counter) => counter + 1); setRender((c) => {
const next = c + 1;
counter = next;
return next;
});
return () => { return () => {
setCounter((counter) => counter - 1); setRender((c) => {
const next = c - 1;
counter = next;
if (!next) {
preferHost = false;
}
return next;
});
}; };
}, [setCounter]); }, [setRender]);
if (!props.__fallback) { if (!props.__fallback) {
preferHost = true; preferHost = true;

View File

@ -1,63 +0,0 @@
import React, {
useMemo,
useContext,
useLayoutEffect,
useState,
createContext,
} from "react";
export const withUpstreamOverride = <P,>(Component: React.ComponentType<P>) => {
type ContextValue = [boolean, React.Dispatch<React.SetStateAction<boolean>>];
const DefaultComponentContext = createContext<ContextValue>([
false,
() => {},
]);
const ComponentContext: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [isRenderedUpstream, setIsRenderedUpstream] = useState(false);
const contextValue: ContextValue = useMemo(
() => [isRenderedUpstream, setIsRenderedUpstream],
[isRenderedUpstream],
);
return (
<DefaultComponentContext.Provider value={contextValue}>
{children}
</DefaultComponentContext.Provider>
);
};
const DefaultComponent = (
props: P & {
// indicates whether component should render when not rendered upstream
/** @private internal */
__isFallback?: boolean;
},
) => {
const [isRenderedUpstream, setIsRenderedUpstream] = useContext(
DefaultComponentContext,
);
useLayoutEffect(() => {
if (!props.__isFallback) {
setIsRenderedUpstream(true);
return () => setIsRenderedUpstream(false);
}
}, [props.__isFallback, setIsRenderedUpstream]);
if (props.__isFallback && isRenderedUpstream) {
return null;
}
return <Component {...props} />;
};
if (Component.name) {
DefaultComponent.displayName = `${Component.name}_upstreamOverrideWrapper`;
ComponentContext.displayName = `${Component.name}_upstreamOverrideContextWrapper`;
}
return [ComponentContext, DefaultComponent] as const;
};

View File

@ -13,7 +13,7 @@ import { t } from "../../i18n";
import { HamburgerMenuIcon } from "../icons"; import { HamburgerMenuIcon } from "../icons";
import { withInternalFallback } from "../hoc/withInternalFallback"; import { withInternalFallback } from "../hoc/withInternalFallback";
import { composeEventHandlers } from "../../utils"; import { composeEventHandlers } from "../../utils";
import { useTunnels } from "../context/tunnels"; import { useTunnels } from "../../context/tunnels";
const MainMenu = Object.assign( const MainMenu = Object.assign(
withInternalFallback( withInternalFallback(
@ -28,7 +28,7 @@ const MainMenu = Object.assign(
*/ */
onSelect?: (event: Event) => void; onSelect?: (event: Event) => void;
}) => { }) => {
const { mainMenuTunnel } = useTunnels(); const { MainMenuTunnel } = useTunnels();
const device = useDevice(); const device = useDevice();
const appState = useExcalidrawAppState(); const appState = useExcalidrawAppState();
const setAppState = useExcalidrawSetAppState(); const setAppState = useExcalidrawSetAppState();
@ -37,7 +37,7 @@ const MainMenu = Object.assign(
: () => setAppState({ openMenu: null }); : () => setAppState({ openMenu: null });
return ( return (
<mainMenuTunnel.In> <MainMenuTunnel.In>
<DropdownMenu open={appState.openMenu === "canvas"}> <DropdownMenu open={appState.openMenu === "canvas"}>
<DropdownMenu.Trigger <DropdownMenu.Trigger
onToggle={() => { onToggle={() => {
@ -66,7 +66,7 @@ const MainMenu = Object.assign(
)} )}
</DropdownMenu.Content> </DropdownMenu.Content>
</DropdownMenu> </DropdownMenu>
</mainMenuTunnel.In> </MainMenuTunnel.In>
); );
}, },
), ),

View File

@ -6,7 +6,7 @@ import {
useExcalidrawActionManager, useExcalidrawActionManager,
useExcalidrawAppState, useExcalidrawAppState,
} from "../App"; } from "../App";
import { useTunnels } from "../context/tunnels"; import { useTunnels } from "../../context/tunnels";
import { ExcalLogo, HelpIcon, LoadIcon, usersIcon } from "../icons"; import { ExcalLogo, HelpIcon, LoadIcon, usersIcon } from "../icons";
const WelcomeScreenMenuItemContent = ({ const WelcomeScreenMenuItemContent = ({
@ -89,9 +89,9 @@ const WelcomeScreenMenuItemLink = ({
WelcomeScreenMenuItemLink.displayName = "WelcomeScreenMenuItemLink"; WelcomeScreenMenuItemLink.displayName = "WelcomeScreenMenuItemLink";
const Center = ({ children }: { children?: React.ReactNode }) => { const Center = ({ children }: { children?: React.ReactNode }) => {
const { welcomeScreenCenterTunnel } = useTunnels(); const { WelcomeScreenCenterTunnel } = useTunnels();
return ( return (
<welcomeScreenCenterTunnel.In> <WelcomeScreenCenterTunnel.In>
<div className="welcome-screen-center"> <div className="welcome-screen-center">
{children || ( {children || (
<> <>
@ -104,7 +104,7 @@ const Center = ({ children }: { children?: React.ReactNode }) => {
</> </>
)} )}
</div> </div>
</welcomeScreenCenterTunnel.In> </WelcomeScreenCenterTunnel.In>
); );
}; };
Center.displayName = "Center"; Center.displayName = "Center";

View File

@ -1,5 +1,5 @@
import { t } from "../../i18n"; import { t } from "../../i18n";
import { useTunnels } from "../context/tunnels"; import { useTunnels } from "../../context/tunnels";
import { import {
WelcomeScreenHelpArrow, WelcomeScreenHelpArrow,
WelcomeScreenMenuArrow, WelcomeScreenMenuArrow,
@ -7,44 +7,44 @@ import {
} from "../icons"; } from "../icons";
const MenuHint = ({ children }: { children?: React.ReactNode }) => { const MenuHint = ({ children }: { children?: React.ReactNode }) => {
const { welcomeScreenMenuHintTunnel } = useTunnels(); const { WelcomeScreenMenuHintTunnel } = useTunnels();
return ( return (
<welcomeScreenMenuHintTunnel.In> <WelcomeScreenMenuHintTunnel.In>
<div className="virgil welcome-screen-decor welcome-screen-decor-hint welcome-screen-decor-hint--menu"> <div className="virgil welcome-screen-decor welcome-screen-decor-hint welcome-screen-decor-hint--menu">
{WelcomeScreenMenuArrow} {WelcomeScreenMenuArrow}
<div className="welcome-screen-decor-hint__label"> <div className="welcome-screen-decor-hint__label">
{children || t("welcomeScreen.defaults.menuHint")} {children || t("welcomeScreen.defaults.menuHint")}
</div> </div>
</div> </div>
</welcomeScreenMenuHintTunnel.In> </WelcomeScreenMenuHintTunnel.In>
); );
}; };
MenuHint.displayName = "MenuHint"; MenuHint.displayName = "MenuHint";
const ToolbarHint = ({ children }: { children?: React.ReactNode }) => { const ToolbarHint = ({ children }: { children?: React.ReactNode }) => {
const { welcomeScreenToolbarHintTunnel } = useTunnels(); const { WelcomeScreenToolbarHintTunnel } = useTunnels();
return ( return (
<welcomeScreenToolbarHintTunnel.In> <WelcomeScreenToolbarHintTunnel.In>
<div className="virgil welcome-screen-decor welcome-screen-decor-hint welcome-screen-decor-hint--toolbar"> <div className="virgil welcome-screen-decor welcome-screen-decor-hint welcome-screen-decor-hint--toolbar">
<div className="welcome-screen-decor-hint__label"> <div className="welcome-screen-decor-hint__label">
{children || t("welcomeScreen.defaults.toolbarHint")} {children || t("welcomeScreen.defaults.toolbarHint")}
</div> </div>
{WelcomeScreenTopToolbarArrow} {WelcomeScreenTopToolbarArrow}
</div> </div>
</welcomeScreenToolbarHintTunnel.In> </WelcomeScreenToolbarHintTunnel.In>
); );
}; };
ToolbarHint.displayName = "ToolbarHint"; ToolbarHint.displayName = "ToolbarHint";
const HelpHint = ({ children }: { children?: React.ReactNode }) => { const HelpHint = ({ children }: { children?: React.ReactNode }) => {
const { welcomeScreenHelpHintTunnel } = useTunnels(); const { WelcomeScreenHelpHintTunnel } = useTunnels();
return ( return (
<welcomeScreenHelpHintTunnel.In> <WelcomeScreenHelpHintTunnel.In>
<div className="virgil welcome-screen-decor welcome-screen-decor-hint welcome-screen-decor-hint--help"> <div className="virgil welcome-screen-decor welcome-screen-decor-hint welcome-screen-decor-hint--help">
<div>{children || t("welcomeScreen.defaults.helpHint")}</div> <div>{children || t("welcomeScreen.defaults.helpHint")}</div>
{WelcomeScreenHelpArrow} {WelcomeScreenHelpArrow}
</div> </div>
</welcomeScreenHelpHintTunnel.In> </WelcomeScreenHelpHintTunnel.In>
); );
}; };
HelpHint.displayName = "HelpHint"; HelpHint.displayName = "HelpHint";

View File

@ -275,3 +275,10 @@ export const DEFAULT_ELEMENT_PROPS: {
opacity: 100, opacity: 100,
locked: false, locked: false,
}; };
export const LIBRARY_SIDEBAR_TAB = "library";
export const DEFAULT_SIDEBAR = {
name: "default",
defaultTab: LIBRARY_SIDEBAR_TAB,
} as const;

36
src/context/tunnels.ts Normal file
View File

@ -0,0 +1,36 @@
import React from "react";
import tunnel from "tunnel-rat";
export type Tunnel = ReturnType<typeof tunnel>;
type TunnelsContextValue = {
MainMenuTunnel: Tunnel;
WelcomeScreenMenuHintTunnel: Tunnel;
WelcomeScreenToolbarHintTunnel: Tunnel;
WelcomeScreenHelpHintTunnel: Tunnel;
WelcomeScreenCenterTunnel: Tunnel;
FooterCenterTunnel: Tunnel;
DefaultSidebarTriggerTunnel: Tunnel;
DefaultSidebarTabTriggersTunnel: Tunnel;
jotaiScope: symbol;
};
export const TunnelsContext = React.createContext<TunnelsContextValue>(null!);
export const useTunnels = () => React.useContext(TunnelsContext);
export const useInitializeTunnels = () => {
return React.useMemo((): TunnelsContextValue => {
return {
MainMenuTunnel: tunnel(),
WelcomeScreenMenuHintTunnel: tunnel(),
WelcomeScreenToolbarHintTunnel: tunnel(),
WelcomeScreenHelpHintTunnel: tunnel(),
WelcomeScreenCenterTunnel: tunnel(),
FooterCenterTunnel: tunnel(),
DefaultSidebarTriggerTunnel: tunnel(),
DefaultSidebarTabTriggersTunnel: tunnel(),
jotaiScope: Symbol(),
};
}, []);
};

View File

@ -0,0 +1,5 @@
import React from "react";
import { AppState } from "../types";
export const UIAppStateContext = React.createContext<AppState>(null!);
export const useUIAppState = () => React.useContext(UIAppStateContext);

View File

@ -567,7 +567,7 @@
border-radius: 0; border-radius: 0;
} }
.library-button { .default-sidebar-trigger {
border: 0; border: 0;
} }
} }

View File

@ -78,10 +78,13 @@
--color-selection: #6965db; --color-selection: #6965db;
--color-icon-white: #{$oc-white};
--color-primary: #6965db; --color-primary: #6965db;
--color-primary-darker: #5b57d1; --color-primary-darker: #5b57d1;
--color-primary-darkest: #4a47b1; --color-primary-darkest: #4a47b1;
--color-primary-light: #e3e2fe; --color-primary-light: #e3e2fe;
--color-primary-light-darker: #d7d5ff;
--color-gray-10: #f5f5f5; --color-gray-10: #f5f5f5;
--color-gray-20: #ebebeb; --color-gray-20: #ebebeb;
@ -161,10 +164,13 @@
// will be inverted to a lighter color. // will be inverted to a lighter color.
--color-selection: #3530c4; --color-selection: #3530c4;
--color-icon-white: var(--color-gray-90);
--color-primary: #a8a5ff; --color-primary: #a8a5ff;
--color-primary-darker: #b2aeff; --color-primary-darker: #b2aeff;
--color-primary-darkest: #beb9ff; --color-primary-darkest: #beb9ff;
--color-primary-light: #4f4d6f; --color-primary-light: #4f4d6f;
--color-primary-light-darker: #43415e;
--color-text-warning: var(--color-gray-80); --color-text-warning: var(--color-gray-80);

View File

@ -72,7 +72,14 @@
&:hover { &:hover {
background-color: var(--button-hover-bg, var(--island-bg-color)); background-color: var(--button-hover-bg, var(--island-bg-color));
border-color: var(--button-hover-border, var(--default-border-color)); border-color: var(
--button-hover-border,
var(--button-border, var(--default-border-color))
);
color: var(
--button-hover-color,
var(--button-color, var(--text-primary-color, inherit))
);
} }
&:active { &:active {
@ -81,11 +88,14 @@
} }
&.active { &.active {
background-color: var(--color-primary-light); background-color: var(--button-selected-bg, var(--color-primary-light));
border-color: var(--color-primary-light); border-color: var(--button-selected-border, var(--color-primary-light));
&:hover { &:hover {
background-color: var(--color-primary-light); background-color: var(
--button-selected-hover-bg,
var(--color-primary-light)
);
} }
svg { svg {

View File

@ -14,7 +14,14 @@ import { getCommonBoundingBox } from "../element/bounds";
import { AbortError } from "../errors"; import { AbortError } from "../errors";
import { t } from "../i18n"; import { t } from "../i18n";
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { URL_HASH_KEYS, URL_QUERY_KEYS, APP_NAME, EVENT } from "../constants"; import {
URL_HASH_KEYS,
URL_QUERY_KEYS,
APP_NAME,
EVENT,
DEFAULT_SIDEBAR,
LIBRARY_SIDEBAR_TAB,
} from "../constants";
export const libraryItemsAtom = atom<{ export const libraryItemsAtom = atom<{
status: "loading" | "loaded"; status: "loading" | "loaded";
@ -148,7 +155,9 @@ class Library {
defaultStatus?: "unpublished" | "published"; defaultStatus?: "unpublished" | "published";
}): Promise<LibraryItems> => { }): Promise<LibraryItems> => {
if (openLibraryMenu) { if (openLibraryMenu) {
this.app.setState({ openSidebar: "library" }); this.app.setState({
openSidebar: { name: DEFAULT_SIDEBAR.name, tab: LIBRARY_SIDEBAR_TAB },
});
} }
return this.setLibrary(() => { return this.setLibrary(() => {
@ -174,6 +183,13 @@ class Library {
}), }),
) )
) { ) {
if (prompt) {
// focus container if we've prompted. We focus conditionally
// lest `props.autoFocus` is disabled (in which case we should
// focus only on user action such as prompt confirm)
this.app.focusContainer();
}
if (merge) { if (merge) {
resolve(mergeLibraryItems(this.lastLibraryItems, nextItems)); resolve(mergeLibraryItems(this.lastLibraryItems, nextItems));
} else { } else {
@ -186,8 +202,6 @@ class Library {
reject(error); reject(error);
} }
}); });
}).finally(() => {
this.app.focusContainer();
}); });
}; };

View File

@ -27,6 +27,7 @@ import {
PRECEDING_ELEMENT_KEY, PRECEDING_ELEMENT_KEY,
FONT_FAMILY, FONT_FAMILY,
ROUNDNESS, ROUNDNESS,
DEFAULT_SIDEBAR,
} from "../constants"; } from "../constants";
import { getDefaultAppState } from "../appState"; import { getDefaultAppState } from "../appState";
import { LinearElementEditor } from "../element/linearElementEditor"; import { LinearElementEditor } from "../element/linearElementEditor";
@ -431,21 +432,15 @@ const LegacyAppStateMigrations: {
defaultAppState: ReturnType<typeof getDefaultAppState>, defaultAppState: ReturnType<typeof getDefaultAppState>,
) => [LegacyAppState[K][1], AppState[LegacyAppState[K][1]]]; ) => [LegacyAppState[K][1], AppState[LegacyAppState[K][1]]];
} = { } = {
isLibraryOpen: (appState, defaultAppState) => { isSidebarDocked: (appState, defaultAppState) => {
return [ return [
"openSidebar", "defaultSidebarDockedPreference",
"isLibraryOpen" in appState appState.isSidebarDocked ??
? appState.isLibraryOpen coalesceAppStateValue(
? "library" "defaultSidebarDockedPreference",
: null appState,
: coalesceAppStateValue("openSidebar", appState, defaultAppState), defaultAppState,
]; ),
},
isLibraryMenuDocked: (appState, defaultAppState) => {
return [
"isSidebarDocked",
appState.isLibraryMenuDocked ??
coalesceAppStateValue("isSidebarDocked", appState, defaultAppState),
]; ];
}, },
}; };
@ -517,13 +512,10 @@ export const restoreAppState = (
: appState.zoom?.value : appState.zoom?.value
? appState.zoom ? appState.zoom
: defaultAppState.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.
openSidebar: openSidebar:
nextAppState.openSidebar === "library" // string (legacy)
? nextAppState.isSidebarDocked typeof (appState.openSidebar as any as string) === "string"
? "library" ? { name: DEFAULT_SIDEBAR.name }
: null
: nextAppState.openSidebar, : nextAppState.openSidebar,
}; };
}; };

View File

@ -25,10 +25,8 @@ export interface ExportedDataState {
* Don't consume on its own. * Don't consume on its own.
*/ */
export type LegacyAppState = { export type LegacyAppState = {
/** @deprecated #5663 TODO remove 22-12-15 */ /** @deprecated #6213 TODO remove 23-06-01 */
isLibraryOpen: [boolean, "openSidebar"]; isSidebarDocked: [boolean, "defaultSidebarDockedPreference"];
/** @deprecated #5663 TODO remove 22-12-15 */
isLibraryMenuDocked: [boolean, "isSidebarDocked"];
}; };
export interface ImportedDataState { export interface ImportedDataState {

View File

@ -1,6 +1,6 @@
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
export const useOutsideClickHook = (handler: (event: Event) => void) => { export const useOutsideClick = (handler: (event: Event) => void) => {
const ref = useRef(null); const ref = useRef(null);
useEffect( useEffect(

View File

@ -11,6 +11,22 @@ The change should be grouped under one of the below section and must contain PR
Please add the latest change on the top under the correct section. Please add the latest change on the top under the correct section.
--> -->
## Unreleased
### Features
- Sidebar component now supports tabs — for more detailed description of new behavior and breaking changes, see the linked PR. [#6213](https://github.com/excalidraw/excalidraw/pull/6213)
- Exposed `DefaultSidebar` component to allow modifying the default sidebar, such as adding custom tabs to it. [#6213](https://github.com/excalidraw/excalidraw/pull/6213)
#### BREAKING CHANGES
- `props.renderSidebar` is removed in favor of rendering as `children`.
- `appState.isSidebarDocked` replaced with `appState.defaultSidebarDockedPreference` with slightly different semantics, and relating only to the default sidebar. You need to handle `docked` state for your custom sidebars yourself.
- Sidebar `props.dockable` is removed. To indicate dockability, supply `props.onDock()` alongside setting `props.docked`.
- `Sidebar.Header` is no longer rendered by default. You need to render it yourself.
- `props.onClose` replaced with `props.onStateChange`.
- `restore()`/`restoreAppState()` now retains `appState.openSidebar` regardless of docked state.
## 0.15.2 (2023-04-20) ## 0.15.2 (2023-04-20)
### Docs ### Docs

View File

@ -494,15 +494,6 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
); );
}; };
const renderSidebar = () => {
return (
<Sidebar>
<Sidebar.Header>Custom header!</Sidebar.Header>
Custom sidebar!
</Sidebar>
);
};
const renderMenu = () => { const renderMenu = () => {
return ( return (
<MainMenu> <MainMenu>
@ -668,23 +659,6 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
</div> </div>
</div> </div>
<div className="excalidraw-wrapper"> <div className="excalidraw-wrapper">
<div
style={{
position: "absolute",
left: "50%",
bottom: "20px",
display: "flex",
zIndex: 9999999999999999,
padding: "5px 10px",
transform: "translateX(-50%)",
background: "rgba(255, 255, 255, 0.8)",
gap: "1rem",
}}
>
<button onClick={() => excalidrawAPI?.toggleMenu("customSidebar")}>
Toggle Custom Sidebar
</button>
</div>
<Excalidraw <Excalidraw
ref={(api: ExcalidrawImperativeAPI) => setExcalidrawAPI(api)} ref={(api: ExcalidrawImperativeAPI) => setExcalidrawAPI(api)}
initialData={initialStatePromiseRef.current.promise} initialData={initialStatePromiseRef.current.promise}
@ -706,7 +680,6 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
onLinkOpen={onLinkOpen} onLinkOpen={onLinkOpen}
onPointerDown={onPointerDown} onPointerDown={onPointerDown}
onScrollChange={rerenderCommentIcons} onScrollChange={rerenderCommentIcons}
renderSidebar={renderSidebar}
> >
{excalidrawAPI && ( {excalidrawAPI && (
<Footer> <Footer>
@ -714,6 +687,30 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
</Footer> </Footer>
)} )}
<WelcomeScreen /> <WelcomeScreen />
<Sidebar name="custom">
<Sidebar.Tabs>
<Sidebar.Header />
<Sidebar.Tab tab="one">Tab one!</Sidebar.Tab>
<Sidebar.Tab tab="two">Tab two!</Sidebar.Tab>
<Sidebar.TabTriggers>
<Sidebar.TabTrigger tab="one">One</Sidebar.TabTrigger>
<Sidebar.TabTrigger tab="two">Two</Sidebar.TabTrigger>
</Sidebar.TabTriggers>
</Sidebar.Tabs>
</Sidebar>
<Sidebar.Trigger
name="custom"
tab="one"
style={{
position: "absolute",
left: "50%",
transform: "translateX(-50%)",
bottom: "20px",
zIndex: 9999999999999999,
}}
>
Toggle Custom Sidebar
</Sidebar.Trigger>
{renderMenu()} {renderMenu()}
</Excalidraw> </Excalidraw>
{Object.keys(commentIcons || []).length > 0 && renderCommentIcons()} {Object.keys(commentIcons || []).length > 0 && renderCommentIcons()}

View File

@ -24,7 +24,6 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
isCollaborating = false, isCollaborating = false,
onPointerUpdate, onPointerUpdate,
renderTopRightUI, renderTopRightUI,
renderSidebar,
langCode = defaultLang.code, langCode = defaultLang.code,
viewModeEnabled, viewModeEnabled,
zenModeEnabled, zenModeEnabled,
@ -47,6 +46,8 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
const canvasActions = props.UIOptions?.canvasActions; const canvasActions = props.UIOptions?.canvasActions;
// FIXME normalize/set defaults in parent component so that the memo resolver
// compares the same values
const UIOptions: AppProps["UIOptions"] = { const UIOptions: AppProps["UIOptions"] = {
...props.UIOptions, ...props.UIOptions,
canvasActions: { canvasActions: {
@ -114,7 +115,6 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
onLinkOpen={onLinkOpen} onLinkOpen={onLinkOpen}
onPointerDown={onPointerDown} onPointerDown={onPointerDown}
onScrollChange={onScrollChange} onScrollChange={onScrollChange}
renderSidebar={renderSidebar}
> >
{children} {children}
</App> </App>
@ -245,3 +245,5 @@ export { MainMenu };
export { useDevice } from "../../components/App"; export { useDevice } from "../../components/App";
export { WelcomeScreen }; export { WelcomeScreen };
export { LiveCollaborationTrigger }; export { LiveCollaborationTrigger };
export { DefaultSidebar } from "../../components/DefaultSidebar";

View File

@ -284,6 +284,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -300,7 +301,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -464,6 +464,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -480,7 +481,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -650,6 +650,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -666,7 +667,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -1005,6 +1005,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -1021,7 +1022,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -1360,6 +1360,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -1376,7 +1377,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -1546,6 +1546,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -1562,7 +1563,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -1768,6 +1768,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -1784,7 +1785,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -2053,6 +2053,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -2069,7 +2070,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -2426,6 +2426,7 @@ Object {
"currentItemStrokeWidth": 2, "currentItemStrokeWidth": 2,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -2442,7 +2443,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -3273,6 +3273,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -3289,7 +3290,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -3628,6 +3628,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -3644,7 +3645,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -3983,6 +3983,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -3999,7 +4000,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -4681,6 +4681,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -4697,7 +4698,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -5231,6 +5231,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -5247,7 +5248,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -5712,6 +5712,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -5728,7 +5729,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -6080,6 +6080,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -6096,7 +6097,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -6426,6 +6426,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -6442,7 +6443,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",

View File

@ -25,6 +25,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -41,7 +42,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -561,6 +561,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -577,7 +578,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -1103,6 +1103,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": "id10", "editingGroupId": "id10",
@ -1119,7 +1120,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -2010,6 +2010,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -2026,7 +2027,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -2240,6 +2240,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -2256,7 +2257,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -2773,6 +2773,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -2789,7 +2790,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -3062,6 +3062,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -3078,7 +3079,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -3246,6 +3246,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -3262,7 +3263,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -3762,6 +3762,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -3778,7 +3779,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -4030,6 +4030,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -4046,7 +4047,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -4260,6 +4260,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -4276,7 +4277,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -4536,6 +4536,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -4552,7 +4553,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -4824,6 +4824,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -4840,7 +4841,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -5242,6 +5242,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "down", "cursorButton": "down",
"defaultSidebarDockedPreference": false,
"draggingElement": Object { "draggingElement": Object {
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
@ -5285,7 +5286,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -5583,6 +5583,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": Object { "draggingElement": Object {
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
@ -5626,7 +5627,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -5897,6 +5897,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "down", "cursorButton": "down",
"defaultSidebarDockedPreference": false,
"draggingElement": Object { "draggingElement": Object {
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
@ -5940,7 +5941,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -6135,6 +6135,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -6151,7 +6152,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -6321,6 +6321,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": "id3", "editingGroupId": "id3",
@ -6337,7 +6338,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -6849,6 +6849,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -6865,7 +6866,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -7214,6 +7214,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -7230,7 +7231,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -9566,6 +9566,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -9582,7 +9583,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -9985,6 +9985,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -10001,7 +10002,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -10274,6 +10274,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -10290,7 +10291,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -10522,6 +10522,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -10538,7 +10539,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -10843,6 +10843,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -10859,7 +10860,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -11027,6 +11027,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -11043,7 +11044,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -11211,6 +11211,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -11227,7 +11228,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -11395,6 +11395,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -11411,7 +11412,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -11632,6 +11632,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -11648,7 +11649,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -11869,6 +11869,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -11885,7 +11886,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -12097,6 +12097,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -12113,7 +12114,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -12334,6 +12334,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -12350,7 +12351,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -12518,6 +12518,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -12534,7 +12535,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -12755,6 +12755,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -12771,7 +12772,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -12939,6 +12939,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -12955,7 +12956,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -13167,6 +13167,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -13183,7 +13184,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -13351,6 +13351,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -13367,7 +13368,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -14190,6 +14190,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -14206,7 +14207,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -14479,6 +14479,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "down", "cursorButton": "down",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -14495,7 +14496,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "touch", "lastPointerDownWith": "touch",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -14590,6 +14590,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -14606,7 +14607,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -14699,6 +14699,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -14715,7 +14716,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -14886,6 +14886,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -14902,7 +14903,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -15254,6 +15254,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -15270,7 +15271,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -15885,6 +15885,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -15901,7 +15902,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -16111,6 +16111,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -16127,7 +16128,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -17074,6 +17074,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -17090,7 +17091,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -17183,6 +17183,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": "id3", "editingGroupId": "id3",
@ -17199,7 +17200,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -18042,6 +18042,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "down", "cursorButton": "down",
"defaultSidebarDockedPreference": false,
"draggingElement": Object { "draggingElement": Object {
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
@ -18085,7 +18086,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -18514,6 +18514,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "down", "cursorButton": "down",
"defaultSidebarDockedPreference": false,
"draggingElement": Object { "draggingElement": Object {
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
@ -18557,7 +18558,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -18855,6 +18855,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "down", "cursorButton": "down",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -18871,7 +18872,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "touch", "lastPointerDownWith": "touch",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -18966,6 +18966,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -18982,7 +18983,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -19537,6 +19537,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -19553,7 +19554,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
@ -19646,6 +19646,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -19662,7 +19663,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",

View File

@ -10,7 +10,7 @@ import { API } from "../helpers/api";
import { getDefaultAppState } from "../../appState"; import { getDefaultAppState } from "../../appState";
import { ImportedDataState } from "../../data/types"; import { ImportedDataState } from "../../data/types";
import { NormalizedZoomValue } from "../../types"; import { NormalizedZoomValue } from "../../types";
import { FONT_FAMILY, ROUNDNESS } from "../../constants"; import { DEFAULT_SIDEBAR, FONT_FAMILY, ROUNDNESS } from "../../constants";
import { newElementWith } from "../../element/mutateElement"; import { newElementWith } from "../../element/mutateElement";
describe("restoreElements", () => { describe("restoreElements", () => {
@ -453,6 +453,29 @@ describe("restoreAppState", () => {
expect(restoredAppState.zoom).toMatchObject(getDefaultAppState().zoom); expect(restoredAppState.zoom).toMatchObject(getDefaultAppState().zoom);
}); });
}); });
it("should handle appState.openSidebar legacy values", () => {
expect(restore.restoreAppState({}, null).openSidebar).toBe(null);
expect(
restore.restoreAppState({ openSidebar: "library" } as any, null)
.openSidebar,
).toEqual({ name: DEFAULT_SIDEBAR.name });
expect(
restore.restoreAppState({ openSidebar: "xxx" } as any, null).openSidebar,
).toEqual({ name: DEFAULT_SIDEBAR.name });
// while "library" was our legacy sidebar name, we can't assume it's legacy
// value as it may be some host app's custom sidebar name ¯\_(ツ)_/¯
expect(
restore.restoreAppState({ openSidebar: { name: "library" } } as any, null)
.openSidebar,
).toEqual({ name: "library" });
expect(
restore.restoreAppState(
{ openSidebar: { name: DEFAULT_SIDEBAR.name, tab: "ola" } } as any,
null,
).openSidebar,
).toEqual({ name: DEFAULT_SIDEBAR.name, tab: "ola" });
});
}); });
describe("restore", () => { describe("restore", () => {

View File

@ -189,10 +189,15 @@ describe("library menu", () => {
const latestLibrary = await h.app.library.getLatestLibrary(); const latestLibrary = await h.app.library.getLatestLibrary();
expect(latestLibrary.length).toBe(0); expect(latestLibrary.length).toBe(0);
const libraryButton = container.querySelector(".library-button"); const libraryButton = container.querySelector(".sidebar-trigger");
fireEvent.click(libraryButton!); fireEvent.click(libraryButton!);
fireEvent.click(container.querySelector(".Sidebar__dropdown-btn")!); fireEvent.click(
queryByTestId(
container.querySelector(".layer-ui__library")!,
"dropdown-menu-button",
)!,
);
queryByTestId(container, "lib-dropdown--load")!.click(); queryByTestId(container, "lib-dropdown--load")!.click();
const libraryItems = parseLibraryJSON(await libraryJSONPromise); const libraryItems = parseLibraryJSON(await libraryJSONPromise);

View File

@ -25,6 +25,7 @@ Object {
"currentItemStrokeWidth": 1, "currentItemStrokeWidth": 1,
"currentItemTextAlign": "left", "currentItemTextAlign": "left",
"cursorButton": "up", "cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null, "draggingElement": null,
"editingElement": null, "editingElement": null,
"editingGroupId": null, "editingGroupId": null,
@ -41,7 +42,6 @@ Object {
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
"isSidebarDocked": false,
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "name", "name": "name",

View File

@ -94,6 +94,9 @@ export type LastActiveTool =
} }
| null; | null;
export type SidebarName = string;
export type SidebarTabName = string;
export type AppState = { export type AppState = {
contextMenu: { contextMenu: {
items: ContextMenuItems; items: ContextMenuItems;
@ -159,16 +162,22 @@ export type AppState = {
isResizing: boolean; isResizing: boolean;
isRotating: boolean; isRotating: boolean;
zoom: Zoom; zoom: Zoom;
// mobile-only
openMenu: "canvas" | "shape" | null; openMenu: "canvas" | "shape" | null;
openPopup: openPopup:
| "canvasColorPicker" | "canvasColorPicker"
| "backgroundColorPicker" | "backgroundColorPicker"
| "strokeColorPicker" | "strokeColorPicker"
| null; | null;
openSidebar: "library" | "customSidebar" | null; openSidebar: { name: SidebarName; tab?: SidebarTabName } | null;
openDialog: "imageExport" | "help" | "jsonExport" | null; openDialog: "imageExport" | "help" | "jsonExport" | null;
isSidebarDocked: boolean; /**
* Reflects user preference for whether the default sidebar should be docked.
*
* NOTE this is only a user preference and does not reflect the actual docked
* state of the sidebar, because the host apps can override this through
* a DefaultSidebar prop, which is not reflected back to the appState.
*/
defaultSidebarDockedPreference: boolean;
lastPointerDownWith: PointerType; lastPointerDownWith: PointerType;
selectedElementIds: { [id: string]: boolean }; selectedElementIds: { [id: string]: boolean };
@ -335,10 +344,6 @@ export interface ExcalidrawProps {
pointerDownState: PointerDownState, pointerDownState: PointerDownState,
) => void; ) => void;
onScrollChange?: (scrollX: number, scrollY: number) => void; onScrollChange?: (scrollX: number, scrollY: number) => void;
/**
* Render function that renders custom <Sidebar /> component.
*/
renderSidebar?: () => JSX.Element | null;
children?: React.ReactNode; children?: React.ReactNode;
} }
@ -426,6 +431,8 @@ export type AppClassProperties = {
device: App["device"]; device: App["device"];
scene: App["scene"]; scene: App["scene"];
pasteFromClipboard: App["pasteFromClipboard"]; pasteFromClipboard: App["pasteFromClipboard"];
id: App["id"];
onInsertElements: App["onInsertElements"];
}; };
export type PointerDownState = Readonly<{ export type PointerDownState = Readonly<{
@ -517,7 +524,7 @@ export type ExcalidrawImperativeAPI = {
setActiveTool: InstanceType<typeof App>["setActiveTool"]; setActiveTool: InstanceType<typeof App>["setActiveTool"];
setCursor: InstanceType<typeof App>["setCursor"]; setCursor: InstanceType<typeof App>["setCursor"];
resetCursor: InstanceType<typeof App>["resetCursor"]; resetCursor: InstanceType<typeof App>["resetCursor"];
toggleMenu: InstanceType<typeof App>["toggleMenu"]; toggleSidebar: InstanceType<typeof App>["toggleSidebar"];
}; };
export type Device = Readonly<{ export type Device = Readonly<{

View File

@ -767,16 +767,30 @@ export const queryFocusableElements = (container: HTMLElement | null) => {
: []; : [];
}; };
export const isShallowEqual = <T extends Record<string, any>>( export const isShallowEqual = <
T extends Record<string, any>,
I extends keyof T,
>(
objA: T, objA: T,
objB: T, objB: T,
comparators?: Record<I, (a: T[I], b: T[I]) => boolean>,
debug = false,
) => { ) => {
const aKeys = Object.keys(objA); const aKeys = Object.keys(objA);
const bKeys = Object.keys(objA); const bKeys = Object.keys(objB);
if (aKeys.length !== bKeys.length) { if (aKeys.length !== bKeys.length) {
return false; return false;
} }
return aKeys.every((key) => objA[key] === objB[key]); return aKeys.every((key) => {
const comparator = comparators?.[key as I];
const ret = comparator
? comparator(objA[key], objB[key])
: objA[key] === objB[key];
if (!ret && debug) {
console.warn(`isShallowEqual: ${key} not equal ->`, objA[key], objB[key]);
}
return ret;
});
}; };
// taken from Radix UI // taken from Radix UI

159
yarn.lock
View File

@ -1262,6 +1262,13 @@
dependencies: dependencies:
regenerator-runtime "^0.13.10" regenerator-runtime "^0.13.10"
"@babel/runtime@^7.13.10":
version "7.20.13"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.13.tgz#7055ab8a7cff2b8f6058bf6ae45ff84ad2aded4b"
integrity sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA==
dependencies:
regenerator-runtime "^0.13.11"
"@babel/template@^7.18.10", "@babel/template@^7.3.3": "@babel/template@^7.18.10", "@babel/template@^7.3.3":
version "7.18.10" version "7.18.10"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.10.tgz#6f9134835970d1dbf0835c0d100c9f38de0c5e71" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.10.tgz#6f9134835970d1dbf0835c0d100c9f38de0c5e71"
@ -1437,13 +1444,6 @@
resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.0.2.tgz#1bfafe4b7ed0f3e4105837e056e0a89b108ebe36" resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.0.2.tgz#1bfafe4b7ed0f3e4105837e056e0a89b108ebe36"
integrity sha512-IkpVW/ehM1hWKln4fCA3NzJU8KwD+kIOvPZA4cqxoJHtE21CCzjyp+Kxbu0i5I4tBNOlXPL9mjwnWlL0VEG4Fg== integrity sha512-IkpVW/ehM1hWKln4fCA3NzJU8KwD+kIOvPZA4cqxoJHtE21CCzjyp+Kxbu0i5I4tBNOlXPL9mjwnWlL0VEG4Fg==
"@dwelle/tunnel-rat@0.1.1":
version "0.1.1"
resolved "https://registry.yarnpkg.com/@dwelle/tunnel-rat/-/tunnel-rat-0.1.1.tgz#0a0b235f8fc22ff1cf47ed102f4cc612eb51bc71"
integrity sha512-jb5/ZsT/af1J7tnbBXp7KO1xEyw61lWSDqJ+Bqdc6JlL3vbAvsifNhe+/mRFs6aSBCRaDqp5f2pJDHtA3MUZLw==
dependencies:
zustand "^4.3.2"
"@eslint/eslintrc@^0.4.3": "@eslint/eslintrc@^0.4.3":
version "0.4.3" version "0.4.3"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c"
@ -2164,6 +2164,131 @@
resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==
"@radix-ui/primitive@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.0.0.tgz#e1d8ef30b10ea10e69c76e896f608d9276352253"
integrity sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-collection@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.0.1.tgz#259506f97c6703b36291826768d3c1337edd1de5"
integrity sha512-uuiFbs+YCKjn3X1DTSx9G7BHApu4GHbi3kgiwsnFUbOKCrwejAJv4eE4Vc8C0Oaxt9T0aV4ox0WCOdx+39Xo+g==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-compose-refs" "1.0.0"
"@radix-ui/react-context" "1.0.0"
"@radix-ui/react-primitive" "1.0.1"
"@radix-ui/react-slot" "1.0.1"
"@radix-ui/react-compose-refs@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz#37595b1f16ec7f228d698590e78eeed18ff218ae"
integrity sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-context@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.0.0.tgz#f38e30c5859a9fb5e9aa9a9da452ee3ed9e0aee0"
integrity sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-direction@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.0.0.tgz#a2e0b552352459ecf96342c79949dd833c1e6e45"
integrity sha512-2HV05lGUgYcA6xgLQ4BKPDmtL+QbIZYH5fCOTAOOcJ5O0QbWS3i9lKaurLzliYUDhORI2Qr3pyjhJh44lKA3rQ==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-id@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.0.0.tgz#8d43224910741870a45a8c9d092f25887bb6d11e"
integrity sha512-Q6iAB/U7Tq3NTolBBQbHTgclPmGWE3OlktGGqrClPozSw4vkQ1DfQAOtzgRPecKsMdJINE05iaoDUG8tRzCBjw==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-layout-effect" "1.0.0"
"@radix-ui/react-presence@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.0.0.tgz#814fe46df11f9a468808a6010e3f3ca7e0b2e84a"
integrity sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-compose-refs" "1.0.0"
"@radix-ui/react-use-layout-effect" "1.0.0"
"@radix-ui/react-primitive@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-1.0.1.tgz#c1ebcce283dd2f02e4fbefdaa49d1cb13dbc990a"
integrity sha512-fHbmislWVkZaIdeF6GZxF0A/NH/3BjrGIYj+Ae6eTmTCr7EB0RQAAVEiqsXK6p3/JcRqVSBQoceZroj30Jj3XA==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-slot" "1.0.1"
"@radix-ui/react-roving-focus@1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.2.tgz#d8ac2e3b8006697bdfc2b0eb06bef7e15b6245de"
integrity sha512-HLK+CqD/8pN6GfJm3U+cqpqhSKYAWiOJDe+A+8MfxBnOue39QEeMa43csUn2CXCHQT0/mewh1LrrG4tfkM9DMA==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "1.0.0"
"@radix-ui/react-collection" "1.0.1"
"@radix-ui/react-compose-refs" "1.0.0"
"@radix-ui/react-context" "1.0.0"
"@radix-ui/react-direction" "1.0.0"
"@radix-ui/react-id" "1.0.0"
"@radix-ui/react-primitive" "1.0.1"
"@radix-ui/react-use-callback-ref" "1.0.0"
"@radix-ui/react-use-controllable-state" "1.0.0"
"@radix-ui/react-slot@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.0.1.tgz#e7868c669c974d649070e9ecbec0b367ee0b4d81"
integrity sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-compose-refs" "1.0.0"
"@radix-ui/react-tabs@1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.0.2.tgz#8f5ec73ca41b151a413bdd6e00553408ff34ce07"
integrity sha512-gOUwh+HbjCuL0UCo8kZ+kdUEG8QtpdO4sMQduJ34ZEz0r4922g9REOBM+vIsfwtGxSug4Yb1msJMJYN2Bk8TpQ==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "1.0.0"
"@radix-ui/react-context" "1.0.0"
"@radix-ui/react-direction" "1.0.0"
"@radix-ui/react-id" "1.0.0"
"@radix-ui/react-presence" "1.0.0"
"@radix-ui/react-primitive" "1.0.1"
"@radix-ui/react-roving-focus" "1.0.2"
"@radix-ui/react-use-controllable-state" "1.0.0"
"@radix-ui/react-use-callback-ref@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz#9e7b8b6b4946fe3cbe8f748c82a2cce54e7b6a90"
integrity sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-controllable-state@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.0.tgz#a64deaafbbc52d5d407afaa22d493d687c538b7f"
integrity sha512-FohDoZvk3mEXh9AWAVyRTYR4Sq7/gavuofglmiXB2g1aKyboUD4YtgWxKj8O5n+Uak52gXQ4wKz5IFST4vtJHg==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-callback-ref" "1.0.0"
"@radix-ui/react-use-layout-effect@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.0.tgz#2fc19e97223a81de64cd3ba1dc42ceffd82374dc"
integrity sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ==
dependencies:
"@babel/runtime" "^7.13.10"
"@rollup/plugin-babel@^5.2.0": "@rollup/plugin-babel@^5.2.0":
version "5.3.1" version "5.3.1"
resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz#04bc0608f4aa4b2e4b1aebf284344d0f68fda283" resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz#04bc0608f4aa4b2e4b1aebf284344d0f68fda283"
@ -9160,7 +9285,7 @@ regenerator-runtime@^0.13.10:
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.10.tgz#ed07b19616bcbec5da6274ebc75ae95634bfc2ee" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.10.tgz#ed07b19616bcbec5da6274ebc75ae95634bfc2ee"
integrity sha512-KepLsg4dU12hryUO7bp/axHAKvwGOCV0sGloQtpagJ12ai+ojVDqkeGSiRX1zlq+kjIMZ1t7gpze+26QqtdGqw== integrity sha512-KepLsg4dU12hryUO7bp/axHAKvwGOCV0sGloQtpagJ12ai+ojVDqkeGSiRX1zlq+kjIMZ1t7gpze+26QqtdGqw==
regenerator-runtime@^0.13.9: regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.9:
version "0.13.11" version "0.13.11"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
@ -10245,12 +10370,12 @@ tsutils@^3.21.0:
dependencies: dependencies:
tslib "^1.8.1" tslib "^1.8.1"
tunnel-rat@0.1.0: tunnel-rat@0.1.2:
version "0.1.0" version "0.1.2"
resolved "https://registry.yarnpkg.com/tunnel-rat/-/tunnel-rat-0.1.0.tgz#62cfbaf1b24cabac9318fe45ef26d70dc40e86fe" resolved "https://registry.yarnpkg.com/tunnel-rat/-/tunnel-rat-0.1.2.tgz#1717efbc474ea2d8aa05a91622457a6e201c0aeb"
integrity sha512-/FKZLBXCoKhA7Wz+dsqitrItaLXYmT2bkZXod+1UuR4JqHtdb54yHvHhmMgLg+eyH1Od/CCnhA2VQQ2A/54Tcw== integrity sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==
dependencies: dependencies:
zustand "^4.1.0" zustand "^4.3.2"
type-check@^0.4.0, type-check@~0.4.0: type-check@^0.4.0, type-check@~0.4.0:
version "0.4.0" version "0.4.0"
@ -11035,9 +11160,9 @@ yocto-queue@^0.1.0:
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
zustand@^4.1.0, zustand@^4.3.2: zustand@^4.3.2:
version "4.3.2" version "4.3.7"
resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.3.2.tgz#bb121fcad84c5a569e94bd1a2695e1a93ba85d39" resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.3.7.tgz#501b1f0393a7f1d103332e45ab574be5747fedce"
integrity sha512-rd4haDmlwMTVWVqwvgy00ny8rtti/klRoZjFbL/MAcDnmD5qSw/RZc+Vddstdv90M5Lv6RPgWvm1Hivyn0QgJw== integrity sha512-dY8ERwB9Nd21ellgkBZFhudER8KVlelZm8388B5nDAXhO/+FZDhYMuRnqDgu5SYyRgz/iaf8RKnbUs/cHfOGlQ==
dependencies: dependencies:
use-sync-external-store "1.2.0" use-sync-external-store "1.2.0"