import polyfill from "../packages/excalidraw/polyfill"; import { useCallback, useEffect, useRef, useState } from "react"; import { trackEvent } from "../packages/excalidraw/analytics"; import { getDefaultAppState } from "../packages/excalidraw/appState"; import { ErrorDialog } from "../packages/excalidraw/components/ErrorDialog"; import { TopErrorBoundary } from "./components/TopErrorBoundary"; import { APP_NAME, EVENT, THEME, TITLE_TIMEOUT, VERSION_TIMEOUT, } from "../packages/excalidraw/constants"; import { loadFromBlob } from "../packages/excalidraw/data/blob"; import type { FileId, NonDeletedExcalidrawElement, OrderedExcalidrawElement, } from "../packages/excalidraw/element/types"; import { useCallbackRefState } from "../packages/excalidraw/hooks/useCallbackRefState"; import { t } from "../packages/excalidraw/i18n"; import { Excalidraw, LiveCollaborationTrigger, TTDDialogTrigger, StoreAction, reconcileElements, } from "../packages/excalidraw"; import type { AppState, ExcalidrawImperativeAPI, BinaryFiles, ExcalidrawInitialDataState, UIAppState, } from "../packages/excalidraw/types"; import type { ResolvablePromise } from "../packages/excalidraw/utils"; import { debounce, getVersion, getFrame, isTestEnv, preventUnload, resolvablePromise, isRunningInIframe, } from "../packages/excalidraw/utils"; import { FIREBASE_STORAGE_PREFIXES, isExcalidrawPlusSignedUser, STORAGE_KEYS, SYNC_BROWSER_TABS_TIMEOUT, } from "./app_constants"; import type { CollabAPI } from "./collab/Collab"; import Collab, { collabAPIAtom, isCollaboratingAtom, isOfflineAtom, } from "./collab/Collab"; import { exportToBackend, getCollaborationLinkData, isCollaborationLink, loadScene, } from "./data"; import { importFromLocalStorage, importUsernameFromLocalStorage, } from "./data/localStorage"; import CustomStats from "./CustomStats"; import type { RestoredDataState } from "../packages/excalidraw/data/restore"; import { restore, restoreAppState } from "../packages/excalidraw/data/restore"; import { ExportToExcalidrawPlus, exportToExcalidrawPlus, } from "./components/ExportToExcalidrawPlus"; import { updateStaleImageStatuses } from "./data/FileManager"; import { newElementWith } from "../packages/excalidraw/element/mutateElement"; import { isInitializedImageElement } from "../packages/excalidraw/element/typeChecks"; import { loadFilesFromFirebase } from "./data/firebase"; import { LibraryIndexedDBAdapter, LibraryLocalStorageMigrationAdapter, LocalData, } from "./data/LocalData"; import { isBrowserStorageStateNewer } from "./data/tabSync"; import clsx from "clsx"; import { parseLibraryTokensFromUrl, useHandleLibrary, } from "../packages/excalidraw/data/library"; import { AppMainMenu } from "./components/AppMainMenu"; import { AppWelcomeScreen } from "./components/AppWelcomeScreen"; import { AppFooter } from "./components/AppFooter"; import { Provider, useAtom, useAtomValue } from "jotai"; import { useAtomWithInitialValue } from "../packages/excalidraw/jotai"; import { appJotaiStore } from "./app-jotai"; import "./index.scss"; import type { ResolutionType } from "../packages/excalidraw/utility-types"; import { ShareableLinkDialog } from "../packages/excalidraw/components/ShareableLinkDialog"; import { openConfirmModal } from "../packages/excalidraw/components/OverwriteConfirm/OverwriteConfirmState"; import { OverwriteConfirmDialog } from "../packages/excalidraw/components/OverwriteConfirm/OverwriteConfirm"; import Trans from "../packages/excalidraw/components/Trans"; import { ShareDialog, shareDialogStateAtom } from "./share/ShareDialog"; import CollabError, { collabErrorIndicatorAtom } from "./collab/CollabError"; import type { RemoteExcalidrawElement } from "../packages/excalidraw/data/reconcile"; import { CommandPalette, DEFAULT_CATEGORIES, } from "../packages/excalidraw/components/CommandPalette/CommandPalette"; import { GithubIcon, XBrandIcon, DiscordIcon, ExcalLogo, usersIcon, exportToPlus, share, youtubeIcon, } from "../packages/excalidraw/components/icons"; import { appThemeAtom, useHandleAppTheme } from "./useHandleAppTheme"; import { getPreferredLanguage } from "./app-language/language-detector"; import { useAppLangCode } from "./app-language/language-state"; import DebugCanvas, { debugRenderer, isVisualDebuggerEnabled, loadSavedDebugState, } from "./components/DebugCanvas"; import { AIComponents } from "./components/AI"; polyfill(); window.EXCALIDRAW_THROTTLE_RENDER = true; declare global { interface BeforeInstallPromptEventChoiceResult { outcome: "accepted" | "dismissed"; } interface BeforeInstallPromptEvent extends Event { prompt(): Promise; userChoice: Promise; } interface WindowEventMap { beforeinstallprompt: BeforeInstallPromptEvent; } } let pwaEvent: BeforeInstallPromptEvent | null = null; // Adding a listener outside of the component as it may (?) need to be // subscribed early to catch the event. // // Also note that it will fire only if certain heuristics are met (user has // used the app for some time, etc.) window.addEventListener( "beforeinstallprompt", (event: BeforeInstallPromptEvent) => { // prevent Chrome <= 67 from automatically showing the prompt event.preventDefault(); // cache for later use pwaEvent = event; }, ); let isSelfEmbedding = false; if (window.self !== window.top) { try { const parentUrl = new URL(document.referrer); const currentUrl = new URL(window.location.href); if (parentUrl.origin === currentUrl.origin) { isSelfEmbedding = true; } } catch (error) { // ignore } } const shareableLinkConfirmDialog = { title: t("overwriteConfirm.modal.shareableLink.title"), description: ( {text}} br={() =>
} /> ), actionLabel: t("overwriteConfirm.modal.shareableLink.button"), color: "danger", } as const; const initializeScene = async (opts: { collabAPI: CollabAPI | null; excalidrawAPI: ExcalidrawImperativeAPI; }): Promise< { scene: ExcalidrawInitialDataState | null } & ( | { isExternalScene: true; id: string; key: string } | { isExternalScene: false; id?: null; key?: null } ) > => { const searchParams = new URLSearchParams(window.location.search); const id = searchParams.get("id"); const jsonBackendMatch = window.location.hash.match( /^#json=([a-zA-Z0-9_-]+),([a-zA-Z0-9_-]+)$/, ); const externalUrlMatch = window.location.hash.match(/^#url=(.*)$/); const localDataState = importFromLocalStorage(); let scene: RestoredDataState & { scrollToContent?: boolean; } = await loadScene(null, null, localDataState); let roomLinkData = getCollaborationLinkData(window.location.href); const isExternalScene = !!(id || jsonBackendMatch || roomLinkData); if (isExternalScene) { if ( // don't prompt if scene is empty !scene.elements.length || // don't prompt for collab scenes because we don't override local storage roomLinkData || // otherwise, prompt whether user wants to override current scene (await openConfirmModal(shareableLinkConfirmDialog)) ) { if (jsonBackendMatch) { scene = await loadScene( jsonBackendMatch[1], jsonBackendMatch[2], localDataState, ); } scene.scrollToContent = true; if (!roomLinkData) { window.history.replaceState({}, APP_NAME, window.location.origin); } } else { // https://github.com/excalidraw/excalidraw/issues/1919 if (document.hidden) { return new Promise((resolve, reject) => { window.addEventListener( "focus", () => initializeScene(opts).then(resolve).catch(reject), { once: true, }, ); }); } roomLinkData = null; window.history.replaceState({}, APP_NAME, window.location.origin); } } else if (externalUrlMatch) { window.history.replaceState({}, APP_NAME, window.location.origin); const url = externalUrlMatch[1]; try { const request = await fetch(window.decodeURIComponent(url)); const data = await loadFromBlob(await request.blob(), null, null); if ( !scene.elements.length || (await openConfirmModal(shareableLinkConfirmDialog)) ) { return { scene: data, isExternalScene }; } } catch (error: any) { return { scene: { appState: { errorMessage: t("alerts.invalidSceneUrl"), }, }, isExternalScene, }; } } if (roomLinkData && opts.collabAPI) { const { excalidrawAPI } = opts; const scene = await opts.collabAPI.startCollaboration(roomLinkData); return { // when collaborating, the state may have already been updated at this // point (we may have received updates from other clients), so reconcile // elements and appState with existing state scene: { ...scene, appState: { ...restoreAppState( { ...scene?.appState, theme: localDataState?.appState?.theme || scene?.appState?.theme, }, excalidrawAPI.getAppState(), ), // necessary if we're invoking from a hashchange handler which doesn't // go through App.initializeScene() that resets this flag isLoading: false, }, elements: reconcileElements( scene?.elements || [], excalidrawAPI.getSceneElementsIncludingDeleted() as RemoteExcalidrawElement[], excalidrawAPI.getAppState(), ), }, isExternalScene: true, id: roomLinkData.roomId, key: roomLinkData.roomKey, }; } else if (scene) { return isExternalScene && jsonBackendMatch ? { scene, isExternalScene, id: jsonBackendMatch[1], key: jsonBackendMatch[2], } : { scene, isExternalScene: false }; } return { scene: null, isExternalScene: false }; }; const ExcalidrawWrapper = () => { const [errorMessage, setErrorMessage] = useState(""); const isCollabDisabled = isRunningInIframe(); const [appTheme, setAppTheme] = useAtom(appThemeAtom); const { editorTheme } = useHandleAppTheme(); const [langCode, setLangCode] = useAppLangCode(); // initial state // --------------------------------------------------------------------------- const initialStatePromiseRef = useRef<{ promise: ResolvablePromise; }>({ promise: null! }); if (!initialStatePromiseRef.current.promise) { initialStatePromiseRef.current.promise = resolvablePromise(); } const debugCanvasRef = useRef(null); useEffect(() => { trackEvent("load", "frame", getFrame()); // Delayed so that the app has a time to load the latest SW setTimeout(() => { trackEvent("load", "version", getVersion()); }, VERSION_TIMEOUT); }, []); const [excalidrawAPI, excalidrawRefCallback] = useCallbackRefState(); const [, setShareDialogState] = useAtom(shareDialogStateAtom); const [collabAPI] = useAtom(collabAPIAtom); const [isCollaborating] = useAtomWithInitialValue(isCollaboratingAtom, () => { return isCollaborationLink(window.location.href); }); const collabError = useAtomValue(collabErrorIndicatorAtom); useHandleLibrary({ excalidrawAPI, adapter: LibraryIndexedDBAdapter, // TODO maybe remove this in several months (shipped: 24-03-11) migrationAdapter: LibraryLocalStorageMigrationAdapter, }); const [, forceRefresh] = useState(false); useEffect(() => { if (import.meta.env.DEV) { const debugState = loadSavedDebugState(); if (debugState.enabled && !window.visualDebug) { window.visualDebug = { data: [], }; } else { delete window.visualDebug; } forceRefresh((prev) => !prev); } }, [excalidrawAPI]); useEffect(() => { if (!excalidrawAPI || (!isCollabDisabled && !collabAPI)) { return; } const loadImages = ( data: ResolutionType, isInitialLoad = false, ) => { if (!data.scene) { return; } if (collabAPI?.isCollaborating()) { if (data.scene.elements) { collabAPI .fetchImageFilesFromFirebase({ elements: data.scene.elements, forceFetchFiles: true, }) .then(({ loadedFiles, erroredFiles }) => { excalidrawAPI.addFiles(loadedFiles); updateStaleImageStatuses({ excalidrawAPI, erroredFiles, elements: excalidrawAPI.getSceneElementsIncludingDeleted(), }); }); } } else { const fileIds = data.scene.elements?.reduce((acc, element) => { if (isInitializedImageElement(element)) { return acc.concat(element.fileId); } return acc; }, [] as FileId[]) || []; if (data.isExternalScene) { loadFilesFromFirebase( `${FIREBASE_STORAGE_PREFIXES.shareLinkFiles}/${data.id}`, data.key, fileIds, ).then(({ loadedFiles, erroredFiles }) => { excalidrawAPI.addFiles(loadedFiles); updateStaleImageStatuses({ excalidrawAPI, erroredFiles, elements: excalidrawAPI.getSceneElementsIncludingDeleted(), }); }); } else if (isInitialLoad) { if (fileIds.length) { LocalData.fileStorage .getFiles(fileIds) .then(({ loadedFiles, erroredFiles }) => { if (loadedFiles.length) { excalidrawAPI.addFiles(loadedFiles); } updateStaleImageStatuses({ excalidrawAPI, erroredFiles, elements: excalidrawAPI.getSceneElementsIncludingDeleted(), }); }); } // on fresh load, clear unused files from IDB (from previous // session) LocalData.fileStorage.clearObsoleteFiles({ currentFileIds: fileIds }); } } }; initializeScene({ collabAPI, excalidrawAPI }).then(async (data) => { loadImages(data, /* isInitialLoad */ true); initialStatePromiseRef.current.promise.resolve(data.scene); }); const onHashChange = async (event: HashChangeEvent) => { event.preventDefault(); const libraryUrlTokens = parseLibraryTokensFromUrl(); if (!libraryUrlTokens) { if ( collabAPI?.isCollaborating() && !isCollaborationLink(window.location.href) ) { collabAPI.stopCollaboration(false); } excalidrawAPI.updateScene({ appState: { isLoading: true } }); initializeScene({ collabAPI, excalidrawAPI }).then((data) => { loadImages(data); if (data.scene) { excalidrawAPI.updateScene({ ...data.scene, ...restore(data.scene, null, null, { repairBindings: true }), storeAction: StoreAction.CAPTURE, }); } }); } }; const titleTimeout = setTimeout( () => (document.title = APP_NAME), TITLE_TIMEOUT, ); const syncData = debounce(() => { if (isTestEnv()) { return; } if ( !document.hidden && ((collabAPI && !collabAPI.isCollaborating()) || isCollabDisabled) ) { // don't sync if local state is newer or identical to browser state if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_DATA_STATE)) { const localDataState = importFromLocalStorage(); const username = importUsernameFromLocalStorage(); setLangCode(getPreferredLanguage()); excalidrawAPI.updateScene({ ...localDataState, storeAction: StoreAction.UPDATE, }); LibraryIndexedDBAdapter.load().then((data) => { if (data) { excalidrawAPI.updateLibrary({ libraryItems: data.libraryItems, }); } }); collabAPI?.setUsername(username || ""); } if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_FILES)) { const elements = excalidrawAPI.getSceneElementsIncludingDeleted(); const currFiles = excalidrawAPI.getFiles(); const fileIds = elements?.reduce((acc, element) => { if ( isInitializedImageElement(element) && // only load and update images that aren't already loaded !currFiles[element.fileId] ) { return acc.concat(element.fileId); } return acc; }, [] as FileId[]) || []; if (fileIds.length) { LocalData.fileStorage .getFiles(fileIds) .then(({ loadedFiles, erroredFiles }) => { if (loadedFiles.length) { excalidrawAPI.addFiles(loadedFiles); } updateStaleImageStatuses({ excalidrawAPI, erroredFiles, elements: excalidrawAPI.getSceneElementsIncludingDeleted(), }); }); } } } }, SYNC_BROWSER_TABS_TIMEOUT); const onUnload = () => { LocalData.flushSave(); }; const visibilityChange = (event: FocusEvent | Event) => { if (event.type === EVENT.BLUR || document.hidden) { LocalData.flushSave(); } if ( event.type === EVENT.VISIBILITY_CHANGE || event.type === EVENT.FOCUS ) { syncData(); } }; window.addEventListener(EVENT.HASHCHANGE, onHashChange, false); window.addEventListener(EVENT.UNLOAD, onUnload, false); window.addEventListener(EVENT.BLUR, visibilityChange, false); document.addEventListener(EVENT.VISIBILITY_CHANGE, visibilityChange, false); window.addEventListener(EVENT.FOCUS, visibilityChange, false); return () => { window.removeEventListener(EVENT.HASHCHANGE, onHashChange, false); window.removeEventListener(EVENT.UNLOAD, onUnload, false); window.removeEventListener(EVENT.BLUR, visibilityChange, false); window.removeEventListener(EVENT.FOCUS, visibilityChange, false); document.removeEventListener( EVENT.VISIBILITY_CHANGE, visibilityChange, false, ); clearTimeout(titleTimeout); }; }, [isCollabDisabled, collabAPI, excalidrawAPI, setLangCode]); useEffect(() => { const unloadHandler = (event: BeforeUnloadEvent) => { LocalData.flushSave(); if ( excalidrawAPI && LocalData.fileStorage.shouldPreventUnload( excalidrawAPI.getSceneElements(), ) ) { preventUnload(event); } }; window.addEventListener(EVENT.BEFORE_UNLOAD, unloadHandler); return () => { window.removeEventListener(EVENT.BEFORE_UNLOAD, unloadHandler); }; }, [excalidrawAPI]); const onChange = ( elements: readonly OrderedExcalidrawElement[], appState: AppState, files: BinaryFiles, ) => { if (collabAPI?.isCollaborating()) { collabAPI.syncElements(elements); } // this check is redundant, but since this is a hot path, it's best // not to evaludate the nested expression every time if (!LocalData.isSavePaused()) { LocalData.save(elements, appState, files, () => { if (excalidrawAPI) { let didChange = false; const elements = excalidrawAPI .getSceneElementsIncludingDeleted() .map((element) => { if ( LocalData.fileStorage.shouldUpdateImageElementStatus(element) ) { const newElement = newElementWith(element, { status: "saved" }); if (newElement !== element) { didChange = true; } return newElement; } return element; }); if (didChange) { excalidrawAPI.updateScene({ elements, storeAction: StoreAction.UPDATE, }); } } }); } // Render the debug scene if the debug canvas is available if (debugCanvasRef.current && excalidrawAPI) { debugRenderer(debugCanvasRef.current, appState, window.devicePixelRatio); } }; const [latestShareableLink, setLatestShareableLink] = useState( null, ); const onExportToBackend = async ( exportedElements: readonly NonDeletedExcalidrawElement[], appState: Partial, files: BinaryFiles, ) => { if (exportedElements.length === 0) { throw new Error(t("alerts.cannotExportEmptyCanvas")); } try { const { url, errorMessage } = await exportToBackend( exportedElements, { ...appState, viewBackgroundColor: appState.exportBackground ? appState.viewBackgroundColor : getDefaultAppState().viewBackgroundColor, }, files, ); if (errorMessage) { throw new Error(errorMessage); } if (url) { setLatestShareableLink(url); } } catch (error: any) { if (error.name !== "AbortError") { const { width, height } = appState; console.error(error, { width, height, devicePixelRatio: window.devicePixelRatio, }); throw new Error(error.message); } } }; const renderCustomStats = ( elements: readonly NonDeletedExcalidrawElement[], appState: UIAppState, ) => { return ( excalidrawAPI!.setToast({ message })} appState={appState} elements={elements} /> ); }; const isOffline = useAtomValue(isOfflineAtom); const onCollabDialogOpen = useCallback( () => setShareDialogState({ isOpen: true, type: "collaborationOnly" }), [setShareDialogState], ); // browsers generally prevent infinite self-embedding, there are // cases where it still happens, and while we disallow self-embedding // by not whitelisting our own origin, this serves as an additional guard if (isSelfEmbedding) { return (

I'm not a pretzel!

); } const ExcalidrawPlusCommand = { label: "Excalidraw+", category: DEFAULT_CATEGORIES.links, predicate: true, icon:
{ExcalLogo}
, keywords: ["plus", "cloud", "server"], perform: () => { window.open( `${ import.meta.env.VITE_APP_PLUS_LP }/plus?utm_source=excalidraw&utm_medium=app&utm_content=command_palette`, "_blank", ); }, }; const ExcalidrawPlusAppCommand = { label: "Sign up", category: DEFAULT_CATEGORIES.links, predicate: true, icon:
{ExcalLogo}
, keywords: [ "excalidraw", "plus", "cloud", "server", "signin", "login", "signup", ], perform: () => { window.open( `${ import.meta.env.VITE_APP_PLUS_APP }?utm_source=excalidraw&utm_medium=app&utm_content=command_palette`, "_blank", ); }, }; return (
{ return ( { excalidrawAPI?.updateScene({ appState: { errorMessage: error.message, }, }); }} onSuccess={() => { excalidrawAPI.updateScene({ appState: { openDialog: null }, }); }} /> ); } : undefined, }, }, }} langCode={langCode} renderCustomStats={renderCustomStats} detectScroll={false} handleKeyboardGlobally={true} autoFocus={true} theme={editorTheme} renderTopRightUI={(isMobile) => { if (isMobile || !collabAPI || isCollabDisabled) { return null; } return (
{collabError.message && } setShareDialogState({ isOpen: true, type: "share" }) } />
); }} > setAppTheme(theme)} refresh={() => forceRefresh((prev) => !prev)} /> {excalidrawAPI && ( { exportToExcalidrawPlus( excalidrawAPI.getSceneElements(), excalidrawAPI.getAppState(), excalidrawAPI.getFiles(), excalidrawAPI.getName(), ); }} > {t("overwriteConfirm.action.excalidrawPlus.description")} )} excalidrawAPI?.refresh()} /> {excalidrawAPI && } {isCollaborating && isOffline && (
{t("alerts.collabOfflineWarning")}
)} {latestShareableLink && ( setLatestShareableLink(null)} setErrorMessage={setErrorMessage} /> )} {excalidrawAPI && !isCollabDisabled && ( )} { if (excalidrawAPI) { try { await onExportToBackend( excalidrawAPI.getSceneElements(), excalidrawAPI.getAppState(), excalidrawAPI.getFiles(), ); } catch (error: any) { setErrorMessage(error.message); } } }} /> {errorMessage && ( setErrorMessage("")}> {errorMessage} )} { setShareDialogState({ isOpen: true, type: "collaborationOnly", }); }, }, { label: t("roomDialog.button_stopSession"), category: DEFAULT_CATEGORIES.app, predicate: () => !!collabAPI?.isCollaborating(), keywords: [ "stop", "session", "end", "leave", "close", "exit", "collaboration", ], perform: () => { if (collabAPI) { collabAPI.stopCollaboration(); if (!collabAPI.isCollaborating()) { setShareDialogState({ isOpen: false }); } } }, }, { label: t("labels.share"), category: DEFAULT_CATEGORIES.app, predicate: true, icon: share, keywords: [ "link", "shareable", "readonly", "export", "publish", "snapshot", "url", "collaborate", "invite", ], perform: async () => { setShareDialogState({ isOpen: true, type: "share" }); }, }, { label: "GitHub", icon: GithubIcon, category: DEFAULT_CATEGORIES.links, predicate: true, keywords: [ "issues", "bugs", "requests", "report", "features", "social", "community", ], perform: () => { window.open( "https://github.com/excalidraw/excalidraw", "_blank", "noopener noreferrer", ); }, }, { label: t("labels.followUs"), icon: XBrandIcon, category: DEFAULT_CATEGORIES.links, predicate: true, keywords: ["twitter", "contact", "social", "community"], perform: () => { window.open( "https://x.com/excalidraw", "_blank", "noopener noreferrer", ); }, }, { label: t("labels.discordChat"), category: DEFAULT_CATEGORIES.links, predicate: true, icon: DiscordIcon, keywords: [ "chat", "talk", "contact", "bugs", "requests", "report", "feedback", "suggestions", "social", "community", ], perform: () => { window.open( "https://discord.gg/UexuTaE", "_blank", "noopener noreferrer", ); }, }, { label: "YouTube", icon: youtubeIcon, category: DEFAULT_CATEGORIES.links, predicate: true, keywords: ["features", "tutorials", "howto", "help", "community"], perform: () => { window.open( "https://youtube.com/@excalidraw", "_blank", "noopener noreferrer", ); }, }, ...(isExcalidrawPlusSignedUser ? [ { ...ExcalidrawPlusAppCommand, label: "Sign in / Go to Excalidraw+", }, ] : [ExcalidrawPlusCommand, ExcalidrawPlusAppCommand]), { label: t("overwriteConfirm.action.excalidrawPlus.button"), category: DEFAULT_CATEGORIES.export, icon: exportToPlus, predicate: true, keywords: ["plus", "export", "save", "backup"], perform: () => { if (excalidrawAPI) { exportToExcalidrawPlus( excalidrawAPI.getSceneElements(), excalidrawAPI.getAppState(), excalidrawAPI.getFiles(), excalidrawAPI.getName(), ); } }, }, { ...CommandPalette.defaultItems.toggleTheme, perform: () => { setAppTheme( editorTheme === THEME.DARK ? THEME.LIGHT : THEME.DARK, ); }, }, { label: t("labels.installPWA"), category: DEFAULT_CATEGORIES.app, predicate: () => !!pwaEvent, perform: () => { if (pwaEvent) { pwaEvent.prompt(); pwaEvent.userChoice.then(() => { // event cannot be reused, but we'll hopefully // grab new one as the event should be fired again pwaEvent = null; }); } }, }, ]} /> {isVisualDebuggerEnabled() && excalidrawAPI && ( )}
); }; const ExcalidrawApp = () => { return ( appJotaiStore}> ); }; export default ExcalidrawApp;