diff --git a/excalidraw-app/App.tsx b/excalidraw-app/App.tsx index 9b7eadff8..48b887c3f 100644 --- a/excalidraw-app/App.tsx +++ b/excalidraw-app/App.tsx @@ -126,6 +126,8 @@ import DebugCanvas, { loadSavedDebugState, } from "./components/DebugCanvas"; import { AIComponents } from "./components/AI"; +import type { SaveWarningRef } from "./components/SaveWarning"; +import { SaveWarning } from "./components/SaveWarning"; polyfill(); @@ -331,6 +333,8 @@ const ExcalidrawWrapper = () => { const [langCode, setLangCode] = useAppLangCode(); + const activityRef = useRef(null); + // initial state // --------------------------------------------------------------------------- @@ -615,6 +619,8 @@ const ExcalidrawWrapper = () => { collabAPI.syncElements(elements); } + activityRef.current?.activity(); + // 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()) { @@ -856,6 +862,7 @@ const ExcalidrawWrapper = () => { setTheme={(theme) => setAppTheme(theme)} refresh={() => forceRefresh((prev) => !prev)} /> + Promise; +}; + +export const SaveWarning = forwardRef((props, ref) => { + const dialogRef = useRef(null); + const timerRef = useRef(null); + + useImperativeHandle(ref, () => ({ + /** + * Call this API method via the ref to hide warning message + * and start an idle timer again. + */ + activity: async () => { + if (timerRef.current != null) { + clearTimeout(timerRef.current); + dialogRef.current?.classList.remove("animate"); + } + + timerRef.current = setTimeout(() => { + timerRef.current = null; + dialogRef.current?.classList.add("animate"); + }, 5000); + }, + })); + + return ( +
+
+ {t("alerts.saveYourContent", { + shortcut: getShortcutKey("CtrlOrCmd + S"), + })} +
+
+ ); +}); diff --git a/excalidraw-app/index.scss b/excalidraw-app/index.scss index cfaaf9cea..8856acb82 100644 --- a/excalidraw-app/index.scss +++ b/excalidraw-app/index.scss @@ -1,3 +1,13 @@ +@mixin animate($animation,$duration,$method,$times){ + animation: $animation $duration $method $times; +} + +@mixin keyframes($name){ + @keyframes #{$name}{ + @content; + } +} + .excalidraw { --color-primary-contrast-offset: #625ee0; // to offset Chubb illusion @@ -18,6 +28,43 @@ margin-inline-start: auto; } + .alert-save { + position: absolute; + z-index: 10; + left: 0; + right: 0; + bottom: 10vh; + margin-inline: auto; + width: fit-content; + + opacity: 0; + transition: all 0s; + + &.animate { + opacity: 1; + transition: all 0.2s ease-in; + } + + .dialog { + margin-inline: 10px; + padding: 1rem; + padding-inline: 1.25rem; + + resize: none; + white-space: pre-wrap; + box-sizing: border-box; + + background-color: var(--color-warning); + border-radius: var(--border-radius-md); + border: 1px solid var(--dialog-border-color); + + font-size: 0.875rem; + text-align: center; + line-height: 1.5; + color: var(--color-text-warning); + } + } + .encrypted-icon { border-radius: var(--space-factor); color: var(--color-primary); diff --git a/packages/excalidraw/locales/en.json b/packages/excalidraw/locales/en.json index e4c5eea44..da3f2b84f 100644 --- a/packages/excalidraw/locales/en.json +++ b/packages/excalidraw/locales/en.json @@ -230,7 +230,8 @@ "resetLibrary": "This will clear your library. Are you sure?", "removeItemsFromsLibrary": "Delete {{count}} item(s) from library?", "invalidEncryptionKey": "Encryption key must be of 22 characters. Live collaboration is disabled.", - "collabOfflineWarning": "No internet connection available.\nYour changes will not be saved!" + "collabOfflineWarning": "No internet connection available.\nYour changes will not be saved!", + "saveYourContent": "Don't forget to save your content ({{shortcut}})!" }, "errors": { "unsupportedFileType": "Unsupported file type.", @@ -609,4 +610,4 @@ "itemNotAvailable": "Command is not available...", "shortcutHint": "For Command palette, use {{shortcut}}" } -} +} \ No newline at end of file