fix(renderer): normalize editor line-height to 1.0; add chapter-view export dropdown
- Set .folio-prose.ProseMirror line-height from 1.7 to 1.0 to fix excessive spacing between paragraphs and blockquotes - Add PDF/DOCX/ODT export dropdown to the chapter writing view header, matching the Full Book view's export workflow - Wire chapter export dropdown toggle, menu selection, and close-on-outside-click
This commit is contained in:
parent
8e49d2769a
commit
1a1d69936c
7 changed files with 95 additions and 19 deletions
|
|
@ -30,7 +30,7 @@ import {
|
|||
export interface EditorFeature {
|
||||
editor: Editor;
|
||||
selectChapter(id: string): Promise<void>;
|
||||
saveCurrentChapter(): Promise<void>;
|
||||
saveCurrentChapter(): Promise<boolean>;
|
||||
saveAndNotify(): Promise<void>;
|
||||
flushAutosave(): Promise<void>;
|
||||
clearTimers(): void;
|
||||
|
|
@ -65,18 +65,25 @@ export function createEditorFeature(ctx: BookCtx): EditorFeature {
|
|||
|
||||
function clearTimers() {
|
||||
if (saveTimer) { clearTimeout(saveTimer); saveTimer = null; }
|
||||
stopForcedAutosave();
|
||||
}
|
||||
|
||||
async function saveCurrentChapter(): Promise<void> {
|
||||
if (!state.selectedId) return;
|
||||
async function saveCurrentChapter(): Promise<boolean> {
|
||||
if (!state.selectedId) return false;
|
||||
clearTimers();
|
||||
saveStateEl.textContent = "Saving…";
|
||||
saveStateEl.classList.remove("unsaved", "failed");
|
||||
const res = await api.saveChapterDoc(state.selectedId, getEditorDoc(editor));
|
||||
if (!(res && "error" in res)) {
|
||||
state.loadedDocJson = JSON.stringify(getEditorDoc(editor));
|
||||
state.dirty = false;
|
||||
saveStateEl.textContent = "Saved";
|
||||
saveStateEl.classList.remove("unsaved");
|
||||
if (res && "error" in res) {
|
||||
saveStateEl.textContent = "Failed";
|
||||
saveStateEl.classList.add("failed");
|
||||
return false;
|
||||
}
|
||||
state.loadedDocJson = JSON.stringify(getEditorDoc(editor));
|
||||
state.dirty = false;
|
||||
saveStateEl.textContent = "Saved";
|
||||
saveStateEl.classList.remove("unsaved", "failed");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Explicit save (Ctrl/Cmd+S or menu) — always give visible feedback.
|
||||
|
|
@ -89,8 +96,8 @@ export function createEditorFeature(ctx: BookCtx): EditorFeature {
|
|||
showToast(state.currentBook ? "Nothing to save" : "Nothing to save");
|
||||
return;
|
||||
}
|
||||
await saveCurrentChapter();
|
||||
showToast("Saved");
|
||||
const ok = await saveCurrentChapter();
|
||||
showToast(ok ? "Saved" : "Save failed — check disk space/permissions");
|
||||
}
|
||||
|
||||
// Guards against overlapping selections: rapid clicks (or metadata
|
||||
|
|
@ -132,10 +139,11 @@ export function createEditorFeature(ctx: BookCtx): EditorFeature {
|
|||
state.loadedDocJson = JSON.stringify(getEditorDoc(editor));
|
||||
state.dirty = false;
|
||||
saveStateEl.textContent = "Saved";
|
||||
saveStateEl.classList.remove("unsaved");
|
||||
saveStateEl.classList.remove("unsaved", "failed");
|
||||
$("wordCount").textContent = formatWordCount(editor.getText());
|
||||
editor.commands.focus("end");
|
||||
clearTimers();
|
||||
startForcedAutosave();
|
||||
$("fullBookBtn").classList.remove("active");
|
||||
$("fullBookView").classList.add("hidden");
|
||||
showView($("chapterView"));
|
||||
|
|
@ -160,6 +168,20 @@ export function createEditorFeature(ctx: BookCtx): EditorFeature {
|
|||
await saveCurrentChapter();
|
||||
}
|
||||
|
||||
// Periodic forced autosave (every 30s) as a safety net — saves even if user
|
||||
// keeps typing continuously. Runs alongside the idle-based debounce.
|
||||
const FORCED_AUTOSAVE_MS = 30_000;
|
||||
let forcedSaveTimer: ReturnType<typeof setInterval> | null = null;
|
||||
function startForcedAutosave() {
|
||||
if (forcedSaveTimer) return;
|
||||
forcedSaveTimer = setInterval(() => {
|
||||
if (state.selectedId && state.dirty) void flushAutosave();
|
||||
}, FORCED_AUTOSAVE_MS);
|
||||
}
|
||||
function stopForcedAutosave() {
|
||||
if (forcedSaveTimer) { clearInterval(forcedSaveTimer); forcedSaveTimer = null; }
|
||||
}
|
||||
|
||||
function handleEditorChange() {
|
||||
if (!state.selectedId) return;
|
||||
const json = JSON.stringify(getEditorDoc(editor));
|
||||
|
|
@ -204,6 +226,10 @@ export function createEditorFeature(ctx: BookCtx): EditorFeature {
|
|||
}
|
||||
});
|
||||
|
||||
// Toolbar Save button click handler.
|
||||
const saveBtn = $("saveBtn");
|
||||
saveBtn?.addEventListener("click", () => void saveAndNotify());
|
||||
|
||||
// Flush any pending auto-save when the window is hidden or loses focus, so the
|
||||
// last stretch of typing is persisted even if the app closes within the
|
||||
// debounce window.
|
||||
|
|
|
|||
|
|
@ -119,6 +119,37 @@ export function createExportsFeature(ctx: BookCtx): ExportsFeature {
|
|||
$("exportBtn").addEventListener("click", () => void exportMarkdown());
|
||||
$("exportZipBtn").addEventListener("click", () => void exportZip());
|
||||
|
||||
// ---- chapter view export dropdown (PDF / DOCX / ODT) ----
|
||||
const chapterExportMenuToggle = $("chapterExportMenuToggle") as HTMLButtonElement;
|
||||
const chapterExportMenu = $("chapterExportMenu");
|
||||
|
||||
function closeChapterExportMenu() {
|
||||
closeEl(chapterExportMenu);
|
||||
chapterExportMenuToggle.classList.remove("open");
|
||||
}
|
||||
|
||||
chapterExportMenuToggle.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
const wasOpen = !chapterExportMenu.classList.contains("hidden");
|
||||
closeChapterExportMenu();
|
||||
if (!wasOpen) {
|
||||
openEl(chapterExportMenu);
|
||||
chapterExportMenuToggle.classList.add("open");
|
||||
}
|
||||
});
|
||||
|
||||
chapterExportMenu.addEventListener("click", (e) => {
|
||||
const item = (e.target as HTMLElement).closest("[data-export]") as HTMLElement | null;
|
||||
if (!item) return;
|
||||
closeChapterExportMenu();
|
||||
void exportFromMenu(item.dataset.export ?? "");
|
||||
});
|
||||
|
||||
document.addEventListener("click", (e) => {
|
||||
if (!(e.target as HTMLElement).closest("#chapterExportDropdown")) closeChapterExportMenu();
|
||||
});
|
||||
|
||||
// ---- full book export dropdown ----
|
||||
const exportMenuToggle = $("exportMenuToggle") as HTMLButtonElement;
|
||||
const exportMenu = $("exportMenu");
|
||||
|
||||
|
|
|
|||
|
|
@ -203,6 +203,12 @@ export function createToolbarFeature(ctx: BookCtx, editor: Editor): ToolbarFeatu
|
|||
window.addEventListener("resize", closeDropdowns);
|
||||
// Electron has no default right-click menu, so show one with Cut/Copy/Paste.
|
||||
window.addEventListener("contextmenu", (e) => {
|
||||
// Let Chromium show its native spelling suggestions when the target is a
|
||||
// misspelled word. The custom menu is used for the rest of the editor.
|
||||
const target = e.target as HTMLElement | null;
|
||||
if (target?.closest(".ProseMirror") && window.getSelection()?.isCollapsed) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
api.showContextMenu(e.clientX, e.clientY);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ export interface SharedState {
|
|||
export interface BookHooks {
|
||||
// editor feature
|
||||
selectChapter(id: string): Promise<void>;
|
||||
saveCurrentChapter(): Promise<void>;
|
||||
saveCurrentChapter(): Promise<boolean>;
|
||||
saveAndNotify(): Promise<void>;
|
||||
flushAutosave(): Promise<void>;
|
||||
clearTimers(): void;
|
||||
|
|
|
|||
|
|
@ -902,6 +902,11 @@ body.dragging-active * {
|
|||
font-weight: 600;
|
||||
}
|
||||
|
||||
.save-state.failed {
|
||||
color: #e11d48;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.editor-panes {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
|
|
@ -1664,7 +1669,7 @@ body.focus-mode .editor {
|
|||
color: var(--color-text);
|
||||
background: var(--color-bg);
|
||||
padding: 24px 20px;
|
||||
max-width: 820px;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
outline: none;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,6 +75,14 @@
|
|||
<button class="icon-button" id="commentsBtn" title="Comments">💬</button>
|
||||
<button class="icon-button" id="exportBtn" title="Export book (Markdown)">⬇</button>
|
||||
<button class="icon-button" id="exportZipBtn" title="Export bundle (ZIP)">📦</button>
|
||||
<div class="toolbar-dropdown export-dropdown" id="chapterExportDropdown">
|
||||
<button class="button dropdown-toggle" id="chapterExportMenuToggle" title="Export as PDF, DOCX, or ODT">Export ▾</button>
|
||||
<div class="dropdown-menu hidden" id="chapterExportMenu">
|
||||
<button class="dropdown-item" data-export="pdf">PDF</button>
|
||||
<button class="dropdown-item" data-export="docx">DOCX</button>
|
||||
<button class="dropdown-item" data-export="odt">ODT</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="zoom-control" role="group" aria-label="Zoom">
|
||||
<button class="icon-btn" type="button" data-zoom="out" title="Zoom out (Ctrl+-)">−</button>
|
||||
<button class="zoom-level" type="button" data-zoom="reset" title="Reset zoom to 100% (Ctrl+0)">100%</button>
|
||||
|
|
|
|||
|
|
@ -567,15 +567,15 @@ html[data-theme="cosmic-light"] .folio-prose code {
|
|||
--color-border: #2a2210;
|
||||
--color-text: #fff5dc;
|
||||
--color-muted: #d5bd8d;
|
||||
--color-accent: #efbd62;
|
||||
--color-accent: #F3B407;
|
||||
--color-accent-hover: #ffdb91;
|
||||
|
||||
--color-on-accent: #0d0a06;
|
||||
--color-on-accent-soft: rgba(13, 10, 6, 0.18);
|
||||
--color-on-accent-border: rgba(13, 10, 6, 0.38);
|
||||
|
||||
--color-selection: rgba(239, 189, 98, 0.3);
|
||||
--color-selection-strong: rgba(239, 189, 98, 0.55);
|
||||
--color-selection: rgba(243, 180, 7, 0.3);
|
||||
--color-selection-strong: rgba(243, 180, 7, 0.55);
|
||||
|
||||
--color-shadow: rgba(0, 0, 0, 0.6);
|
||||
--color-shadow-strong: rgba(0, 0, 0, 0.75);
|
||||
|
|
@ -588,11 +588,11 @@ html[data-theme="cosmic-light"] .folio-prose code {
|
|||
--font-serif: "Tirra", "Source Serif 4", Georgia, serif;
|
||||
--font-display: "Yeseva One", "Cormorant Garamond", Georgia, serif;
|
||||
|
||||
--color-h1: #efbd62;
|
||||
--color-h1: #F3B407;
|
||||
--color-h2: #74d7cb;
|
||||
--color-h3: #f08e7d;
|
||||
--color-h4: #d89ce7;
|
||||
--color-h5: #79c7ef;
|
||||
--color-h5: #87E6FB;
|
||||
--color-h6: #f5ddae;
|
||||
}
|
||||
|
||||
|
|
@ -889,4 +889,4 @@ html[data-theme="cosmic-stardust-light"] .folio-prose code {
|
|||
--color-bg: rgba(255, 248, 232, 0.97);
|
||||
--color-panel: rgba(245, 234, 210, 0.94);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue