From cb506af3745657f91c0ab397630e069af44de253 Mon Sep 17 00:00:00 2001 From: PJ Date: Sat, 20 Jun 2026 13:29:52 -0400 Subject: [PATCH] feat(ui): surface save and export errors as toasts; add webkit backdrop-filter --- src/App.tsx | 55 ++++++++++++++++++++++++++++++++-------------- src/styles/app.css | 19 ++++++++++++++++ 2 files changed, 57 insertions(+), 17 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index c56e8b7..4a61e4c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -16,6 +16,7 @@ function App() { const [editor, setEditor] = useState(null); const [settingsOpen, setSettingsOpen] = useState(false); const [exportOpen, setExportOpen] = useState(false); + const [notice, setNotice] = useState(null); const title = useBook((s) => s.book.metadata.title); const author = useBook((s) => s.book.metadata.author); const chapters = useBook((s) => s.book.chapters); @@ -30,18 +31,36 @@ function App() { const chapter = chapters[idx]; const handleSave = useCallback(async () => { - const { book, path } = useBook.getState(); - const target = path ?? (await chooseSavePath(book)); - if (!target) return; - await writeBook(book, target); - markSaved(target); + try { + const { book, path } = useBook.getState(); + const target = path ?? (await chooseSavePath(book)); + if (!target) return; + await writeBook(book, target); + markSaved(target); + setNotice("Saved"); + } catch (e) { + setNotice(`Save failed: ${e}`); + } }, [markSaved]); const handleOpen = useCallback(async () => { - const result = await openBook(); - if (result) replaceBook(result.book, result.path); + try { + const result = await openBook(); + if (result) replaceBook(result.book, result.path); + } catch (e) { + setNotice(`Open failed: ${e}`); + } }, [replaceBook]); + const runExport = async (label: string, fn: (book: Book) => Promise) => { + setExportOpen(false); + try { + await fn(useBook.getState().book); + } catch (e) { + setNotice(`${label} export failed: ${e}`); + } + }; + useEffect(() => { const onKey = (e: KeyboardEvent) => { const mod = e.metaKey || e.ctrlKey; @@ -58,14 +77,11 @@ function App() { return () => window.removeEventListener("keydown", onKey); }, [handleSave, handleOpen]); - const runExport = async (fn: (book: Book) => Promise) => { - setExportOpen(false); - try { - await fn(useBook.getState().book); - } catch (e) { - console.error(e); - } - }; + useEffect(() => { + if (!notice) return; + const timer = setTimeout(() => setNotice(null), 4000); + return () => clearTimeout(timer); + }, [notice]); return (
@@ -90,8 +106,8 @@ function App() { <>
setExportOpen(false)} />
- - + +
)} @@ -129,6 +145,11 @@ function App() {
{settingsOpen && setSettingsOpen(false)} />} + {notice && ( +
setNotice(null)}> + {notice} +
+ )}
); } diff --git a/src/styles/app.css b/src/styles/app.css index cd88760..e09e827 100644 --- a/src/styles/app.css +++ b/src/styles/app.css @@ -472,6 +472,7 @@ button { gap: 1px; padding: 5px; background: rgba(252, 251, 247, 0.86); + -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); border: 1px solid var(--line); border-radius: 999px; @@ -627,6 +628,7 @@ button { inset: 0; z-index: 20; background: rgba(35, 32, 27, 0.28); + -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px); display: grid; place-items: center; @@ -771,3 +773,20 @@ button { background: var(--accent-wash); color: var(--ink); } + +.toast { + position: fixed; + bottom: 26px; + left: 50%; + transform: translateX(-50%); + z-index: 40; + max-width: 460px; + padding: 11px 16px; + border-radius: var(--r-md); + background: var(--ink); + color: var(--paper); + font-family: var(--font-ui); + font-size: var(--t-3); + box-shadow: var(--shadow-pop); + cursor: pointer; +}