mirror of
https://github.com/priyanshujain/margin.git
synced 2026-10-02 11:07:04 +00:00
feat(ui): titlebar export menu and book-setup access
This commit is contained in:
1 parent
e424008d56
commit
0beac0c240
2 files changed
+182
-2
No files matched your search
+32
-2
@@ -3,14 +3,19 @@ import type { Editor as TiptapEditor } from "@tiptap/react";
|
|||||||
import { Sidebar } from "./components/Sidebar";
|
import { Sidebar } from "./components/Sidebar";
|
||||||
import { Dock } from "./components/Dock";
|
import { Dock } from "./components/Dock";
|
||||||
import { Icon } from "./components/Icon";
|
import { Icon } from "./components/Icon";
|
||||||
|
import { Settings } from "./components/Settings";
|
||||||
import { Editor } from "./editor/Editor";
|
import { Editor } from "./editor/Editor";
|
||||||
import { FloatingToolbar } from "./editor/FloatingToolbar";
|
import { FloatingToolbar } from "./editor/FloatingToolbar";
|
||||||
import { useBook } from "./store/useBook";
|
import { useBook } from "./store/useBook";
|
||||||
|
import type { Book } from "./model/book";
|
||||||
import { chooseSavePath, openBook, writeBook } from "./project";
|
import { chooseSavePath, openBook, writeBook } from "./project";
|
||||||
|
import { exportEpub, exportPdf } from "./export/exporters";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [dock, setDock] = useState(true);
|
const [dock, setDock] = useState(true);
|
||||||
const [editor, setEditor] = useState<TiptapEditor | null>(null);
|
const [editor, setEditor] = useState<TiptapEditor | null>(null);
|
||||||
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||||
|
const [exportOpen, setExportOpen] = useState(false);
|
||||||
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);
|
||||||
@@ -53,14 +58,23 @@ function App() {
|
|||||||
return () => window.removeEventListener("keydown", onKey);
|
return () => window.removeEventListener("keydown", onKey);
|
||||||
}, [handleSave, handleOpen]);
|
}, [handleSave, handleOpen]);
|
||||||
|
|
||||||
|
const runExport = async (fn: (book: Book) => Promise<void>) => {
|
||||||
|
setExportOpen(false);
|
||||||
|
try {
|
||||||
|
await fn(useBook.getState().book);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app">
|
<div className="app">
|
||||||
<header className="titlebar" data-tauri-drag-region>
|
<header className="titlebar" data-tauri-drag-region>
|
||||||
<span className="doc-title">
|
<button className="doc-title" onClick={() => setSettingsOpen(true)} title="Book setup">
|
||||||
{title || "Untitled"}
|
{title || "Untitled"}
|
||||||
{author ? ` — ${author}` : ""}
|
{author ? ` — ${author}` : ""}
|
||||||
{dirty && <span className="dirty-dot" />}
|
{dirty && <span className="dirty-dot" />}
|
||||||
</span>
|
</button>
|
||||||
<div className="actions">
|
<div className="actions">
|
||||||
<button className="icon-btn" onClick={handleOpen} title="Open (⌘O)">
|
<button className="icon-btn" onClick={handleOpen} title="Open (⌘O)">
|
||||||
<Icon d="M3 7l1.7-2.5h4.6L11 7h8a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1z" />
|
<Icon d="M3 7l1.7-2.5h4.6L11 7h8a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1z" />
|
||||||
@@ -68,6 +82,20 @@ function App() {
|
|||||||
<button className="icon-btn" onClick={handleSave} title="Save (⌘S)">
|
<button className="icon-btn" onClick={handleSave} title="Save (⌘S)">
|
||||||
<Icon d="M12 4v10m0 0l-3.5-3.5M12 14l3.5-3.5M5 19h14" />
|
<Icon d="M12 4v10m0 0l-3.5-3.5M12 14l3.5-3.5M5 19h14" />
|
||||||
</button>
|
</button>
|
||||||
|
<div className="menu-wrap">
|
||||||
|
<button className="icon-btn" data-on={exportOpen} onClick={() => setExportOpen((v) => !v)} title="Export">
|
||||||
|
<Icon d="M12 15V4m0 0L8 8m4-4l4 4M5 14v5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-5" />
|
||||||
|
</button>
|
||||||
|
{exportOpen && (
|
||||||
|
<>
|
||||||
|
<div className="menu-backdrop" onClick={() => setExportOpen(false)} />
|
||||||
|
<div className="menu">
|
||||||
|
<button onClick={() => runExport(exportPdf)}>Export PDF…</button>
|
||||||
|
<button onClick={() => runExport(exportEpub)}>Export EPUB…</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<button className="icon-btn" data-on={dock} onClick={() => setDock(!dock)} title="Toggle preview">
|
<button className="icon-btn" data-on={dock} onClick={() => setDock(!dock)} title="Toggle preview">
|
||||||
<Icon d="M3 4.5h18v15H3zM14 4.5v15" />
|
<Icon d="M3 4.5h18v15H3zM14 4.5v15" />
|
||||||
</button>
|
</button>
|
||||||
@@ -99,6 +127,8 @@ function App() {
|
|||||||
</main>
|
</main>
|
||||||
{dock && <Dock />}
|
{dock && <Dock />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{settingsOpen && <Settings onClose={() => setSettingsOpen(false)} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -621,3 +621,153 @@ button {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 44px 0;
|
padding: 44px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 20;
|
||||||
|
background: rgba(35, 32, 27, 0.28);
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
width: 480px;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: var(--paper);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--r-lg);
|
||||||
|
box-shadow: var(--shadow-pop);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px 14px 16px 22px;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-head h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-family: var(--font-book);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
padding: 20px 22px 24px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-label {
|
||||||
|
font-family: var(--font-ui);
|
||||||
|
font-size: var(--t-1);
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--ink-faint);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field input,
|
||||||
|
.field select {
|
||||||
|
font-family: var(--font-ui);
|
||||||
|
font-size: var(--t-4);
|
||||||
|
color: var(--ink);
|
||||||
|
background: var(--raised);
|
||||||
|
border: 1px solid var(--line-strong);
|
||||||
|
border-radius: var(--r-sm);
|
||||||
|
padding: 9px 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field input:focus,
|
||||||
|
.field select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-row .field {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 9px;
|
||||||
|
font-family: var(--font-ui);
|
||||||
|
font-size: var(--t-3);
|
||||||
|
color: var(--ink-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.check input {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
accent-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-title {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-title:hover {
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-wrap {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
position: absolute;
|
||||||
|
top: 36px;
|
||||||
|
right: 0;
|
||||||
|
z-index: 30;
|
||||||
|
min-width: 156px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 5px;
|
||||||
|
background: var(--paper);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--r-md);
|
||||||
|
box-shadow: var(--shadow-pop);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu button {
|
||||||
|
text-align: left;
|
||||||
|
padding: 8px 11px;
|
||||||
|
border-radius: var(--r-sm);
|
||||||
|
font-family: var(--font-ui);
|
||||||
|
font-size: var(--t-3);
|
||||||
|
color: var(--ink-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu button:hover {
|
||||||
|
background: var(--accent-wash);
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user