diff --git a/.gitignore b/.gitignore
index 7c0d64d..cf40553 100644
--- a/.gitignore
+++ b/.gitignore
@@ -171,4 +171,6 @@ dist
dist
.output
-.DS_Store
\ No newline at end of file
+.DS_Store
+
+.omx
\ No newline at end of file
diff --git a/public/themes/default.css b/public/themes/default.css
index d048bda..ef94aea 100644
--- a/public/themes/default.css
+++ b/public/themes/default.css
@@ -54,12 +54,40 @@
display: flex;
align-items: center;
gap: 8px;
+ flex-wrap: wrap;
+ justify-content: flex-end;
}
.export-container {
position: relative;
}
+.font-size-controls {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ margin: 0;
+ padding: 0;
+ border: 0;
+ min-inline-size: auto;
+}
+
+.font-size-btn {
+ min-width: 32px;
+ justify-content: center;
+}
+
+.font-size-icon {
+ width: 18px;
+ height: 18px;
+}
+
+.font-size-btn[disabled] {
+ cursor: default;
+ opacity: 0.35;
+ pointer-events: none;
+}
+
.export-option span {
flex: 1;
}
@@ -93,6 +121,14 @@
}
}
+@media (max-width: 640px) {
+ .top-actions {
+ top: 12px;
+ right: 12px;
+ gap: 6px;
+ }
+}
+
/* Source: theme/presets/article.css */
body {
font-family:
diff --git a/public/themes/github.css b/public/themes/github.css
index d5352a6..56f50f2 100644
--- a/public/themes/github.css
+++ b/public/themes/github.css
@@ -54,12 +54,40 @@
display: flex;
align-items: center;
gap: 8px;
+ flex-wrap: wrap;
+ justify-content: flex-end;
}
.export-container {
position: relative;
}
+.font-size-controls {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ margin: 0;
+ padding: 0;
+ border: 0;
+ min-inline-size: auto;
+}
+
+.font-size-btn {
+ min-width: 32px;
+ justify-content: center;
+}
+
+.font-size-icon {
+ width: 18px;
+ height: 18px;
+}
+
+.font-size-btn[disabled] {
+ cursor: default;
+ opacity: 0.35;
+ pointer-events: none;
+}
+
.export-option span {
flex: 1;
}
@@ -93,6 +121,14 @@
}
}
+@media (max-width: 640px) {
+ .top-actions {
+ top: 12px;
+ right: 12px;
+ gap: 6px;
+ }
+}
+
/* Source: theme/presets/github.css */
body {
font-family:
diff --git a/public/themes/matrix.css b/public/themes/matrix.css
index fbb8005..582b75d 100644
--- a/public/themes/matrix.css
+++ b/public/themes/matrix.css
@@ -27,12 +27,40 @@
display: flex;
align-items: center;
gap: 8px;
+ flex-wrap: wrap;
+ justify-content: flex-end;
}
.export-container {
position: relative;
}
+.font-size-controls {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ margin: 0;
+ padding: 0;
+ border: 0;
+ min-inline-size: auto;
+}
+
+.font-size-btn {
+ min-width: 32px;
+ justify-content: center;
+}
+
+.font-size-icon {
+ width: 18px;
+ height: 18px;
+}
+
+.font-size-btn[disabled] {
+ cursor: default;
+ opacity: 0.35;
+ pointer-events: none;
+}
+
.export-option span {
flex: 1;
}
@@ -66,6 +94,14 @@
}
}
+@media (max-width: 640px) {
+ .top-actions {
+ top: 12px;
+ right: 12px;
+ gap: 6px;
+ }
+}
+
/* Source: theme/presets/matrix.css */
body {
font-family: "Courier New", Courier, monospace;
diff --git a/public/themes/resume.css b/public/themes/resume.css
index 22440ca..01d25d8 100644
--- a/public/themes/resume.css
+++ b/public/themes/resume.css
@@ -25,12 +25,40 @@
display: flex;
align-items: center;
gap: 8px;
+ flex-wrap: wrap;
+ justify-content: flex-end;
}
.export-container {
position: relative;
}
+.font-size-controls {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ margin: 0;
+ padding: 0;
+ border: 0;
+ min-inline-size: auto;
+}
+
+.font-size-btn {
+ min-width: 32px;
+ justify-content: center;
+}
+
+.font-size-icon {
+ width: 18px;
+ height: 18px;
+}
+
+.font-size-btn[disabled] {
+ cursor: default;
+ opacity: 0.35;
+ pointer-events: none;
+}
+
.export-option span {
flex: 1;
}
@@ -64,6 +92,14 @@
}
}
+@media (max-width: 640px) {
+ .top-actions {
+ top: 12px;
+ right: 12px;
+ gap: 6px;
+ }
+}
+
/* Source: theme/presets/resume.css */
body {
font-family:
diff --git a/shared/templates/__tests__/view.template.spec.tsx b/shared/templates/__tests__/view.template.spec.tsx
index 2315370..1d1e24c 100644
--- a/shared/templates/__tests__/view.template.spec.tsx
+++ b/shared/templates/__tests__/view.template.spec.tsx
@@ -36,6 +36,24 @@ describe("ViewTemplate mermaid rendering", () => {
const githubHtml = `${}`;
const resumeHtml = `${}`;
+ expect(defaultHtml).toContain('id="font-size-decrease"');
+ expect(defaultHtml).toContain('id="font-size-increase"');
+ expect(githubHtml).toContain('id="font-size-decrease"');
+ expect(resumeHtml).toContain('id="font-size-increase"');
+ expect(defaultHtml).toContain("decreaseBtn.disabled = currentIndex === 0;");
+ expect(defaultHtml).toContain(
+ "increaseBtn.disabled = currentIndex === sizes.length - 1;",
+ );
+ expect(defaultHtml).toContain("window.getComputedStyle(content).fontSize");
+ expect(defaultHtml).toContain("let currentIndex = resolvedIndex;");
+ expect(defaultHtml).toContain("let hasUserSetFontSize = false;");
+ expect(defaultHtml).toContain("if (hasUserSetFontSize) {");
+ expect(defaultHtml).not.toContain('(e.metaKey || e.ctrlKey)');
+ expect(defaultHtml).not.toContain(
+ "content.style.fontSize = sizes[currentIndex];\n\t\t\tdecreaseBtn.disabled",
+ );
+ expect(defaultHtml).toContain('class="font-size-icon"');
+ expect(defaultHtml).toContain('d="m16 16 4 4"');
expect(defaultHtml).toContain('id="theme-toggle"');
expect(defaultHtml).toContain('id="toc-toggle"');
expect(githubHtml).toContain('id="theme-toggle"');
diff --git a/shared/templates/template-hash.generated.ts b/shared/templates/template-hash.generated.ts
index 7939303..7c4d80b 100644
--- a/shared/templates/template-hash.generated.ts
+++ b/shared/templates/template-hash.generated.ts
@@ -1,6 +1,6 @@
// This file is auto-generated by scripts/generate-template-hash.ts
// Do not edit manually. Run "pnpm generate:hash" to regenerate.
-export const TEMPLATE_HASH = "ae1aa";
+export const TEMPLATE_HASH = "0bdd6";
export function getTemplateHash(): string {
return TEMPLATE_HASH;
diff --git a/shared/templates/view.template.tsx b/shared/templates/view.template.tsx
index 3e26845..ba85fbf 100644
--- a/shared/templates/view.template.tsx
+++ b/shared/templates/view.template.tsx
@@ -35,23 +35,7 @@ function renderAlternateLinks(
);
}
-/**
- * Extract function body from a function, removing the function declaration
- * @param fn - Function to extract body from
- * @returns Function body as string
- */
-function getFunctionBody(fn: () => void): string {
- const fnStr = fn.toString();
- // Extract function body (content between first { and last })
- const match = fnStr.match(/\{([\s\S]*)\}/);
- return match ? match[1].trim() : fnStr;
-}
-
-/**
- * Initialize theme toggle functionality
- * This function will be stringified and inserted into the HTML template
- */
-function initThemeToggle(): void {
+const themeToggleScript = `
try {
const html = document.documentElement;
const storedTheme = localStorage.getItem("theme");
@@ -74,13 +58,9 @@ function initThemeToggle(): void {
} catch (e) {
console.error("Theme toggle failed", e);
}
-}
+`;
-/**
- * Initialize export actions (dropdown with copy and PDF export)
- * This function will be stringified and inserted into the HTML template
- */
-function initExportActions(): void {
+const exportActionsScript = `
try {
const exportBtn = document.getElementById("export-btn");
const dropdown = document.getElementById("export-dropdown");
@@ -90,20 +70,17 @@ function initExportActions(): void {
if (!exportBtn || !dropdown) return;
const markdown = exportBtn.dataset.markdown || "";
- let copyTimeout: ReturnType | null = null;
+ let copyTimeout = null;
- // Toggle dropdown
exportBtn.addEventListener("click", (e) => {
e.stopPropagation();
dropdown.classList.toggle("show");
});
- // Close dropdown when clicking outside
document.addEventListener("click", () => {
dropdown.classList.remove("show");
});
- // Copy markdown option
if (copyOption) {
copyOption.addEventListener("click", async (e) => {
e.stopPropagation();
@@ -135,7 +112,6 @@ function initExportActions(): void {
});
}
- // Export to PDF option
if (pdfOption) {
pdfOption.addEventListener("click", (e) => {
e.stopPropagation();
@@ -146,7 +122,72 @@ function initExportActions(): void {
} catch (e) {
console.error("Export actions initialization failed", e);
}
-}
+`;
+
+const fontSizeControlsScript = `
+ try {
+ const content = document.querySelector(".content");
+ const decreaseBtn = document.getElementById("font-size-decrease");
+ const increaseBtn = document.getElementById("font-size-increase");
+
+ if (!content || !decreaseBtn || !increaseBtn) return;
+
+ const sizes = [
+ "10px",
+ "12px",
+ "14px",
+ "15px",
+ "16px",
+ "17px",
+ "18px",
+ "19px",
+ "20px",
+ ];
+ const computedFontSize = Number.parseFloat(
+ window.getComputedStyle(content).fontSize,
+ );
+ const resolvedIndex = Number.isFinite(computedFontSize)
+ ? sizes.reduce((closestIndex, size, index) => {
+ const closestDistance = Math.abs(
+ Number.parseFloat(sizes[closestIndex]) - computedFontSize,
+ );
+ const currentDistance = Math.abs(
+ Number.parseFloat(size) - computedFontSize,
+ );
+ return currentDistance < closestDistance ? index : closestIndex;
+ }, 0)
+ : 4;
+ let currentIndex = resolvedIndex;
+ let hasUserSetFontSize = false;
+
+ function syncControls() {
+ decreaseBtn.disabled = currentIndex === 0;
+ increaseBtn.disabled = currentIndex === sizes.length - 1;
+ if (hasUserSetFontSize) {
+ content.style.fontSize = sizes[currentIndex];
+ }
+ }
+
+ function setFontSize(index) {
+ if (index < 0 || index >= sizes.length || index === currentIndex) return;
+ currentIndex = index;
+ hasUserSetFontSize = true;
+ syncControls();
+ }
+
+ decreaseBtn.addEventListener("click", () => {
+ setFontSize(currentIndex - 1);
+ });
+
+ increaseBtn.addEventListener("click", () => {
+ setFontSize(currentIndex + 1);
+ });
+
+ syncControls();
+ } catch (e) {
+ console.error("Font size control failed", e);
+ }
+`;
interface ThemeToggleButtonProps {
show: boolean;
@@ -279,6 +320,52 @@ const ExportButton = ({ markdown }: ExportButtonProps) => {
);
};
+const FontSizeControls = () => {
+ return (
+
+ );
+};
+
interface MetaTagsProps {
title: string;
description: string;
@@ -304,8 +391,6 @@ interface ScriptsProps {
}
const Scripts = ({ hasMermaid, hasToc }: ScriptsProps) => {
- const themeToggleBody = getFunctionBody(initThemeToggle);
- const exportActionsBody = getFunctionBody(initExportActions);
const tocScript = hasToc
? `
(function() {
@@ -411,41 +496,14 @@ const Scripts = ({ hasMermaid, hasToc }: ScriptsProps) => {
const mainScript = raw(`
(function() {
- ${themeToggleBody}
+ ${themeToggleScript}
})();
(function() {
- ${exportActionsBody}
+ ${exportActionsScript}
})();
${tocScript}
(function() {
- try {
- const content = document.querySelector(".content");
- if (!content) return;
-
- const SIZES = ["10px", "12px", "14px", "15px", "16px", "17px", "18px", "19px", "20px"];
- const DEFAULT_INDEX = 4;
- let currentIndex = DEFAULT_INDEX;
-
- function setFontSize(index) {
- if (index >= 0 && index < SIZES.length) {
- currentIndex = index;
- content.style.fontSize = SIZES[currentIndex];
- }
- }
-
- document.addEventListener("keydown", function(e) {
- if ((e.metaKey || e.ctrlKey) && (e.key === "=" || e.key === "+" || e.key === "-")) {
- e.preventDefault();
- if (e.key === "=" || e.key === "+") {
- setFontSize(currentIndex + 1);
- } else {
- setFontSize(currentIndex - 1);
- }
- }
- });
- } catch (e) {
- console.error("Font size control failed", e);
- }
+ ${fontSizeControlsScript}
})();
`);
@@ -598,6 +656,7 @@ export function ViewTemplate(options: CreateHtmlPageOptions) {
+