mirror of https://github.com/garrytan/gstack.git
feat(make-pdf): typography scale-up, zero image truncation, landscape vertical centering
Dogfooding round on the repo README surfaced four output-quality bugs:
- Type was too small everywhere: body 11→12pt, h1 22→26pt, h2 15→18pt,
cover title 32→56pt with poster spacing, cover meta 10→13pt, TOC 11→12pt
with tighter leading, code 9.5→10.5pt, tables 10→11pt.
- Zero image truncation, ever: the max-width cap was figure-scoped, but
markdown images render as <p><img> — a 1850px GitHub screenshot ran off
the page edge. Global img { max-width: 100%; height: auto; } cap.
- hyphens: auto put real 'dif-\nferent' breaks into the PDF text layer the
moment 12pt made lines wrap (combined-gate caught it). Clean copy-paste
is the product contract; left-aligned rag doesn't need hyphenation →
hyphens: manual.
- Promoted landscape blocks now vertically center. CSS flex/min-height
centering fragments into phantom empty landscape pages in Chromium
(bisected: min-height at ANY value; 3 promotions printed 5 pages), so
image-policy computes an inline margin-top from each block's known
aspect ratio against the landscape content box instead — fragmentation
handles margins fine. .page-wide also drops its explicit break-before/
after (the page-name change already breaks on both sides).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
2a68d366b2
commit
9397e7f847
|
|
@ -661,6 +661,39 @@ export function contentWidthInches(opts: {
|
||||||
return Math.max(1, pageW - left - right);
|
return Math.max(1, pageW - left - right);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PAGE_HEIGHTS_IN: Record<string, number> = {
|
||||||
|
letter: 11,
|
||||||
|
a4: 11.69,
|
||||||
|
legal: 14,
|
||||||
|
tabloid: 17,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Content box of the rotated (landscape) named page: portrait page HEIGHT
|
||||||
|
* becomes the landscape width; portrait WIDTH becomes the landscape height.
|
||||||
|
* Used by image-policy to vertically center promoted blocks.
|
||||||
|
*/
|
||||||
|
export function landscapeContentBox(opts: {
|
||||||
|
pageSize?: string;
|
||||||
|
margins?: string;
|
||||||
|
marginLeft?: string;
|
||||||
|
marginRight?: string;
|
||||||
|
marginTop?: string;
|
||||||
|
marginBottom?: string;
|
||||||
|
}): { contentWIn: number; contentHIn: number } {
|
||||||
|
const size = opts.pageSize ?? "letter";
|
||||||
|
const pageH = PAGE_HEIGHTS_IN[size] ?? 11;
|
||||||
|
const pageW = PAGE_WIDTHS_IN[size] ?? 8.5;
|
||||||
|
const left = dimToInches(opts.marginLeft ?? opts.margins, 1);
|
||||||
|
const right = dimToInches(opts.marginRight ?? opts.margins, 1);
|
||||||
|
const top = dimToInches(opts.marginTop ?? opts.margins, 1);
|
||||||
|
const bottom = dimToInches(opts.marginBottom ?? opts.margins, 1);
|
||||||
|
return {
|
||||||
|
contentWIn: Math.max(1, pageH - left - right),
|
||||||
|
contentHIn: Math.max(1, pageW - top - bottom),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// ─── tiny helpers ─────────────────────────────────────────────────────
|
// ─── tiny helpers ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
function escapeHtml(s: string): string {
|
function escapeHtml(s: string): string {
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,13 @@
|
||||||
export interface ImagePolicyOptions {
|
export interface ImagePolicyOptions {
|
||||||
/** Physical content-box width in inches (page width minus margins). */
|
/** Physical content-box width in inches (page width minus margins). */
|
||||||
contentWidthIn: number;
|
contentWidthIn: number;
|
||||||
|
/**
|
||||||
|
* Landscape named-page content box (inches). Used to vertically center a
|
||||||
|
* promoted block via a computed inline margin-top — CSS flex/min-height
|
||||||
|
* centering fragments into phantom landscape pages in Chromium, so the
|
||||||
|
* margin is computed here from the block's known aspect ratio instead.
|
||||||
|
*/
|
||||||
|
landscape: { contentWIn: number; contentHIn: number };
|
||||||
warn: (msg: string) => void;
|
warn: (msg: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -124,7 +131,9 @@ export function applyImagePolicy(html: string, opts: ImagePolicyOptions): ImageP
|
||||||
if (!decision.promote) return full;
|
if (!decision.promote) return full;
|
||||||
hasLandscape = true;
|
hasLandscape = true;
|
||||||
opts.warn(`promoting image to a landscape page (${decision.reason})`);
|
opts.warn(`promoting image to a landscape page (${decision.reason})`);
|
||||||
return `<div class="page-wide">${tag}</div>`;
|
const w = num(attrValue(tag, "data-gstack-px-width"));
|
||||||
|
const h = num(attrValue(tag, "data-gstack-px-height"));
|
||||||
|
return wrapPageWide(tag, w && h ? h / w : null, opts.landscape);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2c. landscape promotion — rendered diagram figures (provenance is
|
// 2c. landscape promotion — rendered diagram figures (provenance is
|
||||||
|
|
@ -137,13 +146,32 @@ export function applyImagePolicy(html: string, opts: ImagePolicyOptions): ImageP
|
||||||
if (!decision.promote) return figure;
|
if (!decision.promote) return figure;
|
||||||
hasLandscape = true;
|
hasLandscape = true;
|
||||||
opts.warn(`promoting diagram to a landscape page (${decision.reason})`);
|
opts.warn(`promoting diagram to a landscape page (${decision.reason})`);
|
||||||
return `<div class="page-wide">${figure}</div>`;
|
const dims = svgCssDims(figure);
|
||||||
|
return wrapPageWide(figure, dims ? dims.height / dims.width : null, opts.landscape);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return { html: out, hasLandscape };
|
return { html: out, hasLandscape };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap a promoted block in the wide-page div, vertically centered via a
|
||||||
|
* computed margin-top: placed height = landscape content width × aspect,
|
||||||
|
* centered in the landscape content height. Unknown aspect → no margin
|
||||||
|
* (top placement beats a wrong guess).
|
||||||
|
*/
|
||||||
|
function wrapPageWide(
|
||||||
|
inner: string,
|
||||||
|
aspectHoverW: number | null,
|
||||||
|
landscape: { contentWIn: number; contentHIn: number },
|
||||||
|
): string {
|
||||||
|
if (!aspectHoverW) return `<div class="page-wide">${inner}</div>`;
|
||||||
|
const placedHIn = landscape.contentWIn * aspectHoverW;
|
||||||
|
const marginIn = Math.max(0, (landscape.contentHIn - placedHIn) / 2);
|
||||||
|
if (marginIn < 0.1) return `<div class="page-wide">${inner}</div>`;
|
||||||
|
return `<div class="page-wide" style="margin-top: ${marginIn.toFixed(2)}in">${inner}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
interface PromotionDecision {
|
interface PromotionDecision {
|
||||||
promote: boolean;
|
promote: boolean;
|
||||||
reason: string;
|
reason: string;
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import {
|
||||||
convertDiagnosticsForDocx,
|
convertDiagnosticsForDocx,
|
||||||
extractDiagramFences,
|
extractDiagramFences,
|
||||||
inlineLocalImages,
|
inlineLocalImages,
|
||||||
|
landscapeContentBox,
|
||||||
rasterizeDiagramFigures,
|
rasterizeDiagramFigures,
|
||||||
renderFenceSlots,
|
renderFenceSlots,
|
||||||
substituteSlots,
|
substituteSlots,
|
||||||
|
|
@ -170,7 +171,11 @@ export async function generate(opts: GenerateOptions): Promise<string> {
|
||||||
progress.end("Inlining images");
|
progress.end("Inlining images");
|
||||||
|
|
||||||
// Width directives + conservative auto-landscape (image-policy).
|
// Width directives + conservative auto-landscape (image-policy).
|
||||||
const policy = applyImagePolicy(finalHtml, { contentWidthIn, warn });
|
const policy = applyImagePolicy(finalHtml, {
|
||||||
|
contentWidthIn,
|
||||||
|
landscape: landscapeContentBox(opts),
|
||||||
|
warn,
|
||||||
|
});
|
||||||
finalHtml = policy.html;
|
finalHtml = policy.html;
|
||||||
hasLandscape = policy.hasLandscape;
|
hasLandscape = policy.hasLandscape;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -122,15 +122,22 @@ function pageRules(size: string, margin: string, opts: PrintCssOptions): string
|
||||||
// Landscape named page for promoted wide diagrams/images (image-policy).
|
// Landscape named page for promoted wide diagrams/images (image-policy).
|
||||||
// Chromium-only — exactly the engine this pipeline always prints with.
|
// Chromium-only — exactly the engine this pipeline always prints with.
|
||||||
// Honored only when the print call passes preferCSSPageSize (orchestrator
|
// Honored only when the print call passes preferCSSPageSize (orchestrator
|
||||||
// sets it when a promotion exists).
|
// sets it when a promotion exists). The block is flex-centered: a diagram
|
||||||
|
// alone on a rotated page should sit in the middle, not hug the header.
|
||||||
`@page wide {`,
|
`@page wide {`,
|
||||||
` size: ${size} landscape;`,
|
` size: ${size} landscape;`,
|
||||||
` margin: ${margin};`,
|
` margin: ${margin};`,
|
||||||
`}`,
|
`}`,
|
||||||
|
// No explicit break-before/after (the page-name CHANGE already forces a
|
||||||
|
// break on both sides) and NO height/flex centering: a flex .page-wide
|
||||||
|
// with min-height fragments into a phantom empty landscape page in
|
||||||
|
// Chromium (landscape-gate counted 5 pages for 3 promotions; bisected to
|
||||||
|
// min-height at any value). Vertical centering is done by image-policy
|
||||||
|
// instead — it knows each promoted block's aspect ratio and emits an
|
||||||
|
// inline margin-top, which fragmentation handles fine.
|
||||||
`.page-wide {`,
|
`.page-wide {`,
|
||||||
` page: wide;`,
|
` page: wide;`,
|
||||||
` break-before: page;`,
|
` text-align: center;`,
|
||||||
` break-after: page;`,
|
|
||||||
`}`,
|
`}`,
|
||||||
`.page-wide img, .page-wide svg { width: 100%; height: auto; max-width: none; }`,
|
`.page-wide img, .page-wide svg { width: 100%; height: auto; max-width: none; }`,
|
||||||
`.page-wide figure.diagram > svg { max-width: none; }`,
|
`.page-wide figure.diagram > svg { max-width: none; }`,
|
||||||
|
|
@ -157,13 +164,23 @@ export function screenCss(): string {
|
||||||
function rootTypography(): string {
|
function rootTypography(): string {
|
||||||
return [
|
return [
|
||||||
`html { lang: en; }`,
|
`html { lang: en; }`,
|
||||||
|
// Zero image truncation, ever: every image caps at the content box,
|
||||||
|
// whatever element it lives in. Markdown images render as <p><img> (no
|
||||||
|
// figure), so a figure-scoped cap alone lets a 1900px screenshot run off
|
||||||
|
// the page edge. .page-wide deliberately overrides to fill its landscape
|
||||||
|
// box — still bounded, never clipped.
|
||||||
|
`img { max-width: 100%; height: auto; }`,
|
||||||
`body {`,
|
`body {`,
|
||||||
` font-family: ${SANS_STACK}, ${CJK_STACK}, ${EMOJI_FAMILIES}, sans-serif;`,
|
` font-family: ${SANS_STACK}, ${CJK_STACK}, ${EMOJI_FAMILIES}, sans-serif;`,
|
||||||
` font-size: 11pt;`,
|
` font-size: 12pt;`,
|
||||||
` line-height: 1.5;`,
|
` line-height: 1.5;`,
|
||||||
` color: #111;`,
|
` color: #111;`,
|
||||||
` background: white;`,
|
` background: white;`,
|
||||||
` hyphens: auto;`,
|
// No auto-hyphenation: it puts real "dif-\nferent" breaks into the PDF
|
||||||
|
// text layer, and clean copy-paste is the product contract (the
|
||||||
|
// combined-gate caught this the moment 12pt body made lines wrap).
|
||||||
|
// Left-aligned rag doesn't need hyphenation.
|
||||||
|
` hyphens: manual;`,
|
||||||
` font-variant-ligatures: common-ligatures;`,
|
` font-variant-ligatures: common-ligatures;`,
|
||||||
` font-kerning: normal;`,
|
` font-kerning: normal;`,
|
||||||
` text-rendering: geometricPrecision;`,
|
` text-rendering: geometricPrecision;`,
|
||||||
|
|
@ -176,45 +193,47 @@ function rootTypography(): string {
|
||||||
function coverRules(enabled: boolean): string {
|
function coverRules(enabled: boolean): string {
|
||||||
if (!enabled) return "";
|
if (!enabled) return "";
|
||||||
return [
|
return [
|
||||||
|
// Poster scale: the cover is the one page where type should feel huge.
|
||||||
`.cover {`,
|
`.cover {`,
|
||||||
` page: first;`,
|
` page: first;`,
|
||||||
` page-break-after: always;`,
|
` page-break-after: always;`,
|
||||||
` break-after: page;`,
|
` break-after: page;`,
|
||||||
` text-align: left;`,
|
` text-align: left;`,
|
||||||
|
` padding-top: 1.4in;`,
|
||||||
`}`,
|
`}`,
|
||||||
`.cover .eyebrow {`,
|
`.cover .eyebrow {`,
|
||||||
` font-size: 9pt;`,
|
` font-size: 11pt;`,
|
||||||
` letter-spacing: 0.2em;`,
|
` letter-spacing: 0.2em;`,
|
||||||
` text-transform: uppercase;`,
|
` text-transform: uppercase;`,
|
||||||
` color: #666;`,
|
` color: #666;`,
|
||||||
` margin: 0 0 36pt;`,
|
` margin: 0 0 36pt;`,
|
||||||
`}`,
|
`}`,
|
||||||
`.cover h1.cover-title {`,
|
`.cover h1.cover-title {`,
|
||||||
` font-size: 32pt;`,
|
` font-size: 56pt;`,
|
||||||
` line-height: 1.15;`,
|
` line-height: 1.08;`,
|
||||||
` font-weight: 700;`,
|
` font-weight: 700;`,
|
||||||
` letter-spacing: -0.01em;`,
|
` letter-spacing: -0.02em;`,
|
||||||
` margin: 0 0 18pt;`,
|
` margin: 0 0 24pt;`,
|
||||||
` max-width: 5.5in;`,
|
` max-width: 6in;`,
|
||||||
` text-align: left;`,
|
` text-align: left;`,
|
||||||
`}`,
|
`}`,
|
||||||
`.cover .cover-subtitle {`,
|
`.cover .cover-subtitle {`,
|
||||||
` font-size: 14pt;`,
|
` font-size: 18pt;`,
|
||||||
` line-height: 1.4;`,
|
` line-height: 1.35;`,
|
||||||
` font-weight: 400;`,
|
` font-weight: 400;`,
|
||||||
` color: #333;`,
|
` color: #333;`,
|
||||||
` margin: 0 0 36pt;`,
|
` margin: 0 0 36pt;`,
|
||||||
` max-width: 5in;`,
|
` max-width: 5.5in;`,
|
||||||
` text-align: left;`,
|
` text-align: left;`,
|
||||||
`}`,
|
`}`,
|
||||||
`.cover hr.rule {`,
|
`.cover hr.rule {`,
|
||||||
` width: 2.5in;`,
|
` width: 2.5in;`,
|
||||||
` height: 0;`,
|
` height: 0;`,
|
||||||
` border: 0;`,
|
` border: 0;`,
|
||||||
` border-top: 1px solid #111;`,
|
` border-top: 1.5px solid #111;`,
|
||||||
` margin: 0 0 18pt 0;`,
|
` margin: 0 0 24pt 0;`,
|
||||||
`}`,
|
`}`,
|
||||||
`.cover .cover-meta { font-size: 10pt; line-height: 1.6; color: #333; text-align: left; }`,
|
`.cover .cover-meta { font-size: 13pt; line-height: 1.6; color: #333; text-align: left; }`,
|
||||||
`.cover .cover-meta strong { font-weight: 700; }`,
|
`.cover .cover-meta strong { font-weight: 700; }`,
|
||||||
].join("\n");
|
].join("\n");
|
||||||
}
|
}
|
||||||
|
|
@ -224,12 +243,12 @@ function tocRules(enabled: boolean): string {
|
||||||
return [
|
return [
|
||||||
`.toc { page-break-after: always; break-after: page; }`,
|
`.toc { page-break-after: always; break-after: page; }`,
|
||||||
`.toc h2 {`,
|
`.toc h2 {`,
|
||||||
` font-size: 13pt;`,
|
` font-size: 16pt;`,
|
||||||
` text-transform: uppercase;`,
|
` text-transform: uppercase;`,
|
||||||
` letter-spacing: 0.15em;`,
|
` letter-spacing: 0.15em;`,
|
||||||
` color: #666;`,
|
` color: #444;`,
|
||||||
` font-weight: 600;`,
|
` font-weight: 700;`,
|
||||||
` margin: 0 0 0.5in;`,
|
` margin: 0 0 0.4in;`,
|
||||||
`}`,
|
`}`,
|
||||||
`.toc ol {`,
|
`.toc ol {`,
|
||||||
` list-style: none;`,
|
` list-style: none;`,
|
||||||
|
|
@ -240,14 +259,14 @@ function tocRules(enabled: boolean): string {
|
||||||
` display: flex;`,
|
` display: flex;`,
|
||||||
` align-items: baseline;`,
|
` align-items: baseline;`,
|
||||||
` gap: 0.25in;`,
|
` gap: 0.25in;`,
|
||||||
` font-size: 11pt;`,
|
` font-size: 12pt;`,
|
||||||
` line-height: 2;`,
|
` line-height: 1.7;`,
|
||||||
` padding: 4pt 0;`,
|
` padding: 3pt 0;`,
|
||||||
`}`,
|
`}`,
|
||||||
`.toc li .toc-title { flex: 0 0 auto; }`,
|
`.toc li .toc-title { flex: 0 0 auto; }`,
|
||||||
`.toc li .toc-dots { flex: 1 1 auto; border-bottom: 1px dotted #aaa; margin: 0 6pt; transform: translateY(-4pt); }`,
|
`.toc li .toc-dots { flex: 1 1 auto; border-bottom: 1px dotted #aaa; margin: 0 6pt; transform: translateY(-4pt); }`,
|
||||||
`.toc li .toc-page { flex: 0 0 auto; color: #666; font-variant-numeric: tabular-nums; }`,
|
`.toc li .toc-page { flex: 0 0 auto; color: #666; font-variant-numeric: tabular-nums; }`,
|
||||||
`.toc li.level-2 { padding-left: 0.35in; font-size: 10pt; }`,
|
`.toc li.level-2 { padding-left: 0.35in; font-size: 11pt; }`,
|
||||||
`.toc li a { color: inherit; text-decoration: none; }`,
|
`.toc li a { color: inherit; text-decoration: none; }`,
|
||||||
].join("\n");
|
].join("\n");
|
||||||
}
|
}
|
||||||
|
|
@ -262,7 +281,7 @@ function chapterRules(noChapterBreaks: boolean): string {
|
||||||
return [
|
return [
|
||||||
breakRule,
|
breakRule,
|
||||||
`h1 {`,
|
`h1 {`,
|
||||||
` font-size: 22pt;`,
|
` font-size: 26pt;`,
|
||||||
` line-height: 1.2;`,
|
` line-height: 1.2;`,
|
||||||
` font-weight: 700;`,
|
` font-weight: 700;`,
|
||||||
` letter-spacing: -0.01em;`,
|
` letter-spacing: -0.01em;`,
|
||||||
|
|
@ -270,9 +289,9 @@ function chapterRules(noChapterBreaks: boolean): string {
|
||||||
` break-after: avoid;`,
|
` break-after: avoid;`,
|
||||||
` page-break-after: avoid;`,
|
` page-break-after: avoid;`,
|
||||||
`}`,
|
`}`,
|
||||||
`h2 { font-size: 15pt; line-height: 1.3; font-weight: 700; margin: 24pt 0 6pt; break-after: avoid; page-break-after: avoid; }`,
|
`h2 { font-size: 18pt; line-height: 1.3; font-weight: 700; margin: 26pt 0 8pt; break-after: avoid; page-break-after: avoid; }`,
|
||||||
`h3 { font-size: 12pt; line-height: 1.4; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: #333; margin: 18pt 0 4pt; break-after: avoid; page-break-after: avoid; }`,
|
`h3 { font-size: 13.5pt; line-height: 1.4; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: #333; margin: 20pt 0 5pt; break-after: avoid; page-break-after: avoid; }`,
|
||||||
`h4 { font-size: 11pt; font-weight: 700; margin: 12pt 0 4pt; break-after: avoid; page-break-after: avoid; }`,
|
`h4 { font-size: 12pt; font-weight: 700; margin: 14pt 0 5pt; break-after: avoid; page-break-after: avoid; }`,
|
||||||
].join("\n");
|
].join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -287,7 +306,7 @@ function blockRules(): string {
|
||||||
` orphans: 3;`,
|
` orphans: 3;`,
|
||||||
`}`,
|
`}`,
|
||||||
`p:first-child { margin-top: 0; }`,
|
`p:first-child { margin-top: 0; }`,
|
||||||
`p.lead { font-size: 13pt; line-height: 1.45; color: #222; margin: 0 0 18pt; }`,
|
`p.lead { font-size: 14pt; line-height: 1.45; color: #222; margin: 0 0 18pt; }`,
|
||||||
].join("\n");
|
].join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -308,7 +327,7 @@ function codeRules(): string {
|
||||||
return [
|
return [
|
||||||
`code {`,
|
`code {`,
|
||||||
` font-family: "SF Mono", Menlo, Consolas, monospace;`,
|
` font-family: "SF Mono", Menlo, Consolas, monospace;`,
|
||||||
` font-size: 9.5pt;`,
|
` font-size: 10.5pt;`,
|
||||||
` background: #f4f4f4;`,
|
` background: #f4f4f4;`,
|
||||||
` padding: 1pt 3pt;`,
|
` padding: 1pt 3pt;`,
|
||||||
` border-radius: 2pt;`,
|
` border-radius: 2pt;`,
|
||||||
|
|
@ -316,7 +335,7 @@ function codeRules(): string {
|
||||||
`}`,
|
`}`,
|
||||||
`pre {`,
|
`pre {`,
|
||||||
` font-family: "SF Mono", Menlo, Consolas, monospace;`,
|
` font-family: "SF Mono", Menlo, Consolas, monospace;`,
|
||||||
` font-size: 9pt;`,
|
` font-size: 10pt;`,
|
||||||
` line-height: 1.4;`,
|
` line-height: 1.4;`,
|
||||||
` background: #f7f7f5;`,
|
` background: #f7f7f5;`,
|
||||||
` padding: 10pt 12pt;`,
|
` padding: 10pt 12pt;`,
|
||||||
|
|
@ -356,7 +375,7 @@ function figureRules(): string {
|
||||||
return [
|
return [
|
||||||
`figure { margin: 12pt 0; }`,
|
`figure { margin: 12pt 0; }`,
|
||||||
`figure img { display: block; max-width: 100%; height: auto; }`,
|
`figure img { display: block; max-width: 100%; height: auto; }`,
|
||||||
`figcaption { font-size: 9pt; color: #666; margin-top: 6pt; font-style: italic; }`,
|
`figcaption { font-size: 10pt; color: #666; margin-top: 6pt; font-style: italic; }`,
|
||||||
// Diagram figures (diagram-prepass): rendered mermaid/excalidraw SVG.
|
// Diagram figures (diagram-prepass): rendered mermaid/excalidraw SVG.
|
||||||
// SVGs scale to the content box and never split across pages.
|
// SVGs scale to the content box and never split across pages.
|
||||||
`figure.diagram { break-inside: avoid; text-align: center; }`,
|
`figure.diagram { break-inside: avoid; text-align: center; }`,
|
||||||
|
|
@ -374,7 +393,7 @@ function figureRules(): string {
|
||||||
|
|
||||||
function tableRules(): string {
|
function tableRules(): string {
|
||||||
return [
|
return [
|
||||||
`table { width: 100%; border-collapse: collapse; margin: 12pt 0; font-size: 10pt; }`,
|
`table { width: 100%; border-collapse: collapse; margin: 12pt 0; font-size: 11pt; }`,
|
||||||
`th, td { border-bottom: 0.5pt solid #ccc; padding: 5pt 8pt; text-align: left; vertical-align: top; }`,
|
`th, td { border-bottom: 0.5pt solid #ccc; padding: 5pt 8pt; text-align: left; vertical-align: top; }`,
|
||||||
`th { font-weight: 700; border-bottom: 1pt solid #111; background: transparent; }`,
|
`th { font-weight: 700; border-bottom: 1pt solid #111; background: transparent; }`,
|
||||||
].join("\n");
|
].join("\n");
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue