Add Neon and Meridian glass themes, bundled fonts, configurable recovery backup location, autosave and UI polish

This commit is contained in:
avi 2026-08-24 18:58:16 -05:00
commit b74cfe98ab
59 changed files with 1848 additions and 185 deletions

View file

@ -214,6 +214,27 @@ setEditorDoc(editor5, normalized.doc);
ok(editor5.getText().includes("alpha") && editor5.getText().includes("beta"),
"legacy camelCase hardBreak chapter loads (not empty)");
// --- text alignment via toolbar command ---
const alignEd = makeEditor();
setEditorDoc(alignEd, { type: "doc", content: [{ type: "paragraph", content: [{ type: "text", text: "align me" }] }] });
alignEd.commands.focus("end");
runCommand(alignEd, "align-center");
ok(
(alignEd.getJSON().content[0].attrs?.textAlign || "") === "center",
"align-center sets the paragraph textAlign"
);
// --- standalone Highlight button (no color) must apply a highlight, not be a no-op ---
const hlEd = makeEditor();
setEditorDoc(hlEd, { type: "doc", content: [{ type: "paragraph", content: [{ type: "text", text: "mark me" }] }] });
hlEd.commands.selectAll();
runCommand(hlEd, "highlight");
const hlMarks = hlEd.getJSON().content[0].content[0].marks || [];
ok(
hlMarks.some((m) => m.type === "highlight"),
"standalone Highlight button applies a default highlight to the selection"
);
fs.rmSync(out, { force: true });
fs.rmSync(book, { recursive: true, force: true });

View file

@ -24,7 +24,7 @@ export { createBook, loadBook, saveMeta } from "./src/main/project.ts";
export { setChapterContent, getChapterContent } from "./src/main/chapters.ts";
export { markdownToNative } from "./src/main/markdown-to-native.ts";
export { parseChapter, serializeChapter, migrateDoc, getChapterDoc, nativeChapterFile, CURRENT_DOC_SCHEMA_VERSION, isValidDoc } from "./src/main/native-doc.ts";
export { RECOVERY_ROOT, _resetSessionBackupGuard } from "./src/main/backup.ts";
export { getRecoveryRoot, _resetSessionBackupGuard } from "./src/main/backup.ts";
`
);
await esbuild.build({
@ -54,7 +54,7 @@ const {
serializeChapter,
migrateDoc,
CURRENT_DOC_SCHEMA_VERSION,
RECOVERY_ROOT,
getRecoveryRoot,
_resetSessionBackupGuard,
} = mod;
@ -244,10 +244,10 @@ ok(
// Recovery backup exists under ~/Folio-Recovery and contains folio.json
const bookName = path.basename(book);
const backups = fs
.readdirSync(RECOVERY_ROOT)
.filter((d) => d.startsWith(bookName) && fs.statSync(path.join(RECOVERY_ROOT, d)).isDirectory());
.readdirSync(getRecoveryRoot())
.filter((d) => d.startsWith(bookName) && fs.statSync(path.join(getRecoveryRoot(), d)).isDirectory());
ok(backups.length >= 1, "recovery backup created under ~/Folio-Recovery");
const backupDir = path.join(RECOVERY_ROOT, backups[0]);
const backupDir = path.join(getRecoveryRoot(), backups[0]);
ok(fs.existsSync(path.join(backupDir, "folio.json")), "backup contains folio.json");
ok(
fs.existsSync(path.join(backupDir, "chapters", `${c1}.md`)),
@ -294,7 +294,7 @@ ok(
fs.rmSync(out, { force: true });
fs.rmSync(book, { recursive: true, force: true });
for (const d of backups) {
fs.rmSync(path.join(RECOVERY_ROOT, d), { recursive: true, force: true });
fs.rmSync(path.join(getRecoveryRoot(), d), { recursive: true, force: true });
}
console.log(

View file

@ -90,8 +90,9 @@
!!document.querySelector("#chapterList li.chapter-row.active")
);
// Manual save: typing marks the chapter Unsaved; the Save button (or
// Ctrl/Cmd+S) is required to persist. No auto-save should occur.
// Auto-save: typing marks the chapter Unsaved, then a debounced
// background save persists it automatically (no prompt, no manual
// Save required). Verify the indicator ends in "Saved".
const edSave = window.__folioEditor;
if (edSave) {
edSave.commands.focus("end");
@ -99,11 +100,8 @@
await sleep(150);
const unsaved = document.getElementById("saveState");
step("afterTypeUnsaved", unsaved ? unsaved.textContent : "n/a");
const sb = document.getElementById("saveBtn");
if (sb) {
sb.click();
await sleep(400);
}
// Wait past the autosave debounce for the chapter to self-persist.
await sleep(1600);
const after = document.getElementById("saveState");
step("afterSaveState", after ? after.textContent : "n/a");
} else {