Files
PubScript/packages/exporters/src/themes.ts
T
Schwitzz 978858362d feat(export): Chunk #6 Export Pipeline — 7 exporters, ExportDialog & tests
Add the @pubscript/exporters package exporting a flattened ProseMirror
project to Markdown, HTML, EPUB, DOCX, ODT and PDF, plus a deterministic
generatedAt timestamp threaded through every format (ZIP entry dates,
PDF /ID, DOCX dc:created/modified, EPUB id, HTML/XML footers) so golden
files are byte-stable.

Wire an ExportDialog into the web app (Topbar trigger, App route) with
format selection, theme pick, and download helpers.

Add round-trip and golden-file determinism tests (22) covering
non-empty output, byte-identical re-exports, Markdown round-trip through
the editor, and EPUB/ODT ZIP + mimetype + XML validation.
2026-10-26 23:30:47 -05:00

175 lines
4.4 KiB
TypeScript

/**
* Export themes. A theme is a self-contained CSS stylesheet plus the font and
* colour tokens exporters use to set defaults (EPUB reading-mode overrides,
* DOCX/ODT font families, PDF margins).
*
* Four themes ship by default:
* - Ink — neutral serif, light (the default trade trim size look)
* - Parchment — warm cream paper, dark text
* - Nocturne — dark mode, light text
* - Terminal — monospace, terminal aesthetic
*/
export interface ThemeColors {
text: string;
background: string;
accent: string;
muted: string;
heading: string;
}
export interface Theme {
id: string;
name: string;
/** Body font family. */
fontFamily: string;
/** Heading font family (falls back to fontFamily when null). */
headingFont: string | null;
/** Base body font size (CSS length). */
fontSize: string;
/** Body line height. */
lineHeight: string;
/** Paragraph indent. */
paragraphIndent: string;
colors: ThemeColors;
/** Full stylesheet, scoped under `.ps-doc`, ready to inline in HTML/EPUB. */
css: string;
}
const baseRules = (t: Theme): string => `
.ps-doc {
font-family: ${t.fontFamily};
font-size: ${t.fontSize};
line-height: ${t.lineHeight};
color: ${t.colors.text};
background: ${t.colors.background};
}
.ps-doc h1, .ps-doc h2, .ps-doc h3 {
font-family: ${t.headingFont ?? t.fontFamily};
color: ${t.colors.heading};
line-height: 1.25;
margin: 1.4em 0 0.6em;
}
.ps-doc h1 { font-size: 2em; }
.ps-doc h2 { font-size: 1.5em; }
.ps-doc h3 { font-size: 1.2em; }
.ps-doc p {
margin: 0;
text-indent: ${t.paragraphIndent};
orphans: 2;
widows: 2;
}
.ps-doc p:first-of-type { text-indent: none; }
.ps-doc blockquote {
margin: 1em 0;
padding: 0.2em 1em;
border-left: 3px solid ${t.colors.accent};
color: ${t.colors.muted};
font-style: italic;
}
.ps-doc pre {
white-space: pre-wrap;
word-wrap: break-word;
background: ${t.colors.background};
border: 1px solid ${t.colors.muted};
border-radius: 4px;
padding: 0.6em 0.8em;
font-size: 0.9em;
}
.ps-doc a { color: ${t.colors.accent}; text-decoration: underline; }
.ps-doc .scene-break {
text-align: center;
margin: 1.4em 0;
color: ${t.colors.muted};
}
`;
const makeTheme = (spec: Omit<Theme, "css">): Theme => ({
...spec,
css: baseRules(spec as Theme),
});
const ink: Theme = makeTheme({
id: "ink",
name: "Ink",
fontFamily: '"Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif',
headingFont: '"Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif',
fontSize: "11pt",
lineHeight: "1.55",
paragraphIndent: "1.4em",
colors: {
text: "#1a1a1a",
background: "#ffffff",
accent: "#7a2e2e",
muted: "#6b6b6b",
heading: "#101010",
},
});
const parchment: Theme = makeTheme({
id: "parchment",
name: "Parchment",
fontFamily: '"Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif',
headingFont: '"Garamond", "EB Garamond", Georgia, serif',
fontSize: "11pt",
lineHeight: "1.6",
paragraphIndent: "1.4em",
colors: {
text: "#3a3226",
background: "#f4ecd8",
accent: "#8a5a2b",
muted: "#7a6f5c",
heading: "#4a3a22",
},
});
const nocturne: Theme = makeTheme({
id: "nocturne",
name: "Nocturne",
fontFamily: '"Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif',
headingFont: '"Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif',
fontSize: "11pt",
lineHeight: "1.6",
paragraphIndent: "1.4em",
colors: {
text: "#d8d8d8",
background: "#14161a",
accent: "#7fb3ff",
muted: "#9aa0aa",
heading: "#ffffff",
},
});
const terminal: Theme = makeTheme({
id: "terminal",
name: "Terminal",
fontFamily: '"JetBrains Mono", "Fira Code", "SF Mono", Menlo, Consolas, monospace',
headingFont: '"JetBrains Mono", "Fira Code", "SF Mono", Menlo, Consolas, monospace',
fontSize: "9.5pt",
lineHeight: "1.5",
paragraphIndent: "0",
colors: {
text: "#d6ffdb",
background: "#0b0f10",
accent: "#7dff9c",
muted: "#7fae86",
heading: "#b6ffbf",
},
});
/** All available themes, keyed by id. */
export const THEMES: Record<string, Theme> = {
ink,
parchment,
nocturne,
terminal,
};
/** The default theme id. */
export const DEFAULT_THEME_ID = "ink";
/** Look up a theme by id, falling back to the default. */
export function getTheme(id?: string): Theme {
return THEMES[id ?? DEFAULT_THEME_ID] ?? ink;
}