feat: support WelcomeScreen customization API (#6048)
This commit is contained in:
parent
0982da38fe
commit
599a8f3c6f
@ -283,15 +283,12 @@ const deviceContextInitialValue = {
|
||||
};
|
||||
const DeviceContext = React.createContext<Device>(deviceContextInitialValue);
|
||||
DeviceContext.displayName = "DeviceContext";
|
||||
export const useDevice = () => useContext<Device>(DeviceContext);
|
||||
|
||||
export const ExcalidrawContainerContext = React.createContext<{
|
||||
container: HTMLDivElement | null;
|
||||
id: string | null;
|
||||
}>({ container: null, id: null });
|
||||
ExcalidrawContainerContext.displayName = "ExcalidrawContainerContext";
|
||||
export const useExcalidrawContainer = () =>
|
||||
useContext(ExcalidrawContainerContext);
|
||||
|
||||
const ExcalidrawElementsContext = React.createContext<
|
||||
readonly NonDeletedExcalidrawElement[]
|
||||
@ -309,7 +306,9 @@ ExcalidrawAppStateContext.displayName = "ExcalidrawAppStateContext";
|
||||
|
||||
const ExcalidrawSetAppStateContext = React.createContext<
|
||||
React.Component<any, AppState>["setState"]
|
||||
>(() => {});
|
||||
>(() => {
|
||||
console.warn("unitialized ExcalidrawSetAppStateContext context!");
|
||||
});
|
||||
ExcalidrawSetAppStateContext.displayName = "ExcalidrawSetAppStateContext";
|
||||
|
||||
const ExcalidrawActionManagerContext = React.createContext<ActionManager>(
|
||||
@ -317,6 +316,9 @@ const ExcalidrawActionManagerContext = React.createContext<ActionManager>(
|
||||
);
|
||||
ExcalidrawActionManagerContext.displayName = "ExcalidrawActionManagerContext";
|
||||
|
||||
export const useDevice = () => useContext<Device>(DeviceContext);
|
||||
export const useExcalidrawContainer = () =>
|
||||
useContext(ExcalidrawContainerContext);
|
||||
export const useExcalidrawElements = () =>
|
||||
useContext(ExcalidrawElementsContext);
|
||||
export const useExcalidrawAppState = () =>
|
||||
@ -598,6 +600,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
id={this.id}
|
||||
onImageAction={this.onImageAction}
|
||||
renderWelcomeScreen={
|
||||
!this.state.isLoading &&
|
||||
this.props.UIOptions.welcomeScreen &&
|
||||
this.state.showWelcomeScreen &&
|
||||
this.state.activeTool.type === "selection" &&
|
||||
!this.scene.getElementsIncludingDeleted().length
|
||||
|
@ -14,6 +14,7 @@ import {
|
||||
ExcalidrawProps,
|
||||
BinaryFiles,
|
||||
UIChildrenComponents,
|
||||
UIWelcomeScreenComponents,
|
||||
} from "../types";
|
||||
import { isShallowEqual, muteFSAbortError, getReactChildren } from "../utils";
|
||||
import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
|
||||
@ -45,12 +46,10 @@ import { useDevice } from "../components/App";
|
||||
import { Stats } from "./Stats";
|
||||
import { actionToggleStats } from "../actions/actionToggleStats";
|
||||
import Footer from "./footer/Footer";
|
||||
import { WelcomeScreenMenuArrow, WelcomeScreenTopToolbarArrow } from "./icons";
|
||||
import WelcomeScreen from "./WelcomeScreen";
|
||||
import WelcomeScreen from "./welcome-screen/WelcomeScreen";
|
||||
import { hostSidebarCountersAtom } from "./Sidebar/Sidebar";
|
||||
import { jotaiScope } from "../jotai";
|
||||
import { useAtom } from "jotai";
|
||||
import WelcomeScreenDecor from "./WelcomeScreenDecor";
|
||||
import MainMenu from "./mainMenu/MainMenu";
|
||||
|
||||
interface LayerUIProps {
|
||||
@ -111,8 +110,24 @@ const LayerUI = ({
|
||||
getReactChildren<UIChildrenComponents>(children, {
|
||||
Menu: true,
|
||||
FooterCenter: true,
|
||||
WelcomeScreen: true,
|
||||
});
|
||||
|
||||
const [WelcomeScreenComponents] = getReactChildren<UIWelcomeScreenComponents>(
|
||||
renderWelcomeScreen
|
||||
? (
|
||||
childrenComponents?.WelcomeScreen ?? (
|
||||
<WelcomeScreen>
|
||||
<WelcomeScreen.Center />
|
||||
<WelcomeScreen.Hints.MenuHint />
|
||||
<WelcomeScreen.Hints.ToolbarHint />
|
||||
<WelcomeScreen.Hints.HelpHint />
|
||||
</WelcomeScreen>
|
||||
)
|
||||
)?.props?.children
|
||||
: null,
|
||||
);
|
||||
|
||||
const renderJSONExportDialog = () => {
|
||||
if (!UIOptions.canvasActions.export) {
|
||||
return null;
|
||||
@ -213,15 +228,10 @@ const LayerUI = ({
|
||||
};
|
||||
const renderCanvasActions = () => (
|
||||
<div style={{ position: "relative" }}>
|
||||
<WelcomeScreenDecor
|
||||
shouldRender={renderWelcomeScreen && !appState.isLoading}
|
||||
>
|
||||
<div className="virgil WelcomeScreen-decor WelcomeScreen-decor--menu-pointer">
|
||||
{WelcomeScreenMenuArrow}
|
||||
<div>{t("welcomeScreen.menuHints")}</div>
|
||||
</div>
|
||||
</WelcomeScreenDecor>
|
||||
{renderMenu()}
|
||||
{WelcomeScreenComponents.MenuHint}
|
||||
{/* wrapping to Fragment stops React from occasionally complaining
|
||||
about identical Keys */}
|
||||
<>{renderMenu()}</>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -258,9 +268,7 @@ const LayerUI = ({
|
||||
|
||||
return (
|
||||
<FixedSideContainer side="top">
|
||||
{renderWelcomeScreen && !appState.isLoading && (
|
||||
<WelcomeScreen appState={appState} actionManager={actionManager} />
|
||||
)}
|
||||
{WelcomeScreenComponents.Center}
|
||||
<div className="App-menu App-menu_top">
|
||||
<Stack.Col
|
||||
gap={6}
|
||||
@ -275,17 +283,7 @@ const LayerUI = ({
|
||||
<Section heading="shapes" className="shapes-section">
|
||||
{(heading: React.ReactNode) => (
|
||||
<div style={{ position: "relative" }}>
|
||||
<WelcomeScreenDecor
|
||||
shouldRender={renderWelcomeScreen && !appState.isLoading}
|
||||
>
|
||||
<div className="virgil WelcomeScreen-decor WelcomeScreen-decor--top-toolbar-pointer">
|
||||
<div className="WelcomeScreen-decor--top-toolbar-pointer__label">
|
||||
{t("welcomeScreen.toolbarHints")}
|
||||
</div>
|
||||
{WelcomeScreenTopToolbarArrow}
|
||||
</div>
|
||||
</WelcomeScreenDecor>
|
||||
|
||||
{WelcomeScreenComponents.ToolbarHint}
|
||||
<Stack.Col gap={4} align="start">
|
||||
<Stack.Row
|
||||
gap={1}
|
||||
@ -420,24 +418,22 @@ const LayerUI = ({
|
||||
)}
|
||||
{device.isMobile && (
|
||||
<MobileMenu
|
||||
renderWelcomeScreen={renderWelcomeScreen}
|
||||
appState={appState}
|
||||
elements={elements}
|
||||
actionManager={actionManager}
|
||||
renderJSONExportDialog={renderJSONExportDialog}
|
||||
renderImageExportDialog={renderImageExportDialog}
|
||||
setAppState={setAppState}
|
||||
onCollabButtonClick={onCollabButtonClick}
|
||||
onLockToggle={() => onLockToggle()}
|
||||
onPenModeToggle={onPenModeToggle}
|
||||
canvas={canvas}
|
||||
isCollaborating={isCollaborating}
|
||||
onImageAction={onImageAction}
|
||||
renderTopRightUI={renderTopRightUI}
|
||||
renderCustomStats={renderCustomStats}
|
||||
renderSidebars={renderSidebars}
|
||||
device={device}
|
||||
renderMenu={renderMenu}
|
||||
welcomeScreenCenter={WelcomeScreenComponents.Center}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -462,13 +458,12 @@ const LayerUI = ({
|
||||
>
|
||||
{renderFixedSideContainer()}
|
||||
<Footer
|
||||
renderWelcomeScreen={renderWelcomeScreen}
|
||||
appState={appState}
|
||||
actionManager={actionManager}
|
||||
showExitZenModeBtn={showExitZenModeBtn}
|
||||
footerCenter={childrenComponents.FooterCenter}
|
||||
welcomeScreenHelp={WelcomeScreenComponents.HelpHint}
|
||||
/>
|
||||
|
||||
{appState.showStats && (
|
||||
<Stats
|
||||
appState={appState}
|
||||
|
@ -1,5 +1,10 @@
|
||||
import React from "react";
|
||||
import { AppState, Device, ExcalidrawProps } from "../types";
|
||||
import {
|
||||
AppState,
|
||||
Device,
|
||||
ExcalidrawProps,
|
||||
UIWelcomeScreenComponents,
|
||||
} from "../types";
|
||||
import { ActionManager } from "../actions/manager";
|
||||
import { t } from "../i18n";
|
||||
import Stack from "./Stack";
|
||||
@ -17,7 +22,6 @@ import { LibraryButton } from "./LibraryButton";
|
||||
import { PenModeButton } from "./PenModeButton";
|
||||
import { Stats } from "./Stats";
|
||||
import { actionToggleStats } from "../actions";
|
||||
import WelcomeScreen from "./WelcomeScreen";
|
||||
|
||||
type MobileMenuProps = {
|
||||
appState: AppState;
|
||||
@ -26,11 +30,9 @@ type MobileMenuProps = {
|
||||
renderImageExportDialog: () => React.ReactNode;
|
||||
setAppState: React.Component<any, AppState>["setState"];
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
onCollabButtonClick?: () => void;
|
||||
onLockToggle: () => void;
|
||||
onPenModeToggle: () => void;
|
||||
canvas: HTMLCanvasElement | null;
|
||||
isCollaborating: boolean;
|
||||
|
||||
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
|
||||
renderTopRightUI?: (
|
||||
@ -40,8 +42,8 @@ type MobileMenuProps = {
|
||||
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
|
||||
renderSidebars: () => JSX.Element | null;
|
||||
device: Device;
|
||||
renderWelcomeScreen?: boolean;
|
||||
renderMenu: () => React.ReactNode;
|
||||
welcomeScreenCenter: UIWelcomeScreenComponents["Center"];
|
||||
};
|
||||
|
||||
export const MobileMenu = ({
|
||||
@ -52,21 +54,18 @@ export const MobileMenu = ({
|
||||
onLockToggle,
|
||||
onPenModeToggle,
|
||||
canvas,
|
||||
isCollaborating,
|
||||
onImageAction,
|
||||
renderTopRightUI,
|
||||
renderCustomStats,
|
||||
renderSidebars,
|
||||
device,
|
||||
renderWelcomeScreen,
|
||||
renderMenu,
|
||||
welcomeScreenCenter,
|
||||
}: MobileMenuProps) => {
|
||||
const renderToolbar = () => {
|
||||
return (
|
||||
<FixedSideContainer side="top" className="App-top-bar">
|
||||
{renderWelcomeScreen && !appState.isLoading && (
|
||||
<WelcomeScreen appState={appState} actionManager={actionManager} />
|
||||
)}
|
||||
{welcomeScreenCenter}
|
||||
<Section heading="shapes">
|
||||
{(heading: React.ReactNode) => (
|
||||
<Stack.Col gap={4} align="center">
|
||||
@ -74,20 +73,6 @@ export const MobileMenu = ({
|
||||
<Island padding={1} className="App-toolbar App-toolbar--mobile">
|
||||
{heading}
|
||||
<Stack.Row gap={1}>
|
||||
{/* <PenModeButton
|
||||
checked={appState.penMode}
|
||||
onChange={onPenModeToggle}
|
||||
title={t("toolBar.penMode")}
|
||||
isMobile
|
||||
penDetected={appState.penDetected}
|
||||
/>
|
||||
<LockButton
|
||||
checked={appState.activeTool.locked}
|
||||
onChange={onLockToggle}
|
||||
title={t("toolBar.lock")}
|
||||
isMobile
|
||||
/>
|
||||
<div className="App-toolbar__divider"></div> */}
|
||||
<ShapesSwitcher
|
||||
appState={appState}
|
||||
canvas={canvas}
|
||||
@ -109,7 +94,6 @@ export const MobileMenu = ({
|
||||
title={t("toolBar.penMode")}
|
||||
isMobile
|
||||
penDetected={appState.penDetected}
|
||||
// penDetected={true}
|
||||
/>
|
||||
<LockButton
|
||||
checked={appState.activeTool.locked}
|
||||
|
@ -1,121 +0,0 @@
|
||||
import { actionLoadScene, actionShortcuts } from "../actions";
|
||||
import { ActionManager } from "../actions/manager";
|
||||
import { getShortcutFromShortcutName } from "../actions/shortcuts";
|
||||
import { isExcalidrawPlusSignedUser } from "../constants";
|
||||
import { t } from "../i18n";
|
||||
import { AppState } from "../types";
|
||||
import { ExcalLogo, HelpIcon, LoadIcon, PlusPromoIcon } from "./icons";
|
||||
import "./WelcomeScreen.scss";
|
||||
|
||||
const WelcomeScreenItem = ({
|
||||
label,
|
||||
shortcut,
|
||||
onClick,
|
||||
icon,
|
||||
link,
|
||||
}: {
|
||||
label: string;
|
||||
shortcut: string | null;
|
||||
onClick?: () => void;
|
||||
icon: JSX.Element;
|
||||
link?: string;
|
||||
}) => {
|
||||
if (link) {
|
||||
return (
|
||||
<a
|
||||
className="WelcomeScreen-item"
|
||||
href={link}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<div className="WelcomeScreen-item__label">
|
||||
{icon}
|
||||
{label}
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button className="WelcomeScreen-item" type="button" onClick={onClick}>
|
||||
<div className="WelcomeScreen-item__label">
|
||||
{icon}
|
||||
{label}
|
||||
</div>
|
||||
{shortcut && (
|
||||
<div className="WelcomeScreen-item__shortcut">{shortcut}</div>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const WelcomeScreen = ({
|
||||
appState,
|
||||
actionManager,
|
||||
}: {
|
||||
appState: AppState;
|
||||
actionManager: ActionManager;
|
||||
}) => {
|
||||
let subheadingJSX;
|
||||
|
||||
if (isExcalidrawPlusSignedUser) {
|
||||
subheadingJSX = t("welcomeScreen.switchToPlusApp")
|
||||
.split(/(Excalidraw\+)/)
|
||||
.map((bit, idx) => {
|
||||
if (bit === "Excalidraw+") {
|
||||
return (
|
||||
<a
|
||||
style={{ pointerEvents: "all" }}
|
||||
href={`${process.env.REACT_APP_PLUS_APP}?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenSignedInUser`}
|
||||
key={idx}
|
||||
>
|
||||
Excalidraw+
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return bit;
|
||||
});
|
||||
} else {
|
||||
subheadingJSX = t("welcomeScreen.data");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="WelcomeScreen-container">
|
||||
<div className="WelcomeScreen-logo virgil WelcomeScreen-decor">
|
||||
{ExcalLogo} Excalidraw
|
||||
</div>
|
||||
<div className="virgil WelcomeScreen-decor WelcomeScreen-decor--subheading">
|
||||
{subheadingJSX}
|
||||
</div>
|
||||
<div className="WelcomeScreen-items">
|
||||
{!appState.viewModeEnabled && (
|
||||
<WelcomeScreenItem
|
||||
// TODO barnabasmolnar/editor-redesign
|
||||
// do we want the internationalized labels here that are currently
|
||||
// in use elsewhere or new ones?
|
||||
label={t("buttons.load")}
|
||||
onClick={() => actionManager.executeAction(actionLoadScene)}
|
||||
shortcut={getShortcutFromShortcutName("loadScene")}
|
||||
icon={LoadIcon}
|
||||
/>
|
||||
)}
|
||||
<WelcomeScreenItem
|
||||
onClick={() => actionManager.executeAction(actionShortcuts)}
|
||||
label={t("helpDialog.title")}
|
||||
shortcut="?"
|
||||
icon={HelpIcon}
|
||||
/>
|
||||
{!isExcalidrawPlusSignedUser && (
|
||||
<WelcomeScreenItem
|
||||
link="https://plus.excalidraw.com/plus?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenGuest"
|
||||
label="Try Excalidraw Plus!"
|
||||
shortcut={null}
|
||||
icon={PlusPromoIcon}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WelcomeScreen;
|
@ -1,11 +0,0 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
const WelcomeScreenDecor = ({
|
||||
children,
|
||||
shouldRender,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
shouldRender: boolean;
|
||||
}) => (shouldRender ? <>{children}</> : null);
|
||||
|
||||
export default WelcomeScreenDecor;
|
@ -1,8 +1,11 @@
|
||||
import clsx from "clsx";
|
||||
import { actionShortcuts } from "../../actions";
|
||||
import { ActionManager } from "../../actions/manager";
|
||||
import { t } from "../../i18n";
|
||||
import { AppState, UIChildrenComponents } from "../../types";
|
||||
import {
|
||||
AppState,
|
||||
UIChildrenComponents,
|
||||
UIWelcomeScreenComponents,
|
||||
} from "../../types";
|
||||
import {
|
||||
ExitZenModeAction,
|
||||
FinalizeAction,
|
||||
@ -11,23 +14,21 @@ import {
|
||||
} from "../Actions";
|
||||
import { useDevice } from "../App";
|
||||
import { HelpButton } from "../HelpButton";
|
||||
import { WelcomeScreenHelpArrow } from "../icons";
|
||||
import { Section } from "../Section";
|
||||
import Stack from "../Stack";
|
||||
import WelcomeScreenDecor from "../WelcomeScreenDecor";
|
||||
|
||||
const Footer = ({
|
||||
appState,
|
||||
actionManager,
|
||||
showExitZenModeBtn,
|
||||
renderWelcomeScreen,
|
||||
footerCenter,
|
||||
welcomeScreenHelp,
|
||||
}: {
|
||||
appState: AppState;
|
||||
actionManager: ActionManager;
|
||||
showExitZenModeBtn: boolean;
|
||||
renderWelcomeScreen: boolean;
|
||||
footerCenter: UIChildrenComponents["FooterCenter"];
|
||||
welcomeScreenHelp: UIWelcomeScreenComponents["HelpHint"];
|
||||
}) => {
|
||||
const device = useDevice();
|
||||
const showFinalize =
|
||||
@ -79,17 +80,8 @@ const Footer = ({
|
||||
})}
|
||||
>
|
||||
<div style={{ position: "relative" }}>
|
||||
<WelcomeScreenDecor
|
||||
shouldRender={renderWelcomeScreen && !appState.isLoading}
|
||||
>
|
||||
<div className="virgil WelcomeScreen-decor WelcomeScreen-decor--help-pointer">
|
||||
<div>{t("welcomeScreen.helpHints")}</div>
|
||||
{WelcomeScreenHelpArrow}
|
||||
</div>
|
||||
</WelcomeScreenDecor>
|
||||
|
||||
{welcomeScreenHelp}
|
||||
<HelpButton
|
||||
title={t("helpDialog.title")}
|
||||
onClick={() => actionManager.executeAction(actionShortcuts)}
|
||||
/>
|
||||
</div>
|
||||
|
176
src/components/welcome-screen/WelcomeScreen.Center.tsx
Normal file
176
src/components/welcome-screen/WelcomeScreen.Center.tsx
Normal file
@ -0,0 +1,176 @@
|
||||
import { actionLoadScene, actionShortcuts } from "../../actions";
|
||||
import { getShortcutFromShortcutName } from "../../actions/shortcuts";
|
||||
import { t } from "../../i18n";
|
||||
import {
|
||||
useDevice,
|
||||
useExcalidrawActionManager,
|
||||
useExcalidrawAppState,
|
||||
} from "../App";
|
||||
import { ExcalLogo, HelpIcon, LoadIcon } from "../icons";
|
||||
|
||||
const WelcomeScreenMenuItemContent = ({
|
||||
icon,
|
||||
shortcut,
|
||||
children,
|
||||
}: {
|
||||
icon?: JSX.Element;
|
||||
shortcut?: string | null;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const device = useDevice();
|
||||
return (
|
||||
<>
|
||||
<div className="welcome-screen-menu-item__icon">{icon}</div>
|
||||
<div className="welcome-screen-menu-item__text">{children}</div>
|
||||
{shortcut && !device.isMobile && (
|
||||
<div className="welcome-screen-menu-item__shortcut">{shortcut}</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
WelcomeScreenMenuItemContent.displayName = "WelcomeScreenMenuItemContent";
|
||||
|
||||
const WelcomeScreenMenuItem = ({
|
||||
onSelect,
|
||||
children,
|
||||
icon,
|
||||
shortcut,
|
||||
className = "",
|
||||
...props
|
||||
}: {
|
||||
onSelect: () => void;
|
||||
children: React.ReactNode;
|
||||
icon?: JSX.Element;
|
||||
shortcut?: string | null;
|
||||
} & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
type="button"
|
||||
className={`welcome-screen-menu-item ${className}`}
|
||||
onClick={onSelect}
|
||||
>
|
||||
<WelcomeScreenMenuItemContent icon={icon} shortcut={shortcut}>
|
||||
{children}
|
||||
</WelcomeScreenMenuItemContent>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
WelcomeScreenMenuItem.displayName = "WelcomeScreenMenuItem";
|
||||
|
||||
const WelcomeScreenMenuItemLink = ({
|
||||
children,
|
||||
href,
|
||||
icon,
|
||||
shortcut,
|
||||
className = "",
|
||||
...props
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
href: string;
|
||||
icon?: JSX.Element;
|
||||
shortcut?: string | null;
|
||||
} & React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
||||
return (
|
||||
<a
|
||||
{...props}
|
||||
className={`welcome-screen-menu-item ${className}`}
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<WelcomeScreenMenuItemContent icon={icon} shortcut={shortcut}>
|
||||
{children}
|
||||
</WelcomeScreenMenuItemContent>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
WelcomeScreenMenuItemLink.displayName = "WelcomeScreenMenuItemLink";
|
||||
|
||||
const Center = ({ children }: { children?: React.ReactNode }) => {
|
||||
return (
|
||||
<div className="welcome-screen-center">
|
||||
{children || (
|
||||
<>
|
||||
<Logo />
|
||||
<Heading>{t("welcomeScreen.defaults.center_heading")}</Heading>
|
||||
<Menu>
|
||||
<MenuItemLoadScene />
|
||||
<MenuItemHelp />
|
||||
</Menu>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Center.displayName = "Center";
|
||||
|
||||
const Logo = ({ children }: { children?: React.ReactNode }) => {
|
||||
return (
|
||||
<div className="welcome-screen-center__logo virgil welcome-screen-decor">
|
||||
{children || <>{ExcalLogo} Excalidraw</>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Logo.displayName = "Logo";
|
||||
|
||||
const Heading = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<div className="welcome-screen-center__heading welcome-screen-decor virgil">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Heading.displayName = "Heading";
|
||||
|
||||
const Menu = ({ children }: { children?: React.ReactNode }) => {
|
||||
return <div className="welcome-screen-menu">{children}</div>;
|
||||
};
|
||||
Menu.displayName = "Menu";
|
||||
|
||||
const MenuItemHelp = () => {
|
||||
const actionManager = useExcalidrawActionManager();
|
||||
|
||||
return (
|
||||
<WelcomeScreenMenuItem
|
||||
onSelect={() => actionManager.executeAction(actionShortcuts)}
|
||||
shortcut="?"
|
||||
icon={HelpIcon}
|
||||
>
|
||||
{t("helpDialog.title")}
|
||||
</WelcomeScreenMenuItem>
|
||||
);
|
||||
};
|
||||
MenuItemHelp.displayName = "MenuItemHelp";
|
||||
|
||||
const MenuItemLoadScene = () => {
|
||||
const appState = useExcalidrawAppState();
|
||||
const actionManager = useExcalidrawActionManager();
|
||||
|
||||
if (appState.viewModeEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<WelcomeScreenMenuItem
|
||||
onSelect={() => actionManager.executeAction(actionLoadScene)}
|
||||
shortcut={getShortcutFromShortcutName("loadScene")}
|
||||
icon={LoadIcon}
|
||||
>
|
||||
{t("buttons.load")}
|
||||
</WelcomeScreenMenuItem>
|
||||
);
|
||||
};
|
||||
MenuItemLoadScene.displayName = "MenuItemLoadScene";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
Center.Logo = Logo;
|
||||
Center.Heading = Heading;
|
||||
Center.Menu = Menu;
|
||||
Center.MenuItem = WelcomeScreenMenuItem;
|
||||
Center.MenuItemLink = WelcomeScreenMenuItemLink;
|
||||
Center.MenuItemHelp = MenuItemHelp;
|
||||
Center.MenuItemLoadScene = MenuItemLoadScene;
|
||||
|
||||
export { Center };
|
42
src/components/welcome-screen/WelcomeScreen.Hints.tsx
Normal file
42
src/components/welcome-screen/WelcomeScreen.Hints.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import { t } from "../../i18n";
|
||||
import {
|
||||
WelcomeScreenHelpArrow,
|
||||
WelcomeScreenMenuArrow,
|
||||
WelcomeScreenTopToolbarArrow,
|
||||
} from "../icons";
|
||||
|
||||
const MenuHint = ({ children }: { children?: React.ReactNode }) => {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
MenuHint.displayName = "MenuHint";
|
||||
|
||||
const ToolbarHint = ({ children }: { children?: React.ReactNode }) => {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
ToolbarHint.displayName = "ToolbarHint";
|
||||
|
||||
const HelpHint = ({ children }: { children?: React.ReactNode }) => {
|
||||
return (
|
||||
<div className="virgil welcome-screen-decor welcome-screen-decor-hint welcome-screen-decor-hint--help">
|
||||
<div>{children || t("welcomeScreen.defaults.helpHint")}</div>
|
||||
{WelcomeScreenHelpArrow}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
HelpHint.displayName = "HelpHint";
|
||||
|
||||
export { HelpHint, MenuHint, ToolbarHint };
|
@ -3,29 +3,39 @@
|
||||
font-family: "Virgil";
|
||||
}
|
||||
|
||||
.WelcomeScreen-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 0.75rem;
|
||||
font-size: 2.25rem;
|
||||
// WelcomeSreen common
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
svg {
|
||||
width: 1.625rem;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.WelcomeScreen-decor {
|
||||
.welcome-screen-decor {
|
||||
pointer-events: none;
|
||||
|
||||
color: var(--color-gray-40);
|
||||
|
||||
&--subheading {
|
||||
font-size: 1.125rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&--help-pointer {
|
||||
&.theme--dark {
|
||||
.welcome-screen-decor {
|
||||
color: var(--color-gray-60);
|
||||
}
|
||||
}
|
||||
|
||||
// WelcomeScreen.Hints
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
.welcome-screen-decor-hint {
|
||||
@media (max-height: 599px) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px), (max-width: 800px) {
|
||||
.welcome-screen-decor {
|
||||
&--help,
|
||||
&--menu {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--help {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
@ -49,7 +59,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
&--top-toolbar-pointer {
|
||||
&--toolbar {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
@ -58,7 +68,7 @@
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
||||
&__label {
|
||||
.welcome-screen-decor-hint__label {
|
||||
width: 120px;
|
||||
position: relative;
|
||||
top: -0.5rem;
|
||||
@ -74,7 +84,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
&--menu-pointer {
|
||||
&--menu {
|
||||
position: absolute;
|
||||
width: 320px;
|
||||
font-size: 1rem;
|
||||
@ -95,10 +105,19 @@
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.welcome-screen-decor-hint__label {
|
||||
max-width: 160px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.WelcomeScreen-container {
|
||||
// WelcomeSreen.Center
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
.welcome-screen-center {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
@ -112,7 +131,24 @@
|
||||
bottom: 1rem;
|
||||
}
|
||||
|
||||
.WelcomeScreen-items {
|
||||
.welcome-screen-center__logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 0.75rem;
|
||||
font-size: 2.25rem;
|
||||
|
||||
svg {
|
||||
width: 1.625rem;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.welcome-screen-center__heading {
|
||||
font-size: 1.125rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.welcome-screen-menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
@ -120,7 +156,7 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.WelcomeScreen-item {
|
||||
.welcome-screen-menu-item {
|
||||
box-sizing: border-box;
|
||||
|
||||
pointer-events: all;
|
||||
@ -128,8 +164,10 @@
|
||||
color: var(--color-gray-50);
|
||||
font-size: 0.875rem;
|
||||
|
||||
width: 100%;
|
||||
min-width: 300px;
|
||||
display: flex;
|
||||
max-width: 400px;
|
||||
display: grid;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
@ -140,44 +178,49 @@
|
||||
|
||||
border-radius: var(--border-radius-md);
|
||||
|
||||
&__label {
|
||||
grid-template-columns: calc(var(--default-icon-size) + 0.5rem) 1fr 3rem;
|
||||
|
||||
&__text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: auto;
|
||||
text-align: left;
|
||||
column-gap: 0.5rem;
|
||||
}
|
||||
|
||||
svg {
|
||||
&__icon {
|
||||
width: var(--default-icon-size);
|
||||
height: var(--default-icon-size);
|
||||
}
|
||||
}
|
||||
|
||||
&__shortcut {
|
||||
margin-left: auto;
|
||||
color: var(--color-gray-40);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:active) .WelcomeScreen-item:hover {
|
||||
&:not(:active) .welcome-screen-menu-item:hover {
|
||||
text-decoration: none;
|
||||
background: var(--color-gray-10);
|
||||
|
||||
.WelcomeScreen-item__shortcut {
|
||||
.welcome-screen-menu-item__shortcut {
|
||||
color: var(--color-gray-50);
|
||||
}
|
||||
|
||||
.WelcomeScreen-item__label {
|
||||
.welcome-screen-menu-item__text {
|
||||
color: var(--color-gray-100);
|
||||
}
|
||||
}
|
||||
|
||||
.WelcomeScreen-item:active {
|
||||
.welcome-screen-menu-item:active {
|
||||
background: var(--color-gray-20);
|
||||
|
||||
.WelcomeScreen-item__shortcut {
|
||||
.welcome-screen-menu-item__shortcut {
|
||||
color: var(--color-gray-50);
|
||||
}
|
||||
|
||||
.WelcomeScreen-item__label {
|
||||
.welcome-screen-menu-item__text {
|
||||
color: var(--color-gray-100);
|
||||
}
|
||||
|
||||
@ -185,7 +228,7 @@
|
||||
color: var(--color-promo) !important;
|
||||
|
||||
&:hover {
|
||||
.WelcomeScreen-item__label {
|
||||
.welcome-screen-menu-item__text {
|
||||
color: var(--color-promo) !important;
|
||||
}
|
||||
}
|
||||
@ -193,11 +236,7 @@
|
||||
}
|
||||
|
||||
&.theme--dark {
|
||||
.WelcomeScreen-decor {
|
||||
color: var(--color-gray-60);
|
||||
}
|
||||
|
||||
.WelcomeScreen-item {
|
||||
.welcome-screen-menu-item {
|
||||
color: var(--color-gray-60);
|
||||
|
||||
&__shortcut {
|
||||
@ -205,69 +244,41 @@
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:active) .WelcomeScreen-item:hover {
|
||||
&:not(:active) .welcome-screen-menu-item:hover {
|
||||
background: var(--color-gray-85);
|
||||
|
||||
.WelcomeScreen-item__shortcut {
|
||||
.welcome-screen-menu-item__shortcut {
|
||||
color: var(--color-gray-50);
|
||||
}
|
||||
|
||||
.WelcomeScreen-item__label {
|
||||
.welcome-screen-menu-item__text {
|
||||
color: var(--color-gray-10);
|
||||
}
|
||||
}
|
||||
|
||||
.WelcomeScreen-item:active {
|
||||
.welcome-screen-menu-item:active {
|
||||
background-color: var(--color-gray-90);
|
||||
.WelcomeScreen-item__label {
|
||||
.welcome-screen-menu-item__text {
|
||||
color: var(--color-gray-10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Can tweak these values but for an initial effort, it looks OK to me
|
||||
@media (max-width: 1024px) {
|
||||
.WelcomeScreen-decor {
|
||||
&--help-pointer,
|
||||
&--menu-pointer {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @media (max-height: 400px) {
|
||||
// .WelcomeScreen-container {
|
||||
// margin-top: 0;
|
||||
// }
|
||||
// }
|
||||
@media (max-height: 599px) {
|
||||
.WelcomeScreen-container {
|
||||
.welcome-screen-center {
|
||||
margin-top: 4rem;
|
||||
}
|
||||
}
|
||||
@media (min-height: 600px) and (max-height: 900px) {
|
||||
.WelcomeScreen-container {
|
||||
.welcome-screen-center {
|
||||
margin-top: 8rem;
|
||||
}
|
||||
}
|
||||
@media (max-height: 630px) {
|
||||
.WelcomeScreen-decor--top-toolbar-pointer {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@media (max-height: 500px) {
|
||||
.WelcomeScreen-container {
|
||||
@media (max-height: 500px), (max-width: 320px) {
|
||||
.welcome-screen-center {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// @media (max-height: 740px) {
|
||||
// .WelcomeScreen-decor {
|
||||
// &--help-pointer,
|
||||
// &--top-toolbar-pointer,
|
||||
// &--menu-pointer {
|
||||
// display: none;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// ---------------------------------------------------------------------------
|
||||
}
|
17
src/components/welcome-screen/WelcomeScreen.tsx
Normal file
17
src/components/welcome-screen/WelcomeScreen.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { Center } from "./WelcomeScreen.Center";
|
||||
import { MenuHint, ToolbarHint, HelpHint } from "./WelcomeScreen.Hints";
|
||||
|
||||
import "./WelcomeScreen.scss";
|
||||
|
||||
const WelcomeScreen = (props: { children: React.ReactNode }) => {
|
||||
// NOTE this component is used as a dummy wrapper to retrieve child props
|
||||
// from, and will never be rendered to DOM directly. As such, we can't
|
||||
// do anything here (use hooks and such)
|
||||
return null;
|
||||
};
|
||||
WelcomeScreen.displayName = "WelcomeScreen";
|
||||
|
||||
WelcomeScreen.Center = Center;
|
||||
WelcomeScreen.Hints = { MenuHint, ToolbarHint, HelpHint };
|
||||
|
||||
export default WelcomeScreen;
|
@ -150,6 +150,7 @@ export const DEFAULT_UI_OPTIONS: AppProps["UIOptions"] = {
|
||||
toggleTheme: null,
|
||||
saveAsImage: true,
|
||||
},
|
||||
welcomeScreen: true,
|
||||
};
|
||||
|
||||
// breakpoints
|
||||
@ -236,14 +237,6 @@ export const ROUNDNESS = {
|
||||
ADAPTIVE_RADIUS: 3,
|
||||
} as const;
|
||||
|
||||
export const COOKIES = {
|
||||
AUTH_STATE_COOKIE: "excplus-auth",
|
||||
} as const;
|
||||
|
||||
/** key containt id of precedeing elemnt id we use in reconciliation during
|
||||
* collaboration */
|
||||
export const PRECEDING_ELEMENT_KEY = "__precedingElement__";
|
||||
|
||||
export const isExcalidrawPlusSignedUser = document.cookie.includes(
|
||||
COOKIES.AUTH_STATE_COOKIE,
|
||||
);
|
||||
|
@ -38,3 +38,11 @@ export const STORAGE_KEYS = {
|
||||
VERSION_DATA_STATE: "version-dataState",
|
||||
VERSION_FILES: "version-files",
|
||||
} as const;
|
||||
|
||||
export const COOKIES = {
|
||||
AUTH_STATE_COOKIE: "excplus-auth",
|
||||
} as const;
|
||||
|
||||
export const isExcalidrawPlusSignedUser = document.cookie.includes(
|
||||
COOKIES.AUTH_STATE_COOKIE,
|
||||
);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { isExcalidrawPlusSignedUser } from "../../constants";
|
||||
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
||||
|
||||
export const ExcalidrawPlusAppLink = () => {
|
||||
if (!isExcalidrawPlusSignedUser) {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import polyfill from "../polyfill";
|
||||
import LanguageDetector from "i18next-browser-languagedetector";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { trackEvent } from "../analytics";
|
||||
import { getDefaultAppState } from "../appState";
|
||||
import { ErrorDialog } from "../components/ErrorDialog";
|
||||
@ -26,6 +26,7 @@ import {
|
||||
defaultLang,
|
||||
Footer,
|
||||
MainMenu,
|
||||
WelcomeScreen,
|
||||
} from "../packages/excalidraw/index";
|
||||
import {
|
||||
AppState,
|
||||
@ -45,6 +46,7 @@ import {
|
||||
} from "../utils";
|
||||
import {
|
||||
FIREBASE_STORAGE_PREFIXES,
|
||||
isExcalidrawPlusSignedUser,
|
||||
STORAGE_KEYS,
|
||||
SYNC_BROWSER_TABS_TIMEOUT,
|
||||
} from "./app_constants";
|
||||
@ -85,7 +87,7 @@ import { parseLibraryTokensFromUrl, useHandleLibrary } from "../data/library";
|
||||
import { EncryptedIcon } from "./components/EncryptedIcon";
|
||||
import { ExcalidrawPlusAppLink } from "./components/ExcalidrawPlusAppLink";
|
||||
import { LanguageList } from "./components/LanguageList";
|
||||
import { PlusPromoIcon } from "../components/icons";
|
||||
import { PlusPromoIcon, UsersIcon } from "../components/icons";
|
||||
|
||||
polyfill();
|
||||
|
||||
@ -634,6 +636,69 @@ const ExcalidrawWrapper = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const welcomeScreenJSX = useMemo(() => {
|
||||
let headingContent;
|
||||
|
||||
if (isExcalidrawPlusSignedUser) {
|
||||
headingContent = t("welcomeScreen.app.center_heading_plus")
|
||||
.split(/(Excalidraw\+)/)
|
||||
.map((bit, idx) => {
|
||||
if (bit === "Excalidraw+") {
|
||||
return (
|
||||
<a
|
||||
style={{ pointerEvents: "all" }}
|
||||
href={`${process.env.REACT_APP_PLUS_APP}?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenSignedInUser`}
|
||||
key={idx}
|
||||
>
|
||||
Excalidraw+
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return bit;
|
||||
});
|
||||
} else {
|
||||
headingContent = t("welcomeScreen.app.center_heading");
|
||||
}
|
||||
|
||||
return (
|
||||
<WelcomeScreen>
|
||||
<WelcomeScreen.Hints.MenuHint>
|
||||
{t("welcomeScreen.app.menuHint")}
|
||||
</WelcomeScreen.Hints.MenuHint>
|
||||
<WelcomeScreen.Hints.ToolbarHint />
|
||||
<WelcomeScreen.Hints.HelpHint />
|
||||
<WelcomeScreen.Center>
|
||||
<WelcomeScreen.Center.Logo />
|
||||
<WelcomeScreen.Center.Heading>
|
||||
{headingContent}
|
||||
</WelcomeScreen.Center.Heading>
|
||||
<WelcomeScreen.Center.Menu>
|
||||
<WelcomeScreen.Center.MenuItemLoadScene />
|
||||
<WelcomeScreen.Center.MenuItemHelp />
|
||||
|
||||
<WelcomeScreen.Center.MenuItem
|
||||
shortcut={null}
|
||||
onSelect={() => setCollabDialogShown(true)}
|
||||
icon={UsersIcon}
|
||||
>
|
||||
{t("labels.liveCollaboration")}
|
||||
</WelcomeScreen.Center.MenuItem>
|
||||
|
||||
{!isExcalidrawPlusSignedUser && (
|
||||
<WelcomeScreen.Center.MenuItemLink
|
||||
href="https://plus.excalidraw.com/plus?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenGuest"
|
||||
shortcut={null}
|
||||
icon={PlusPromoIcon}
|
||||
>
|
||||
Try Excalidraw Plus!
|
||||
</WelcomeScreen.Center.MenuItemLink>
|
||||
)}
|
||||
</WelcomeScreen.Center.Menu>
|
||||
</WelcomeScreen.Center>
|
||||
</WelcomeScreen>
|
||||
);
|
||||
}, [setCollabDialogShown]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ height: "100%" }}
|
||||
@ -687,6 +752,7 @@ const ExcalidrawWrapper = () => {
|
||||
<EncryptedIcon />
|
||||
</div>
|
||||
</Footer>
|
||||
{welcomeScreenJSX}
|
||||
</Excalidraw>
|
||||
{excalidrawAPI && <Collab excalidrawAPI={excalidrawAPI} />}
|
||||
{errorMessage && (
|
||||
|
@ -448,10 +448,16 @@
|
||||
"d9480f": "Orange 9"
|
||||
},
|
||||
"welcomeScreen": {
|
||||
"data": "All your data is saved locally in your browser.",
|
||||
"switchToPlusApp": "Did you want to go to the Excalidraw+ instead?",
|
||||
"menuHints": "Export, preferences, languages, ...",
|
||||
"toolbarHints": "Pick a tool & Start drawing!",
|
||||
"helpHints": "Shortcuts & help"
|
||||
"app": {
|
||||
"center_heading": "All your data is saved locally in your browser.",
|
||||
"center_heading_plus": "Did you want to go to the Excalidraw+ instead?",
|
||||
"menuHint": "Export, preferences, languages, ..."
|
||||
},
|
||||
"defaults": {
|
||||
"menuHint": "Export, preferences, and more...",
|
||||
"center_heading": "Diagrams. Made. Simple.",
|
||||
"toolbarHint": "Pick a tool & Start drawing!",
|
||||
"helpHint": "Shortcuts & help"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,15 +15,19 @@ Please add the latest change on the top under the correct section.
|
||||
|
||||
### Features
|
||||
|
||||
- Any top-level children passed to the `<Excalidraw/>` component that do not belong to one of the officially supported Excalidraw children components are now rendered directly inside the Excalidraw container (previously, they weren't rendered at all) [#6096](https://github.com/excalidraw/excalidraw/pull/6096).
|
||||
- Support customization for the editor [welcome screen](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#WelcomeScreen) [#6048](https://github.com/excalidraw/excalidraw/pull/6048).
|
||||
|
||||
- Expose component API for the Excalidraw main menu [#6034](https://github.com/excalidraw/excalidraw/pull/6034), You can read more about its usage [here](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#MainMenu)
|
||||
|
||||
- Render Footer as a component instead of render prop [#5970](https://github.com/excalidraw/excalidraw/pull/5970). You can read more about its usage [here](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#Footer)
|
||||
- Support customization for the Excalidraw [main menu](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#MainMenu) [#6034](https://github.com/excalidraw/excalidraw/pull/6034).
|
||||
|
||||
- [Footer](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#Footer) is now rendered as child component instead of passed as a render prop [#5970](https://github.com/excalidraw/excalidraw/pull/5970).
|
||||
|
||||
- Any top-level children passed to the `<Excalidraw/>` component that do not belong to one of the officially supported Excalidraw children components are now rendered directly inside the Excalidraw container (previously, they weren't rendered at all) [#6096](https://github.com/excalidraw/excalidraw/pull/6096).
|
||||
|
||||
#### BREAKING CHANGE
|
||||
|
||||
- With this change, the prop `renderFooter` is now removed.
|
||||
- The prop `renderFooter` is now removed in favor of rendering as a child component.
|
||||
|
||||
### Excalidraw schema
|
||||
|
||||
|
@ -405,15 +405,14 @@ const App = () => {
|
||||
};
|
||||
```
|
||||
|
||||
This will only for `Desktop` devices.
|
||||
Footer is only rendered in the desktop view.
|
||||
|
||||
For `mobile` you will need to render it inside the [MainMenu](#mainmenu). You can use the [`useDevice`](#useDevice) hook to check the type of device, this will be available only inside the `children` of `Excalidraw` component.
|
||||
In the mobile view you can render it inside the [MainMenu](#mainmenu) (later we will expose other ways to customize the UI). You can use the [`useDevice`](#useDevice) hook to check the type of device, this will be available only inside the `children` of `Excalidraw` component.
|
||||
|
||||
```js
|
||||
import { useDevice, Footer } from "@excalidraw/excalidraw";
|
||||
|
||||
const MobileFooter = ({
|
||||
}) => {
|
||||
const MobileFooter = () => {
|
||||
const device = useDevice();
|
||||
if (device.isMobile) {
|
||||
return (
|
||||
@ -429,18 +428,21 @@ const MobileFooter = ({
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
||||
};
|
||||
|
||||
const App = () => {
|
||||
<Excalidraw>
|
||||
<MainMenu>
|
||||
<MainMenu.Item onSelect={() => window.alert("Item1")}> Item1 </MainMenu.Item>
|
||||
<MainMenu.Item onSelect={() => window.alert("Item2")}> Item 2 </>
|
||||
<MainMenu.Item onSelect={() => window.alert("Item1")}>
|
||||
Item1
|
||||
</MainMenu.Item>
|
||||
<MainMenu.Item onSelect={() => window.alert("Item2")}>
|
||||
Item2
|
||||
</MainMenu.Item>
|
||||
<MobileFooter />
|
||||
</MainMenu>
|
||||
</Excalidraw>
|
||||
}
|
||||
|
||||
</Excalidraw>;
|
||||
};
|
||||
```
|
||||
|
||||
You can visit the [example](https://ehlz3.csb.app/) for working demo.
|
||||
@ -456,11 +458,15 @@ import { MainMenu } from "@excalidraw/excalidraw";
|
||||
const App = () => {
|
||||
<Excalidraw>
|
||||
<MainMenu>
|
||||
<MainMenu.Item onSelect={() => window.alert("Item1")}> Item1 </MainMenu.Item>
|
||||
<MainMenu.Item onSelect={() => window.alert("Item2")}> Item 2 </>
|
||||
<MainMenu.Item onSelect={() => window.alert("Item1")}>
|
||||
Item1
|
||||
</MainMenu.Item>
|
||||
<MainMenu.Item onSelect={() => window.alert("Item2")}>
|
||||
Item2
|
||||
</MainMenu.Item>
|
||||
</MainMenu>
|
||||
</Excalidraw>
|
||||
}
|
||||
</Excalidraw>;
|
||||
};
|
||||
```
|
||||
|
||||
**MainMenu**
|
||||
@ -469,28 +475,28 @@ This is the `MainMenu` component which you need to import to render the menu wit
|
||||
|
||||
**MainMenu.Item**
|
||||
|
||||
To render an item, its recommended to use `MainMenu.Item`.
|
||||
Use this component to render a menu item.
|
||||
|
||||
| Prop | Type | Required | Default | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `onSelect` | `Function` | Yes | `undefined` | The handler is triggered when the item is selected. |
|
||||
| `children` | `React.ReactNode` | Yes | `undefined` | The content of the menu item |
|
||||
| `icon` | `JSX.Element` | No | `undefined` | The icon used in the menu item |
|
||||
| `shortcut` | `string` | No | `undefined` | The shortcut to be shown for the menu item |
|
||||
| `className` | `string` | No | "" | The class names to be added to the menu item |
|
||||
| `style` | `React.CSSProperties` | No | `undefined` | The inline styles to be added to the menu item |
|
||||
| `ariaLabel` | `string` | `undefined` | No | The `aria-label` to be added to the item for accessibility |
|
||||
| `dataTestId` | `string` | `undefined` | No | The `data-testid` to be added to the item. |
|
||||
| `onSelect` | `Function` | Yes | | The handler is triggered when the item is selected. |
|
||||
| `children` | `React.ReactNode` | Yes | | The content of the menu item |
|
||||
| `icon` | `JSX.Element` | No | | The icon used in the menu item |
|
||||
| `shortcut` | `string` | No | | The keyboard shortcut (label-only, does not affect behavior) |
|
||||
| `className` | `string` | No | | The class names to be added to the menu item |
|
||||
| `style` | `React.CSSProperties` | No | | The inline styles to be added to the menu item |
|
||||
| `ariaLabel` | `string` | | No | The `aria-label` to be added to the item for accessibility |
|
||||
| `dataTestId` | `string` | | No | The `data-testid` to be added to the item. |
|
||||
|
||||
**MainMenu.ItemLink**
|
||||
|
||||
To render an item as a link, its recommended to use `MainMenu.ItemLink`.
|
||||
To render an external link in a menu item, you can use this component.
|
||||
|
||||
**Usage**
|
||||
|
||||
```js
|
||||
import { MainMenu } from "@excalidraw/excalidraw";
|
||||
const App = () => {
|
||||
const App = () => (
|
||||
<Excalidraw>
|
||||
<MainMenu>
|
||||
<MainMenu.ItemLink href="https://google.com">Google</MainMenu.ItemLink>
|
||||
@ -499,19 +505,19 @@ const App = () => {
|
||||
</MainMenu.ItemLink>
|
||||
</MainMenu>
|
||||
</Excalidraw>;
|
||||
};
|
||||
);
|
||||
```
|
||||
|
||||
| Prop | Type | Required | Default | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `href` | `string` | Yes | `undefined` | The `href` attribute to be added to the `anchor` element. |
|
||||
| `children` | `React.ReactNode` | Yes | `undefined` | The content of the menu item |
|
||||
| `icon` | `JSX.Element` | No | `undefined` | The icon used in the menu item |
|
||||
| `shortcut` | `string` | No | `undefined` | The shortcut to be shown for the menu item |
|
||||
| `href` | `string` | Yes | | The `href` attribute to be added to the `anchor` element. |
|
||||
| `children` | `React.ReactNode` | Yes | | The content of the menu item |
|
||||
| `icon` | `JSX.Element` | No | | The icon used in the menu item |
|
||||
| `shortcut` | `string` | No | | The keyboard shortcut (label-only, does not affect behavior) |
|
||||
| `className` | `string` | No | "" | The class names to be added to the menu item |
|
||||
| `style` | `React.CSSProperties` | No | `undefined` | The inline styles to be added to the menu item |
|
||||
| `ariaLabel` | `string` | No | `undefined` | The `aria-label` to be added to the item for accessibility |
|
||||
| `dataTestId` | `string` | No | `undefined` | The `data-testid` to be added to the item. |
|
||||
| `style` | `React.CSSProperties` | No | | The inline styles to be added to the menu item |
|
||||
| `ariaLabel` | `string` | No | | The `aria-label` to be added to the item for accessibility |
|
||||
| `dataTestId` | `string` | No | | The `data-testid` to be added to the item. |
|
||||
|
||||
**MainMenu.ItemCustom**
|
||||
|
||||
@ -521,7 +527,7 @@ To render a custom item, you can use `MainMenu.ItemCustom`.
|
||||
|
||||
```js
|
||||
import { MainMenu } from "@excalidraw/excalidraw";
|
||||
const App = () => {
|
||||
const App = () => (
|
||||
<Excalidraw>
|
||||
<MainMenu>
|
||||
<MainMenu.ItemCustom>
|
||||
@ -535,7 +541,7 @@ const App = () => {
|
||||
</MainMenu.ItemCustom>
|
||||
</MainMenu>
|
||||
</Excalidraw>;
|
||||
};
|
||||
);
|
||||
```
|
||||
|
||||
| Prop | Type | Required | Default | Description |
|
||||
@ -551,7 +557,7 @@ For the items which are shown in the menu in [excalidraw.com](https://excalidraw
|
||||
|
||||
```js
|
||||
import { MainMenu } from "@excalidraw/excalidraw";
|
||||
const App = () => {
|
||||
const App = () => (
|
||||
<Excalidraw>
|
||||
<MainMenu>
|
||||
<MainMenu.DefaultItems.Socials/>
|
||||
@ -560,7 +566,7 @@ const App = () => {
|
||||
<MainMenu.Item onSelect={() => window.alert("Item2")}> Item 2 </>
|
||||
</MainMenu>
|
||||
</Excalidraw>
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
Here is a [complete list](https://github.com/excalidraw/excalidraw/blob/master/src/components/mainMenu/DefaultItems.tsx) of the default items.
|
||||
@ -571,7 +577,7 @@ To Group item in the main menu, you can use `MainMenu.Group`
|
||||
|
||||
```js
|
||||
import { MainMenu } from "@excalidraw/excalidraw";
|
||||
const App = () => {
|
||||
const App = () => (
|
||||
<Excalidraw>
|
||||
<MainMenu>
|
||||
<MainMenu.Group title="Excalidraw items">
|
||||
@ -584,16 +590,149 @@ const App = () => {
|
||||
</MainMenu.Group>
|
||||
</MainMenu>
|
||||
</Excalidraw>
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
| Prop | Type | Required | Default | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `children ` | `React.ReactNode` | Yes | `undefined` | The content of the `Menu Group` |
|
||||
| `children ` | `React.ReactNode` | Yes | `undefined` | The content of the `MenuItem Group` |
|
||||
| `title` | `string` | No | `undefined` | The `title` for the grouped items |
|
||||
| `className` | `string` | No | "" | The `classname` to be added to the group |
|
||||
| `style` | `React.CSsSProperties` | No | `undefined` | The inline `styles` to be added to the group |
|
||||
|
||||
### WelcomeScreen
|
||||
|
||||
When the canvas is empty, Excalidraw shows a welcome "splash" screen with a logo, a few quick action items, and hints explaining what some of the UI buttons do. You can customize the welcome screen by rendering the `WelcomeScreen` component inside your Excalidraw instance.
|
||||
|
||||
You can also disable the welcome screen altogether by setting `UIOptions.welcomeScreen` to `false`.
|
||||
|
||||
**Usage**
|
||||
|
||||
```jsx
|
||||
import { WelcomScreen } from "@excalidraw/excalidraw";
|
||||
const App = () => (
|
||||
<Excalidraw>
|
||||
<WelcomeScreen>
|
||||
<WelcomeScreen.Center>
|
||||
<WelcomeScreen.Center.Heading>
|
||||
Your data are autosaved to the cloud.
|
||||
</WelcomeScreen.Center.Heading>
|
||||
<WelcomeScreen.Center.Menu>
|
||||
<WelcomeScreen.Center.MenuItem
|
||||
onClick={() => console.log("clicked!")}
|
||||
>
|
||||
Click me!
|
||||
</WelcomeScreen.Center.MenuItem>
|
||||
<WelcomeScreen.Center.MenuItemLink href="https://github.com/excalidraw/excalidraw">
|
||||
Excalidraw GitHub
|
||||
</WelcomeScreen.Center.MenuItemLink>
|
||||
<WelcomeScreen.Center.MenuItemHelp />
|
||||
</WelcomeScreen.Center.Menu>
|
||||
</WelcomeScreen.Center>
|
||||
</WelcomeScreen>
|
||||
</Excalidraw>
|
||||
);
|
||||
```
|
||||
|
||||
To disable the WelcomeScreen:
|
||||
|
||||
```jsx
|
||||
import { WelcomScreen } from "@excalidraw/excalidraw";
|
||||
const App = () => <Excalidraw UIOptions={{ welcomeScreen: false }} />;
|
||||
```
|
||||
|
||||
**WelcomeScreen**
|
||||
|
||||
If you render the `<WelcomeScreen>` component, you are responsible for rendering the content.
|
||||
|
||||
There are 2 main parts: 1) welcome screen center component, and 2) welcome screen hints.
|
||||
|
||||
![WelcomeScreen overview](./welcome-screen-overview.png)
|
||||
|
||||
**WelcomeScreen.Center**
|
||||
|
||||
This is the center piece of the welcome screen, containing the logo, heading, and menu. All three sub-components are optional, and you can render whatever you wish into the center component.
|
||||
|
||||
**WelcomeScreen.Center.Logo**
|
||||
|
||||
By default renders the Excalidraw logo and name. Supply `children` to customize.
|
||||
|
||||
**WelcomeScreen.Center.Heading**
|
||||
|
||||
Supply `children` to change the default message.
|
||||
|
||||
**WelcomeScreen.Center.Menu**
|
||||
|
||||
Wrapper component for the menu items. You can build your menu using the `<WelcomeScreen.Center.MenuItem>` and `<WelcomeScreen.Center.MenuItemLink>` components, render your own, or render one of the default menu items.
|
||||
|
||||
The default menu items are:
|
||||
|
||||
- `<WelcomeScreen.Center.MenuItemHelp/>` - opens the help dialog.
|
||||
- `<WelcomeScreen.Center.MenuItemLoadScene/>` - open the load file dialog.
|
||||
|
||||
**Usage**
|
||||
|
||||
```jsx
|
||||
import { WelcomScreen } from "@excalidraw/excalidraw";
|
||||
const App = () => (
|
||||
<Excalidraw>
|
||||
<WelcomeScreen>
|
||||
<WelcomeScreen.Center>
|
||||
<WelcomeScreen.Center.Menu>
|
||||
<WelcomeScreen.Center.MenuItem
|
||||
onClick={() => console.log("clicked!")}
|
||||
>
|
||||
Click me!
|
||||
</WelcomeScreen.Center.MenuItem>
|
||||
<WelcomeScreen.Center.MenuItemLink href="https://github.com/excalidraw/excalidraw">
|
||||
Excalidraw GitHub
|
||||
</WelcomeScreen.Center.MenuItemLink>
|
||||
<WelcomeScreen.Center.MenuItemHelp />
|
||||
</WelcomeScreen.Center.Menu>
|
||||
</WelcomeScreen.Center>
|
||||
</WelcomeScreen>
|
||||
</Excalidraw>
|
||||
);
|
||||
```
|
||||
|
||||
**WelcomeScreen.Center.MenuItem**
|
||||
|
||||
Use this component to render a menu item.
|
||||
|
||||
| Prop | Type | Required | Default | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `onSelect` | `Function` | Yes | | The handler is triggered when the item is selected. |
|
||||
| `children` | `React.ReactNode` | Yes | | The content of the menu item |
|
||||
| `icon` | `JSX.Element` | No | | The icon used in the menu item |
|
||||
| `shortcut` | `string` | No | | The keyboard shortcut (label-only, does not affect behavior) |
|
||||
|
||||
**WelcomeScreen.Center.MenuItemLink**
|
||||
|
||||
To render an external link in a menu item, you can use this component.
|
||||
|
||||
| Prop | Type | Required | Default | Description |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `href` | `string` | Yes | | The `href` attribute to be added to the `anchor` element. |
|
||||
| `children` | `React.ReactNode` | Yes | | The content of the menu item |
|
||||
| `icon` | `JSX.Element` | No | | The icon used in the menu item |
|
||||
| `shortcut` | `string` | No | | The keyboard shortcut (label-only, does not affect behavior) |
|
||||
|
||||
**WelcomeScreen.Hints**
|
||||
|
||||
These subcomponents render the UI hints. Text of each hint can be customized by supplying `children`.
|
||||
|
||||
**WelcomeScreen.Hints.Menu**
|
||||
|
||||
Hint for the main menu. Supply `children` to customize the hint text.
|
||||
|
||||
**WelcomeScreen.Hints.Toolbar**
|
||||
|
||||
Hint for the toolbar. Supply `children` to customize the hint text.
|
||||
|
||||
**WelcomeScreen.Hints.Help**
|
||||
|
||||
Hint for the help dialog. Supply `children` to customize the hint text.
|
||||
|
||||
### Props
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
@ -1565,8 +1704,7 @@ This hook can be used to check the type of device which is being used. It can on
|
||||
```js
|
||||
import { useDevice, Footer } from "@excalidraw/excalidraw";
|
||||
|
||||
const MobileFooter = ({
|
||||
}) => {
|
||||
const MobileFooter = () => {
|
||||
const device = useDevice();
|
||||
if (device.isMobile) {
|
||||
return (
|
||||
|
@ -13,6 +13,7 @@ import { Provider } from "jotai";
|
||||
import { jotaiScope, jotaiStore } from "../../jotai";
|
||||
import Footer from "../../components/footer/FooterCenter";
|
||||
import MainMenu from "../../components/mainMenu/MainMenu";
|
||||
import WelcomeScreen from "../../components/welcome-screen/WelcomeScreen";
|
||||
|
||||
const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||
const {
|
||||
@ -52,6 +53,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||
...DEFAULT_UI_OPTIONS.canvasActions,
|
||||
...canvasActions,
|
||||
},
|
||||
welcomeScreen: props.UIOptions?.welcomeScreen ?? true,
|
||||
};
|
||||
|
||||
if (canvasActions?.export) {
|
||||
@ -162,7 +164,7 @@ const areEqual = (
|
||||
const canvasOptionKeys = Object.keys(
|
||||
prevUIOptions.canvasActions!,
|
||||
) as (keyof Partial<typeof DEFAULT_UI_OPTIONS.canvasActions>)[];
|
||||
canvasOptionKeys.every((key) => {
|
||||
return canvasOptionKeys.every((key) => {
|
||||
if (
|
||||
key === "export" &&
|
||||
prevUIOptions?.canvasActions?.export &&
|
||||
@ -179,7 +181,7 @@ const areEqual = (
|
||||
);
|
||||
});
|
||||
}
|
||||
return true;
|
||||
return prevUIOptions[key] === nextUIOptions[key];
|
||||
});
|
||||
|
||||
return isUIOptionsSame && isShallowEqual(prev, next);
|
||||
@ -243,3 +245,4 @@ export { Button } from "../../components/Button";
|
||||
export { Footer };
|
||||
export { MainMenu };
|
||||
export { useDevice } from "../../components/App";
|
||||
export { WelcomeScreen };
|
||||
|
BIN
src/packages/excalidraw/welcome-screen-overview.png
Normal file
BIN
src/packages/excalidraw/welcome-screen-overview.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 101 KiB |
67
src/types.ts
67
src/types.ts
@ -313,10 +313,7 @@ export interface ExcalidrawProps {
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
appState: AppState,
|
||||
) => JSX.Element;
|
||||
UIOptions?: {
|
||||
dockedSidebarBreakpoint?: number;
|
||||
canvasActions?: CanvasActions;
|
||||
};
|
||||
UIOptions?: Partial<UIOptions>;
|
||||
detectScroll?: boolean;
|
||||
handleKeyboardGlobally?: boolean;
|
||||
onLibraryChange?: (libraryItems: LibraryItems) => void | Promise<any>;
|
||||
@ -373,23 +370,31 @@ export type ExportOpts = {
|
||||
// truthiness value will determine whether the action is rendered or not
|
||||
// (see manager renderAction). We also override canvasAction values in
|
||||
// excalidraw package index.tsx.
|
||||
type CanvasActions = {
|
||||
changeViewBackgroundColor?: boolean;
|
||||
clearCanvas?: boolean;
|
||||
export?: false | ExportOpts;
|
||||
loadScene?: boolean;
|
||||
saveToActiveFile?: boolean;
|
||||
toggleTheme?: boolean | null;
|
||||
saveAsImage?: boolean;
|
||||
};
|
||||
type CanvasActions = Partial<{
|
||||
changeViewBackgroundColor: boolean;
|
||||
clearCanvas: boolean;
|
||||
export: false | ExportOpts;
|
||||
loadScene: boolean;
|
||||
saveToActiveFile: boolean;
|
||||
toggleTheme: boolean | null;
|
||||
saveAsImage: boolean;
|
||||
}>;
|
||||
|
||||
type UIOptions = Partial<{
|
||||
dockedSidebarBreakpoint: number;
|
||||
welcomeScreen: boolean;
|
||||
canvasActions: CanvasActions;
|
||||
}>;
|
||||
|
||||
export type AppProps = Merge<
|
||||
ExcalidrawProps,
|
||||
{
|
||||
UIOptions: {
|
||||
UIOptions: Merge<
|
||||
MarkRequired<UIOptions, "welcomeScreen">,
|
||||
{
|
||||
canvasActions: Required<CanvasActions> & { export: ExportOpts };
|
||||
dockedSidebarBreakpoint?: number;
|
||||
};
|
||||
}
|
||||
>;
|
||||
detectScroll: boolean;
|
||||
handleKeyboardGlobally: boolean;
|
||||
isCollaborating: boolean;
|
||||
@ -517,7 +522,31 @@ export type Device = Readonly<{
|
||||
}>;
|
||||
|
||||
export type UIChildrenComponents = {
|
||||
[k in "FooterCenter" | "Menu"]?:
|
||||
| React.ReactPortal
|
||||
| React.ReactElement<unknown, string | React.JSXElementConstructor<any>>;
|
||||
[k in "FooterCenter" | "Menu" | "WelcomeScreen"]?: React.ReactElement<
|
||||
{ children?: React.ReactNode },
|
||||
React.JSXElementConstructor<any>
|
||||
>;
|
||||
};
|
||||
|
||||
export type UIWelcomeScreenComponents = {
|
||||
[k in
|
||||
| "Center"
|
||||
| "MenuHint"
|
||||
| "ToolbarHint"
|
||||
| "HelpHint"]?: React.ReactElement<
|
||||
{ children?: React.ReactNode },
|
||||
React.JSXElementConstructor<any>
|
||||
>;
|
||||
};
|
||||
|
||||
export type UIWelcomeScreenCenterComponents = {
|
||||
[k in
|
||||
| "Logo"
|
||||
| "Heading"
|
||||
| "Menu"
|
||||
| "MenuItemLoadScene"
|
||||
| "MenuItemHelp"]?: React.ReactElement<
|
||||
{ children?: React.ReactNode },
|
||||
React.JSXElementConstructor<any>
|
||||
>;
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user