refactor changes

This commit is contained in:
pj committed 2026-08-30 16:59:00 +05:30
1 parent df1a447a79
commit d4318554f6
36 files changed
+1251 -291

No files matched your search

+12 -11
View File
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
import { listen } from "@tauri-apps/api/event";
import type { Editor as TiptapEditor } from "@tiptap/react";
import { icons } from "margin-shared";
import { Sidebar } from "./Sidebar";
import { Dock } from "./Dock";
import { ResizeHandle } from "./ResizeHandle";
@@ -281,7 +282,7 @@ export function EditorView() {
<header className="titlebar" data-tauri-drag-region>
<div className="lead">
<button className="icon-btn" data-on={sidebarOpen} onClick={toggleSidebar} title="Toggle chapters" aria-label="Toggle chapters">
<Icon d="M3 4.5h18v15H3zM9 4.5v15" />
<Icon d={icons.SIDEBAR} />
</button>
</div>
<button className="doc-title" onClick={() => setSettingsOpen(true)} title="Book setup">
@@ -291,22 +292,22 @@ export function EditorView() {
<div className="actions">
{isDesktop && !isCompact && <BackupButton />}
<button className="icon-btn" data-on={findOpen} onClick={() => (findOpen ? setFindOpen(false) : openFind(false))} title="Find (⌘F)">
<Icon d="M11 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14zM20 20l-4-4" />
<Icon d={icons.SEARCH} />
</button>
{!isCompact && proofingAvailable && !coverActive && (
<>
<button className="icon-btn" data-on={spelling} onClick={toggleSpelling} title="Check spelling">
<Icon d="M4 17l4-10 4 10M5.4 13.4h5.2M15 17l2.5 2.5L22 14" />
<Icon d={icons.SPELLING} />
</button>
<button className="icon-btn" data-on={grammar} onClick={toggleGrammar} title="Check grammar">
<Icon d="M4 7h16M4 12h12M4 17h7M13.5 18.5c1-1.2 2-1.2 3 0s2 1.2 3 0" />
<Icon d={icons.GRAMMAR} />
</button>
</>
)}
{!isCompact && (
<div className="menu-wrap">
<button className="icon-btn" data-on={widthOpen} onClick={() => setWidthOpen((v) => !v)} title="Editor width">
<Icon d="M3 5v14M21 5v14M7 12h10M7 12l3-3M7 12l3 3M17 12l-3-3M17 12l-3 3" />
<Icon d={icons.WIDTH} />
</button>
<Menu open={widthOpen} onClose={() => setWidthOpen(false)}>
{WIDTH_OPTIONS.map((w) => (
@@ -327,7 +328,7 @@ export function EditorView() {
{!isCompact && (
<div className="menu-wrap">
<button className="icon-btn" data-on={exportOpen} onClick={() => setExportOpen((v) => !v)} title="Export">
<Icon d="M5 13v6h14v-6M12 16V3M8 7l4-4 4 4" />
<Icon d={icons.EXPORT} />
</button>
<Menu open={exportOpen} onClose={() => setExportOpen(false)}>
<button onClick={() => handleExport("pdf")}>Export PDF…</button>
@@ -339,21 +340,21 @@ export function EditorView() {
<button className="icon-btn" onClick={toggleTheme} title={theme === "dark" ? "Light mode" : "Dark mode"}>
{theme === "dark" ? (
<Icon>
<circle cx="12" cy="12" r="4" />
<path d="M12 2v2M12 20v2M2 12h2M20 12h2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M19.1 4.9l-1.4 1.4M6.3 17.7l-1.4 1.4" />
<circle cx={icons.SUN_DISC.cx} cy={icons.SUN_DISC.cy} r={icons.SUN_DISC.r} />
<path d={icons.SUN_RAYS} />
</Icon>
) : (
<Icon d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" />
<Icon d={icons.MOON} />
)}
</button>
)}
<button className="icon-btn" data-on={dock} onClick={toggleDock} title="Toggle preview">
<Icon d="M3 4.5h18v15H3zM14 4.5v15" />
<Icon d={icons.DOCK} />
</button>
{isCompact && (
<div className="menu-wrap">
<button className="icon-btn" data-on={moreOpen} onClick={() => setMoreOpen((v) => !v)} title="More" aria-label="More options">
<Icon d="M5 12h.01M12 12h.01M19 12h.01" />
<Icon d={icons.MORE} />
</button>
<Menu open={moreOpen} onClose={() => setMoreOpen(false)}>
<div className="menu-label">Editor width</div>
+2 -1
View File
@@ -13,6 +13,7 @@ import {
fontStack,
pairingFor,
} from "../model/fonts";
import { icons } from "margin-shared";
import { useEscapeLayer } from "../escape";
import { useFocusTrap } from "../focus";
import { Icon } from "./Icon";
@@ -128,7 +129,7 @@ export function Settings({ onClose, onSave }: { onClose: () => void; onSave: ()
<div className="panel-head">
<h2>Book setup</h2>
<button className="icon-btn" onClick={onClose} title="Close">
<Icon d="M6 6l12 12M18 6L6 18" />
<Icon d={icons.CLOSE} />
</button>
</div>
<div className="panel-body">
+6 -2
View File
@@ -1,7 +1,7 @@
import type { JSONContent } from "@tiptap/core";
import { invoke } from "@tauri-apps/api/core";
import { type Book, type FigurePlacement, TRIM_DIMS, FIGURE_WIDTH, bodyNumber, chapterKind, isResizablePlacement, partNumber, partRoman } from "../model/book";
import { fontStack, fontsUsed } from "../model/fonts";
import { bundledFont, fontStack, fontsUsed } from "../model/fonts";
export interface EpubFile {
path: string;
@@ -66,7 +66,11 @@ async function fetchFontBase64(file: string): Promise<string | null> {
}
async function loadFontAssets(book: Book): Promise<FontAssets> {
const used = fontsUsed(book.settings.fonts).bundled;
// Ids from the shared helper, resolved back to records: an EPUB embeds the font files, so this
// is the one caller that needs a face's filenames rather than its name.
const used = fontsUsed(book.settings.fonts)
.bundled.map((id) => bundledFont(id))
.filter((font) => font !== undefined);
const files: EpubFile[] = [];
const faces: string[] = [];
const manifest: string[] = [];
+1 -1
View File
@@ -10,7 +10,7 @@ export interface PdfFonts {
function pdfFonts(book: Book): PdfFonts {
const used = fontsUsed(book.settings.fonts);
return { bundled: used.bundled.map((f) => f.id), system: used.system };
return { bundled: used.bundled, system: used.system };
}
const TRIM: Record<TrimSize, { w: string; h: string }> = {
+37 -102
View File
@@ -1,106 +1,41 @@
export type FontCategory = "serif" | "sans" | "display";
// The faces a book can be set in.
//
// The catalogue itself is margin-shared's, because Margin and Margin Docs have to offer the same
// six families under the same ids: a book and a document set in "EB Garamond" must mean the same
// file, and the two apps drifting apart on which faces exist is how one of them ends up exporting a
// PDF in a fallback. What is left here is the one thing that is this app's own, which is the noun.
export interface BundledFont {
id: string;
label: string;
family: string;
category: FontCategory;
regular: string;
italic?: string;
weight: string;
}
export type {
BundledFont,
FontCategory,
FontPairing,
FontRef,
} from "margin-shared/fonts";
export const BUNDLED_FONTS: BundledFont[] = [
{ id: "literata", label: "Literata", family: "Literata", category: "serif", regular: "Literata-VF.ttf", italic: "Literata-Italic-VF.ttf", weight: "200 900" },
{ id: "eb-garamond", label: "EB Garamond", family: "EB Garamond", category: "serif", regular: "EBGaramond-VF.ttf", italic: "EBGaramond-Italic-VF.ttf", weight: "400 800" },
{ id: "lora", label: "Lora", family: "Lora", category: "serif", regular: "Lora-VF.ttf", italic: "Lora-Italic-VF.ttf", weight: "400 700" },
{ id: "source-serif", label: "Source Serif 4", family: "Source Serif 4", category: "serif", regular: "SourceSerif4-VF.ttf", italic: "SourceSerif4-Italic-VF.ttf", weight: "200 900" },
{ id: "fraunces", label: "Fraunces", family: "Fraunces", category: "display", regular: "Fraunces-VF.ttf", italic: "Fraunces-Italic-VF.ttf", weight: "100 900" },
{ id: "hanken", label: "Hanken Grotesk", family: "Hanken Grotesk", category: "sans", regular: "HankenGrotesk-VF.ttf", italic: "HankenGrotesk-Italic-VF.ttf", weight: "100 900" },
];
export {
BUNDLED_FONTS,
DEFAULT_FONTS,
FONT_PAIRINGS,
bundledFont,
decodeRef,
encodeRef,
fontFamilyName,
fontLabel,
fontStack,
fontsEqual,
fontsUsed,
isFontRef,
pairingFor,
refsEqual,
} from "margin-shared/fonts";
export type FontRef =
| { kind: "bundled"; id: string }
| { kind: "system"; family: string };
import type { FontPair } from "margin-shared/fonts";
export interface BookFonts {
body: FontRef;
heading: FontRef;
}
export interface FontPairing {
id: string;
label: string;
body: FontRef;
heading: FontRef;
}
const bundled = (id: string): FontRef => ({ kind: "bundled", id });
export const FONT_PAIRINGS: FontPairing[] = [
{ id: "quiet-press", label: "Quiet Press", body: bundled("literata"), heading: bundled("literata") },
{ id: "classic", label: "Classic", body: bundled("eb-garamond"), heading: bundled("eb-garamond") },
{ id: "editorial", label: "Editorial", body: bundled("source-serif"), heading: bundled("fraunces") },
{ id: "modern", label: "Modern", body: bundled("lora"), heading: bundled("hanken") },
{ id: "contrast", label: "Contrast", body: bundled("literata"), heading: bundled("hanken") },
];
export const DEFAULT_FONTS: BookFonts = { body: bundled("literata"), heading: bundled("literata") };
const SERIF_FALLBACK = `Georgia, "Times New Roman", serif`;
const SANS_FALLBACK = `ui-sans-serif, system-ui, -apple-system, sans-serif`;
export function bundledFont(id: string): BundledFont | undefined {
return BUNDLED_FONTS.find((f) => f.id === id);
}
export function encodeRef(ref: FontRef): string {
return ref.kind === "bundled" ? `b:${ref.id}` : `s:${ref.family}`;
}
export function decodeRef(value: string): FontRef {
return value.startsWith("s:") ? { kind: "system", family: value.slice(2) } : { kind: "bundled", id: value.slice(2) };
}
export function fontFamilyName(ref: FontRef): string {
if (ref.kind === "system") return ref.family;
return bundledFont(ref.id)?.family ?? "Literata";
}
export function fontLabel(ref: FontRef): string {
if (ref.kind === "system") return ref.family;
return bundledFont(ref.id)?.label ?? ref.id;
}
export function fontStack(ref: FontRef): string {
if (ref.kind === "bundled") {
const font = bundledFont(ref.id);
if (!font) return SERIF_FALLBACK;
return `"${font.family}", ${font.category === "sans" ? SANS_FALLBACK : SERIF_FALLBACK}`;
}
return `"${ref.family}", ${SERIF_FALLBACK}`;
}
export function refsEqual(a: FontRef, b: FontRef): boolean {
if (a.kind !== b.kind) return false;
return a.kind === "bundled" ? a.id === (b as { id: string }).id : a.family === (b as { family: string }).family;
}
export function pairingFor(fonts: BookFonts): string | null {
const match = FONT_PAIRINGS.find((p) => refsEqual(p.body, fonts.body) && refsEqual(p.heading, fonts.heading));
return match?.id ?? null;
}
export function fontsUsed(fonts: BookFonts): { bundled: BundledFont[]; system: string[] } {
const usedBundled = new Map<string, BundledFont>();
const usedSystem = new Set<string>();
for (const ref of [fonts.body, fonts.heading]) {
if (ref.kind === "bundled") {
const font = bundledFont(ref.id);
if (font) usedBundled.set(font.id, font);
} else {
usedSystem.add(ref.family);
}
}
return { bundled: [...usedBundled.values()], system: [...usedSystem] };
}
/**
* What one book is set in.
*
* An alias rather than a second interface, so every call site in this app keeps reading in the
* app's own noun while the shape stays the shared one. Margin Docs calls the same pair
* `DocumentFonts`.
*/
export type BookFonts = FontPair;
+4 -95
View File
@@ -1,95 +1,4 @@
@font-face {
font-family: "Literata";
src: url("/fonts/Literata-VF.ttf") format("truetype-variations");
font-weight: 200 900;
font-style: normal;
font-display: block;
}
@font-face {
font-family: "Literata";
src: url("/fonts/Literata-Italic-VF.ttf") format("truetype-variations");
font-weight: 200 900;
font-style: italic;
font-display: block;
}
@font-face {
font-family: "Hanken Grotesk";
src: url("/fonts/HankenGrotesk-VF.ttf") format("truetype-variations");
font-weight: 100 900;
font-style: normal;
font-display: block;
}
@font-face {
font-family: "Hanken Grotesk";
src: url("/fonts/HankenGrotesk-Italic-VF.ttf") format("truetype-variations");
font-weight: 100 900;
font-style: italic;
font-display: block;
}
@font-face {
font-family: "EB Garamond";
src: url("/fonts/EBGaramond-VF.ttf") format("truetype-variations");
font-weight: 400 800;
font-style: normal;
font-display: block;
}
@font-face {
font-family: "EB Garamond";
src: url("/fonts/EBGaramond-Italic-VF.ttf") format("truetype-variations");
font-weight: 400 800;
font-style: italic;
font-display: block;
}
@font-face {
font-family: "Lora";
src: url("/fonts/Lora-VF.ttf") format("truetype-variations");
font-weight: 400 700;
font-style: normal;
font-display: block;
}
@font-face {
font-family: "Lora";
src: url("/fonts/Lora-Italic-VF.ttf") format("truetype-variations");
font-weight: 400 700;
font-style: italic;
font-display: block;
}
@font-face {
font-family: "Source Serif 4";
src: url("/fonts/SourceSerif4-VF.ttf") format("truetype-variations");
font-weight: 200 900;
font-style: normal;
font-display: block;
}
@font-face {
font-family: "Source Serif 4";
src: url("/fonts/SourceSerif4-Italic-VF.ttf") format("truetype-variations");
font-weight: 200 900;
font-style: italic;
font-display: block;
}
@font-face {
font-family: "Fraunces";
src: url("/fonts/Fraunces-VF.ttf") format("truetype-variations");
font-weight: 100 900;
font-style: normal;
font-display: block;
}
@font-face {
font-family: "Fraunces";
src: url("/fonts/Fraunces-Italic-VF.ttf") format("truetype-variations");
font-weight: 100 900;
font-style: italic;
font-display: block;
}
/* The six families, declared once in margin-shared so that Margin and Margin Docs bind the same
files to the same family names. The faces themselves are served from this app's public/fonts,
which `pnpm fonts:sync` fills from the package and `pnpm fonts:check` verifies. */
@import "margin-shared/css/fonts.css";
+6 -77
View File
@@ -1,80 +1,9 @@
/* The palette, the type scale and the shape constants come from margin-shared, which is the one
copy of the values both apps have to agree on. */
@import "margin-shared/css/tokens.css";
/* The one token only this app has: the width of the preview dock, which Margin Docs has no
equivalent of. */
:root {
--font-ui: "Hanken Grotesk", ui-sans-serif, system-ui, -apple-system, sans-serif;
--font-book: "Literata", Georgia, "Times New Roman", serif;
--font-heading: "Literata", Georgia, "Times New Roman", serif;
--r-sm: 5px;
--r-md: 8px;
--r-lg: 12px;
--pane-sidebar: 248px;
--pane-dock: 384px;
--measure: 46em;
--titlebar-h: 46px;
--t-1: 11px;
--t-2: 12px;
--t-3: 13px;
--t-4: 15px;
--ease: cubic-bezier(0.22, 0.61, 0.36, 1);
}
:root,
:root[data-theme="light"] {
--paper: #fcfbf7;
--shell: #f1ece2;
--sidebar: #ece6da;
--raised: #fbfaf6;
--ink: #23201b;
--ink-soft: #6b6458;
--ink-faint: #9b9484;
--line: #e3ddce;
--line-strong: #d6cfbd;
--accent: #2a2622;
--accent-ink: #100e0b;
--accent-wash: rgba(35, 32, 27, 0.08);
--accent-contrast: #faf7f0;
--selection: rgba(35, 32, 27, 0.14);
--glass: rgba(252, 251, 247, 0.86);
--danger: #b4453a;
--danger-ink: #963327;
--danger-wash: rgba(180, 69, 58, 0.1);
--danger-contrast: #faf7f0;
--shadow-page: 0 1px 2px rgba(35, 32, 27, 0.06), 0 14px 30px rgba(35, 32, 27, 0.09);
--shadow-pop: 0 8px 24px rgba(35, 32, 27, 0.14);
}
:root[data-theme="dark"] {
--paper: #1d1a16;
--shell: #151310;
--sidebar: #1a1713;
--raised: #232019;
--ink: #ece6da;
--ink-soft: #a89f8e;
--ink-faint: #756d5e;
--line: #2c2823;
--line-strong: #3a352d;
--accent: #efe9dc;
--accent-ink: #ffffff;
--accent-wash: rgba(239, 233, 220, 0.12);
--accent-contrast: #1a1714;
--selection: rgba(239, 233, 220, 0.18);
--glass: rgba(33, 30, 25, 0.88);
--danger: #d97a6f;
--danger-ink: #e58e84;
--danger-wash: rgba(217, 122, 111, 0.16);
--danger-contrast: #1a1714;
--shadow-page: 0 1px 2px rgba(0, 0, 0, 0.4), 0 14px 34px rgba(0, 0, 0, 0.5);
--shadow-pop: 0 8px 24px rgba(0, 0, 0, 0.55);
}