diff options
| author | kj_sh604 | 2026-03-15 16:19:35 -0400 |
|---|---|---|
| committer | kj_sh604 | 2026-03-15 16:19:35 -0400 |
| commit | 6ec259a0e71174651bae95d4628138bf6fd68742 (patch) | |
| tree | 5e33c6a5ec091ecabfcb257fdc7b6a88ed8754ac /packages/excalidraw/components/welcome-screen | |
| parent | 16c8578b15c727f22921f8a80a56ee4d4e7f2272 (diff) | |
refactor: packages/
Diffstat (limited to 'packages/excalidraw/components/welcome-screen')
4 files changed, 545 insertions, 0 deletions
diff --git a/packages/excalidraw/components/welcome-screen/WelcomeScreen.Center.tsx b/packages/excalidraw/components/welcome-screen/WelcomeScreen.Center.tsx new file mode 100644 index 0000000..4faa41b --- /dev/null +++ b/packages/excalidraw/components/welcome-screen/WelcomeScreen.Center.tsx @@ -0,0 +1,195 @@ +import type { JSX } from "react"; +import { actionLoadScene, actionShortcuts } from "../../actions"; +import { getShortcutFromShortcutName } from "../../actions/shortcuts"; +import { t, useI18n } from "../../i18n"; +import { useDevice, useExcalidrawActionManager } from "../App"; +import { useTunnels } from "../../context/tunnels"; +import { HelpIcon, LoadIcon, usersIcon } from "../icons"; +import { useUIAppState } from "../../context/ui-appState"; +import { ExcalidrawLogo } from "../ExcalidrawLogo"; + +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.editor.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 }) => { + const { WelcomeScreenCenterTunnel } = useTunnels(); + return ( + <WelcomeScreenCenterTunnel.In> + <div className="welcome-screen-center"> + {children || ( + <> + <Logo /> + <Heading>{t("welcomeScreen.defaults.center_heading")}</Heading> + <Menu> + <MenuItemLoadScene /> + <MenuItemHelp /> + </Menu> + </> + )} + </div> + </WelcomeScreenCenterTunnel.In> + ); +}; +Center.displayName = "Center"; + +const Logo = ({ children }: { children?: React.ReactNode }) => { + return ( + <div className="welcome-screen-center__logo excalifont welcome-screen-decor"> + {children || <ExcalidrawLogo withText />} + </div> + ); +}; +Logo.displayName = "Logo"; + +const Heading = ({ children }: { children: React.ReactNode }) => { + return ( + <div className="welcome-screen-center__heading welcome-screen-decor excalifont"> + {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 = useUIAppState(); + 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"; + +const MenuItemLiveCollaborationTrigger = ({ + onSelect, +}: { + onSelect: () => any; +}) => { + const { t } = useI18n(); + return ( + <WelcomeScreenMenuItem shortcut={null} onSelect={onSelect} icon={usersIcon}> + {t("labels.liveCollaboration")} + </WelcomeScreenMenuItem> + ); +}; +MenuItemLiveCollaborationTrigger.displayName = + "MenuItemLiveCollaborationTrigger"; + +// ----------------------------------------------------------------------------- + +Center.Logo = Logo; +Center.Heading = Heading; +Center.Menu = Menu; +Center.MenuItem = WelcomeScreenMenuItem; +Center.MenuItemLink = WelcomeScreenMenuItemLink; +Center.MenuItemHelp = MenuItemHelp; +Center.MenuItemLoadScene = MenuItemLoadScene; +Center.MenuItemLiveCollaborationTrigger = MenuItemLiveCollaborationTrigger; + +export { Center }; diff --git a/packages/excalidraw/components/welcome-screen/WelcomeScreen.Hints.tsx b/packages/excalidraw/components/welcome-screen/WelcomeScreen.Hints.tsx new file mode 100644 index 0000000..896f401 --- /dev/null +++ b/packages/excalidraw/components/welcome-screen/WelcomeScreen.Hints.tsx @@ -0,0 +1,52 @@ +import { t } from "../../i18n"; +import { useTunnels } from "../../context/tunnels"; +import { + WelcomeScreenHelpArrow, + WelcomeScreenMenuArrow, + WelcomeScreenTopToolbarArrow, +} from "../icons"; + +const MenuHint = ({ children }: { children?: React.ReactNode }) => { + const { WelcomeScreenMenuHintTunnel } = useTunnels(); + return ( + <WelcomeScreenMenuHintTunnel.In> + <div className="excalifont welcome-screen-decor welcome-screen-decor-hint welcome-screen-decor-hint--menu"> + {WelcomeScreenMenuArrow} + <div className="welcome-screen-decor-hint__label"> + {children || t("welcomeScreen.defaults.menuHint")} + </div> + </div> + </WelcomeScreenMenuHintTunnel.In> + ); +}; +MenuHint.displayName = "MenuHint"; + +const ToolbarHint = ({ children }: { children?: React.ReactNode }) => { + const { WelcomeScreenToolbarHintTunnel } = useTunnels(); + return ( + <WelcomeScreenToolbarHintTunnel.In> + <div className="excalifont welcome-screen-decor welcome-screen-decor-hint welcome-screen-decor-hint--toolbar"> + <div className="welcome-screen-decor-hint__label"> + {children || t("welcomeScreen.defaults.toolbarHint")} + </div> + {WelcomeScreenTopToolbarArrow} + </div> + </WelcomeScreenToolbarHintTunnel.In> + ); +}; +ToolbarHint.displayName = "ToolbarHint"; + +const HelpHint = ({ children }: { children?: React.ReactNode }) => { + const { WelcomeScreenHelpHintTunnel } = useTunnels(); + return ( + <WelcomeScreenHelpHintTunnel.In> + <div className="excalifont welcome-screen-decor welcome-screen-decor-hint welcome-screen-decor-hint--help"> + <div>{children || t("welcomeScreen.defaults.helpHint")}</div> + {WelcomeScreenHelpArrow} + </div> + </WelcomeScreenHelpHintTunnel.In> + ); +}; +HelpHint.displayName = "HelpHint"; + +export { HelpHint, MenuHint, ToolbarHint }; diff --git a/packages/excalidraw/components/welcome-screen/WelcomeScreen.scss b/packages/excalidraw/components/welcome-screen/WelcomeScreen.scss new file mode 100644 index 0000000..8e3a010 --- /dev/null +++ b/packages/excalidraw/components/welcome-screen/WelcomeScreen.scss @@ -0,0 +1,272 @@ +.excalidraw { + .excalifont { + font-family: "Excalifont", "Xiaolai"; + } + + // WelcomeSreen common + // --------------------------------------------------------------------------- + + .welcome-screen-decor { + pointer-events: none; + + color: var(--color-gray-40); + + a { + --color: var(--color-primary); + color: var(--color); + text-decoration: none; + margin-bottom: -6px; + } + } + + &.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; + bottom: 100%; + + :root[dir="rtl"] & { + left: 0; + right: auto; + } + + svg { + margin-top: 0.5rem; + width: 85px; + height: 71px; + + transform: scaleX(-1) rotate(80deg); + + :root[dir="rtl"] & { + transform: rotate(80deg); + } + } + } + + &--toolbar { + position: absolute; + top: 100%; + left: 50%; + transform: translateX(-50%); + margin-top: 2.5rem; + display: flex; + align-items: baseline; + + .welcome-screen-decor-hint__label { + width: 120px; + position: relative; + top: -0.5rem; + } + + svg { + width: 38px; + height: 78px; + + :root[dir="rtl"] & { + transform: scaleX(-1); + } + } + } + + &--menu { + position: absolute; + width: 320px; + font-size: 1rem; + + top: 100%; + margin-top: 0.25rem; + margin-inline-start: 0.6rem; + + display: flex; + align-items: flex-end; + gap: 0.5rem; + + svg { + width: 41px; + height: 94px; + + :root[dir="rtl"] & { + transform: scaleX(-1); + } + } + + @media (max-width: 860px) { + .welcome-screen-decor-hint__label { + max-width: 160px; + } + } + } + } + + // WelcomeSreen.Center + // --------------------------------------------------------------------------- + + .welcome-screen-center { + display: flex; + flex-direction: column; + gap: 2rem; + justify-content: center; + align-items: center; + position: absolute; + pointer-events: none; + left: 1rem; + top: 1rem; + right: 1rem; + bottom: 1rem; + } + + .welcome-screen-center__logo { + display: flex; + align-items: center; + column-gap: 0.75rem; + font-size: 2.25rem; + } + + .welcome-screen-center__heading { + font-size: 1.125rem; + text-align: center; + } + + .welcome-screen-menu { + display: flex; + flex-direction: column; + gap: 2px; + justify-content: center; + align-items: center; + } + + .welcome-screen-menu-item { + box-sizing: border-box; + + pointer-events: var(--ui-pointerEvents); + + color: var(--color-gray-50); + font-size: 0.875rem; + + width: 100%; + min-width: 300px; + max-width: 400px; + display: grid; + align-items: center; + justify-content: space-between; + + background: none; + border: 1px solid transparent; + + padding: 0.75rem; + + border-radius: var(--border-radius-md); + + 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; + } + + &__icon { + width: var(--default-icon-size); + height: var(--default-icon-size); + } + + &__shortcut { + margin-left: auto; + color: var(--color-gray-40); + font-size: 0.75rem; + } + } + + .welcome-screen-menu-item:hover { + text-decoration: none; + background: var(--button-hover-bg); + + .welcome-screen-menu-item__shortcut, + .welcome-screen-menu-item__icon, + .welcome-screen-menu-item__text { + color: var(--color-gray-100); + } + } + + .welcome-screen-menu-item:active { + background: var(--button-hover-bg); + border-color: var(--color-brand-active); + + .welcome-screen-menu-item__shortcut, + .welcome-screen-menu-item__icon, + .welcome-screen-menu-item__text { + color: var(--color-gray-100); + } + } + + &.theme--dark { + .welcome-screen-menu-item { + color: var(--color-gray-60); + + &__shortcut { + color: var(--color-gray-60); + } + } + + .welcome-screen-menu-item:hover { + background-color: var(--color-surface-low); + + .welcome-screen-menu-item__icon, + .welcome-screen-menu-item__shortcut, + .welcome-screen-menu-item__text { + color: var(--color-gray-10); + } + } + + .welcome-screen-menu-item:active { + .welcome-screen-menu-item__icon, + .welcome-screen-menu-item__shortcut, + .welcome-screen-menu-item__text { + color: var(--color-gray-10); + } + } + } + + @media (max-height: 599px) { + .welcome-screen-center { + margin-top: 4rem; + } + } + @media (min-height: 600px) and (max-height: 900px) { + .welcome-screen-center { + margin-top: 8rem; + } + } + @media (max-height: 500px), (max-width: 320px) { + .welcome-screen-center { + display: none; + } + } + + // --------------------------------------------------------------------------- +} diff --git a/packages/excalidraw/components/welcome-screen/WelcomeScreen.tsx b/packages/excalidraw/components/welcome-screen/WelcomeScreen.tsx new file mode 100644 index 0000000..1f38b1c --- /dev/null +++ b/packages/excalidraw/components/welcome-screen/WelcomeScreen.tsx @@ -0,0 +1,26 @@ +import { Center } from "./WelcomeScreen.Center"; +import { MenuHint, ToolbarHint, HelpHint } from "./WelcomeScreen.Hints"; + +import "./WelcomeScreen.scss"; + +const WelcomeScreen = (props: { children?: React.ReactNode }) => { + return ( + <> + {props.children || ( + <> + <Center /> + <MenuHint /> + <ToolbarHint /> + <HelpHint /> + </> + )} + </> + ); +}; + +WelcomeScreen.displayName = "WelcomeScreen"; + +WelcomeScreen.Center = Center; +WelcomeScreen.Hints = { MenuHint, ToolbarHint, HelpHint }; + +export default WelcomeScreen; |
