mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-12 05:55:02 +10:00
62f8270b3e
commit b2b0470a1d9267d042ec0ac66523c6635bf5b199
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Tue May 19 13:13:38 2026 +0200
chore: update .gitignore to include .vite-hooks and modify pnpm-lock.yaml for dependencies
commit d28fadb5cd8706c874e616102878b4a394ec84c1
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Tue May 19 13:08:04 2026 +0200
fix: remove timestamp conflict guard
commit c6998d9dbab19d09d3c8054feef1d2e4117555eb
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Tue May 19 12:11:51 2026 +0200
chore(release): v5.1.5
commit f33d168711804880e1f12e88d24290aae16cc258
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Tue May 19 11:58:35 2026 +0200
revert: compose.yml
commit d961e6535811a10c335525fb33a08d03e737278d
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Tue May 19 11:58:08 2026 +0200
refactor(agent): replace 'revert' terminology with 'restore' for clarity, resolves #3086
commit 17f351171be218e33f01c469d95e4164d4c8dc57
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Tue May 19 11:10:41 2026 +0200
refactor(pdf): simplify sidebar section filtering and update summary feature logic
commit d55179b9d76879e3204de185e8b53fadd0a107ed
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Tue May 19 09:53:37 2026 +0200
chore: update pnpm-lock.yaml and turbo.json
commit 7cade6980e1a04352536bd44ef773f338c4ef599
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Tue May 19 09:38:30 2026 +0200
fix(polyfill): add tested polyfill for Map Upsert methods
commit 26d175bb9c53d93225d1e907678445252c13d660
Merge: 1cf33dc6c 5b1297fa2
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Tue May 19 09:23:29 2026 +0200
Merge remote-tracking branch 'origin/main' into feat/explore-hono-orpc-migration
# Conflicts:
# packages/api/src/services/agent-url.ts
# packages/runtime-externals/package.json
commit 1cf33dc6c9d81735730ad656e16dab6501c6d6a1
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Tue May 19 09:22:12 2026 +0200
chore: preserve branch changes before main sync
commit b380a4b00fdbcdd81ff4f8ef72b330fd027ccda5
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Mon May 18 07:50:28 2026 +0200
chore: lot of fixes for monorepo migration
commit 8fcf0ec64e1c29572ebaff494338368bfcf75760
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Fri May 15 13:57:17 2026 +0200
chore: update knip version and refine web app routing with new SEO endpoints
commit 234e68086ff15610a93877354c98e2c020364533
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Fri May 15 12:10:06 2026 +0200
refactor(auth): update OAuth routes to include API prefix and remove unused schema endpoint
commit 91c84b9a8496b0ce21d71cae9f8b2a027638c9ac
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Fri May 15 11:54:29 2026 +0200
chore: update dependencies and enhance PWA metadata in web app
commit 150117d4a5a9dd6cd92c64891aad8cae90f6a7af
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Fri May 15 11:12:35 2026 +0200
docs: revise manifest-only pwa testing scope
commit 6b939a55661aec9dd8122b184e4b60a5c7325fb5
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Fri May 15 11:11:33 2026 +0200
docs: add manifest-only pwa design
commit 1422e1fc96c400948b273210a1067251087d15d4
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Fri May 15 11:05:04 2026 +0200
chore(dev): simplify server proxy config
commit bc2ff5a9f6fda41e6c40333c8f163aa23a6c5e48
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Fri May 15 11:04:50 2026 +0200
docs: add unsafe oauth redirect plan
commit 445359ebe9b96c1515bf1c4c3f73ba8a8448ec12
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Fri May 15 11:04:34 2026 +0200
feat(auth): add unsafe oauth redirect flag
commit 73fffdd24598e56b2793f7657919bc794835892e
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Fri May 15 10:55:02 2026 +0200
docs: design unsafe oauth redirect flag
commit c0066aa19c15fc8a4c8e5179ed49889c117519f4
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Fri May 15 10:22:04 2026 +0200
chore: update translation source paths
commit 9033da082418d252aafd6c2eed72f71f014be3d9
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Fri May 15 10:09:25 2026 +0200
refactor(arch): react spa + hono migration
commit 6f27936c11bda895977dc63ee550c3346d4ce24b
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Fri May 15 01:10:47 2026 +0200
docs: add docker nightly tagging design
commit ecc1fd9a88a0ee1dca2f1977dfc17f74527fe1da
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Thu May 14 20:05:44 2026 +0200
feat: migrate to hono spa server
527 lines
15 KiB
TypeScript
527 lines
15 KiB
TypeScript
import type { ResumeData, SectionType } from "@reactive-resume/schema/resume/data";
|
|
import type { Template } from "@reactive-resume/schema/templates";
|
|
import {
|
|
BorderStyle,
|
|
convertMillimetersToTwip,
|
|
Document,
|
|
ExternalHyperlink,
|
|
HeadingLevel,
|
|
Paragraph,
|
|
ShadingType,
|
|
Table,
|
|
TableCell,
|
|
TableRow,
|
|
TextRun,
|
|
WidthType,
|
|
} from "docx";
|
|
import { parseColorString } from "@reactive-resume/utils/color";
|
|
import { toSafeDocxLink } from "./link-utils";
|
|
import { renderBuiltInSection, renderCustomSection, renderSummary, setRenderConfig } from "./section-renderers";
|
|
|
|
// --- Color helpers ---
|
|
|
|
function rgbToHex(r: number, g: number, b: number): string {
|
|
return [r, g, b].map((c) => c.toString(16).padStart(2, "0")).join("");
|
|
}
|
|
|
|
function getColorHex(rgba: string, fallback: string): string {
|
|
const parsed = parseColorString(rgba);
|
|
if (!parsed) return fallback;
|
|
return rgbToHex(parsed.r, parsed.g, parsed.b);
|
|
}
|
|
|
|
// --- Unit conversion helpers ---
|
|
|
|
/** Convert pt font size to docx half-points (docx uses half-pt units). */
|
|
function ptToHalfPt(pt: number): number {
|
|
return Math.round(pt * 2);
|
|
}
|
|
|
|
/** Convert pt spacing to twips (1pt = 20 twips). */
|
|
function ptToTwips(pt: number): number {
|
|
return Math.round(pt * 20);
|
|
}
|
|
|
|
// --- Page size constants (in mm) ---
|
|
|
|
interface PageSize {
|
|
width: number;
|
|
height: number;
|
|
}
|
|
|
|
const DEFAULT_PAGE_SIZE: PageSize = { width: 210, height: 297 };
|
|
|
|
const PAGE_SIZES = {
|
|
a4: DEFAULT_PAGE_SIZE,
|
|
letter: { width: 215.9, height: 279.4 },
|
|
} satisfies Record<string, PageSize>;
|
|
|
|
type DocxPageFormat = keyof typeof PAGE_SIZES;
|
|
|
|
const resolveDocxPageFormat = (format: ResumeData["metadata"]["page"]["format"]): DocxPageFormat => {
|
|
if (format === "letter") return "letter";
|
|
|
|
return "a4";
|
|
};
|
|
|
|
// --- Invisible border preset for table cells ---
|
|
|
|
const NO_BORDERS = {
|
|
top: { style: BorderStyle.NONE, size: 0 },
|
|
bottom: { style: BorderStyle.NONE, size: 0 },
|
|
left: { style: BorderStyle.NONE, size: 0 },
|
|
right: { style: BorderStyle.NONE, size: 0 },
|
|
} as const;
|
|
|
|
// --- Template layout config ---
|
|
|
|
interface TemplateConfig {
|
|
/** Which side the sidebar appears on */
|
|
sidebarSide: "left" | "right" | "none";
|
|
/** Sidebar background: "solid" = full primary color, "tint" = 20% opacity, "none" = no background */
|
|
sidebarBackground: "solid" | "tint" | "none";
|
|
/** Where the header is rendered */
|
|
headerPosition: "full-width" | "main-only" | "sidebar-only";
|
|
}
|
|
|
|
const TEMPLATE_CONFIGS: Record<Template, TemplateConfig> = {
|
|
azurill: { sidebarSide: "left", sidebarBackground: "none", headerPosition: "full-width" },
|
|
bronzor: { sidebarSide: "right", sidebarBackground: "none", headerPosition: "full-width" },
|
|
chikorita: { sidebarSide: "right", sidebarBackground: "solid", headerPosition: "main-only" },
|
|
ditgar: { sidebarSide: "left", sidebarBackground: "tint", headerPosition: "sidebar-only" },
|
|
ditto: { sidebarSide: "left", sidebarBackground: "none", headerPosition: "full-width" },
|
|
gengar: { sidebarSide: "left", sidebarBackground: "tint", headerPosition: "sidebar-only" },
|
|
glalie: { sidebarSide: "left", sidebarBackground: "tint", headerPosition: "sidebar-only" },
|
|
kakuna: { sidebarSide: "right", sidebarBackground: "none", headerPosition: "full-width" },
|
|
lapras: { sidebarSide: "right", sidebarBackground: "none", headerPosition: "full-width" },
|
|
leafish: { sidebarSide: "right", sidebarBackground: "none", headerPosition: "full-width" },
|
|
meowth: { sidebarSide: "left", sidebarBackground: "none", headerPosition: "full-width" },
|
|
onyx: { sidebarSide: "right", sidebarBackground: "none", headerPosition: "full-width" },
|
|
pikachu: { sidebarSide: "left", sidebarBackground: "none", headerPosition: "main-only" },
|
|
rhyhorn: { sidebarSide: "right", sidebarBackground: "none", headerPosition: "full-width" },
|
|
scizor: { sidebarSide: "left", sidebarBackground: "none", headerPosition: "full-width" },
|
|
};
|
|
|
|
const DEFAULT_TEMPLATE_CONFIG: TemplateConfig = {
|
|
sidebarSide: "left",
|
|
sidebarBackground: "none",
|
|
headerPosition: "full-width",
|
|
};
|
|
|
|
/**
|
|
* Blends a hex color toward white at the given opacity (0-1).
|
|
* Used to approximate CSS `background-color: rgba(r,g,b, 0.2)` on a white background.
|
|
*/
|
|
function blendWithWhite(hex: string, opacity: number): string {
|
|
const r = Number.parseInt(hex.slice(0, 2), 16);
|
|
const g = Number.parseInt(hex.slice(2, 4), 16);
|
|
const b = Number.parseInt(hex.slice(4, 6), 16);
|
|
const blend = (c: number) => Math.round(c * opacity + 255 * (1 - opacity));
|
|
return [blend(r), blend(g), blend(b)].map((c) => c.toString(16).padStart(2, "0")).join("");
|
|
}
|
|
|
|
// --- Section rendering dispatch ---
|
|
|
|
const BUILT_IN_SECTIONS = new Set<string>([
|
|
"profiles",
|
|
"experience",
|
|
"education",
|
|
"projects",
|
|
"skills",
|
|
"languages",
|
|
"interests",
|
|
"awards",
|
|
"certifications",
|
|
"publications",
|
|
"volunteer",
|
|
"references",
|
|
]);
|
|
|
|
function renderSection(sectionId: string, data: ResumeData, colorHex: string): Paragraph[] {
|
|
if (sectionId === "summary") {
|
|
return renderSummary(data.summary, colorHex);
|
|
}
|
|
|
|
if (BUILT_IN_SECTIONS.has(sectionId)) {
|
|
const section = data.sections[sectionId as SectionType];
|
|
if (section) {
|
|
return renderBuiltInSection(sectionId as SectionType, section, colorHex);
|
|
}
|
|
return [];
|
|
}
|
|
|
|
const customSection = data.customSections.find((cs) => cs.id === sectionId);
|
|
if (customSection) {
|
|
return renderCustomSection(customSection, colorHex);
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
// --- Header ---
|
|
|
|
function buildHeader(data: ResumeData, colorHex: string, textColorHex: string): Paragraph[] {
|
|
const paragraphs: Paragraph[] = [];
|
|
const { basics } = data;
|
|
const headingFont = data.metadata.typography.heading.fontFamily || "Calibri";
|
|
const bodyFont = data.metadata.typography.body.fontFamily || "Calibri";
|
|
// h2 = calc(var(--page-heading-font-size) * 1.25pt)
|
|
const nameSize = ptToHalfPt(data.metadata.typography.heading.fontSize * 1.25);
|
|
const bodySize = ptToHalfPt(data.metadata.typography.body.fontSize);
|
|
|
|
if (basics.name) {
|
|
paragraphs.push(
|
|
new Paragraph({
|
|
heading: HeadingLevel.TITLE,
|
|
spacing: { after: 60 },
|
|
children: [
|
|
new TextRun({
|
|
text: basics.name,
|
|
bold: true,
|
|
size: nameSize,
|
|
font: headingFont,
|
|
color: colorHex,
|
|
}),
|
|
],
|
|
}),
|
|
);
|
|
}
|
|
|
|
if (basics.headline) {
|
|
paragraphs.push(
|
|
new Paragraph({
|
|
spacing: { after: 60 },
|
|
children: [
|
|
new TextRun({
|
|
text: basics.headline,
|
|
italics: true,
|
|
size: bodySize,
|
|
font: bodyFont,
|
|
color: textColorHex,
|
|
}),
|
|
],
|
|
}),
|
|
);
|
|
}
|
|
|
|
const contactParts: (TextRun | ExternalHyperlink)[] = [];
|
|
const addSeparator = () => {
|
|
if (contactParts.length > 0) {
|
|
contactParts.push(new TextRun({ text: " | ", color: "999999", size: bodySize, font: bodyFont }));
|
|
}
|
|
};
|
|
|
|
if (basics.email) {
|
|
const mailtoLink = toSafeDocxLink(`mailto:${basics.email}`);
|
|
if (mailtoLink) {
|
|
addSeparator();
|
|
contactParts.push(
|
|
new ExternalHyperlink({
|
|
link: mailtoLink,
|
|
children: [
|
|
new TextRun({ text: basics.email, color: colorHex, underline: {}, size: bodySize, font: bodyFont }),
|
|
],
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
if (basics.phone) {
|
|
addSeparator();
|
|
contactParts.push(new TextRun({ text: basics.phone, size: bodySize, font: bodyFont, color: textColorHex }));
|
|
}
|
|
|
|
if (basics.location) {
|
|
addSeparator();
|
|
contactParts.push(new TextRun({ text: basics.location, size: bodySize, font: bodyFont, color: textColorHex }));
|
|
}
|
|
|
|
if (basics.website.url) {
|
|
const websiteLink = toSafeDocxLink(basics.website.url);
|
|
if (websiteLink) {
|
|
addSeparator();
|
|
contactParts.push(
|
|
new ExternalHyperlink({
|
|
link: websiteLink,
|
|
children: [
|
|
new TextRun({
|
|
text: basics.website.label || websiteLink,
|
|
color: colorHex,
|
|
underline: {},
|
|
size: bodySize,
|
|
font: bodyFont,
|
|
}),
|
|
],
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
for (const field of basics.customFields) {
|
|
if (!field.text) continue;
|
|
addSeparator();
|
|
if (field.link) {
|
|
const customLink = toSafeDocxLink(field.link);
|
|
if (customLink) {
|
|
contactParts.push(
|
|
new ExternalHyperlink({
|
|
link: customLink,
|
|
children: [
|
|
new TextRun({
|
|
text: field.text,
|
|
color: colorHex,
|
|
underline: {},
|
|
size: bodySize,
|
|
font: bodyFont,
|
|
}),
|
|
],
|
|
}),
|
|
);
|
|
} else {
|
|
contactParts.push(new TextRun({ text: field.text, size: bodySize, font: bodyFont, color: textColorHex }));
|
|
}
|
|
} else {
|
|
contactParts.push(new TextRun({ text: field.text, size: bodySize, font: bodyFont, color: textColorHex }));
|
|
}
|
|
}
|
|
|
|
if (contactParts.length > 0) {
|
|
paragraphs.push(
|
|
new Paragraph({
|
|
spacing: { after: 200 },
|
|
children: contactParts,
|
|
}),
|
|
);
|
|
}
|
|
|
|
return paragraphs;
|
|
}
|
|
|
|
// --- Two-column table layout ---
|
|
|
|
function buildTwoColumnTable(
|
|
mainParagraphs: Paragraph[],
|
|
sidebarParagraphs: Paragraph[],
|
|
sidebarWidthPct: number,
|
|
gapXTwips: number,
|
|
sidebarSide: "left" | "right" | "none",
|
|
sidebarShadingHex?: string,
|
|
): Table {
|
|
const mainWidthPct = 100 - sidebarWidthPct;
|
|
|
|
// DOCX table cells require at least one child
|
|
const mainChildren = mainParagraphs.length > 0 ? mainParagraphs : [new Paragraph({})];
|
|
const sidebarChildren = sidebarParagraphs.length > 0 ? sidebarParagraphs : [new Paragraph({})];
|
|
|
|
const sidebarShading = sidebarShadingHex
|
|
? { fill: sidebarShadingHex, type: ShadingType.CLEAR, color: "auto" }
|
|
: undefined;
|
|
|
|
const margins: { right?: number; left?: number } = {};
|
|
|
|
if (sidebarSide === "left") {
|
|
margins.right = gapXTwips;
|
|
} else if (sidebarSide === "right") {
|
|
margins.left = gapXTwips;
|
|
}
|
|
|
|
const sidebarCell = new TableCell({
|
|
width: { size: sidebarWidthPct, type: WidthType.PERCENTAGE },
|
|
borders: NO_BORDERS,
|
|
margins,
|
|
children: sidebarChildren,
|
|
...(sidebarShading ? { shading: sidebarShading } : {}),
|
|
});
|
|
|
|
const mainCell = new TableCell({
|
|
width: { size: mainWidthPct, type: WidthType.PERCENTAGE },
|
|
borders: NO_BORDERS,
|
|
margins,
|
|
children: mainChildren,
|
|
});
|
|
|
|
const cells = sidebarSide === "left" ? [sidebarCell, mainCell] : [mainCell, sidebarCell];
|
|
|
|
return new Table({
|
|
rows: [new TableRow({ children: cells })],
|
|
width: { size: 100, type: WidthType.PERCENTAGE },
|
|
borders: {
|
|
top: { style: BorderStyle.NONE, size: 0 },
|
|
bottom: { style: BorderStyle.NONE, size: 0 },
|
|
left: { style: BorderStyle.NONE, size: 0 },
|
|
right: { style: BorderStyle.NONE, size: 0 },
|
|
insideHorizontal: { style: BorderStyle.NONE, size: 0 },
|
|
insideVertical: { style: BorderStyle.NONE, size: 0 },
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Builds a complete docx Document from resume data.
|
|
*
|
|
* Mirrors the resume builder's visual layout:
|
|
* - Header with name, headline, and contact info (centered, full-width)
|
|
* - Two-column table layout (main + sidebar) matching `metadata.layout`
|
|
* - Typography (font family, size, line height) from `metadata.typography`
|
|
* - Page margins and fixed DOCX page format from `metadata.page`; free-form exports as A4
|
|
* - Primary, text, and background colors from `metadata.design.colors`
|
|
*/
|
|
export function buildDocument(data: ResumeData): Document {
|
|
const colorHex = getColorHex(data.metadata.design.colors.primary, "DC2626");
|
|
const textColorHex = getColorHex(data.metadata.design.colors.text, "000000");
|
|
const bgColorHex = getColorHex(data.metadata.design.colors.background, "FFFFFF");
|
|
|
|
const bodyFont = data.metadata.typography.body.fontFamily || "Calibri";
|
|
const bodySize = ptToHalfPt(data.metadata.typography.body.fontSize);
|
|
const lineSpacing = Math.round(data.metadata.typography.body.lineHeight * 240);
|
|
|
|
const { page } = data.metadata;
|
|
const pageSize = PAGE_SIZES[resolveDocxPageFormat(page.format)];
|
|
// Margins and gaps are defined in points (pt), not mm
|
|
const marginXTwips = ptToTwips(page.marginX);
|
|
const marginYTwips = ptToTwips(page.marginY);
|
|
const gapXTwips = ptToTwips(page.gapX);
|
|
|
|
const sidebarWidth = data.metadata.layout.sidebarWidth;
|
|
|
|
// Template-aware layout config
|
|
const templateConfig = TEMPLATE_CONFIGS[data.metadata.template] ?? DEFAULT_TEMPLATE_CONFIG;
|
|
|
|
// Compute sidebar background shading hex
|
|
let sidebarShadingHex: string | undefined;
|
|
if (templateConfig.sidebarBackground === "solid") {
|
|
sidebarShadingHex = colorHex;
|
|
} else if (templateConfig.sidebarBackground === "tint") {
|
|
sidebarShadingHex = blendWithWhite(colorHex, 0.2);
|
|
}
|
|
|
|
// Determine sidebar text colors — inverted when sidebar has a solid background
|
|
const sidebarTextColorHex = templateConfig.sidebarBackground === "solid" ? bgColorHex : textColorHex;
|
|
const sidebarHeadingColorHex = templateConfig.sidebarBackground === "solid" ? bgColorHex : colorHex;
|
|
|
|
// Configure heading typography for section renderers
|
|
const headingFont = data.metadata.typography.heading.fontFamily || "Calibri";
|
|
const headingSize = ptToHalfPt(data.metadata.typography.heading.fontSize);
|
|
|
|
const documentChildren: (Paragraph | Table)[] = [];
|
|
|
|
// Header placement depends on template
|
|
if (templateConfig.headerPosition === "full-width") {
|
|
// Configure colors for main content
|
|
setRenderConfig({
|
|
headingFont,
|
|
headingSizeHalfPt: headingSize,
|
|
bodyFont,
|
|
bodySizeHalfPt: bodySize,
|
|
textColorHex,
|
|
primaryColorHex: colorHex,
|
|
});
|
|
documentChildren.push(...buildHeader(data, colorHex, textColorHex));
|
|
}
|
|
|
|
// Process each page in the layout
|
|
for (const layoutPage of data.metadata.layout.pages) {
|
|
const isFullWidth = layoutPage.fullWidth || layoutPage.sidebar.length === 0;
|
|
|
|
if (isFullWidth) {
|
|
setRenderConfig({
|
|
headingFont,
|
|
headingSizeHalfPt: headingSize,
|
|
bodyFont,
|
|
bodySizeHalfPt: bodySize,
|
|
textColorHex,
|
|
primaryColorHex: colorHex,
|
|
});
|
|
for (const sectionId of [...layoutPage.main, ...layoutPage.sidebar]) {
|
|
documentChildren.push(...renderSection(sectionId, data, colorHex));
|
|
}
|
|
} else {
|
|
// Render main sections with normal colors
|
|
setRenderConfig({
|
|
headingFont,
|
|
headingSizeHalfPt: headingSize,
|
|
bodyFont,
|
|
bodySizeHalfPt: bodySize,
|
|
textColorHex,
|
|
primaryColorHex: colorHex,
|
|
});
|
|
|
|
const mainParagraphs: Paragraph[] = [];
|
|
if (templateConfig.headerPosition === "main-only") {
|
|
mainParagraphs.push(...buildHeader(data, colorHex, textColorHex));
|
|
}
|
|
for (const sectionId of layoutPage.main) {
|
|
mainParagraphs.push(...renderSection(sectionId, data, colorHex));
|
|
}
|
|
|
|
// Render sidebar sections with potentially inverted colors
|
|
setRenderConfig({
|
|
headingFont,
|
|
headingSizeHalfPt: headingSize,
|
|
bodyFont,
|
|
bodySizeHalfPt: bodySize,
|
|
textColorHex: sidebarTextColorHex,
|
|
primaryColorHex: sidebarHeadingColorHex,
|
|
});
|
|
|
|
const sidebarParagraphs: Paragraph[] = [];
|
|
if (templateConfig.headerPosition === "sidebar-only") {
|
|
sidebarParagraphs.push(...buildHeader(data, sidebarHeadingColorHex, sidebarTextColorHex));
|
|
}
|
|
for (const sectionId of layoutPage.sidebar) {
|
|
sidebarParagraphs.push(...renderSection(sectionId, data, sidebarHeadingColorHex));
|
|
}
|
|
|
|
if (mainParagraphs.length > 0 || sidebarParagraphs.length > 0) {
|
|
documentChildren.push(
|
|
buildTwoColumnTable(
|
|
mainParagraphs,
|
|
sidebarParagraphs,
|
|
sidebarWidth,
|
|
gapXTwips,
|
|
templateConfig.sidebarSide,
|
|
sidebarShadingHex,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
return new Document({
|
|
styles: {
|
|
default: {
|
|
document: {
|
|
run: {
|
|
font: bodyFont,
|
|
size: bodySize,
|
|
color: textColorHex,
|
|
},
|
|
paragraph: {
|
|
spacing: { line: lineSpacing },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
...(bgColorHex !== "FFFFFF" ? { background: { color: bgColorHex } } : {}),
|
|
sections: [
|
|
{
|
|
properties: {
|
|
page: {
|
|
size: {
|
|
width: convertMillimetersToTwip(pageSize.width),
|
|
height: convertMillimetersToTwip(pageSize.height),
|
|
},
|
|
margin: {
|
|
top: marginYTwips,
|
|
bottom: marginYTwips,
|
|
left: marginXTwips,
|
|
right: marginXTwips,
|
|
},
|
|
},
|
|
},
|
|
children: documentChildren,
|
|
},
|
|
],
|
|
});
|
|
}
|