From 2de29eec096fe886cced18c4ea6ea0d603d6b1b5 Mon Sep 17 00:00:00 2001 From: hhhjin Date: Mon, 6 Apr 2026 17:47:00 +0900 Subject: [PATCH 1/2] feat: add font size controls to view template --- .gitignore | 4 +- public/themes/default.css | 36 ++++ public/themes/github.css | 36 ++++ public/themes/matrix.css | 36 ++++ public/themes/resume.css | 36 ++++ .../__tests__/view.template.spec.tsx | 15 ++ shared/templates/template-hash.generated.ts | 2 +- shared/templates/view.template.tsx | 181 ++++++++++++------ shared/themes/source/theme/base.css | 36 ++++ 9 files changed, 319 insertions(+), 63 deletions(-) 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..b0f0d86 100644 --- a/shared/templates/__tests__/view.template.spec.tsx +++ b/shared/templates/__tests__/view.template.spec.tsx @@ -36,6 +36,21 @@ 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("let hasUserSetFontSize = false;"); + expect(defaultHtml).toContain("if (hasUserSetFontSize) {"); + 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..73c3165 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 = "90d6d"; export function getTemplateHash(): string { return TEMPLATE_HASH; diff --git a/shared/templates/view.template.tsx b/shared/templates/view.template.tsx index 3e26845..40f8943 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", + ]; + let currentIndex = 4; + 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); + }); + + document.addEventListener("keydown", (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); + } + } + }); + + 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) {
+ Date: Mon, 6 Apr 2026 17:52:26 +0900 Subject: [PATCH 2/2] fix: align font size controls with theme defaults --- .../__tests__/view.template.spec.tsx | 3 ++ shared/templates/template-hash.generated.ts | 2 +- shared/templates/view.template.tsx | 30 +++++++++---------- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/shared/templates/__tests__/view.template.spec.tsx b/shared/templates/__tests__/view.template.spec.tsx index b0f0d86..1d1e24c 100644 --- a/shared/templates/__tests__/view.template.spec.tsx +++ b/shared/templates/__tests__/view.template.spec.tsx @@ -44,8 +44,11 @@ describe("ViewTemplate mermaid rendering", () => { 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", ); diff --git a/shared/templates/template-hash.generated.ts b/shared/templates/template-hash.generated.ts index 73c3165..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 = "90d6d"; +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 40f8943..ba85fbf 100644 --- a/shared/templates/view.template.tsx +++ b/shared/templates/view.template.tsx @@ -143,7 +143,21 @@ const fontSizeControlsScript = ` "19px", "20px", ]; - let currentIndex = 4; + 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() { @@ -169,20 +183,6 @@ const fontSizeControlsScript = ` setFontSize(currentIndex + 1); }); - document.addEventListener("keydown", (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); - } - } - }); - syncControls(); } catch (e) { console.error("Font size control failed", e);