feat(ui): surface save and export errors as toasts; add webkit backdrop-filter

This commit is contained in:
pj committed 2026-06-20 13:29:52 -04:00
1 parent c4b69f3641
commit cb506af374
2 files changed
+57 -17

No files matched your search

+38 -17
View File
@@ -16,6 +16,7 @@ function App() {
const [editor, setEditor] = useState<TiptapEditor | null>(null); const [editor, setEditor] = useState<TiptapEditor | null>(null);
const [settingsOpen, setSettingsOpen] = useState(false); const [settingsOpen, setSettingsOpen] = useState(false);
const [exportOpen, setExportOpen] = useState(false); const [exportOpen, setExportOpen] = useState(false);
const [notice, setNotice] = useState<string | null>(null);
const title = useBook((s) => s.book.metadata.title); const title = useBook((s) => s.book.metadata.title);
const author = useBook((s) => s.book.metadata.author); const author = useBook((s) => s.book.metadata.author);
const chapters = useBook((s) => s.book.chapters); const chapters = useBook((s) => s.book.chapters);
@@ -30,18 +31,36 @@ function App() {
const chapter = chapters[idx]; const chapter = chapters[idx];
const handleSave = useCallback(async () => { const handleSave = useCallback(async () => {
const { book, path } = useBook.getState(); try {
const target = path ?? (await chooseSavePath(book)); const { book, path } = useBook.getState();
if (!target) return; const target = path ?? (await chooseSavePath(book));
await writeBook(book, target); if (!target) return;
markSaved(target); await writeBook(book, target);
markSaved(target);
setNotice("Saved");
} catch (e) {
setNotice(`Save failed: ${e}`);
}
}, [markSaved]); }, [markSaved]);
const handleOpen = useCallback(async () => { const handleOpen = useCallback(async () => {
const result = await openBook(); try {
if (result) replaceBook(result.book, result.path); const result = await openBook();
if (result) replaceBook(result.book, result.path);
} catch (e) {
setNotice(`Open failed: ${e}`);
}
}, [replaceBook]); }, [replaceBook]);
const runExport = async (label: string, fn: (book: Book) => Promise<void>) => {
setExportOpen(false);
try {
await fn(useBook.getState().book);
} catch (e) {
setNotice(`${label} export failed: ${e}`);
}
};
useEffect(() => { useEffect(() => {
const onKey = (e: KeyboardEvent) => { const onKey = (e: KeyboardEvent) => {
const mod = e.metaKey || e.ctrlKey; const mod = e.metaKey || e.ctrlKey;
@@ -58,14 +77,11 @@ function App() {
return () => window.removeEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey);
}, [handleSave, handleOpen]); }, [handleSave, handleOpen]);
const runExport = async (fn: (book: Book) => Promise<void>) => { useEffect(() => {
setExportOpen(false); if (!notice) return;
try { const timer = setTimeout(() => setNotice(null), 4000);
await fn(useBook.getState().book); return () => clearTimeout(timer);
} catch (e) { }, [notice]);
console.error(e);
}
};
return ( return (
<div className="app"> <div className="app">
@@ -90,8 +106,8 @@ function App() {
<> <>
<div className="menu-backdrop" onClick={() => setExportOpen(false)} /> <div className="menu-backdrop" onClick={() => setExportOpen(false)} />
<div className="menu"> <div className="menu">
<button onClick={() => runExport(exportPdf)}>Export PDF…</button> <button onClick={() => runExport("PDF", exportPdf)}>Export PDF…</button>
<button onClick={() => runExport(exportEpub)}>Export EPUB…</button> <button onClick={() => runExport("EPUB", exportEpub)}>Export EPUB…</button>
</div> </div>
</> </>
)} )}
@@ -129,6 +145,11 @@ function App() {
</div> </div>
{settingsOpen && <Settings onClose={() => setSettingsOpen(false)} />} {settingsOpen && <Settings onClose={() => setSettingsOpen(false)} />}
{notice && (
<div className="toast" onClick={() => setNotice(null)}>
{notice}
</div>
)}
</div> </div>
); );
} }
+19
View File
@@ -472,6 +472,7 @@ button {
gap: 1px; gap: 1px;
padding: 5px; padding: 5px;
background: rgba(252, 251, 247, 0.86); background: rgba(252, 251, 247, 0.86);
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 999px; border-radius: 999px;
@@ -627,6 +628,7 @@ button {
inset: 0; inset: 0;
z-index: 20; z-index: 20;
background: rgba(35, 32, 27, 0.28); background: rgba(35, 32, 27, 0.28);
-webkit-backdrop-filter: blur(2px);
backdrop-filter: blur(2px); backdrop-filter: blur(2px);
display: grid; display: grid;
place-items: center; place-items: center;
@@ -771,3 +773,20 @@ button {
background: var(--accent-wash); background: var(--accent-wash);
color: var(--ink); 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;
}