978858362d
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.
175 lines
4.4 KiB
TypeScript
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;
|
|
}
|