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 [settingsOpen, setSettingsOpen] = useState(false);
const [exportOpen, setExportOpen] = useState(false);
const [notice, setNotice] = useState<string | null>(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<void>) => {
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<void>) => {
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 (
<div className="app">
@@ -90,8 +106,8 @@ function App() {
<>
<div className="menu-backdrop" onClick={() => setExportOpen(false)} />
<div className="menu">
<button onClick={() => runExport(exportPdf)}>Export PDF…</button>
<button onClick={() => runExport(exportEpub)}>Export EPUB…</button>
<button onClick={() => runExport("PDF", exportPdf)}>Export PDF…</button>
<button onClick={() => runExport("EPUB", exportEpub)}>Export EPUB…</button>
</div>
</>
)}
@@ -129,6 +145,11 @@ function App() {
</div>
{settingsOpen && <Settings onClose={() => setSettingsOpen(false)} />}
{notice && (
<div className="toast" onClick={() => setNotice(null)}>
{notice}
</div>
)}
</div>
);
}
+19
View File
@@ -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;
}