diff --git a/src/components/App.tsx b/src/components/App.tsx index 9ce9dfc4..51b8c302 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -283,15 +283,12 @@ const deviceContextInitialValue = { }; const DeviceContext = React.createContext(deviceContextInitialValue); DeviceContext.displayName = "DeviceContext"; -export const useDevice = () => useContext(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["setState"] ->(() => {}); +>(() => { + console.warn("unitialized ExcalidrawSetAppStateContext context!"); +}); ExcalidrawSetAppStateContext.displayName = "ExcalidrawSetAppStateContext"; const ExcalidrawActionManagerContext = React.createContext( @@ -317,6 +316,9 @@ const ExcalidrawActionManagerContext = React.createContext( ); ExcalidrawActionManagerContext.displayName = "ExcalidrawActionManagerContext"; +export const useDevice = () => useContext(DeviceContext); +export const useExcalidrawContainer = () => + useContext(ExcalidrawContainerContext); export const useExcalidrawElements = () => useContext(ExcalidrawElementsContext); export const useExcalidrawAppState = () => @@ -598,6 +600,8 @@ class App extends React.Component { 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 diff --git a/src/components/LayerUI.tsx b/src/components/LayerUI.tsx index aacbef36..5d4084a1 100644 --- a/src/components/LayerUI.tsx +++ b/src/components/LayerUI.tsx @@ -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(children, { Menu: true, FooterCenter: true, + WelcomeScreen: true, }); + const [WelcomeScreenComponents] = getReactChildren( + renderWelcomeScreen + ? ( + childrenComponents?.WelcomeScreen ?? ( + + + + + + + ) + )?.props?.children + : null, + ); + const renderJSONExportDialog = () => { if (!UIOptions.canvasActions.export) { return null; @@ -213,15 +228,10 @@ const LayerUI = ({ }; const renderCanvasActions = () => (
- -
- {WelcomeScreenMenuArrow} -
{t("welcomeScreen.menuHints")}
-
-
- {renderMenu()} + {WelcomeScreenComponents.MenuHint} + {/* wrapping to Fragment stops React from occasionally complaining + about identical Keys */} + <>{renderMenu()}
); @@ -258,9 +268,7 @@ const LayerUI = ({ return ( - {renderWelcomeScreen && !appState.isLoading && ( - - )} + {WelcomeScreenComponents.Center}
{(heading: React.ReactNode) => (
- -
-
- {t("welcomeScreen.toolbarHints")} -
- {WelcomeScreenTopToolbarArrow} -
-
- + {WelcomeScreenComponents.ToolbarHint} 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()}
- {appState.showStats && ( React.ReactNode; setAppState: React.Component["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 ( - {renderWelcomeScreen && !appState.isLoading && ( - - )} + {welcomeScreenCenter}
{(heading: React.ReactNode) => ( @@ -74,20 +73,6 @@ export const MobileMenu = ({ {heading} - {/* - -
*/} void; - icon: JSX.Element; - link?: string; -}) => { - if (link) { - return ( - -
- {icon} - {label} -
-
- ); - } - - return ( - - ); -}; - -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 ( - - Excalidraw+ - - ); - } - return bit; - }); - } else { - subheadingJSX = t("welcomeScreen.data"); - } - - return ( -
-
- {ExcalLogo} Excalidraw -
-
- {subheadingJSX} -
-
- {!appState.viewModeEnabled && ( - actionManager.executeAction(actionLoadScene)} - shortcut={getShortcutFromShortcutName("loadScene")} - icon={LoadIcon} - /> - )} - actionManager.executeAction(actionShortcuts)} - label={t("helpDialog.title")} - shortcut="?" - icon={HelpIcon} - /> - {!isExcalidrawPlusSignedUser && ( - - )} -
-
- ); -}; - -export default WelcomeScreen; diff --git a/src/components/WelcomeScreenDecor.tsx b/src/components/WelcomeScreenDecor.tsx deleted file mode 100644 index 1c7d6cc8..00000000 --- a/src/components/WelcomeScreenDecor.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { ReactNode } from "react"; - -const WelcomeScreenDecor = ({ - children, - shouldRender, -}: { - children: ReactNode; - shouldRender: boolean; -}) => (shouldRender ? <>{children} : null); - -export default WelcomeScreenDecor; diff --git a/src/components/footer/Footer.tsx b/src/components/footer/Footer.tsx index cfd38959..3cb1dd29 100644 --- a/src/components/footer/Footer.tsx +++ b/src/components/footer/Footer.tsx @@ -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 = ({ })} >
- -
-
{t("welcomeScreen.helpHints")}
- {WelcomeScreenHelpArrow} -
-
- + {welcomeScreenHelp} actionManager.executeAction(actionShortcuts)} />
diff --git a/src/components/welcome-screen/WelcomeScreen.Center.tsx b/src/components/welcome-screen/WelcomeScreen.Center.tsx new file mode 100644 index 00000000..7875c0cc --- /dev/null +++ b/src/components/welcome-screen/WelcomeScreen.Center.tsx @@ -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 ( + <> +
{icon}
+
{children}
+ {shortcut && !device.isMobile && ( +
{shortcut}
+ )} + + ); +}; +WelcomeScreenMenuItemContent.displayName = "WelcomeScreenMenuItemContent"; + +const WelcomeScreenMenuItem = ({ + onSelect, + children, + icon, + shortcut, + className = "", + ...props +}: { + onSelect: () => void; + children: React.ReactNode; + icon?: JSX.Element; + shortcut?: string | null; +} & React.ButtonHTMLAttributes) => { + return ( + + ); +}; +WelcomeScreenMenuItem.displayName = "WelcomeScreenMenuItem"; + +const WelcomeScreenMenuItemLink = ({ + children, + href, + icon, + shortcut, + className = "", + ...props +}: { + children: React.ReactNode; + href: string; + icon?: JSX.Element; + shortcut?: string | null; +} & React.AnchorHTMLAttributes) => { + return ( + + + {children} + + + ); +}; +WelcomeScreenMenuItemLink.displayName = "WelcomeScreenMenuItemLink"; + +const Center = ({ children }: { children?: React.ReactNode }) => { + return ( +
+ {children || ( + <> + + {t("welcomeScreen.defaults.center_heading")} + + + + + + )} +
+ ); +}; +Center.displayName = "Center"; + +const Logo = ({ children }: { children?: React.ReactNode }) => { + return ( +
+ {children || <>{ExcalLogo} Excalidraw} +
+ ); +}; +Logo.displayName = "Logo"; + +const Heading = ({ children }: { children: React.ReactNode }) => { + return ( +
+ {children} +
+ ); +}; +Heading.displayName = "Heading"; + +const Menu = ({ children }: { children?: React.ReactNode }) => { + return
{children}
; +}; +Menu.displayName = "Menu"; + +const MenuItemHelp = () => { + const actionManager = useExcalidrawActionManager(); + + return ( + actionManager.executeAction(actionShortcuts)} + shortcut="?" + icon={HelpIcon} + > + {t("helpDialog.title")} + + ); +}; +MenuItemHelp.displayName = "MenuItemHelp"; + +const MenuItemLoadScene = () => { + const appState = useExcalidrawAppState(); + const actionManager = useExcalidrawActionManager(); + + if (appState.viewModeEnabled) { + return null; + } + + return ( + actionManager.executeAction(actionLoadScene)} + shortcut={getShortcutFromShortcutName("loadScene")} + icon={LoadIcon} + > + {t("buttons.load")} + + ); +}; +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 }; diff --git a/src/components/welcome-screen/WelcomeScreen.Hints.tsx b/src/components/welcome-screen/WelcomeScreen.Hints.tsx new file mode 100644 index 00000000..d961a6c6 --- /dev/null +++ b/src/components/welcome-screen/WelcomeScreen.Hints.tsx @@ -0,0 +1,42 @@ +import { t } from "../../i18n"; +import { + WelcomeScreenHelpArrow, + WelcomeScreenMenuArrow, + WelcomeScreenTopToolbarArrow, +} from "../icons"; + +const MenuHint = ({ children }: { children?: React.ReactNode }) => { + return ( +
+ {WelcomeScreenMenuArrow} +
+ {children || t("welcomeScreen.defaults.menuHint")} +
+
+ ); +}; +MenuHint.displayName = "MenuHint"; + +const ToolbarHint = ({ children }: { children?: React.ReactNode }) => { + return ( +
+
+ {children || t("welcomeScreen.defaults.toolbarHint")} +
+ {WelcomeScreenTopToolbarArrow} +
+ ); +}; +ToolbarHint.displayName = "ToolbarHint"; + +const HelpHint = ({ children }: { children?: React.ReactNode }) => { + return ( +
+
{children || t("welcomeScreen.defaults.helpHint")}
+ {WelcomeScreenHelpArrow} +
+ ); +}; +HelpHint.displayName = "HelpHint"; + +export { HelpHint, MenuHint, ToolbarHint }; diff --git a/src/components/WelcomeScreen.scss b/src/components/welcome-screen/WelcomeScreen.scss similarity index 61% rename from src/components/WelcomeScreen.scss rename to src/components/welcome-screen/WelcomeScreen.scss index 0ccb6380..f856d459 100644 --- a/src/components/WelcomeScreen.scss +++ b/src/components/welcome-screen/WelcomeScreen.scss @@ -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; + &.theme--dark { + .welcome-screen-decor { + color: var(--color-gray-60); + } + } + + // WelcomeScreen.Hints + // --------------------------------------------------------------------------- + + .welcome-screen-decor-hint { + @media (max-height: 599px) { + display: none !important; } - &--help-pointer { + @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 { - width: var(--default-icon-size); - height: var(--default-icon-size); - } + &__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; - // } - // } - // } + // --------------------------------------------------------------------------- } diff --git a/src/components/welcome-screen/WelcomeScreen.tsx b/src/components/welcome-screen/WelcomeScreen.tsx new file mode 100644 index 00000000..9f8c7d73 --- /dev/null +++ b/src/components/welcome-screen/WelcomeScreen.tsx @@ -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; diff --git a/src/constants.ts b/src/constants.ts index 47ddf2b2..d6b744bb 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -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, -); diff --git a/src/excalidraw-app/app_constants.ts b/src/excalidraw-app/app_constants.ts index 55047ddf..179fe52e 100644 --- a/src/excalidraw-app/app_constants.ts +++ b/src/excalidraw-app/app_constants.ts @@ -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, +); diff --git a/src/excalidraw-app/components/ExcalidrawPlusAppLink.tsx b/src/excalidraw-app/components/ExcalidrawPlusAppLink.tsx index febb66d5..616a2a05 100644 --- a/src/excalidraw-app/components/ExcalidrawPlusAppLink.tsx +++ b/src/excalidraw-app/components/ExcalidrawPlusAppLink.tsx @@ -1,4 +1,4 @@ -import { isExcalidrawPlusSignedUser } from "../../constants"; +import { isExcalidrawPlusSignedUser } from "../app_constants"; export const ExcalidrawPlusAppLink = () => { if (!isExcalidrawPlusSignedUser) { diff --git a/src/excalidraw-app/index.tsx b/src/excalidraw-app/index.tsx index 3b24291f..beef0943 100644 --- a/src/excalidraw-app/index.tsx +++ b/src/excalidraw-app/index.tsx @@ -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 ( + + Excalidraw+ + + ); + } + return bit; + }); + } else { + headingContent = t("welcomeScreen.app.center_heading"); + } + + return ( + + + {t("welcomeScreen.app.menuHint")} + + + + + + + {headingContent} + + + + + + setCollabDialogShown(true)} + icon={UsersIcon} + > + {t("labels.liveCollaboration")} + + + {!isExcalidrawPlusSignedUser && ( + + Try Excalidraw Plus! + + )} + + + + ); + }, [setCollabDialogShown]); + return (
{
+ {welcomeScreenJSX} {excalidrawAPI && } {errorMessage && ( diff --git a/src/locales/en.json b/src/locales/en.json index 50424c3f..2fe9e7c8 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -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" + } } } diff --git a/src/packages/excalidraw/CHANGELOG.md b/src/packages/excalidraw/CHANGELOG.md index c5dabf07..94db2746 100644 --- a/src/packages/excalidraw/CHANGELOG.md +++ b/src/packages/excalidraw/CHANGELOG.md @@ -15,15 +15,19 @@ Please add the latest change on the top under the correct section. ### Features -- Any top-level children passed to the `` 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 `` 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 diff --git a/src/packages/excalidraw/README.md b/src/packages/excalidraw/README.md index b946e432..5b9f3034 100644 --- a/src/packages/excalidraw/README.md +++ b/src/packages/excalidraw/README.md @@ -376,7 +376,7 @@ Most notably, you can customize the primary colors, by overriding these variable For a complete list of variables, check [theme.scss](https://github.com/excalidraw/excalidraw/blob/master/src/css/theme.scss), though most of them will not make sense to override. -### Does this package support collaboration ? +### Does this package support collaboration? No, Excalidraw package doesn't come with collaboration built in, since the implementation is specific to each host app. We expose APIs which you can use to communicate with Excalidraw which you can use to implement it. You can check our own implementation [here](https://github.com/excalidraw/excalidraw/blob/master/src/excalidraw-app/index.tsx). @@ -405,45 +405,47 @@ 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 (
- +
); } return null; - }; + const App = () => { - window.alert("Item1")}> Item1 - window.alert("Item2")}> Item 2 - + window.alert("Item1")}> + Item1 + + window.alert("Item2")}> + Item2 + + - -} - +
; +}; ``` -You can visit the[ example](https://ehlz3.csb.app/) for working demo. +You can visit the [example](https://ehlz3.csb.app/) for working demo. #### MainMenu @@ -456,11 +458,15 @@ import { MainMenu } from "@excalidraw/excalidraw"; const App = () => { - window.alert("Item1")}> Item1 - window.alert("Item2")}> Item 2 + window.alert("Item1")}> + Item1 + + window.alert("Item2")}> + Item2 + - -} +
; +}; ``` **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 = () => ( Google @@ -499,19 +505,19 @@ const App = () => { ; -}; +); ``` | 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 = () => ( @@ -535,7 +541,7 @@ const App = () => { ; -}; +); ``` | 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 = () => ( @@ -560,7 +566,7 @@ const App = () => { window.alert("Item2")}> Item 2 -} +) ``` 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 = () => ( @@ -584,16 +590,149 @@ const App = () => { -} +) ``` | 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 = () => ( + + + + + Your data are autosaved to the cloud. + + + console.log("clicked!")} + > + Click me! + + + Excalidraw GitHub + + + + + + +); +``` + +To disable the WelcomeScreen: + +```jsx +import { WelcomScreen } from "@excalidraw/excalidraw"; +const App = () => ; +``` + +**WelcomeScreen** + +If you render the `` 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 `` and `` components, render your own, or render one of the default menu items. + +The default menu items are: + +- `` - opens the help dialog. +- `` - open the load file dialog. + +**Usage** + +```jsx +import { WelcomScreen } from "@excalidraw/excalidraw"; +const App = () => ( + + + + + console.log("clicked!")} + > + Click me! + + + Excalidraw GitHub + + + + + + +); +``` + +**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 ( diff --git a/src/packages/excalidraw/index.tsx b/src/packages/excalidraw/index.tsx index abc7bdd3..9be20a99 100644 --- a/src/packages/excalidraw/index.tsx +++ b/src/packages/excalidraw/index.tsx @@ -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)[]; - 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 }; diff --git a/src/packages/excalidraw/welcome-screen-overview.png b/src/packages/excalidraw/welcome-screen-overview.png new file mode 100644 index 00000000..10677616 Binary files /dev/null and b/src/packages/excalidraw/welcome-screen-overview.png differ diff --git a/src/types.ts b/src/types.ts index 8ee85d27..343789e5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -313,10 +313,7 @@ export interface ExcalidrawProps { elements: readonly NonDeletedExcalidrawElement[], appState: AppState, ) => JSX.Element; - UIOptions?: { - dockedSidebarBreakpoint?: number; - canvasActions?: CanvasActions; - }; + UIOptions?: Partial; detectScroll?: boolean; handleKeyboardGlobally?: boolean; onLibraryChange?: (libraryItems: LibraryItems) => void | Promise; @@ -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: { - canvasActions: Required & { export: ExportOpts }; - dockedSidebarBreakpoint?: number; - }; + UIOptions: Merge< + MarkRequired, + { + canvasActions: Required & { export: ExportOpts }; + } + >; 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>; + [k in "FooterCenter" | "Menu" | "WelcomeScreen"]?: React.ReactElement< + { children?: React.ReactNode }, + React.JSXElementConstructor + >; +}; + +export type UIWelcomeScreenComponents = { + [k in + | "Center" + | "MenuHint" + | "ToolbarHint" + | "HelpHint"]?: React.ReactElement< + { children?: React.ReactNode }, + React.JSXElementConstructor + >; +}; + +export type UIWelcomeScreenCenterComponents = { + [k in + | "Logo" + | "Heading" + | "Menu" + | "MenuItemLoadScene" + | "MenuItemHelp"]?: React.ReactElement< + { children?: React.ReactNode }, + React.JSXElementConstructor + >; };