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:
parent
b1311a407a
commit
e9cae918a7
@ -19,7 +19,7 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@dwelle/tunnel-rat": "0.1.1",
|
||||
"@radix-ui/react-tabs": "1.0.2",
|
||||
"@sentry/browser": "6.2.5",
|
||||
"@sentry/integrations": "6.2.5",
|
||||
"@testing-library/jest-dom": "5.16.2",
|
||||
@ -51,7 +51,7 @@
|
||||
"roughjs": "4.5.2",
|
||||
"sass": "1.51.0",
|
||||
"socket.io-client": "2.3.1",
|
||||
"tunnel-rat": "0.1.0",
|
||||
"tunnel-rat": "0.1.2",
|
||||
"workbox-background-sync": "^6.5.4",
|
||||
"workbox-broadcast-update": "^6.5.4",
|
||||
"workbox-cacheable-response": "^6.5.4",
|
||||
|
@ -58,7 +58,7 @@ export const getDefaultAppState = (): Omit<
|
||||
fileHandle: null,
|
||||
gridSize: null,
|
||||
isBindingEnabled: true,
|
||||
isSidebarDocked: false,
|
||||
defaultSidebarDockedPreference: false,
|
||||
isLoading: false,
|
||||
isResizing: false,
|
||||
isRotating: false,
|
||||
@ -150,7 +150,11 @@ const APP_STATE_STORAGE_CONF = (<
|
||||
gridSize: { browser: true, export: true, server: true },
|
||||
height: { browser: false, export: false, server: false },
|
||||
isBindingEnabled: { browser: false, export: false, server: false },
|
||||
isSidebarDocked: { browser: true, export: false, server: false },
|
||||
defaultSidebarDockedPreference: {
|
||||
browser: true,
|
||||
export: false,
|
||||
server: false,
|
||||
},
|
||||
isLoading: { browser: false, export: false, server: false },
|
||||
isResizing: { browser: false, export: false, server: false },
|
||||
isRotating: { browser: false, export: false, server: false },
|
||||
|
@ -210,6 +210,8 @@ import {
|
||||
PointerDownState,
|
||||
SceneData,
|
||||
Device,
|
||||
SidebarName,
|
||||
SidebarTabName,
|
||||
} from "../types";
|
||||
import {
|
||||
debounce,
|
||||
@ -299,6 +301,9 @@ import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
|
||||
import { actionWrapTextInContainer } from "../actions/actionBoundText";
|
||||
import BraveMeasureTextError from "./BraveMeasureTextError";
|
||||
|
||||
const AppContext = React.createContext<AppClassProperties>(null!);
|
||||
const AppPropsContext = React.createContext<AppProps>(null!);
|
||||
|
||||
const deviceContextInitialValue = {
|
||||
isSmScreen: false,
|
||||
isMobile: false,
|
||||
@ -340,6 +345,8 @@ const ExcalidrawActionManagerContext = React.createContext<ActionManager>(
|
||||
);
|
||||
ExcalidrawActionManagerContext.displayName = "ExcalidrawActionManagerContext";
|
||||
|
||||
export const useApp = () => useContext(AppContext);
|
||||
export const useAppProps = () => useContext(AppPropsContext);
|
||||
export const useDevice = () => useContext<Device>(DeviceContext);
|
||||
export const useExcalidrawContainer = () =>
|
||||
useContext(ExcalidrawContainerContext);
|
||||
@ -400,7 +407,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
private nearestScrollableContainer: HTMLElement | Document | undefined;
|
||||
public library: AppClassProperties["library"];
|
||||
public libraryItemsFromStorage: LibraryItems | undefined;
|
||||
private id: string;
|
||||
public id: string;
|
||||
private history: History;
|
||||
private excalidrawContainerValue: {
|
||||
container: HTMLDivElement | null;
|
||||
@ -438,7 +445,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
showHyperlinkPopup: false,
|
||||
isSidebarDocked: false,
|
||||
defaultSidebarDockedPreference: false,
|
||||
};
|
||||
|
||||
this.id = nanoid();
|
||||
@ -469,7 +476,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
setActiveTool: this.setActiveTool,
|
||||
setCursor: this.setCursor,
|
||||
resetCursor: this.resetCursor,
|
||||
toggleMenu: this.toggleMenu,
|
||||
toggleSidebar: this.toggleSidebar,
|
||||
} as const;
|
||||
if (typeof excalidrawRef === "function") {
|
||||
excalidrawRef(api);
|
||||
@ -577,101 +584,91 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.props.handleKeyboardGlobally ? undefined : this.onKeyDown
|
||||
}
|
||||
>
|
||||
<ExcalidrawContainerContext.Provider
|
||||
value={this.excalidrawContainerValue}
|
||||
>
|
||||
<DeviceContext.Provider value={this.device}>
|
||||
<ExcalidrawSetAppStateContext.Provider value={this.setAppState}>
|
||||
<ExcalidrawAppStateContext.Provider value={this.state}>
|
||||
<ExcalidrawElementsContext.Provider
|
||||
value={this.scene.getNonDeletedElements()}
|
||||
>
|
||||
<ExcalidrawActionManagerContext.Provider
|
||||
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
|
||||
}
|
||||
<AppContext.Provider value={this}>
|
||||
<AppPropsContext.Provider value={this.props}>
|
||||
<ExcalidrawContainerContext.Provider
|
||||
value={this.excalidrawContainerValue}
|
||||
>
|
||||
<DeviceContext.Provider value={this.device}>
|
||||
<ExcalidrawSetAppStateContext.Provider value={this.setAppState}>
|
||||
<ExcalidrawAppStateContext.Provider value={this.state}>
|
||||
<ExcalidrawElementsContext.Provider
|
||||
value={this.scene.getNonDeletedElements()}
|
||||
>
|
||||
{this.props.children}
|
||||
</LayerUI>
|
||||
<div className="excalidraw-textEditorContainer" />
|
||||
<div className="excalidraw-contextMenuContainer" />
|
||||
{selectedElement.length === 1 &&
|
||||
!this.state.contextMenu &&
|
||||
this.state.showHyperlinkPopup && (
|
||||
<Hyperlink
|
||||
key={selectedElement[0].id}
|
||||
element={selectedElement[0]}
|
||||
<ExcalidrawActionManagerContext.Provider
|
||||
value={this.actionManager}
|
||||
>
|
||||
<LayerUI
|
||||
canvas={this.canvas}
|
||||
appState={this.state}
|
||||
files={this.files}
|
||||
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>
|
||||
actionManager={this.actionManager}
|
||||
elements={this.scene.getNonDeletedElements()}
|
||||
onLockToggle={this.toggleLock}
|
||||
onPenModeToggle={this.togglePenMode}
|
||||
onHandToolToggle={this.onHandToolToggle}
|
||||
langCode={getLanguage().code}
|
||||
renderTopRightUI={renderTopRightUI}
|
||||
renderCustomStats={renderCustomStats}
|
||||
showExitZenModeBtn={
|
||||
typeof this.props?.zenModeEnabled === "undefined" &&
|
||||
this.state.zenModeEnabled
|
||||
}
|
||||
UIOptions={this.props.UIOptions}
|
||||
onImageAction={this.onImageAction}
|
||||
renderWelcomeScreen={
|
||||
!this.state.isLoading &&
|
||||
this.state.showWelcomeScreen &&
|
||||
this.state.activeTool.type === "selection" &&
|
||||
!this.scene.getElementsIncludingDeleted().length
|
||||
}
|
||||
>
|
||||
{this.props.children}
|
||||
</LayerUI>
|
||||
<div className="excalidraw-textEditorContainer" />
|
||||
<div className="excalidraw-contextMenuContainer" />
|
||||
{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>
|
||||
);
|
||||
}
|
||||
|
||||
public focusContainer: AppClassProperties["focusContainer"] = () => {
|
||||
if (this.props.autoFocus) {
|
||||
this.excalidrawContainerRef.current?.focus();
|
||||
}
|
||||
this.excalidrawContainerRef.current?.focus();
|
||||
};
|
||||
|
||||
public getSceneElementsIncludingDeleted = () => {
|
||||
@ -682,6 +679,14 @@ class App extends React.Component<AppProps, AppState> {
|
||||
return this.scene.getNonDeletedElements();
|
||||
};
|
||||
|
||||
public onInsertElements = (elements: readonly ExcalidrawElement[]) => {
|
||||
this.addElementsFromPasteOrLibrary({
|
||||
elements,
|
||||
position: "center",
|
||||
files: null,
|
||||
});
|
||||
};
|
||||
|
||||
private syncActionResult = withBatchedUpdates(
|
||||
(actionResult: ActionResult) => {
|
||||
if (this.unmounted || actionResult === false) {
|
||||
@ -951,7 +956,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.scene.addCallback(this.onSceneUpdated);
|
||||
this.addEventListeners();
|
||||
|
||||
if (this.excalidrawContainerRef.current) {
|
||||
if (this.props.autoFocus && this.excalidrawContainerRef.current) {
|
||||
this.focusContainer();
|
||||
}
|
||||
|
||||
@ -1679,7 +1684,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
openSidebar:
|
||||
this.state.openSidebar &&
|
||||
this.device.canDeviceFitSidebar &&
|
||||
this.state.isSidebarDocked
|
||||
this.state.defaultSidebarDockedPreference
|
||||
? this.state.openSidebar
|
||||
: null,
|
||||
selectedElementIds: newElements.reduce(
|
||||
@ -2017,30 +2022,24 @@ class App extends React.Component<AppProps, AppState> {
|
||||
/**
|
||||
* @returns whether the menu was toggled on or off
|
||||
*/
|
||||
public toggleMenu = (
|
||||
type: "library" | "customSidebar",
|
||||
force?: boolean,
|
||||
): boolean => {
|
||||
if (type === "customSidebar" && !this.props.renderSidebar) {
|
||||
console.warn(
|
||||
`attempting to toggle "customSidebar", but no "props.renderSidebar" is defined`,
|
||||
);
|
||||
return false;
|
||||
public toggleSidebar = ({
|
||||
name,
|
||||
tab,
|
||||
force,
|
||||
}: {
|
||||
name: SidebarName;
|
||||
tab?: SidebarTabName;
|
||||
force?: boolean;
|
||||
}): 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") {
|
||||
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;
|
||||
return !!nextName;
|
||||
};
|
||||
|
||||
private updateCurrentCursorPosition = withBatchedUpdates(
|
||||
|
@ -1,8 +1,12 @@
|
||||
import clsx from "clsx";
|
||||
import { composeEventHandlers } from "../utils";
|
||||
import "./Button.scss";
|
||||
|
||||
interface ButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
|
||||
type?: "button" | "submit" | "reset";
|
||||
onSelect: () => any;
|
||||
/** whether button is in active state */
|
||||
selected?: boolean;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
@ -15,18 +19,18 @@ interface ButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
|
||||
export const Button = ({
|
||||
type = "button",
|
||||
onSelect,
|
||||
selected,
|
||||
children,
|
||||
className = "",
|
||||
...rest
|
||||
}: ButtonProps) => {
|
||||
return (
|
||||
<button
|
||||
onClick={(event) => {
|
||||
onClick={composeEventHandlers(rest.onClick, (event) => {
|
||||
onSelect();
|
||||
rest.onClick?.(event);
|
||||
}}
|
||||
})}
|
||||
type={type}
|
||||
className={`excalidraw-button ${className}`}
|
||||
className={clsx("excalidraw-button", className, { selected })}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
|
@ -4,8 +4,8 @@ import { Dialog, DialogProps } from "./Dialog";
|
||||
import "./ConfirmDialog.scss";
|
||||
import DialogActionButton from "./DialogActionButton";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { isLibraryMenuOpenAtom } from "./LibraryMenuHeaderContent";
|
||||
import { useExcalidrawSetAppState } from "./App";
|
||||
import { isLibraryMenuOpenAtom } from "./LibraryMenu";
|
||||
import { useExcalidrawContainer, useExcalidrawSetAppState } from "./App";
|
||||
import { jotaiScope } from "../jotai";
|
||||
|
||||
interface Props extends Omit<DialogProps, "onCloseRequest"> {
|
||||
@ -26,6 +26,7 @@ const ConfirmDialog = (props: Props) => {
|
||||
} = props;
|
||||
const setAppState = useExcalidrawSetAppState();
|
||||
const setIsLibraryMenuOpen = useSetAtom(isLibraryMenuOpenAtom, jotaiScope);
|
||||
const { container } = useExcalidrawContainer();
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
@ -42,6 +43,7 @@ const ConfirmDialog = (props: Props) => {
|
||||
setAppState({ openMenu: null });
|
||||
setIsLibraryMenuOpen(false);
|
||||
onCancel();
|
||||
container?.focus();
|
||||
}}
|
||||
/>
|
||||
<DialogActionButton
|
||||
@ -50,6 +52,7 @@ const ConfirmDialog = (props: Props) => {
|
||||
setAppState({ openMenu: null });
|
||||
setIsLibraryMenuOpen(false);
|
||||
onConfirm();
|
||||
container?.focus();
|
||||
}}
|
||||
actionType="danger"
|
||||
/>
|
||||
|
144
src/components/DefaultSidebar.test.tsx
Normal file
144
src/components/DefaultSidebar.test.tsx
Normal 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");
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
118
src/components/DefaultSidebar.tsx
Normal file
118
src/components/DefaultSidebar.tsx
Normal 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,
|
||||
},
|
||||
);
|
@ -15,7 +15,7 @@ import { Modal } from "./Modal";
|
||||
import { AppState } from "../types";
|
||||
import { queryFocusableElements } from "../utils";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { isLibraryMenuOpenAtom } from "./LibraryMenuHeaderContent";
|
||||
import { isLibraryMenuOpenAtom } from "./LibraryMenu";
|
||||
import { jotaiScope } from "../jotai";
|
||||
|
||||
export interface DialogProps {
|
||||
|
@ -29,7 +29,7 @@ const getHints = ({
|
||||
const { activeTool, isResizing, isRotating, lastPointerDownWith } = appState;
|
||||
const multiMode = appState.multiElement !== null;
|
||||
|
||||
if (appState.openSidebar === "library" && !device.canDeviceFitSidebar) {
|
||||
if (appState.openSidebar && !device.canDeviceFitSidebar) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import clsx from "clsx";
|
||||
import React from "react";
|
||||
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 { isTextElement, showSelectedShapeActions } from "../element";
|
||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||
@ -9,7 +9,7 @@ import { Language, t } from "../i18n";
|
||||
import { calculateScrollCenter } from "../scene";
|
||||
import { ExportType } from "../scene/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 { ErrorDialog } from "./ErrorDialog";
|
||||
import { ExportCB, ImageExportDialog } from "./ImageExportDialog";
|
||||
@ -24,28 +24,28 @@ import { Section } from "./Section";
|
||||
import { HelpDialog } from "./HelpDialog";
|
||||
import Stack from "./Stack";
|
||||
import { UserList } from "./UserList";
|
||||
import Library from "../data/library";
|
||||
import { JSONExportDialog } from "./JSONExportDialog";
|
||||
import { LibraryButton } from "./LibraryButton";
|
||||
import { isImageFileHandle } from "../data/blob";
|
||||
import { LibraryMenu } from "./LibraryMenu";
|
||||
|
||||
import "./LayerUI.scss";
|
||||
import "./Toolbar.scss";
|
||||
import { PenModeButton } from "./PenModeButton";
|
||||
import { trackEvent } from "../analytics";
|
||||
import { useDevice } from "../components/App";
|
||||
import { Stats } from "./Stats";
|
||||
import { actionToggleStats } from "../actions/actionToggleStats";
|
||||
import Footer from "./footer/Footer";
|
||||
import { hostSidebarCountersAtom } from "./Sidebar/Sidebar";
|
||||
import { isSidebarDockedAtom } from "./Sidebar/Sidebar";
|
||||
import { jotaiScope } from "../jotai";
|
||||
import { Provider, useAtom } from "jotai";
|
||||
import { Provider, useAtomValue } from "jotai";
|
||||
import MainMenu from "./main-menu/MainMenu";
|
||||
import { ActiveConfirmDialog } from "./ActiveConfirmDialog";
|
||||
import { HandButton } from "./HandButton";
|
||||
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 {
|
||||
actionManager: ActionManager;
|
||||
@ -57,17 +57,11 @@ interface LayerUIProps {
|
||||
onLockToggle: () => void;
|
||||
onHandToolToggle: () => void;
|
||||
onPenModeToggle: () => void;
|
||||
onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void;
|
||||
showExitZenModeBtn: boolean;
|
||||
langCode: Language["code"];
|
||||
renderTopRightUI?: ExcalidrawProps["renderTopRightUI"];
|
||||
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
|
||||
renderCustomSidebar?: ExcalidrawProps["renderSidebar"];
|
||||
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
||||
UIOptions: AppProps["UIOptions"];
|
||||
focusContainer: () => void;
|
||||
library: Library;
|
||||
id: string;
|
||||
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
|
||||
renderWelcomeScreen: boolean;
|
||||
children?: React.ReactNode;
|
||||
@ -109,16 +103,10 @@ const LayerUI = ({
|
||||
onLockToggle,
|
||||
onHandToolToggle,
|
||||
onPenModeToggle,
|
||||
onInsertElements,
|
||||
showExitZenModeBtn,
|
||||
renderTopRightUI,
|
||||
renderCustomStats,
|
||||
renderCustomSidebar,
|
||||
libraryReturnUrl,
|
||||
UIOptions,
|
||||
focusContainer,
|
||||
library,
|
||||
id,
|
||||
onImageAction,
|
||||
renderWelcomeScreen,
|
||||
children,
|
||||
@ -197,8 +185,8 @@ const LayerUI = ({
|
||||
<div style={{ position: "relative" }}>
|
||||
{/* wrapping to Fragment stops React from occasionally complaining
|
||||
about identical Keys */}
|
||||
<tunnels.mainMenuTunnel.Out />
|
||||
{renderWelcomeScreen && <tunnels.welcomeScreenMenuHintTunnel.Out />}
|
||||
<tunnels.MainMenuTunnel.Out />
|
||||
{renderWelcomeScreen && <tunnels.WelcomeScreenMenuHintTunnel.Out />}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -250,7 +238,7 @@ const LayerUI = ({
|
||||
{(heading: React.ReactNode) => (
|
||||
<div style={{ position: "relative" }}>
|
||||
{renderWelcomeScreen && (
|
||||
<tunnels.welcomeScreenToolbarHintTunnel.Out />
|
||||
<tunnels.WelcomeScreenToolbarHintTunnel.Out />
|
||||
)}
|
||||
<Stack.Col gap={4} align="start">
|
||||
<Stack.Row
|
||||
@ -324,9 +312,12 @@ const LayerUI = ({
|
||||
>
|
||||
<UserList collaborators={appState.collaborators} />
|
||||
{renderTopRightUI?.(device.isMobile, appState)}
|
||||
{!appState.viewModeEnabled && (
|
||||
<LibraryButton appState={appState} setAppState={setAppState} />
|
||||
)}
|
||||
{!appState.viewModeEnabled &&
|
||||
// hide button when sidebar docked
|
||||
(!isSidebarDocked ||
|
||||
appState.openSidebar?.name !== DEFAULT_SIDEBAR.name) && (
|
||||
<tunnels.DefaultSidebarTriggerTunnel.Out />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</FixedSideContainer>
|
||||
@ -334,21 +325,21 @@ const LayerUI = ({
|
||||
};
|
||||
|
||||
const renderSidebars = () => {
|
||||
return appState.openSidebar === "customSidebar" ? (
|
||||
renderCustomSidebar?.() || null
|
||||
) : appState.openSidebar === "library" ? (
|
||||
<LibraryMenu
|
||||
appState={appState}
|
||||
onInsertElements={onInsertElements}
|
||||
libraryReturnUrl={libraryReturnUrl}
|
||||
focusContainer={focusContainer}
|
||||
library={library}
|
||||
id={id}
|
||||
return (
|
||||
<DefaultSidebar
|
||||
__fallback
|
||||
onDock={(docked) => {
|
||||
trackEvent(
|
||||
"sidebar",
|
||||
`toggleDock (${docked ? "dock" : "undock"})`,
|
||||
`(${device.isMobile ? "mobile" : "desktop"})`,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
) : null;
|
||||
);
|
||||
};
|
||||
|
||||
const [hostSidebarCounters] = useAtom(hostSidebarCountersAtom, jotaiScope);
|
||||
const isSidebarDocked = useAtomValue(isSidebarDockedAtom, jotaiScope);
|
||||
|
||||
const layerUIJSX = (
|
||||
<>
|
||||
@ -358,8 +349,25 @@ const LayerUI = ({
|
||||
{children}
|
||||
{/* render component fallbacks. Can be rendered anywhere as they'll be
|
||||
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} />
|
||||
<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} />}
|
||||
@ -382,7 +390,6 @@ const LayerUI = ({
|
||||
<PasteChartDialog
|
||||
setAppState={setAppState}
|
||||
appState={appState}
|
||||
onInsertChart={onInsertElements}
|
||||
onClose={() =>
|
||||
setAppState({
|
||||
pasteDialog: { shown: false, data: null },
|
||||
@ -410,7 +417,6 @@ const LayerUI = ({
|
||||
renderWelcomeScreen={renderWelcomeScreen}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!device.isMobile && (
|
||||
<>
|
||||
<div
|
||||
@ -422,15 +428,14 @@ const LayerUI = ({
|
||||
!isTextElement(appState.editingElement)),
|
||||
})}
|
||||
style={
|
||||
((appState.openSidebar === "library" &&
|
||||
appState.isSidebarDocked) ||
|
||||
hostSidebarCounters.docked) &&
|
||||
appState.openSidebar &&
|
||||
isSidebarDocked &&
|
||||
device.canDeviceFitSidebar
|
||||
? { width: `calc(100% - ${LIBRARY_SIDEBAR_WIDTH}px)` }
|
||||
: {}
|
||||
}
|
||||
>
|
||||
{renderWelcomeScreen && <tunnels.welcomeScreenCenterTunnel.Out />}
|
||||
{renderWelcomeScreen && <tunnels.WelcomeScreenCenterTunnel.Out />}
|
||||
{renderFixedSideContainer()}
|
||||
<Footer
|
||||
appState={appState}
|
||||
@ -469,17 +474,22 @@ const LayerUI = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<Provider scope={tunnels.jotaiScope}>
|
||||
<TunnelsContext.Provider value={tunnels}>
|
||||
{layerUIJSX}
|
||||
</TunnelsContext.Provider>
|
||||
</Provider>
|
||||
<UIAppStateContext.Provider value={appState}>
|
||||
<Provider scope={tunnels.jotaiScope}>
|
||||
<TunnelsContext.Provider value={tunnels}>
|
||||
{layerUIJSX}
|
||||
</TunnelsContext.Provider>
|
||||
</Provider>
|
||||
</UIAppStateContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const stripIrrelevantAppStateProps = (
|
||||
appState: AppState,
|
||||
): Partial<AppState> => {
|
||||
): Omit<
|
||||
AppState,
|
||||
"suggestedBindings" | "startBoundElement" | "cursorButton"
|
||||
> => {
|
||||
const { suggestedBindings, startBoundElement, cursorButton, ...ret } =
|
||||
appState;
|
||||
return ret;
|
||||
@ -491,24 +501,17 @@ const areEqual = (prevProps: LayerUIProps, nextProps: LayerUIProps) => {
|
||||
return false;
|
||||
}
|
||||
|
||||
const {
|
||||
canvas: _prevCanvas,
|
||||
// not stable, but shouldn't matter in our case
|
||||
onInsertElements: _prevOnInsertElements,
|
||||
appState: prevAppState,
|
||||
...prev
|
||||
} = prevProps;
|
||||
const {
|
||||
canvas: _nextCanvas,
|
||||
onInsertElements: _nextOnInsertElements,
|
||||
appState: nextAppState,
|
||||
...next
|
||||
} = nextProps;
|
||||
const { canvas: _prevCanvas, appState: prevAppState, ...prev } = prevProps;
|
||||
const { canvas: _nextCanvas, appState: nextAppState, ...next } = nextProps;
|
||||
|
||||
return (
|
||||
isShallowEqual(
|
||||
stripIrrelevantAppStateProps(prevAppState),
|
||||
stripIrrelevantAppStateProps(nextAppState),
|
||||
{
|
||||
selectedElementIds: isShallowEqual,
|
||||
selectedGroupIds: isShallowEqual,
|
||||
},
|
||||
) && isShallowEqual(prev, next)
|
||||
);
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
};
|
@ -1,9 +1,9 @@
|
||||
@import "open-color/open-color";
|
||||
|
||||
.excalidraw {
|
||||
.layer-ui__library-sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.library-menu-items-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.layer-ui__library {
|
||||
@ -11,28 +11,6 @@
|
||||
flex-direction: column;
|
||||
|
||||
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 {
|
||||
@ -87,10 +65,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
.library-menu-browse-button {
|
||||
margin: 1rem auto;
|
||||
.library-menu-control-buttons {
|
||||
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;
|
||||
align-items: center;
|
||||
@ -122,30 +107,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
.library-menu-browse-button--mobile {
|
||||
min-height: 22px;
|
||||
margin-left: auto;
|
||||
a {
|
||||
padding-right: 0;
|
||||
}
|
||||
&.excalidraw--mobile .library-menu-browse-button {
|
||||
height: var(--default-button-size);
|
||||
}
|
||||
|
||||
.layer-ui__sidebar__header .dropdown-menu {
|
||||
&.dropdown-menu--mobile {
|
||||
top: 100%;
|
||||
}
|
||||
.layer-ui__library .dropdown-menu {
|
||||
width: auto;
|
||||
top: initial;
|
||||
right: 0;
|
||||
left: initial;
|
||||
bottom: 100%;
|
||||
margin-bottom: 0.625rem;
|
||||
|
||||
.dropdown-menu-container {
|
||||
--gap: 0;
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
|
||||
:root[dir="rtl"] & {
|
||||
right: 0;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
width: 196px;
|
||||
box-shadow: var(--library-dropdown-shadow);
|
||||
border-radius: var(--border-radius-lg);
|
||||
|
@ -1,11 +1,4 @@
|
||||
import {
|
||||
useRef,
|
||||
useState,
|
||||
useEffect,
|
||||
useCallback,
|
||||
RefObject,
|
||||
forwardRef,
|
||||
} from "react";
|
||||
import React, { useState, useCallback } from "react";
|
||||
import Library, {
|
||||
distributeLibraryItemsOnSquareGrid,
|
||||
libraryItemsAtom,
|
||||
@ -13,65 +6,29 @@ import Library, {
|
||||
import { t } from "../i18n";
|
||||
import { randomId } from "../random";
|
||||
import { LibraryItems, LibraryItem, AppState, ExcalidrawProps } from "../types";
|
||||
|
||||
import "./LibraryMenu.scss";
|
||||
import LibraryMenuItems from "./LibraryMenuItems";
|
||||
import { EVENT } from "../constants";
|
||||
import { KEYS } from "../keys";
|
||||
import { trackEvent } from "../analytics";
|
||||
import { useAtom } from "jotai";
|
||||
import { atom, useAtom } from "jotai";
|
||||
import { jotaiScope } from "../jotai";
|
||||
import Spinner from "./Spinner";
|
||||
import {
|
||||
useDevice,
|
||||
useApp,
|
||||
useAppProps,
|
||||
useExcalidrawElements,
|
||||
useExcalidrawSetAppState,
|
||||
} from "./App";
|
||||
import { Sidebar } from "./Sidebar/Sidebar";
|
||||
import { getSelectedElements } from "../scene";
|
||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||
import { LibraryMenuHeader } from "./LibraryMenuHeaderContent";
|
||||
import LibraryMenuBrowseButton from "./LibraryMenuBrowseButton";
|
||||
import { useUIAppState } from "../context/ui-appState";
|
||||
|
||||
const useOnClickOutside = (
|
||||
ref: RefObject<HTMLElement>,
|
||||
cb: (event: MouseEvent) => void,
|
||||
) => {
|
||||
useEffect(() => {
|
||||
const listener = (event: MouseEvent) => {
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
import "./LibraryMenu.scss";
|
||||
import { LibraryMenuControlButtons } from "./LibraryMenuControlButtons";
|
||||
|
||||
if (
|
||||
event.target instanceof Element &&
|
||||
(ref.current.contains(event.target) ||
|
||||
!document.body.contains(event.target))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
export const isLibraryMenuOpenAtom = atom(false);
|
||||
|
||||
cb(event);
|
||||
};
|
||||
document.addEventListener("pointerdown", listener, false);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("pointerdown", listener);
|
||||
};
|
||||
}, [ref, cb]);
|
||||
const LibraryMenuWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
return <div className="layer-ui__library">{children}</div>;
|
||||
};
|
||||
|
||||
const LibraryMenuWrapper = forwardRef<
|
||||
HTMLDivElement,
|
||||
{ children: React.ReactNode }
|
||||
>(({ children }, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="layer-ui__library">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const LibraryMenuContent = ({
|
||||
onInsertLibraryItems,
|
||||
pendingElements,
|
||||
@ -158,81 +115,31 @@ export const LibraryMenuContent = ({
|
||||
theme={appState.theme}
|
||||
/>
|
||||
{showBtn && (
|
||||
<LibraryMenuBrowseButton
|
||||
<LibraryMenuControlButtons
|
||||
style={{ padding: "16px 12px 0 12px" }}
|
||||
id={id}
|
||||
libraryReturnUrl={libraryReturnUrl}
|
||||
theme={appState.theme}
|
||||
selectedItems={selectedItems}
|
||||
onSelectItems={onSelectItems}
|
||||
/>
|
||||
)}
|
||||
</LibraryMenuWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export const LibraryMenu: React.FC<{
|
||||
appState: AppState;
|
||||
onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void;
|
||||
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
|
||||
focusContainer: () => void;
|
||||
library: Library;
|
||||
id: string;
|
||||
}> = ({
|
||||
appState,
|
||||
onInsertElements,
|
||||
libraryReturnUrl,
|
||||
focusContainer,
|
||||
library,
|
||||
id,
|
||||
}) => {
|
||||
/**
|
||||
* This component is meant to be rendered inside <Sidebar.Tab/> inside our
|
||||
* <DefaultSidebar/> or host apps Sidebar components.
|
||||
*/
|
||||
export const LibraryMenu = () => {
|
||||
const { library, id, onInsertElements } = useApp();
|
||||
const appProps = useAppProps();
|
||||
const appState = useUIAppState();
|
||||
const setAppState = useExcalidrawSetAppState();
|
||||
const elements = useExcalidrawElements();
|
||||
const device = useDevice();
|
||||
|
||||
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(() => {
|
||||
setAppState({
|
||||
@ -241,69 +148,20 @@ export const LibraryMenu: React.FC<{
|
||||
});
|
||||
}, [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 (
|
||||
<Sidebar
|
||||
__isInternal
|
||||
// necessary to remount when switching between internal
|
||||
// and custom (host app) sidebar, so that the `props.onClose`
|
||||
// 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"})`,
|
||||
);
|
||||
<LibraryMenuContent
|
||||
pendingElements={getSelectedElements(elements, appState, true)}
|
||||
onInsertLibraryItems={(libraryItems) => {
|
||||
onInsertElements(distributeLibraryItemsOnSquareGrid(libraryItems));
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
<Sidebar.Header className="layer-ui__library-header">
|
||||
<LibraryMenuHeader
|
||||
appState={appState}
|
||||
setAppState={setAppState}
|
||||
selectedItems={selectedItems}
|
||||
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>
|
||||
onAddToLibrary={deselectItems}
|
||||
setAppState={setAppState}
|
||||
libraryReturnUrl={appProps.libraryReturnUrl}
|
||||
library={library}
|
||||
id={id}
|
||||
appState={appState}
|
||||
selectedItems={selectedItems}
|
||||
onSelectItems={setSelectedItems}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
33
src/components/LibraryMenuControlButtons.tsx
Normal file
33
src/components/LibraryMenuControlButtons.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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 Library, { libraryItemsAtom } from "../data/library";
|
||||
import { t } from "../i18n";
|
||||
import { AppState, LibraryItem, LibraryItems } from "../types";
|
||||
import {
|
||||
DotsIcon,
|
||||
ExportIcon,
|
||||
@ -13,22 +15,19 @@ import {
|
||||
import { ToolButton } from "./ToolButton";
|
||||
import { fileOpen } from "../data/filesystem";
|
||||
import { muteFSAbortError } from "../utils";
|
||||
import { atom, useAtom } from "jotai";
|
||||
import { jotaiScope } from "../jotai";
|
||||
import { useAtom } from "jotai";
|
||||
import ConfirmDialog from "./ConfirmDialog";
|
||||
import PublishLibrary from "./PublishLibrary";
|
||||
import { Dialog } from "./Dialog";
|
||||
|
||||
import DropdownMenu from "./dropdownMenu/DropdownMenu";
|
||||
|
||||
export const isLibraryMenuOpenAtom = atom(false);
|
||||
import { isLibraryMenuOpenAtom } from "./LibraryMenu";
|
||||
|
||||
const getSelectedItems = (
|
||||
libraryItems: LibraryItems,
|
||||
selectedItems: LibraryItem["id"][],
|
||||
) => libraryItems.filter((item) => selectedItems.includes(item.id));
|
||||
|
||||
export const LibraryMenuHeader: React.FC<{
|
||||
export const LibraryDropdownMenuButton: React.FC<{
|
||||
setAppState: React.Component<any, AppState>["setState"];
|
||||
selectedItems: LibraryItem["id"][];
|
||||
library: Library;
|
||||
@ -50,6 +49,7 @@ export const LibraryMenuHeader: React.FC<{
|
||||
isLibraryMenuOpenAtom,
|
||||
jotaiScope,
|
||||
);
|
||||
|
||||
const renderRemoveLibAlert = useCallback(() => {
|
||||
const content = selectedItems.length
|
||||
? t("alerts.removeItemsFromsLibrary", { count: selectedItems.length })
|
||||
@ -181,7 +181,6 @@ export const LibraryMenuHeader: React.FC<{
|
||||
return (
|
||||
<DropdownMenu open={isLibraryMenuOpen}>
|
||||
<DropdownMenu.Trigger
|
||||
className="Sidebar__dropdown-btn"
|
||||
onToggle={() => setIsLibraryMenuOpen(!isLibraryMenuOpen)}
|
||||
>
|
||||
{DotsIcon}
|
||||
@ -230,6 +229,7 @@ export const LibraryMenuHeader: React.FC<{
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ position: "relative" }}>
|
||||
{renderLibraryMenu()}
|
||||
@ -261,3 +261,48 @@ export const LibraryMenuHeader: React.FC<{
|
||||
</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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -47,7 +47,7 @@
|
||||
|
||||
&__items {
|
||||
row-gap: 0.5rem;
|
||||
padding: var(--container-padding-y) var(--container-padding-x);
|
||||
padding: var(--container-padding-y) 0;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
@ -61,7 +61,7 @@
|
||||
margin-bottom: 0.75rem;
|
||||
|
||||
&--excal {
|
||||
margin-top: 2.5rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -10,9 +10,8 @@ import Stack from "./Stack";
|
||||
import "./LibraryMenuItems.scss";
|
||||
import { MIME_TYPES } from "../constants";
|
||||
import Spinner from "./Spinner";
|
||||
import LibraryMenuBrowseButton from "./LibraryMenuBrowseButton";
|
||||
import clsx from "clsx";
|
||||
import { duplicateElements } from "../element/newElement";
|
||||
import { LibraryMenuControlButtons } from "./LibraryMenuControlButtons";
|
||||
|
||||
const CELLS_PER_ROW = 4;
|
||||
|
||||
@ -201,11 +200,7 @@ const LibraryMenuItems = ({
|
||||
(item) => item.status === "published",
|
||||
);
|
||||
|
||||
const showBtn =
|
||||
!libraryItems.length &&
|
||||
!unpublishedItems.length &&
|
||||
!publishedItems.length &&
|
||||
!pendingElements.length;
|
||||
const showBtn = !libraryItems.length && !pendingElements.length;
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -215,7 +210,7 @@ const LibraryMenuItems = ({
|
||||
unpublishedItems.length ||
|
||||
publishedItems.length
|
||||
? { justifyContent: "flex-start" }
|
||||
: {}
|
||||
: { borderBottom: 0 }
|
||||
}
|
||||
>
|
||||
<Stack.Col
|
||||
@ -251,11 +246,7 @@ const LibraryMenuItems = ({
|
||||
</div>
|
||||
{!pendingElements.length && !unpublishedItems.length ? (
|
||||
<div className="library-menu-items__no-items">
|
||||
<div
|
||||
className={clsx({
|
||||
"library-menu-items__no-items__label": showBtn,
|
||||
})}
|
||||
>
|
||||
<div className="library-menu-items__no-items__label">
|
||||
{t("library.noItems")}
|
||||
</div>
|
||||
<div className="library-menu-items__no-items__hint">
|
||||
@ -303,10 +294,13 @@ const LibraryMenuItems = ({
|
||||
</>
|
||||
|
||||
{showBtn && (
|
||||
<LibraryMenuBrowseButton
|
||||
<LibraryMenuControlButtons
|
||||
style={{ padding: "16px 0", width: "100%" }}
|
||||
id={id}
|
||||
libraryReturnUrl={libraryReturnUrl}
|
||||
theme={theme}
|
||||
selectedItems={selectedItems}
|
||||
onSelectItems={onSelectItems}
|
||||
/>
|
||||
)}
|
||||
</Stack.Col>
|
||||
|
@ -13,13 +13,12 @@ import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
|
||||
import { Section } from "./Section";
|
||||
import { SCROLLBAR_WIDTH, SCROLLBAR_MARGIN } from "../scene/scrollbars";
|
||||
import { LockButton } from "./LockButton";
|
||||
import { LibraryButton } from "./LibraryButton";
|
||||
import { PenModeButton } from "./PenModeButton";
|
||||
import { Stats } from "./Stats";
|
||||
import { actionToggleStats } from "../actions";
|
||||
import { HandButton } from "./HandButton";
|
||||
import { isHandToolActive } from "../appState";
|
||||
import { useTunnels } from "./context/tunnels";
|
||||
import { useTunnels } from "../context/tunnels";
|
||||
|
||||
type MobileMenuProps = {
|
||||
appState: AppState;
|
||||
@ -60,11 +59,15 @@ export const MobileMenu = ({
|
||||
device,
|
||||
renderWelcomeScreen,
|
||||
}: MobileMenuProps) => {
|
||||
const { welcomeScreenCenterTunnel, mainMenuTunnel } = useTunnels();
|
||||
const {
|
||||
WelcomeScreenCenterTunnel,
|
||||
MainMenuTunnel,
|
||||
DefaultSidebarTriggerTunnel,
|
||||
} = useTunnels();
|
||||
const renderToolbar = () => {
|
||||
return (
|
||||
<FixedSideContainer side="top" className="App-top-bar">
|
||||
{renderWelcomeScreen && <welcomeScreenCenterTunnel.Out />}
|
||||
{renderWelcomeScreen && <WelcomeScreenCenterTunnel.Out />}
|
||||
<Section heading="shapes">
|
||||
{(heading: React.ReactNode) => (
|
||||
<Stack.Col gap={4} align="center">
|
||||
@ -88,11 +91,7 @@ export const MobileMenu = ({
|
||||
{renderTopRightUI && renderTopRightUI(true, appState)}
|
||||
<div className="mobile-misc-tools-container">
|
||||
{!appState.viewModeEnabled && (
|
||||
<LibraryButton
|
||||
appState={appState}
|
||||
setAppState={setAppState}
|
||||
isMobile
|
||||
/>
|
||||
<DefaultSidebarTriggerTunnel.Out />
|
||||
)}
|
||||
<PenModeButton
|
||||
checked={appState.penMode}
|
||||
@ -132,14 +131,14 @@ export const MobileMenu = ({
|
||||
if (appState.viewModeEnabled) {
|
||||
return (
|
||||
<div className="App-toolbar-content">
|
||||
<mainMenuTunnel.Out />
|
||||
<MainMenuTunnel.Out />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="App-toolbar-content">
|
||||
<mainMenuTunnel.Out />
|
||||
<MainMenuTunnel.Out />
|
||||
{actionManager.renderAction("toggleEditMenu")}
|
||||
{actionManager.renderAction("undo")}
|
||||
{actionManager.renderAction("redo")}
|
||||
@ -190,7 +189,7 @@ export const MobileMenu = ({
|
||||
{renderAppToolbar()}
|
||||
{appState.scrolledOutside &&
|
||||
!appState.openMenu &&
|
||||
appState.openSidebar !== "library" && (
|
||||
!appState.openSidebar && (
|
||||
<button
|
||||
className="scroll-back-to-content"
|
||||
onClick={() => {
|
||||
|
@ -5,7 +5,8 @@ import { ChartElements, renderSpreadsheet, Spreadsheet } from "../charts";
|
||||
import { ChartType } from "../element/types";
|
||||
import { t } from "../i18n";
|
||||
import { exportToSvg } from "../scene/export";
|
||||
import { AppState, LibraryItem } from "../types";
|
||||
import { AppState } from "../types";
|
||||
import { useApp } from "./App";
|
||||
import { Dialog } from "./Dialog";
|
||||
import "./PasteChartDialog.scss";
|
||||
|
||||
@ -78,13 +79,12 @@ export const PasteChartDialog = ({
|
||||
setAppState,
|
||||
appState,
|
||||
onClose,
|
||||
onInsertChart,
|
||||
}: {
|
||||
appState: AppState;
|
||||
onClose: () => void;
|
||||
setAppState: React.Component<any, AppState>["setState"];
|
||||
onInsertChart: (elements: LibraryItem["elements"]) => void;
|
||||
}) => {
|
||||
const { onInsertElements } = useApp();
|
||||
const handleClose = React.useCallback(() => {
|
||||
if (onClose) {
|
||||
onClose();
|
||||
@ -92,7 +92,7 @@ export const PasteChartDialog = ({
|
||||
}, [onClose]);
|
||||
|
||||
const handleChartClick = (chartType: ChartType, elements: ChartElements) => {
|
||||
onInsertChart(elements);
|
||||
onInsertElements(elements);
|
||||
trackEvent("magic", "chart", chartType);
|
||||
setAppState({
|
||||
currentChartType: chartType,
|
||||
|
@ -2,67 +2,26 @@
|
||||
@import "../../css/variables.module";
|
||||
|
||||
.excalidraw {
|
||||
.Sidebar {
|
||||
&__close-btn,
|
||||
&__pin-btn,
|
||||
&__dropdown-btn {
|
||||
@include outlineButtonStyles;
|
||||
width: var(--lg-button-size);
|
||||
height: var(--lg-button-size);
|
||||
padding: 0;
|
||||
|
||||
svg {
|
||||
width: var(--lg-icon-size);
|
||||
height: var(--lg-icon-size);
|
||||
}
|
||||
}
|
||||
|
||||
&__pin-btn {
|
||||
&--pinned {
|
||||
background-color: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
|
||||
svg {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
background-color: var(--color-primary-darker);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.theme--dark {
|
||||
.Sidebar {
|
||||
&__pin-btn {
|
||||
&--pinned {
|
||||
svg {
|
||||
color: var(--color-gray-90);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.layer-ui__sidebar {
|
||||
.sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 5;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
|
||||
background-color: var(--sidebar-bg-color);
|
||||
box-shadow: var(--sidebar-shadow);
|
||||
|
||||
:root[dir="rtl"] & {
|
||||
left: 0;
|
||||
right: auto;
|
||||
}
|
||||
|
||||
background-color: var(--sidebar-bg-color);
|
||||
|
||||
box-shadow: var(--sidebar-shadow);
|
||||
|
||||
&--docked {
|
||||
box-shadow: none;
|
||||
}
|
||||
@ -77,52 +36,134 @@
|
||||
border-right: 1px solid var(--sidebar-border-color);
|
||||
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;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid var(--sidebar-border-color);
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.layer-ui__sidebar__header__buttons {
|
||||
.sidebar__header__buttons {
|
||||
gap: 0;
|
||||
display: flex;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,9 @@
|
||||
import React from "react";
|
||||
import { DEFAULT_SIDEBAR } from "../../constants";
|
||||
import { Excalidraw, Sidebar } from "../../packages/excalidraw/index";
|
||||
import {
|
||||
act,
|
||||
fireEvent,
|
||||
GlobalTestState,
|
||||
queryAllByTestId,
|
||||
queryByTestId,
|
||||
render,
|
||||
@ -10,346 +11,321 @@ import {
|
||||
withExcalidrawDimensions,
|
||||
} 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", () => {
|
||||
it("should render custom sidebar", async () => {
|
||||
const { container } = await render(
|
||||
<Excalidraw
|
||||
initialData={{ appState: { openSidebar: "customSidebar" } }}
|
||||
renderSidebar={() => (
|
||||
<Sidebar>
|
||||
describe("General behavior", () => {
|
||||
it("should render custom sidebar", async () => {
|
||||
const { container } = await render(
|
||||
<Excalidraw
|
||||
initialData={{ appState: { openSidebar: { name: "customSidebar" } } }}
|
||||
>
|
||||
<Sidebar name="customSidebar">
|
||||
<div id="test-sidebar-content">42</div>
|
||||
</Sidebar>
|
||||
)}
|
||||
/>,
|
||||
);
|
||||
</Excalidraw>,
|
||||
);
|
||||
|
||||
const node = container.querySelector("#test-sidebar-content");
|
||||
expect(node).not.toBe(null);
|
||||
const node = container.querySelector("#test-sidebar-content");
|
||||
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 () => {
|
||||
const { container } = await render(
|
||||
<Excalidraw
|
||||
initialData={{ appState: { openSidebar: "customSidebar" } }}
|
||||
renderSidebar={() => (
|
||||
<Sidebar>
|
||||
describe("<Sidebar.Header/>", () => {
|
||||
it("should render custom sidebar header", async () => {
|
||||
const { container } = await render(
|
||||
<Excalidraw
|
||||
initialData={{ appState: { openSidebar: { name: "customSidebar" } } }}
|
||||
>
|
||||
<Sidebar name="customSidebar">
|
||||
<Sidebar.Header>
|
||||
<div id="test-sidebar-header-content">42</div>
|
||||
</Sidebar.Header>
|
||||
</Sidebar>
|
||||
)}
|
||||
/>,
|
||||
);
|
||||
</Excalidraw>,
|
||||
);
|
||||
|
||||
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");
|
||||
const node = container.querySelector("#test-sidebar-header-content");
|
||||
expect(node).not.toBe(null);
|
||||
|
||||
// make sure only one sidebar is rendered
|
||||
const sidebars = container.querySelectorAll(".layer-ui__sidebar");
|
||||
expect(sidebars.length).toBe(1);
|
||||
// make sure we don't render the default fallback header,
|
||||
// just the custom one
|
||||
expect(queryAllByTestId(container, "sidebar-header").length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("should always render custom sidebar with close button & close on click", async () => {
|
||||
const onClose = jest.fn();
|
||||
const CustomExcalidraw = () => {
|
||||
return (
|
||||
<Excalidraw
|
||||
initialData={{ appState: { openSidebar: "customSidebar" } }}
|
||||
renderSidebar={() => (
|
||||
<Sidebar className="test-sidebar" onClose={onClose}>
|
||||
it("should not render <Sidebar.Header> for custom sidebars by default", async () => {
|
||||
const CustomExcalidraw = () => {
|
||||
return (
|
||||
<Excalidraw
|
||||
initialData={{
|
||||
appState: { openSidebar: { name: "customSidebar" } },
|
||||
}}
|
||||
>
|
||||
<Sidebar name="customSidebar" className="test-sidebar">
|
||||
hello
|
||||
</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");
|
||||
expect(sidebar).not.toBe(null);
|
||||
const closeButton = queryByTestId(sidebar!, "sidebar-dock");
|
||||
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");
|
||||
const closeButton = queryByTestId(sidebar!, "sidebar-close");
|
||||
expect(closeButton).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
it("should support controlled docking", async () => {
|
||||
let _setDockable: (dockable: boolean) => void = null!;
|
||||
|
||||
const CustomExcalidraw = () => {
|
||||
const [dockable, setDockable] = React.useState(false);
|
||||
_setDockable = setDockable;
|
||||
return (
|
||||
<Excalidraw
|
||||
initialData={{ appState: { openSidebar: "customSidebar" } }}
|
||||
renderSidebar={() => (
|
||||
it("<Sidebar.Header> should render close button", async () => {
|
||||
const onStateChange = jest.fn();
|
||||
const CustomExcalidraw = () => {
|
||||
return (
|
||||
<Excalidraw
|
||||
initialData={{
|
||||
appState: { openSidebar: { name: "customSidebar" } },
|
||||
}}
|
||||
>
|
||||
<Sidebar
|
||||
name="customSidebar"
|
||||
className="test-sidebar"
|
||||
docked={false}
|
||||
dockable={dockable}
|
||||
onStateChange={onStateChange}
|
||||
>
|
||||
hello
|
||||
<Sidebar.Header />
|
||||
</Sidebar>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
</Excalidraw>
|
||||
);
|
||||
};
|
||||
|
||||
const { container } = await render(<CustomExcalidraw />);
|
||||
const { container } = await render(<CustomExcalidraw />);
|
||||
|
||||
await withExcalidrawDimensions({ width: 1920, height: 1080 }, async () => {
|
||||
// should not show dock button when `dockable` is `false`
|
||||
// -------------------------------------------------------------------------
|
||||
// initial open
|
||||
expect(onStateChange).toHaveBeenCalledWith({ name: "customSidebar" });
|
||||
|
||||
act(() => {
|
||||
_setDockable(false);
|
||||
});
|
||||
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(() => {
|
||||
const sidebar = container.querySelector<HTMLElement>(".test-sidebar");
|
||||
expect(sidebar).not.toBe(null);
|
||||
const closeButton = queryByTestId(sidebar!, "sidebar-dock");
|
||||
expect(closeButton).toBe(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);
|
||||
expect(container.querySelector<HTMLElement>(".test-sidebar")).toBe(
|
||||
null,
|
||||
);
|
||||
expect(onStateChange).toHaveBeenCalledWith(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should support controlled docking", async () => {
|
||||
let _setDocked: (docked?: boolean) => void = null!;
|
||||
describe("Docking behavior", () => {
|
||||
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 = () => {
|
||||
const [docked, setDocked] = React.useState<boolean | undefined>();
|
||||
_setDocked = setDocked;
|
||||
return (
|
||||
it("shouldn't be user-dockable if `onDock` not supplied & `docked={true}`", async () => {
|
||||
await assertExcalidrawWithSidebar(
|
||||
<Sidebar name="customSidebar" docked={true}>
|
||||
<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
|
||||
initialData={{ appState: { openSidebar: "customSidebar" } }}
|
||||
renderSidebar={() => (
|
||||
<Sidebar className="test-sidebar" docked={docked}>
|
||||
hello
|
||||
</Sidebar>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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>
|
||||
initialData={{ appState: { openSidebar: { name: "customSidebar" } } }}
|
||||
>
|
||||
<Sidebar
|
||||
name="customSidebar"
|
||||
className="test-sidebar"
|
||||
onDock={() => {}}
|
||||
docked
|
||||
>
|
||||
<Sidebar.Header />
|
||||
</Sidebar>
|
||||
)}
|
||||
/>,
|
||||
);
|
||||
</Excalidraw>,
|
||||
);
|
||||
|
||||
// sidebar isn't rendered initially
|
||||
// -------------------------------------------------------------------------
|
||||
await waitFor(() => {
|
||||
const node = container.querySelector("#test-sidebar-content");
|
||||
expect(node).toBe(null);
|
||||
await withExcalidrawDimensions(
|
||||
{ width: 1920, height: 1080 },
|
||||
async () => {
|
||||
await assertSidebarDockButton(true);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// toggle sidebar on
|
||||
// -------------------------------------------------------------------------
|
||||
expect(window.h.app.toggleMenu("customSidebar")).toBe(true);
|
||||
it("shouldn't be user-dockable when only `onDock` supplied w/o `docked`", async () => {
|
||||
await render(
|
||||
<Excalidraw
|
||||
initialData={{ appState: { openSidebar: { name: "customSidebar" } } }}
|
||||
>
|
||||
<Sidebar
|
||||
name="customSidebar"
|
||||
className="test-sidebar"
|
||||
onDock={() => {}}
|
||||
>
|
||||
<Sidebar.Header />
|
||||
</Sidebar>
|
||||
</Excalidraw>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
const node = container.querySelector("#test-sidebar-content");
|
||||
expect(node).not.toBe(null);
|
||||
});
|
||||
|
||||
// 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);
|
||||
await withExcalidrawDimensions(
|
||||
{ width: 1920, height: 1080 },
|
||||
async () => {
|
||||
await assertSidebarDockButton(false);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,151 +1,249 @@
|
||||
import {
|
||||
import React, {
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
useCallback,
|
||||
RefObject,
|
||||
} from "react";
|
||||
import { Island } from ".././Island";
|
||||
import { atom, useAtom } from "jotai";
|
||||
import { atom, useSetAtom } from "jotai";
|
||||
import { jotaiScope } from "../../jotai";
|
||||
import {
|
||||
SidebarPropsContext,
|
||||
SidebarProps,
|
||||
SidebarPropsContextValue,
|
||||
} from "./common";
|
||||
|
||||
import { SidebarHeaderComponents } from "./SidebarHeader";
|
||||
import { SidebarHeader } 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 clsx from "clsx";
|
||||
import { useExcalidrawSetAppState } from "../App";
|
||||
import { updateObject } from "../../utils";
|
||||
|
||||
/** using a counter instead of boolean to handle race conditions where
|
||||
* the host app may render (mount/unmount) multiple different sidebar */
|
||||
export const hostSidebarCountersAtom = atom({ rendered: 0, docked: 0 });
|
||||
|
||||
export const Sidebar = Object.assign(
|
||||
forwardRef(
|
||||
(
|
||||
{
|
||||
children,
|
||||
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;
|
||||
// FIXME replace this with the implem from ColorPicker once it's merged
|
||||
const useOnClickOutside = (
|
||||
ref: RefObject<HTMLElement>,
|
||||
cb: (event: MouseEvent) => void,
|
||||
) => {
|
||||
useEffect(() => {
|
||||
const listener = (event: MouseEvent) => {
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<Island
|
||||
className={clsx(
|
||||
"layer-ui__sidebar",
|
||||
{ "layer-ui__sidebar--docked": isDockedFallback },
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
>
|
||||
<SidebarPropsContext.Provider value={headerPropsRef.current}>
|
||||
<SidebarHeaderComponents.Context>
|
||||
<SidebarHeaderComponents.Component __isFallback />
|
||||
{children}
|
||||
</SidebarHeaderComponents.Context>
|
||||
</SidebarPropsContext.Provider>
|
||||
</Island>
|
||||
if (
|
||||
event.target instanceof Element &&
|
||||
(ref.current.contains(event.target) ||
|
||||
!document.body.contains(event.target))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
cb(event);
|
||||
};
|
||||
document.addEventListener("pointerdown", listener, false);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("pointerdown", listener);
|
||||
};
|
||||
}, [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`",
|
||||
);
|
||||
},
|
||||
),
|
||||
{
|
||||
Header: SidebarHeaderComponents.Component,
|
||||
}
|
||||
|
||||
const setAppState = useExcalidrawSetAppState();
|
||||
|
||||
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";
|
||||
|
@ -4,86 +4,54 @@ import { t } from "../../i18n";
|
||||
import { useDevice } from "../App";
|
||||
import { SidebarPropsContext } from "./common";
|
||||
import { CloseIcon, PinIcon } from "../icons";
|
||||
import { withUpstreamOverride } from "../hoc/withUpstreamOverride";
|
||||
import { Tooltip } from "../Tooltip";
|
||||
import { Button } from "../Button";
|
||||
|
||||
export const SidebarDockButton = (props: {
|
||||
checked: boolean;
|
||||
onChange?(): void;
|
||||
}) => {
|
||||
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<{
|
||||
export const SidebarHeader = ({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}> = ({ children, className }) => {
|
||||
}) => {
|
||||
const device = useDevice();
|
||||
const props = useContext(SidebarPropsContext);
|
||||
|
||||
const renderDockButton = !!(device.canDeviceFitSidebar && props.dockable);
|
||||
const renderCloseButton = !!props.onClose;
|
||||
const renderDockButton = !!(
|
||||
device.canDeviceFitSidebar && props.shouldRenderDockButton
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx("layer-ui__sidebar__header", className)}
|
||||
className={clsx("sidebar__header", className)}
|
||||
data-testid="sidebar-header"
|
||||
>
|
||||
{children}
|
||||
{(renderDockButton || renderCloseButton) && (
|
||||
<div className="layer-ui__sidebar__header__buttons">
|
||||
{renderDockButton && (
|
||||
<SidebarDockButton
|
||||
checked={!!props.docked}
|
||||
onChange={() => {
|
||||
props.onDock?.(!props.docked);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{renderCloseButton && (
|
||||
<button
|
||||
data-testid="sidebar-close"
|
||||
className="Sidebar__close-btn"
|
||||
onClick={props.onClose}
|
||||
aria-label={t("buttons.close")}
|
||||
<div className="sidebar__header__buttons">
|
||||
{renderDockButton && (
|
||||
<Tooltip label={t("labels.sidebarLock")}>
|
||||
<Button
|
||||
onSelect={() => props.onDock?.(!props.docked)}
|
||||
selected={!!props.docked}
|
||||
className="sidebar__dock"
|
||||
data-testid="sidebar-dock"
|
||||
aria-label={t("labels.sidebarLock")}
|
||||
>
|
||||
{CloseIcon}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{PinIcon}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Button
|
||||
data-testid="sidebar-close"
|
||||
className="sidebar__close"
|
||||
onSelect={props.onCloseRequest}
|
||||
aria-label={t("buttons.close")}
|
||||
>
|
||||
{CloseIcon}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const [Context, Component] = withUpstreamOverride(_SidebarHeader);
|
||||
|
||||
/** @private */
|
||||
export const SidebarHeaderComponents = { Context, Component };
|
||||
SidebarHeader.displayName = "SidebarHeader";
|
||||
|
18
src/components/Sidebar/SidebarTab.tsx
Normal file
18
src/components/Sidebar/SidebarTab.tsx
Normal 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";
|
26
src/components/Sidebar/SidebarTabTrigger.tsx
Normal file
26
src/components/Sidebar/SidebarTabTrigger.tsx
Normal 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";
|
16
src/components/Sidebar/SidebarTabTriggers.tsx
Normal file
16
src/components/Sidebar/SidebarTabTriggers.tsx
Normal 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";
|
36
src/components/Sidebar/SidebarTabs.tsx
Normal file
36
src/components/Sidebar/SidebarTabs.tsx
Normal 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";
|
34
src/components/Sidebar/SidebarTrigger.scss
Normal file
34
src/components/Sidebar/SidebarTrigger.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
45
src/components/Sidebar/SidebarTrigger.tsx
Normal file
45
src/components/Sidebar/SidebarTrigger.tsx
Normal 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";
|
@ -1,23 +1,41 @@
|
||||
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 = {}> = {
|
||||
name: SidebarName;
|
||||
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;
|
||||
docked?: boolean;
|
||||
initialDockedState?: boolean;
|
||||
dockable?: boolean;
|
||||
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;
|
||||
|
||||
export type SidebarPropsContextValue = Pick<
|
||||
SidebarProps,
|
||||
"onClose" | "onDock" | "docked" | "dockable"
|
||||
>;
|
||||
"onDock" | "docked"
|
||||
> & { onCloseRequest: () => void; shouldRenderDockButton: boolean };
|
||||
|
||||
export const SidebarPropsContext =
|
||||
React.createContext<SidebarPropsContextValue>({});
|
||||
React.createContext<SidebarPropsContextValue>({} as SidebarPropsContextValue);
|
||||
|
@ -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(),
|
||||
};
|
||||
}, []);
|
||||
};
|
@ -1,4 +1,4 @@
|
||||
import { useOutsideClickHook } from "../../hooks/useOutsideClick";
|
||||
import { useOutsideClick } from "../../hooks/useOutsideClick";
|
||||
import { Island } from "../Island";
|
||||
|
||||
import { useDevice } from "../App";
|
||||
@ -24,7 +24,7 @@ const MenuContent = ({
|
||||
style?: React.CSSProperties;
|
||||
}) => {
|
||||
const device = useDevice();
|
||||
const menuRef = useOutsideClickHook(() => {
|
||||
const menuRef = useOutsideClick(() => {
|
||||
onClickOutside?.();
|
||||
});
|
||||
|
||||
|
@ -9,7 +9,7 @@ import {
|
||||
ZoomActions,
|
||||
} from "../Actions";
|
||||
import { useDevice } from "../App";
|
||||
import { useTunnels } from "../context/tunnels";
|
||||
import { useTunnels } from "../../context/tunnels";
|
||||
import { HelpButton } from "../HelpButton";
|
||||
import { Section } from "../Section";
|
||||
import Stack from "../Stack";
|
||||
@ -25,7 +25,7 @@ const Footer = ({
|
||||
showExitZenModeBtn: boolean;
|
||||
renderWelcomeScreen: boolean;
|
||||
}) => {
|
||||
const { footerCenterTunnel, welcomeScreenHelpHintTunnel } = useTunnels();
|
||||
const { FooterCenterTunnel, WelcomeScreenHelpHintTunnel } = useTunnels();
|
||||
|
||||
const device = useDevice();
|
||||
const showFinalize =
|
||||
@ -70,14 +70,14 @@ const Footer = ({
|
||||
</Section>
|
||||
</Stack.Col>
|
||||
</div>
|
||||
<footerCenterTunnel.Out />
|
||||
<FooterCenterTunnel.Out />
|
||||
<div
|
||||
className={clsx("layer-ui__wrapper__footer-right zen-mode-transition", {
|
||||
"transition-right disable-pointerEvents": appState.zenModeEnabled,
|
||||
})}
|
||||
>
|
||||
<div style={{ position: "relative" }}>
|
||||
{renderWelcomeScreen && <welcomeScreenHelpHintTunnel.Out />}
|
||||
{renderWelcomeScreen && <WelcomeScreenHelpHintTunnel.Out />}
|
||||
<HelpButton
|
||||
onClick={() => actionManager.executeAction(actionShortcuts)}
|
||||
/>
|
||||
|
@ -1,13 +1,13 @@
|
||||
import clsx from "clsx";
|
||||
import { useExcalidrawAppState } from "../App";
|
||||
import { useTunnels } from "../context/tunnels";
|
||||
import { useTunnels } from "../../context/tunnels";
|
||||
import "./FooterCenter.scss";
|
||||
|
||||
const FooterCenter = ({ children }: { children?: React.ReactNode }) => {
|
||||
const { footerCenterTunnel } = useTunnels();
|
||||
const { FooterCenterTunnel } = useTunnels();
|
||||
const appState = useExcalidrawAppState();
|
||||
return (
|
||||
<footerCenterTunnel.In>
|
||||
<FooterCenterTunnel.In>
|
||||
<div
|
||||
className={clsx("footer-center zen-mode-transition", {
|
||||
"layer-ui__wrapper__footer-left--transition-bottom":
|
||||
@ -16,7 +16,7 @@ const FooterCenter = ({ children }: { children?: React.ReactNode }) => {
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</footerCenterTunnel.In>
|
||||
</FooterCenterTunnel.In>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,32 +1,46 @@
|
||||
import { atom, useAtom } from "jotai";
|
||||
import React, { useLayoutEffect } from "react";
|
||||
import { useTunnels } from "../context/tunnels";
|
||||
import { useTunnels } from "../../context/tunnels";
|
||||
|
||||
export const withInternalFallback = <P,>(
|
||||
componentName: string,
|
||||
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
|
||||
// render until mount counter are initialized. This is because the counter
|
||||
// is initialized in an effect, and thus we could end rendering both
|
||||
// components at the same time until counter is initialized.
|
||||
let preferHost = false;
|
||||
|
||||
let counter = 0;
|
||||
|
||||
const WrapperComponent: React.FC<
|
||||
P & {
|
||||
__fallback?: boolean;
|
||||
}
|
||||
> = (props) => {
|
||||
const { jotaiScope } = useTunnels();
|
||||
const [counter, setCounter] = useAtom(counterAtom, jotaiScope);
|
||||
const [, setRender] = useAtom(renderAtom, jotaiScope);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setCounter((counter) => counter + 1);
|
||||
setRender((c) => {
|
||||
const next = c + 1;
|
||||
counter = next;
|
||||
|
||||
return next;
|
||||
});
|
||||
return () => {
|
||||
setCounter((counter) => counter - 1);
|
||||
setRender((c) => {
|
||||
const next = c - 1;
|
||||
counter = next;
|
||||
if (!next) {
|
||||
preferHost = false;
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
}, [setCounter]);
|
||||
}, [setRender]);
|
||||
|
||||
if (!props.__fallback) {
|
||||
preferHost = true;
|
||||
|
@ -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;
|
||||
};
|
@ -13,7 +13,7 @@ import { t } from "../../i18n";
|
||||
import { HamburgerMenuIcon } from "../icons";
|
||||
import { withInternalFallback } from "../hoc/withInternalFallback";
|
||||
import { composeEventHandlers } from "../../utils";
|
||||
import { useTunnels } from "../context/tunnels";
|
||||
import { useTunnels } from "../../context/tunnels";
|
||||
|
||||
const MainMenu = Object.assign(
|
||||
withInternalFallback(
|
||||
@ -28,7 +28,7 @@ const MainMenu = Object.assign(
|
||||
*/
|
||||
onSelect?: (event: Event) => void;
|
||||
}) => {
|
||||
const { mainMenuTunnel } = useTunnels();
|
||||
const { MainMenuTunnel } = useTunnels();
|
||||
const device = useDevice();
|
||||
const appState = useExcalidrawAppState();
|
||||
const setAppState = useExcalidrawSetAppState();
|
||||
@ -37,7 +37,7 @@ const MainMenu = Object.assign(
|
||||
: () => setAppState({ openMenu: null });
|
||||
|
||||
return (
|
||||
<mainMenuTunnel.In>
|
||||
<MainMenuTunnel.In>
|
||||
<DropdownMenu open={appState.openMenu === "canvas"}>
|
||||
<DropdownMenu.Trigger
|
||||
onToggle={() => {
|
||||
@ -66,7 +66,7 @@ const MainMenu = Object.assign(
|
||||
)}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu>
|
||||
</mainMenuTunnel.In>
|
||||
</MainMenuTunnel.In>
|
||||
);
|
||||
},
|
||||
),
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
useExcalidrawActionManager,
|
||||
useExcalidrawAppState,
|
||||
} from "../App";
|
||||
import { useTunnels } from "../context/tunnels";
|
||||
import { useTunnels } from "../../context/tunnels";
|
||||
import { ExcalLogo, HelpIcon, LoadIcon, usersIcon } from "../icons";
|
||||
|
||||
const WelcomeScreenMenuItemContent = ({
|
||||
@ -89,9 +89,9 @@ const WelcomeScreenMenuItemLink = ({
|
||||
WelcomeScreenMenuItemLink.displayName = "WelcomeScreenMenuItemLink";
|
||||
|
||||
const Center = ({ children }: { children?: React.ReactNode }) => {
|
||||
const { welcomeScreenCenterTunnel } = useTunnels();
|
||||
const { WelcomeScreenCenterTunnel } = useTunnels();
|
||||
return (
|
||||
<welcomeScreenCenterTunnel.In>
|
||||
<WelcomeScreenCenterTunnel.In>
|
||||
<div className="welcome-screen-center">
|
||||
{children || (
|
||||
<>
|
||||
@ -104,7 +104,7 @@ const Center = ({ children }: { children?: React.ReactNode }) => {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</welcomeScreenCenterTunnel.In>
|
||||
</WelcomeScreenCenterTunnel.In>
|
||||
);
|
||||
};
|
||||
Center.displayName = "Center";
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { t } from "../../i18n";
|
||||
import { useTunnels } from "../context/tunnels";
|
||||
import { useTunnels } from "../../context/tunnels";
|
||||
import {
|
||||
WelcomeScreenHelpArrow,
|
||||
WelcomeScreenMenuArrow,
|
||||
@ -7,44 +7,44 @@ import {
|
||||
} from "../icons";
|
||||
|
||||
const MenuHint = ({ children }: { children?: React.ReactNode }) => {
|
||||
const { welcomeScreenMenuHintTunnel } = useTunnels();
|
||||
const { WelcomeScreenMenuHintTunnel } = useTunnels();
|
||||
return (
|
||||
<welcomeScreenMenuHintTunnel.In>
|
||||
<WelcomeScreenMenuHintTunnel.In>
|
||||
<div className="virgil welcome-screen-decor welcome-screen-decor-hint welcome-screen-decor-hint--menu">
|
||||
{WelcomeScreenMenuArrow}
|
||||
<div className="welcome-screen-decor-hint__label">
|
||||
{children || t("welcomeScreen.defaults.menuHint")}
|
||||
</div>
|
||||
</div>
|
||||
</welcomeScreenMenuHintTunnel.In>
|
||||
</WelcomeScreenMenuHintTunnel.In>
|
||||
);
|
||||
};
|
||||
MenuHint.displayName = "MenuHint";
|
||||
|
||||
const ToolbarHint = ({ children }: { children?: React.ReactNode }) => {
|
||||
const { welcomeScreenToolbarHintTunnel } = useTunnels();
|
||||
const { WelcomeScreenToolbarHintTunnel } = useTunnels();
|
||||
return (
|
||||
<welcomeScreenToolbarHintTunnel.In>
|
||||
<WelcomeScreenToolbarHintTunnel.In>
|
||||
<div className="virgil welcome-screen-decor welcome-screen-decor-hint welcome-screen-decor-hint--toolbar">
|
||||
<div className="welcome-screen-decor-hint__label">
|
||||
{children || t("welcomeScreen.defaults.toolbarHint")}
|
||||
</div>
|
||||
{WelcomeScreenTopToolbarArrow}
|
||||
</div>
|
||||
</welcomeScreenToolbarHintTunnel.In>
|
||||
</WelcomeScreenToolbarHintTunnel.In>
|
||||
);
|
||||
};
|
||||
ToolbarHint.displayName = "ToolbarHint";
|
||||
|
||||
const HelpHint = ({ children }: { children?: React.ReactNode }) => {
|
||||
const { welcomeScreenHelpHintTunnel } = useTunnels();
|
||||
const { WelcomeScreenHelpHintTunnel } = useTunnels();
|
||||
return (
|
||||
<welcomeScreenHelpHintTunnel.In>
|
||||
<WelcomeScreenHelpHintTunnel.In>
|
||||
<div className="virgil welcome-screen-decor welcome-screen-decor-hint welcome-screen-decor-hint--help">
|
||||
<div>{children || t("welcomeScreen.defaults.helpHint")}</div>
|
||||
{WelcomeScreenHelpArrow}
|
||||
</div>
|
||||
</welcomeScreenHelpHintTunnel.In>
|
||||
</WelcomeScreenHelpHintTunnel.In>
|
||||
);
|
||||
};
|
||||
HelpHint.displayName = "HelpHint";
|
||||
|
@ -275,3 +275,10 @@ export const DEFAULT_ELEMENT_PROPS: {
|
||||
opacity: 100,
|
||||
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
36
src/context/tunnels.ts
Normal 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(),
|
||||
};
|
||||
}, []);
|
||||
};
|
5
src/context/ui-appState.ts
Normal file
5
src/context/ui-appState.ts
Normal 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);
|
@ -567,7 +567,7 @@
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.library-button {
|
||||
.default-sidebar-trigger {
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
@ -78,10 +78,13 @@
|
||||
|
||||
--color-selection: #6965db;
|
||||
|
||||
--color-icon-white: #{$oc-white};
|
||||
|
||||
--color-primary: #6965db;
|
||||
--color-primary-darker: #5b57d1;
|
||||
--color-primary-darkest: #4a47b1;
|
||||
--color-primary-light: #e3e2fe;
|
||||
--color-primary-light-darker: #d7d5ff;
|
||||
|
||||
--color-gray-10: #f5f5f5;
|
||||
--color-gray-20: #ebebeb;
|
||||
@ -161,10 +164,13 @@
|
||||
// will be inverted to a lighter color.
|
||||
--color-selection: #3530c4;
|
||||
|
||||
--color-icon-white: var(--color-gray-90);
|
||||
|
||||
--color-primary: #a8a5ff;
|
||||
--color-primary-darker: #b2aeff;
|
||||
--color-primary-darkest: #beb9ff;
|
||||
--color-primary-light: #4f4d6f;
|
||||
--color-primary-light-darker: #43415e;
|
||||
|
||||
--color-text-warning: var(--color-gray-80);
|
||||
|
||||
|
@ -72,7 +72,14 @@
|
||||
|
||||
&:hover {
|
||||
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 {
|
||||
@ -81,11 +88,14 @@
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: var(--color-primary-light);
|
||||
border-color: var(--color-primary-light);
|
||||
background-color: var(--button-selected-bg, var(--color-primary-light));
|
||||
border-color: var(--button-selected-border, var(--color-primary-light));
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-primary-light);
|
||||
background-color: var(
|
||||
--button-selected-hover-bg,
|
||||
var(--color-primary-light)
|
||||
);
|
||||
}
|
||||
|
||||
svg {
|
||||
|
@ -14,7 +14,14 @@ import { getCommonBoundingBox } from "../element/bounds";
|
||||
import { AbortError } from "../errors";
|
||||
import { t } from "../i18n";
|
||||
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<{
|
||||
status: "loading" | "loaded";
|
||||
@ -148,7 +155,9 @@ class Library {
|
||||
defaultStatus?: "unpublished" | "published";
|
||||
}): Promise<LibraryItems> => {
|
||||
if (openLibraryMenu) {
|
||||
this.app.setState({ openSidebar: "library" });
|
||||
this.app.setState({
|
||||
openSidebar: { name: DEFAULT_SIDEBAR.name, tab: LIBRARY_SIDEBAR_TAB },
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
resolve(mergeLibraryItems(this.lastLibraryItems, nextItems));
|
||||
} else {
|
||||
@ -186,8 +202,6 @@ class Library {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}).finally(() => {
|
||||
this.app.focusContainer();
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -27,6 +27,7 @@ import {
|
||||
PRECEDING_ELEMENT_KEY,
|
||||
FONT_FAMILY,
|
||||
ROUNDNESS,
|
||||
DEFAULT_SIDEBAR,
|
||||
} from "../constants";
|
||||
import { getDefaultAppState } from "../appState";
|
||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||
@ -431,21 +432,15 @@ const LegacyAppStateMigrations: {
|
||||
defaultAppState: ReturnType<typeof getDefaultAppState>,
|
||||
) => [LegacyAppState[K][1], AppState[LegacyAppState[K][1]]];
|
||||
} = {
|
||||
isLibraryOpen: (appState, defaultAppState) => {
|
||||
isSidebarDocked: (appState, defaultAppState) => {
|
||||
return [
|
||||
"openSidebar",
|
||||
"isLibraryOpen" in appState
|
||||
? appState.isLibraryOpen
|
||||
? "library"
|
||||
: null
|
||||
: coalesceAppStateValue("openSidebar", appState, defaultAppState),
|
||||
];
|
||||
},
|
||||
isLibraryMenuDocked: (appState, defaultAppState) => {
|
||||
return [
|
||||
"isSidebarDocked",
|
||||
appState.isLibraryMenuDocked ??
|
||||
coalesceAppStateValue("isSidebarDocked", appState, defaultAppState),
|
||||
"defaultSidebarDockedPreference",
|
||||
appState.isSidebarDocked ??
|
||||
coalesceAppStateValue(
|
||||
"defaultSidebarDockedPreference",
|
||||
appState,
|
||||
defaultAppState,
|
||||
),
|
||||
];
|
||||
},
|
||||
};
|
||||
@ -517,13 +512,10 @@ export const restoreAppState = (
|
||||
: appState.zoom?.value
|
||||
? appState.zoom
|
||||
: defaultAppState.zoom,
|
||||
// when sidebar docked and user left it open in last session,
|
||||
// keep it open. If not docked, keep it closed irrespective of last state.
|
||||
openSidebar:
|
||||
nextAppState.openSidebar === "library"
|
||||
? nextAppState.isSidebarDocked
|
||||
? "library"
|
||||
: null
|
||||
// string (legacy)
|
||||
typeof (appState.openSidebar as any as string) === "string"
|
||||
? { name: DEFAULT_SIDEBAR.name }
|
||||
: nextAppState.openSidebar,
|
||||
};
|
||||
};
|
||||
|
@ -25,10 +25,8 @@ export interface ExportedDataState {
|
||||
* Don't consume on its own.
|
||||
*/
|
||||
export type LegacyAppState = {
|
||||
/** @deprecated #5663 TODO remove 22-12-15 */
|
||||
isLibraryOpen: [boolean, "openSidebar"];
|
||||
/** @deprecated #5663 TODO remove 22-12-15 */
|
||||
isLibraryMenuDocked: [boolean, "isSidebarDocked"];
|
||||
/** @deprecated #6213 TODO remove 23-06-01 */
|
||||
isSidebarDocked: [boolean, "defaultSidebarDockedPreference"];
|
||||
};
|
||||
|
||||
export interface ImportedDataState {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
export const useOutsideClickHook = (handler: (event: Event) => void) => {
|
||||
export const useOutsideClick = (handler: (event: Event) => void) => {
|
||||
const ref = useRef(null);
|
||||
|
||||
useEffect(
|
||||
|
@ -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.
|
||||
-->
|
||||
|
||||
## 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)
|
||||
|
||||
### Docs
|
||||
|
@ -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 = () => {
|
||||
return (
|
||||
<MainMenu>
|
||||
@ -668,23 +659,6 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
||||
</div>
|
||||
</div>
|
||||
<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
|
||||
ref={(api: ExcalidrawImperativeAPI) => setExcalidrawAPI(api)}
|
||||
initialData={initialStatePromiseRef.current.promise}
|
||||
@ -706,7 +680,6 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
||||
onLinkOpen={onLinkOpen}
|
||||
onPointerDown={onPointerDown}
|
||||
onScrollChange={rerenderCommentIcons}
|
||||
renderSidebar={renderSidebar}
|
||||
>
|
||||
{excalidrawAPI && (
|
||||
<Footer>
|
||||
@ -714,6 +687,30 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
||||
</Footer>
|
||||
)}
|
||||
<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()}
|
||||
</Excalidraw>
|
||||
{Object.keys(commentIcons || []).length > 0 && renderCommentIcons()}
|
||||
|
@ -24,7 +24,6 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||
isCollaborating = false,
|
||||
onPointerUpdate,
|
||||
renderTopRightUI,
|
||||
renderSidebar,
|
||||
langCode = defaultLang.code,
|
||||
viewModeEnabled,
|
||||
zenModeEnabled,
|
||||
@ -47,6 +46,8 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||
|
||||
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"] = {
|
||||
...props.UIOptions,
|
||||
canvasActions: {
|
||||
@ -114,7 +115,6 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||
onLinkOpen={onLinkOpen}
|
||||
onPointerDown={onPointerDown}
|
||||
onScrollChange={onScrollChange}
|
||||
renderSidebar={renderSidebar}
|
||||
>
|
||||
{children}
|
||||
</App>
|
||||
@ -245,3 +245,5 @@ export { MainMenu };
|
||||
export { useDevice } from "../../components/App";
|
||||
export { WelcomeScreen };
|
||||
export { LiveCollaborationTrigger };
|
||||
|
||||
export { DefaultSidebar } from "../../components/DefaultSidebar";
|
||||
|
@ -284,6 +284,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -300,7 +301,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -464,6 +464,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -480,7 +481,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -650,6 +650,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -666,7 +667,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -1005,6 +1005,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -1021,7 +1022,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -1360,6 +1360,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -1376,7 +1377,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -1546,6 +1546,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -1562,7 +1563,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -1768,6 +1768,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -1784,7 +1785,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -2053,6 +2053,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -2069,7 +2070,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -2426,6 +2426,7 @@ Object {
|
||||
"currentItemStrokeWidth": 2,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -2442,7 +2443,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -3273,6 +3273,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -3289,7 +3290,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -3628,6 +3628,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -3644,7 +3645,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -3983,6 +3983,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -3999,7 +4000,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -4681,6 +4681,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -4697,7 +4698,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -5231,6 +5231,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -5247,7 +5248,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -5712,6 +5712,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -5728,7 +5729,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -6080,6 +6080,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -6096,7 +6097,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -6426,6 +6426,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -6442,7 +6443,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
|
@ -25,6 +25,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -41,7 +42,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -561,6 +561,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -577,7 +578,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -1103,6 +1103,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": "id10",
|
||||
@ -1119,7 +1120,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -2010,6 +2010,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -2026,7 +2027,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -2240,6 +2240,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -2256,7 +2257,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -2773,6 +2773,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -2789,7 +2790,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -3062,6 +3062,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -3078,7 +3079,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -3246,6 +3246,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -3262,7 +3263,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -3762,6 +3762,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -3778,7 +3779,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -4030,6 +4030,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -4046,7 +4047,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -4260,6 +4260,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -4276,7 +4277,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -4536,6 +4536,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -4552,7 +4553,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -4824,6 +4824,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -4840,7 +4841,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -5242,6 +5242,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "down",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": Object {
|
||||
"angle": 0,
|
||||
"backgroundColor": "transparent",
|
||||
@ -5285,7 +5286,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -5583,6 +5583,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": Object {
|
||||
"angle": 0,
|
||||
"backgroundColor": "transparent",
|
||||
@ -5626,7 +5627,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -5897,6 +5897,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "down",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": Object {
|
||||
"angle": 0,
|
||||
"backgroundColor": "transparent",
|
||||
@ -5940,7 +5941,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -6135,6 +6135,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -6151,7 +6152,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -6321,6 +6321,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": "id3",
|
||||
@ -6337,7 +6338,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -6849,6 +6849,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -6865,7 +6866,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -7214,6 +7214,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -7230,7 +7231,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -9566,6 +9566,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -9582,7 +9583,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -9985,6 +9985,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -10001,7 +10002,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -10274,6 +10274,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -10290,7 +10291,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -10522,6 +10522,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -10538,7 +10539,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -10843,6 +10843,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -10859,7 +10860,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -11027,6 +11027,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -11043,7 +11044,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -11211,6 +11211,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -11227,7 +11228,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -11395,6 +11395,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -11411,7 +11412,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -11632,6 +11632,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -11648,7 +11649,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -11869,6 +11869,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -11885,7 +11886,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -12097,6 +12097,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -12113,7 +12114,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -12334,6 +12334,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -12350,7 +12351,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -12518,6 +12518,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -12534,7 +12535,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -12755,6 +12755,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -12771,7 +12772,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -12939,6 +12939,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -12955,7 +12956,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -13167,6 +13167,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -13183,7 +13184,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -13351,6 +13351,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -13367,7 +13368,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -14190,6 +14190,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -14206,7 +14207,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -14479,6 +14479,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "down",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -14495,7 +14496,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "touch",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -14590,6 +14590,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -14606,7 +14607,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -14699,6 +14699,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -14715,7 +14716,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -14886,6 +14886,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -14902,7 +14903,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -15254,6 +15254,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -15270,7 +15271,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -15885,6 +15885,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -15901,7 +15902,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -16111,6 +16111,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -16127,7 +16128,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -17074,6 +17074,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -17090,7 +17091,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -17183,6 +17183,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": "id3",
|
||||
@ -17199,7 +17200,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -18042,6 +18042,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "down",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": Object {
|
||||
"angle": 0,
|
||||
"backgroundColor": "transparent",
|
||||
@ -18085,7 +18086,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -18514,6 +18514,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "down",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": Object {
|
||||
"angle": 0,
|
||||
"backgroundColor": "transparent",
|
||||
@ -18557,7 +18558,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -18855,6 +18855,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "down",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -18871,7 +18872,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "touch",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -18966,6 +18966,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -18982,7 +18983,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -19537,6 +19537,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -19553,7 +19554,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
@ -19646,6 +19646,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -19662,7 +19663,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "Untitled-201933152653",
|
||||
|
@ -10,7 +10,7 @@ import { API } from "../helpers/api";
|
||||
import { getDefaultAppState } from "../../appState";
|
||||
import { ImportedDataState } from "../../data/types";
|
||||
import { NormalizedZoomValue } from "../../types";
|
||||
import { FONT_FAMILY, ROUNDNESS } from "../../constants";
|
||||
import { DEFAULT_SIDEBAR, FONT_FAMILY, ROUNDNESS } from "../../constants";
|
||||
import { newElementWith } from "../../element/mutateElement";
|
||||
|
||||
describe("restoreElements", () => {
|
||||
@ -453,6 +453,29 @@ describe("restoreAppState", () => {
|
||||
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", () => {
|
||||
|
@ -189,10 +189,15 @@ describe("library menu", () => {
|
||||
const latestLibrary = await h.app.library.getLatestLibrary();
|
||||
expect(latestLibrary.length).toBe(0);
|
||||
|
||||
const libraryButton = container.querySelector(".library-button");
|
||||
const libraryButton = container.querySelector(".sidebar-trigger");
|
||||
|
||||
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();
|
||||
|
||||
const libraryItems = parseLibraryJSON(await libraryJSONPromise);
|
||||
|
@ -25,6 +25,7 @@ Object {
|
||||
"currentItemStrokeWidth": 1,
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingGroupId": null,
|
||||
@ -41,7 +42,6 @@ Object {
|
||||
"isLoading": false,
|
||||
"isResizing": false,
|
||||
"isRotating": false,
|
||||
"isSidebarDocked": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"name": "name",
|
||||
|
23
src/types.ts
23
src/types.ts
@ -94,6 +94,9 @@ export type LastActiveTool =
|
||||
}
|
||||
| null;
|
||||
|
||||
export type SidebarName = string;
|
||||
export type SidebarTabName = string;
|
||||
|
||||
export type AppState = {
|
||||
contextMenu: {
|
||||
items: ContextMenuItems;
|
||||
@ -159,16 +162,22 @@ export type AppState = {
|
||||
isResizing: boolean;
|
||||
isRotating: boolean;
|
||||
zoom: Zoom;
|
||||
// mobile-only
|
||||
openMenu: "canvas" | "shape" | null;
|
||||
openPopup:
|
||||
| "canvasColorPicker"
|
||||
| "backgroundColorPicker"
|
||||
| "strokeColorPicker"
|
||||
| null;
|
||||
openSidebar: "library" | "customSidebar" | null;
|
||||
openSidebar: { name: SidebarName; tab?: SidebarTabName } | 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;
|
||||
selectedElementIds: { [id: string]: boolean };
|
||||
@ -335,10 +344,6 @@ export interface ExcalidrawProps {
|
||||
pointerDownState: PointerDownState,
|
||||
) => void;
|
||||
onScrollChange?: (scrollX: number, scrollY: number) => void;
|
||||
/**
|
||||
* Render function that renders custom <Sidebar /> component.
|
||||
*/
|
||||
renderSidebar?: () => JSX.Element | null;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
@ -426,6 +431,8 @@ export type AppClassProperties = {
|
||||
device: App["device"];
|
||||
scene: App["scene"];
|
||||
pasteFromClipboard: App["pasteFromClipboard"];
|
||||
id: App["id"];
|
||||
onInsertElements: App["onInsertElements"];
|
||||
};
|
||||
|
||||
export type PointerDownState = Readonly<{
|
||||
@ -517,7 +524,7 @@ export type ExcalidrawImperativeAPI = {
|
||||
setActiveTool: InstanceType<typeof App>["setActiveTool"];
|
||||
setCursor: InstanceType<typeof App>["setCursor"];
|
||||
resetCursor: InstanceType<typeof App>["resetCursor"];
|
||||
toggleMenu: InstanceType<typeof App>["toggleMenu"];
|
||||
toggleSidebar: InstanceType<typeof App>["toggleSidebar"];
|
||||
};
|
||||
|
||||
export type Device = Readonly<{
|
||||
|
20
src/utils.ts
20
src/utils.ts
@ -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,
|
||||
objB: T,
|
||||
comparators?: Record<I, (a: T[I], b: T[I]) => boolean>,
|
||||
debug = false,
|
||||
) => {
|
||||
const aKeys = Object.keys(objA);
|
||||
const bKeys = Object.keys(objA);
|
||||
const bKeys = Object.keys(objB);
|
||||
if (aKeys.length !== bKeys.length) {
|
||||
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
|
||||
|
159
yarn.lock
159
yarn.lock
@ -1262,6 +1262,13 @@
|
||||
dependencies:
|
||||
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":
|
||||
version "7.18.10"
|
||||
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"
|
||||
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":
|
||||
version "0.4.3"
|
||||
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"
|
||||
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":
|
||||
version "5.3.1"
|
||||
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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
|
||||
integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
|
||||
@ -10245,12 +10370,12 @@ tsutils@^3.21.0:
|
||||
dependencies:
|
||||
tslib "^1.8.1"
|
||||
|
||||
tunnel-rat@0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/tunnel-rat/-/tunnel-rat-0.1.0.tgz#62cfbaf1b24cabac9318fe45ef26d70dc40e86fe"
|
||||
integrity sha512-/FKZLBXCoKhA7Wz+dsqitrItaLXYmT2bkZXod+1UuR4JqHtdb54yHvHhmMgLg+eyH1Od/CCnhA2VQQ2A/54Tcw==
|
||||
tunnel-rat@0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/tunnel-rat/-/tunnel-rat-0.1.2.tgz#1717efbc474ea2d8aa05a91622457a6e201c0aeb"
|
||||
integrity sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==
|
||||
dependencies:
|
||||
zustand "^4.1.0"
|
||||
zustand "^4.3.2"
|
||||
|
||||
type-check@^0.4.0, type-check@~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"
|
||||
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
||||
|
||||
zustand@^4.1.0, zustand@^4.3.2:
|
||||
version "4.3.2"
|
||||
resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.3.2.tgz#bb121fcad84c5a569e94bd1a2695e1a93ba85d39"
|
||||
integrity sha512-rd4haDmlwMTVWVqwvgy00ny8rtti/klRoZjFbL/MAcDnmD5qSw/RZc+Vddstdv90M5Lv6RPgWvm1Hivyn0QgJw==
|
||||
zustand@^4.3.2:
|
||||
version "4.3.7"
|
||||
resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.3.7.tgz#501b1f0393a7f1d103332e45ab574be5747fedce"
|
||||
integrity sha512-dY8ERwB9Nd21ellgkBZFhudER8KVlelZm8388B5nDAXhO/+FZDhYMuRnqDgu5SYyRgz/iaf8RKnbUs/cHfOGlQ==
|
||||
dependencies:
|
||||
use-sync-external-store "1.2.0"
|
||||
|
Loading…
x
Reference in New Issue
Block a user