From e9fcd411a706a0a525eb39a99667841464a585ff Mon Sep 17 00:00:00 2001 From: xiaomai Date: Wed, 8 Apr 2026 14:39:39 +0800 Subject: [PATCH] feat(theme): add style switcher and centralize diff logic Extract duplicated diff rendering logic into shared/diff-page.js Implement theme switcher component across all templates --- .codex | 0 cyberpunk/index.html | 231 ++++++++-------------------------- exe/index.html | 240 ++++++++--------------------------- index.html | 230 ++++++--------------------------- macos/index.html | 224 ++++++++------------------------- matrix/index.html | 231 ++++++++-------------------------- notion/index.html | 219 +++++++------------------------- shared/diff-page.js | 293 +++++++++++++++++++++++++++++++++++++++++++ synthwave/index.html | 245 ++++++++---------------------------- 9 files changed, 642 insertions(+), 1271 deletions(-) create mode 100644 .codex create mode 100644 shared/diff-page.js diff --git a/.codex b/.codex new file mode 100644 index 0000000..e69de29 diff --git a/cyberpunk/index.html b/cyberpunk/index.html index 53492d4..a104924 100644 --- a/cyberpunk/index.html +++ b/cyberpunk/index.html @@ -83,6 +83,27 @@ .view-option.active { @apply text-cyber-cyan border-cyber-cyan bg-cyber-cyan/10 shadow-[inset_0_0_10px_rgba(0,240,255,0.2)]; } + .style-switcher { + @apply flex items-center gap-3; + } + .style-switcher-label { + @apply text-xs text-gray-500 tracking-widest; + } + .style-switcher-select { + background: #050505; + border: 1px solid #333; + color: #fcee0a; + padding: 0.25rem 0.75rem; + font-family: "Fira Code", monospace; + font-size: 0.75rem; + letter-spacing: 0.12em; + outline: none; + } + .style-switcher-select:hover, + .style-switcher-select:focus { + border-color: #00f0ff; + box-shadow: 0 0 12px rgba(0, 240, 255, 0.15); + } /* 面板边框发光效果 */ .cyber-panel { @@ -313,6 +334,8 @@ function initCyberware() { UNIFIED + +
@@ -351,184 +374,40 @@ function initCyberware() { - + diff --git a/exe/index.html b/exe/index.html index 9141553..a9e003e 100644 --- a/exe/index.html +++ b/exe/index.html @@ -72,6 +72,26 @@ .view-option.active { @apply border-t-win-borderDark border-l-win-borderDark border-b-win-borderLight border-r-win-borderLight pt-[5px] pb-[3px] pl-[17px] pr-[15px] bg-[url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAABZJREFUeNpi2rVrdz8DAwMDMwIGAAgwAEYQAWtO1y8gAAAAAElFTkSuQmCC')] /* 经典棋盘格背景 */; } + .style-switcher { + @apply flex items-center gap-2; + } + .style-switcher-label { + @apply text-lg; + } + .style-switcher-select { + background: #ffffff; + border: 2px solid; + border-top-color: #000000; + border-left-color: #000000; + border-bottom-color: #ffffff; + border-right-color: #ffffff; + color: #000000; + cursor: pointer; + font-family: "VT323", monospace; + font-size: 1.125rem; + outline: none; + padding: 0.125rem 0.5rem; + } /* JS 动态插入的空状态提示 */ .empty-message { @apply p-8 text-center font-retro text-xl text-gray-600 bg-white; @@ -269,6 +289,8 @@ function hello() { UNIFIED + +
@@ -312,196 +334,38 @@ function hello() { - + diff --git a/index.html b/index.html index b30b2d1..107d067 100644 --- a/index.html +++ b/index.html @@ -43,6 +43,15 @@ .view-option.active { @apply bg-white text-blue-700 shadow-sm font-semibold; } + .style-switcher { + @apply flex items-center gap-2 bg-slate-100 px-4 py-1.5 rounded-full; + } + .style-switcher-label { + @apply text-sm font-medium text-slate-700; + } + .style-switcher-select { + @apply bg-white border border-slate-300 rounded-full px-3 py-1 text-sm font-medium text-slate-800 outline-none cursor-pointer shadow-sm hover:border-blue-500 transition-colors; + } /* JS 动态插入的空状态提示 */ .empty-message { @apply p-10 text-center text-slate-500 bg-slate-50/50; @@ -197,6 +206,8 @@ function hello() { 📋 行内 + +
@@ -254,197 +265,38 @@ function hello() { - + diff --git a/macos/index.html b/macos/index.html index 1c86199..29f78d7 100644 --- a/macos/index.html +++ b/macos/index.html @@ -81,6 +81,21 @@ .view-option.active { @apply bg-white/80 text-blue-600 shadow-sm border border-white/80; } + .style-switcher { + @apply flex items-center gap-2 bg-white/40 p-1.5 rounded-xl border border-white/50; + } + .style-switcher-label { + @apply text-sm font-medium text-slate-500 pl-2; + } + .style-switcher-select { + background: transparent; + border: 0; + color: #334155; + font-size: 0.875rem; + font-weight: 500; + outline: none; + padding: 0.25rem 0.75rem 0.25rem 0.25rem; + } } /* ========================================== @@ -338,6 +353,8 @@ function hello() { Unified + +
@@ -416,183 +433,40 @@ function hello() { - + diff --git a/matrix/index.html b/matrix/index.html index a0f24b5..446ca0c 100644 --- a/matrix/index.html +++ b/matrix/index.html @@ -75,6 +75,26 @@ .view-option.active { @apply text-black bg-matrix-green shadow-[0_0_10px_#00FF41]; } + .style-switcher { + @apply flex items-center gap-2; + } + .style-switcher-label { + @apply text-sm tracking-widest text-matrix-darkGreen; + } + .style-switcher-select { + background: #000; + border: 1px solid #008f11; + color: #00ff41; + padding: 0.25rem 0.75rem; + font-family: "Share Tech Mono", monospace; + font-size: 0.875rem; + outline: none; + } + .style-switcher-select:hover, + .style-switcher-select:focus { + border-color: #00ff41; + box-shadow: 0 0 10px #00ff41; + } } /* ========================================== @@ -325,6 +345,8 @@ function enterMatrix() { UNIFIED + +
@@ -418,185 +440,40 @@ function enterMatrix() { }); - + diff --git a/notion/index.html b/notion/index.html index 1bc7c8e..36683e9 100644 --- a/notion/index.html +++ b/notion/index.html @@ -92,6 +92,15 @@ .view-option.active { @apply text-notion-text font-medium bg-notion-hover; } + .style-switcher { + @apply flex items-center gap-2; + } + .style-switcher-label { + @apply text-sm text-notion-gray; + } + .style-switcher-select { + @apply bg-transparent hover:bg-notion-hover px-2 py-1 rounded-[4px] text-sm text-notion-text outline-none cursor-pointer transition-colors appearance-none; + } /* Notion 代码块输入框 */ .notion-input-wrapper { @@ -279,6 +288,10 @@ function hello() { Unified view + + + +
@@ -330,184 +343,38 @@ function hello() { - + diff --git a/shared/diff-page.js b/shared/diff-page.js new file mode 100644 index 0000000..5baba89 --- /dev/null +++ b/shared/diff-page.js @@ -0,0 +1,293 @@ +(function (global) { + "use strict"; + + const DEFAULT_THEMES = [ + { id: "light", label: "轻量", path: "index.html" }, + { id: "cyberpunk", label: "赛博", path: "cyberpunk/index.html" }, + { id: "macos", label: "macOS", path: "macos/index.html" }, + { id: "matrix", label: "Matrix", path: "matrix/index.html" }, + { id: "notion", label: "Notion", path: "notion/index.html" }, + { id: "synthwave", label: "蒸汽波", path: "synthwave/index.html" }, + { id: "exe", label: "EXE", path: "exe/index.html" }, + ]; + + function resolveThemeUrl(targetPath, currentPath) { + try { + const url = new URL(global.location.href); + if (url.pathname.endsWith(currentPath)) { + url.pathname = + url.pathname.slice(0, -currentPath.length) + targetPath; + return url.toString(); + } + return new URL(targetPath, global.location.href).toString(); + } catch (error) { + return targetPath; + } + } + + function initStyleSwitcher(config) { + const host = document.querySelector(config.hostSelector || "[data-style-switcher]"); + if (!host) { + return; + } + + const themes = config.themes || DEFAULT_THEMES; + const wrapper = document.createElement("div"); + wrapper.className = "style-switcher"; + + const label = document.createElement("label"); + label.className = "style-switcher-label"; + label.textContent = config.switcherLabel || "风格"; + + const select = document.createElement("select"); + select.className = "style-switcher-select"; + select.setAttribute( + "aria-label", + config.switcherAriaLabel || "切换界面风格", + ); + + themes.forEach((theme) => { + const option = document.createElement("option"); + option.value = resolveThemeUrl(theme.path, config.currentThemePath); + option.textContent = theme.label; + option.selected = theme.id === config.themeId; + select.appendChild(option); + }); + + select.addEventListener("change", () => { + global.location.href = select.value; + }); + + wrapper.appendChild(label); + wrapper.appendChild(select); + host.replaceChildren(wrapper); + } + + function getElement(id) { + return document.getElementById(id); + } + + function initDiffPage(config) { + const leftTextarea = getElement(config.ids?.leftTextarea || "leftTextarea"); + const rightTextarea = getElement( + config.ids?.rightTextarea || "rightTextarea", + ); + const compareBtn = getElement(config.ids?.compareBtn || "compareBtn"); + const swapBtn = getElement(config.ids?.swapBtn || "swapBtn"); + const exampleBtn = getElement(config.ids?.exampleBtn || "exampleBtn"); + const clearLeftBtn = getElement(config.ids?.clearLeftBtn || "clearLeftBtn"); + const clearRightBtn = getElement( + config.ids?.clearRightBtn || "clearRightBtn", + ); + const languageSelect = getElement( + config.ids?.languageSelect || "languageSelect", + ); + const diffOutput = getElement(config.ids?.diffOutput || "diff-output"); + const diffStats = getElement(config.ids?.diffStats || "diffStats"); + const viewOptions = document.querySelectorAll( + config.selectors?.viewOptions || ".view-option", + ); + + if ( + !leftTextarea || + !rightTextarea || + !compareBtn || + !swapBtn || + !exampleBtn || + !clearLeftBtn || + !clearRightBtn || + !languageSelect || + !diffOutput || + !diffStats || + viewOptions.length === 0 + ) { + throw new Error("Diff page initialization failed: missing required DOM nodes."); + } + + const messages = config.messages || {}; + const example = config.example || {}; + const normalizeLanguage = + config.normalizeLanguage || ((language) => language); + const formatStats = + config.formatStats || + ((result) => { + if (result.identical) { + return "No changes"; + } + return `+${result.added} / -${result.deleted}`; + }); + + let currentView = + config.initialView || + Array.from(viewOptions).find((button) => button.classList.contains("active")) + ?.getAttribute("data-view") || + "side-by-side"; + + function setOutput(html, statsHtml) { + diffOutput.innerHTML = html; + diffStats.innerHTML = statsHtml || ""; + } + + function getSelectedLanguage() { + return normalizeLanguage(languageSelect.value); + } + + function getHighlightConfig() { + const language = getSelectedLanguage(); + if (language === "plaintext") { + return false; + } + return { enabled: true, language: language }; + } + + function generateUnifiedDiff(original, modified) { + return Diff.createTwoFilesPatch( + config.fileLabels?.left || "Original", + config.fileLabels?.right || "Modified", + original || "", + modified || "", + "", + "", + { context: config.contextLines || 4 }, + ); + } + + function applyHighlighting() { + const selectedLanguage = getSelectedLanguage(); + if (selectedLanguage === "plaintext" || !global.hljs) { + return; + } + + const codeBlocks = diffOutput.querySelectorAll("code"); + codeBlocks.forEach((block) => { + block.classList.forEach((className) => { + if (className.startsWith("language-")) { + block.classList.remove(className); + } + }); + block.classList.add("language-" + selectedLanguage); + delete block.dataset.highlighted; + try { + global.hljs.highlightElement(block); + } catch (error) {} + }); + } + + function renderDiff() { + let diffString = ""; + try { + diffString = generateUnifiedDiff(leftTextarea.value, rightTextarea.value); + } catch (error) { + setOutput( + typeof messages.generateError === "function" + ? messages.generateError(error) + : "
Diff generation failed.
", + messages.generateErrorStats || "", + ); + return; + } + + let diffHtml = ""; + try { + diffHtml = Diff2Html.html(diffString, { + drawFileList: false, + matching: "lines", + outputFormat: currentView, + highlight: getHighlightConfig(), + renderNothingWhenEmpty: false, + }); + } catch (error) { + setOutput( + typeof messages.renderError === "function" + ? messages.renderError(error) + : "
Diff rendering failed.
", + messages.renderErrorStats || "", + ); + return; + } + + diffOutput.innerHTML = diffHtml; + applyHighlighting(); + + const added = diffOutput.querySelectorAll(".d2h-ins").length; + const deleted = diffOutput.querySelectorAll(".d2h-del").length; + const identical = added === 0 && deleted === 0; + + diffStats.innerHTML = formatStats({ + added: added, + deleted: deleted, + identical: identical, + }); + + if (!diffHtml.trim() || diffOutput.innerText.trim() === "") { + setOutput(messages.blankResult || "
Files are identical.
", messages.blankStats || ""); + } + } + + function setActiveView(view, shouldRender) { + currentView = view; + viewOptions.forEach((button) => { + button.classList.toggle( + "active", + button.getAttribute("data-view") === view, + ); + }); + + if (shouldRender !== false) { + renderDiff(); + } + } + + function loadExample() { + leftTextarea.value = example.left || ""; + rightTextarea.value = example.right || ""; + if (example.language) { + languageSelect.value = example.language; + } + renderDiff(); + } + + function swapTexts() { + const currentLeft = leftTextarea.value; + leftTextarea.value = rightTextarea.value; + rightTextarea.value = currentLeft; + renderDiff(); + } + + function clearLeft() { + leftTextarea.value = ""; + renderDiff(); + } + + function clearRight() { + rightTextarea.value = ""; + renderDiff(); + } + + compareBtn.addEventListener("click", renderDiff); + swapBtn.addEventListener("click", swapTexts); + exampleBtn.addEventListener("click", loadExample); + clearLeftBtn.addEventListener("click", clearLeft); + clearRightBtn.addEventListener("click", clearRight); + languageSelect.addEventListener("change", renderDiff); + + viewOptions.forEach((button) => { + button.addEventListener("click", (event) => { + setActiveView(event.currentTarget.getAttribute("data-view")); + }); + }); + + document.addEventListener("keydown", (event) => { + if ((event.ctrlKey || event.metaKey) && event.key === "Enter") { + event.preventDefault(); + renderDiff(); + } + }); + + initStyleSwitcher(config); + setActiveView(currentView, false); + renderDiff(); + } + + global.initDiffPage = initDiffPage; +})(window); diff --git a/synthwave/index.html b/synthwave/index.html index 9c481de..c27230b 100644 --- a/synthwave/index.html +++ b/synthwave/index.html @@ -23,11 +23,7 @@ @@ -90,6 +86,26 @@ Space+Mono:ital,wght@0,400;0,700;1,400&display=swap" .view-option.active { @apply text-vapor-bg bg-vapor-purple shadow-[0_0_15px_#b967ff]; } + .style-switcher { + @apply flex items-center gap-3; + } + .style-switcher-label { + @apply font-vapor text-sm tracking-widest text-vapor-yellow drop-shadow-[0_0_5px_#fffb96]; + } + .style-switcher-select { + background: #120458; + border: 2px solid #b967ff; + color: #01cdfe; + padding: 0.375rem 0.75rem; + font-family: "Righteous", cursive; + font-size: 0.875rem; + outline: none; + } + .style-switcher-select:hover, + .style-switcher-select:focus { + border-color: #01cdfe; + box-shadow: 0 0 10px #01cdfe; + } } /* ========================================== @@ -377,6 +393,8 @@ function playSynthwave() { MONO + +
@@ -425,193 +443,40 @@ function playSynthwave() { - +