主题与字号从页面顶栏搬进设置页的「外观」,顶部导航加上预设入口。

顶栏那套汉堡菜单在每页都要单独挂载、样式也重复,收进设置页后只剩一处改;顺手把「整组都没有曾用型号」的配置组整组收起,不再留空卡片。
This commit is contained in:
2026-10-26 01:09:43 +08:00
parent f3303038fe
commit e2413b0677
11 changed files with 251 additions and 462 deletions
+10
View File
@@ -45,3 +45,13 @@ describe("forgetApiProfileUsage", () => {
expect(forgetApiProfileUsage("missing")).toBe(false);
});
});
describe("groups with no used model", () => {
it("shows nothing, so the settings page hides the whole group", () => {
recordApiProfileUsage("used");
const members = [{ id: "unused-a" }, { id: "unused-b" }];
expect(filterUsedApiProfiles(members, loadApiProfileUsage(), null)).toEqual(
[],
);
});
});
+2 -55
View File
@@ -23,17 +23,7 @@ import {
isModuleOpeningWaiting,
renderQuestionsCard,
} from "./questions-ui.js";
import {
applyColorTheme,
bumpUiFontScale,
getStoredColorTheme,
getStoredUiFontScale,
initColorTheme,
initUiFontScale,
themeById,
UI_FONT_SCALE_MAX,
UI_FONT_SCALE_MIN,
} from "./theme.js";
import { initColorTheme, initUiFontScale } from "./theme.js";
import {
applyMarkdownRender,
initMarkdownRender,
@@ -1366,29 +1356,6 @@ function syncMarkdownMenuUi() {
});
}
function syncThemeMenuUi() {
const colorId = document.documentElement.getAttribute("data-color-theme") || getStoredColorTheme();
const percent = getStoredUiFontScale();
const colorMeta = themeById(colorId);
const sum = $("theme-menu-sum");
if (sum) {
sum.title = `${colorMeta.name} · 字号 ${percent}`;
if (!sum.querySelector(".hamburger-icon")) {
sum.innerHTML = `<span class="hamburger-icon" aria-hidden="true"></span>`;
}
}
document.querySelectorAll("[data-color-theme].more-theme-item").forEach((btn) => {
btn.classList.toggle("is-active", btn.getAttribute("data-color-theme") === colorMeta.id);
});
const valueEl = $("theme-font-scale-value");
if (valueEl) valueEl.textContent = String(percent);
const minus = document.querySelector("[data-font-scale-bump='-1']");
const plus = document.querySelector("[data-font-scale-bump='1']");
if (minus) minus.disabled = percent <= UI_FONT_SCALE_MIN;
if (plus) plus.disabled = percent >= UI_FONT_SCALE_MAX;
syncMarkdownMenuUi();
}
function stashComposerDraftFromDom() {
if (skipComposerStash) {
skipComposerStash = false;
@@ -4598,26 +4565,6 @@ $("btn-save-instance")?.addEventListener("click", () => {
void saveCurrentInstance();
});
$("theme-menu")?.addEventListener("click", (e) => {
const menu = $("theme-menu");
if (!menu) return;
const colorBtn = e.target.closest("[data-color-theme]");
if (colorBtn && menu.contains(colorBtn)) {
e.preventDefault();
applyColorTheme(colorBtn.getAttribute("data-color-theme"));
syncThemeMenuUi();
menu.removeAttribute("open");
return;
}
const bumpBtn = e.target.closest("[data-font-scale-bump]");
if (bumpBtn && menu.contains(bumpBtn)) {
e.preventDefault();
e.stopPropagation();
bumpUiFontScale(bumpBtn.getAttribute("data-font-scale-bump"));
syncThemeMenuUi();
}
});
$("composer")?.addEventListener("click", (e) => {
const menu = e.target.closest("#composer-more-menu");
if (!menu) return;
@@ -4764,7 +4711,7 @@ async function init() {
initUiFontScale();
initMarkdownRender();
initPlayAuxLlmBubbles();
syncThemeMenuUi();
syncMarkdownMenuUi();
try {
await Promise.all([loadBooks(), loadPersonas(), loadApiProfiles()]);
if (books.length) {
+1 -31
View File
@@ -42,6 +42,7 @@
<a href="/modules.html">技能</a>
<a href="/libraries.html">库</a>
<a href="/stats.html">Token</a>
<a href="/presets.html">预设</a>
<a href="/settings.html">设置</a>
</nav>
<div class="main-head-right">
@@ -54,37 +55,6 @@
<button type="button" class="btn-sm main-head-action" id="btn-export" disabled>
导出
</button>
<details class="more-menu theme-menu" id="theme-menu">
<summary class="btn-sm more-menu-sum" id="theme-menu-sum" title="主题">
<span class="hamburger-icon" aria-hidden="true"></span>
</summary>
<div class="more-menu-panel" role="menu">
<div class="more-menu-label">主题</div>
<button type="button" class="more-menu-item more-theme-item" data-color-theme="paper" role="menuitem">
<span class="more-theme-icon" aria-hidden="true">📖</span>
<span class="more-theme-copy"><span class="more-theme-name">仿纸暖黄</span><span class="more-theme-desc">经典护眼</span></span>
</button>
<button type="button" class="more-menu-item more-theme-item" data-color-theme="modern" role="menuitem">
<span class="more-theme-icon" aria-hidden="true">✨</span>
<span class="more-theme-copy"><span class="more-theme-name">现代清爽</span><span class="more-theme-desc">干净明快</span></span>
</button>
<button type="button" class="more-menu-item more-theme-item" data-color-theme="dark" role="menuitem">
<span class="more-theme-icon" aria-hidden="true">🌙</span>
<span class="more-theme-copy"><span class="more-theme-name">夜间暗色</span><span class="more-theme-desc">黑底灰字</span></span>
</button>
<button type="button" class="more-menu-item more-theme-item" data-color-theme="green" role="menuitem">
<span class="more-theme-icon" aria-hidden="true">🍃</span>
<span class="more-theme-copy"><span class="more-theme-name">豆沙绿</span><span class="more-theme-desc">自然疗愈</span></span>
</button>
<div class="more-menu-sep" role="separator"></div>
<div class="more-menu-label">字号</div>
<div class="more-font-scale" role="group" aria-label="字号">
<button type="button" class="more-font-btn" data-font-scale-bump="-1" title="缩小" aria-label="缩小">−</button>
<span class="more-font-value" id="theme-font-scale-value">100</span>
<button type="button" class="more-font-btn" data-font-scale-bump="1" title="放大" aria-label="放大">+</button>
</div>
</div>
</details>
</div>
</header>
</div>
+1 -1
View File
@@ -23,6 +23,7 @@
<a href="/modules.html">技能</a>
<a href="/libraries.html" aria-current="page">库</a>
<a href="/stats.html">Token</a>
<a href="/presets.html">预设</a>
<a href="/settings.html">设置</a>
</nav>
</header>
@@ -127,4 +128,3 @@
<script type="module" src="/libraries.js"></script>
</body>
</html>
+1
View File
@@ -32,6 +32,7 @@
<a href="/modules.html" aria-current="page">技能</a>
<a href="/libraries.html">库</a>
<a href="/stats.html">Token</a>
<a href="/presets.html">预设</a>
<a href="/settings.html">设置</a>
</nav>
</header>
+89 -201
View File
@@ -165,194 +165,6 @@ code {
font-weight: 600;
}
.st-top-actions {
display: flex;
align-items: center;
justify-content: flex-end;
min-width: 0;
}
.st-theme-menu {
position: relative;
}
.st-theme-summary {
width: 32px;
height: 32px;
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid var(--st-border-strong);
border-radius: 8px;
background: var(--st-surface);
color: var(--st-muted);
cursor: pointer;
list-style: none;
}
.st-theme-summary::-webkit-details-marker {
display: none;
}
.st-theme-summary:hover,
.st-theme-menu[open] .st-theme-summary {
color: var(--st-text);
border-color: var(--st-accent);
background: var(--st-accent-soft);
}
.st-theme-summary:focus-visible,
.st-theme-item:focus-visible,
.st-font-scale button:focus-visible {
outline: 2px solid color-mix(in srgb, var(--st-accent) 70%, transparent);
outline-offset: 2px;
}
.st-hamburger,
.st-hamburger::before,
.st-hamburger::after {
width: 16px;
height: 1.5px;
display: block;
border-radius: 2px;
background: currentColor;
}
.st-hamburger {
position: relative;
}
.st-hamburger::before,
.st-hamburger::after {
content: "";
position: absolute;
left: 0;
}
.st-hamburger::before {
top: -5px;
}
.st-hamburger::after {
top: 5px;
}
.st-theme-panel {
position: absolute;
top: calc(100% + 8px);
right: 0;
z-index: 100;
width: 258px;
padding: 10px;
border: 1px solid var(--st-border-strong);
border-radius: 12px;
background: var(--st-surface);
color: var(--st-text);
box-shadow: 0 12px 32px color-mix(in srgb, var(--st-text) 12%, transparent);
}
.st-theme-label {
padding: 4px 10px 7px;
color: var(--st-muted);
font-size: 12px;
font-weight: 600;
}
.st-theme-item {
width: 100%;
min-height: 54px;
display: grid;
grid-template-columns: 26px minmax(0, 1fr);
align-items: center;
gap: 10px;
padding: 7px 10px;
border: 0;
border-radius: 8px;
background: transparent;
color: var(--st-text);
text-align: left;
cursor: pointer;
}
.st-theme-item:hover {
background: color-mix(in srgb, var(--st-surface) 58%, var(--st-bg));
}
.st-theme-item.is-active {
background: var(--st-accent-soft);
color: var(--st-accent);
}
.st-theme-icon {
font-size: 18px;
line-height: 1;
text-align: center;
}
.st-theme-copy {
display: flex;
flex-direction: column;
min-width: 0;
line-height: 1.35;
}
.st-theme-name {
font-size: 14px;
font-weight: 650;
}
.st-theme-desc {
color: var(--st-muted);
font-size: 12px;
}
.st-theme-item.is-active .st-theme-desc {
color: color-mix(in srgb, var(--st-accent) 72%, var(--st-muted));
}
.st-theme-separator {
height: 1px;
margin: 8px 4px;
background: var(--st-border);
}
.st-font-scale {
display: grid;
grid-template-columns: 38px minmax(0, 1fr) 38px;
align-items: center;
gap: 8px;
padding: 0 10px 2px;
}
.st-font-scale button {
height: 38px;
border: 1px solid var(--st-border-strong);
border-radius: 8px;
background: var(--st-surface);
color: var(--st-text);
cursor: pointer;
font-size: 18px;
line-height: 1;
}
.st-font-scale button:hover:not(:disabled) {
border-color: var(--st-accent);
background: var(--st-accent-soft);
}
.st-font-scale button:disabled {
cursor: not-allowed;
opacity: 0.4;
}
.st-font-value {
color: var(--st-text);
font-size: 16px;
font-variant-numeric: tabular-nums;
font-weight: 600;
text-align: center;
}
/* —— Shell —— */
.st-shell {
display: grid;
@@ -804,14 +616,6 @@ code {
margin-top: 4px;
}
.profile-group-empty {
margin: 0;
padding: 12px 16px 14px;
font-size: 12px;
color: var(--st-muted);
border-top: 1px solid var(--st-border);
}
@media (max-width: 640px) {
.profile-model-row {
flex-direction: column;
@@ -1583,6 +1387,95 @@ code {
margin-top: 16px;
}
/* —— Appearance (设置 · 外观) —— */
.appearance-themes {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 10px;
}
.appearance-theme {
display: grid;
grid-template-columns: 26px minmax(0, 1fr);
align-items: center;
gap: 10px;
padding: 9px 12px;
border: 1px solid var(--st-border-strong);
border-radius: var(--st-radius);
background: var(--st-surface);
color: var(--st-text);
font: inherit;
text-align: left;
cursor: pointer;
}
.appearance-theme:hover {
border-color: var(--st-accent);
background: color-mix(in srgb, var(--st-surface) 62%, var(--st-bg));
}
.appearance-theme.is-active {
border-color: var(--st-accent);
background: var(--st-accent-soft);
color: var(--st-accent);
}
.appearance-theme:focus-visible,
.appearance-scale button:focus-visible {
outline: 2px solid color-mix(in srgb, var(--st-accent) 70%, transparent);
outline-offset: 2px;
}
.appearance-theme-icon {
font-size: 18px;
line-height: 1;
text-align: center;
}
.appearance-theme-copy {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
line-height: 1.35;
}
.appearance-theme-name {
font-size: 14px;
font-weight: 650;
}
.appearance-theme-desc {
font-size: 12px;
color: var(--st-muted);
}
.appearance-theme.is-active .appearance-theme-desc {
color: color-mix(in srgb, var(--st-accent) 70%, var(--st-muted));
}
.appearance-scale {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
}
.appearance-scale button {
min-width: 44px;
font-size: 15px;
line-height: 1;
}
.appearance-scale-value {
min-width: 4.5em;
padding: 0 2px;
font-size: 15px;
font-variant-numeric: tabular-nums;
font-weight: 600;
text-align: center;
}
/* —— Dialog —— */
.profile-dialog {
border: none;
@@ -1895,11 +1788,6 @@ code {
scrollbar-width: thin;
}
.st-top-actions {
grid-column: 2;
grid-row: 1;
}
.st-nav a {
flex: 0 0 auto;
}
+27
View File
@@ -22,6 +22,7 @@
<a href="/modules.html">技能</a>
<a href="/libraries.html">库</a>
<a href="/stats.html">Token</a>
<a href="/presets.html">预设</a>
<a href="/settings.html" aria-current="page">设置</a>
</nav>
</header>
@@ -35,6 +36,7 @@
<button type="button" class="st-rail-item" data-section="preset">预设</button>
<button type="button" class="st-rail-item" data-section="storage">上下文</button>
<button type="button" class="st-rail-item" data-section="chat">直聊</button>
<button type="button" class="st-rail-item" data-section="appearance">外观</button>
<p class="st-rail-note">保存在本机 <code>~/.writing-agent/</code>,明文、不同步。</p>
</aside>
@@ -230,6 +232,31 @@
</div>
</div>
</section>
<section class="settings-section hidden" id="section-appearance">
<div class="settings-card storage-card">
<h3>主题</h3>
<p class="settings-help">
只改这台浏览器看到的配色,跟着本机走,不写进作品。点一张即生效。
</p>
<div class="appearance-themes" id="appearance-themes" role="radiogroup" aria-label="主题"></div>
</div>
<div class="settings-card storage-card">
<h3>字号</h3>
<p class="settings-help">
整体缩放界面文字,100% 为默认;范围 80–150%,步进 5%。
</p>
<div class="appearance-scale" role="group" aria-label="字号">
<button type="button" class="btn-secondary" id="appearance-scale-minus" aria-label="缩小字号">−</button>
<output class="appearance-scale-value" id="appearance-scale-value" aria-live="polite">100%</output>
<button type="button" class="btn-secondary" id="appearance-scale-plus" aria-label="放大字号">+</button>
<button type="button" class="btn-secondary" id="appearance-scale-reset">恢复默认</button>
</div>
<p class="settings-help muted">
同一浏览器的其他标签会自己跟上;回到别的页面刷新即生效。
</p>
</div>
</section>
</div>
</main>
</div>
+112 -14
View File
@@ -9,6 +9,17 @@ import {
sortByApiProfileUsage,
} from "./api-profile-usage.js";
import { readSniffModels, writeSniffModels } from "./model-sniff-cache.js";
import {
applyColorTheme,
applyUiFontScale,
bumpUiFontScale,
COLOR_THEMES,
getStoredColorTheme,
getStoredUiFontScale,
UI_FONT_SCALE_DEFAULT,
UI_FONT_SCALE_MAX,
UI_FONT_SCALE_MIN,
} from "./theme.js";
let state = {
settings: { activeProfileId: null, activePresetId: null },
@@ -29,6 +40,8 @@ let sniffBootId = "";
let sniffRestoreSeq = 0;
/** 组 id → 是否展开显示该组全部配置(默认只显示曾选用);有搜索词时按匹配显示 */
const showAllProfilesByGroup = new Set();
/** 上一轮渲染里因为一个曾用型号都没有而整组收起的组 id */
let lastHiddenGroupIds = [];
let profileSearchQuery = "";
let editingPersonaId = null;
let activeSection = "api";
@@ -59,6 +72,12 @@ const sectionPreferenceEl = document.getElementById("section-preference");
const sectionPresetEl = document.getElementById("section-preset");
const sectionStorageEl = document.getElementById("section-storage");
const sectionChatEl = document.getElementById("section-chat");
const sectionAppearanceEl = document.getElementById("section-appearance");
const appearanceThemesEl = document.getElementById("appearance-themes");
const appearanceScaleValueEl = document.getElementById("appearance-scale-value");
const appearanceScaleMinusEl = document.getElementById("appearance-scale-minus");
const appearanceScalePlusEl = document.getElementById("appearance-scale-plus");
const appearanceScaleResetEl = document.getElementById("appearance-scale-reset");
const preferenceCollectEveryEl = document.getElementById("preference-collect-every");
const activeSettingsBarEl = document.getElementById("active-settings-bar");
const settingsToastEl = document.getElementById("settings-toast");
@@ -126,6 +145,35 @@ function showToast(message, isError = false) {
}, 4000);
}
/** 外观(主题/字号)只存在本机浏览器里,不走服务端设置。 */
const APPEARANCE_STORAGE_KEYS = ["wa-color-theme", "wa-ui-font-scale"];
function renderAppearanceThemes() {
if (!appearanceThemesEl || appearanceThemesEl.childElementCount) return;
appearanceThemesEl.innerHTML = COLOR_THEMES.map(
(theme) => `<button type="button" class="appearance-theme" data-appearance-theme="${theme.id}" role="radio" aria-checked="false">
<span class="appearance-theme-icon" aria-hidden="true">${theme.icon}</span>
<span class="appearance-theme-copy">
<span class="appearance-theme-name">${escapeHtml(theme.name)}</span>
<span class="appearance-theme-desc">${escapeHtml(theme.description)}</span>
</span>
</button>`,
).join("");
}
function syncAppearanceUi() {
const activeTheme = getStoredColorTheme();
appearanceThemesEl?.querySelectorAll("[data-appearance-theme]").forEach((btn) => {
const on = btn.getAttribute("data-appearance-theme") === activeTheme;
btn.classList.toggle("is-active", on);
btn.setAttribute("aria-checked", String(on));
});
const percent = getStoredUiFontScale();
if (appearanceScaleValueEl) appearanceScaleValueEl.textContent = `${percent}%`;
if (appearanceScaleMinusEl) appearanceScaleMinusEl.disabled = percent <= UI_FONT_SCALE_MIN;
if (appearanceScalePlusEl) appearanceScalePlusEl.disabled = percent >= UI_FONT_SCALE_MAX;
}
function renderActiveBar() {
const profile = state.profiles.find(
(p) => p.id === state.settings.activeProfileId,
@@ -159,6 +207,8 @@ function switchSection(section) {
sectionPresetEl.classList.toggle("hidden", section !== "preset");
sectionStorageEl?.classList.toggle("hidden", section !== "storage");
sectionChatEl?.classList.toggle("hidden", section !== "chat");
sectionAppearanceEl?.classList.toggle("hidden", section !== "appearance");
if (section === "appearance") syncAppearanceUi();
renderPanelHeader();
}
@@ -197,6 +247,9 @@ function renderPanelHeader() {
btn.textContent = "导入 JSON";
btn.addEventListener("click", () => presetFileEl.click());
panelActionsEl.appendChild(btn);
} else if (activeSection === "appearance") {
panelTitleEl.textContent = "外观";
panelSubtitleEl.textContent = "主题与字号,只影响这台浏览器";
} else if (activeSection === "chat") {
panelTitleEl.textContent = "直聊";
panelSubtitleEl.textContent = "卡片拖动的跟手倍率";
@@ -338,6 +391,7 @@ function renderProfiles() {
? state.groups
: [{ id: "", name: "未分组", baseUrl: "" }];
const seen = new Set();
const hiddenGroupIds = [];
let shownGroups = 0;
for (const group of groups) {
@@ -352,6 +406,11 @@ function renderProfiles() {
activeId,
);
if (query && !visible.length) continue;
// 默认「只看曾用」时,整组都没有曾用型号就整组收起,不留空卡片。
if (!showAll && !visible.length) {
hiddenGroupIds.push(group.id);
continue;
}
const hasActive = members.some((p) => p.id === activeId);
const sample = members[0];
@@ -409,19 +468,10 @@ function renderProfiles() {
</div>
</div>`;
if (!visible.length) {
const empty = document.createElement("p");
empty.className = "profile-group-empty";
empty.textContent = query
? "没有匹配的模型"
: "本组还没有曾选用过的型号。点「显示全部」,或「添加型号」。";
card.appendChild(empty);
} else {
const rows = document.createElement("div");
rows.className = "profile-model-rows";
for (const p of visible) rows.appendChild(renderModelRow(p));
card.appendChild(rows);
}
const rows = document.createElement("div");
rows.className = "profile-model-rows";
for (const p of visible) rows.appendChild(renderModelRow(p));
card.appendChild(rows);
profilesListEl.appendChild(card);
shownGroups += 1;
@@ -452,7 +502,17 @@ function renderProfiles() {
shownGroups += 1;
}
if (!shownGroups) {
lastHiddenGroupIds = hiddenGroupIds;
// 一个曾用型号都没有的组整组收起后,留一行提示,需要时还能展开。
if (hiddenGroupIds.length) {
const hint = document.createElement("p");
hint.className = "empty-hint profile-hidden-groups";
hint.innerHTML = `已自动隐藏 ${hiddenGroupIds.length} 个还没有曾用型号的组
<button type="button" class="profile-group-toggle" data-action="reveal-hidden-groups">显示这些组</button>`;
profilesListEl.appendChild(hint);
}
if (!shownGroups && !hiddenGroupIds.length) {
profilesListEl.innerHTML = query
? `<p class="empty-hint">没有匹配「${escapeHtml(profileSearchQuery.trim())}」的配置。</p>`
: '<p class="empty-hint">暂无配置,点击右上角「新增配置」。</p>';
@@ -1525,6 +1585,13 @@ profilesListEl.addEventListener("click", async (e) => {
const action = btn.dataset.action;
try {
if (action === "reveal-hidden-groups") {
for (const hiddenId of lastHiddenGroupIds) {
showAllProfilesByGroup.add(hiddenId);
}
renderProfiles();
return;
}
if (action === "toggle-group-all") {
if (showAllProfilesByGroup.has(id)) showAllProfilesByGroup.delete(id);
else showAllProfilesByGroup.add(id);
@@ -1924,6 +1991,37 @@ document.getElementById("btn-clear-context")?.addEventListener("click", async ()
}
});
appearanceThemesEl?.addEventListener("click", (e) => {
const btn = e.target.closest("[data-appearance-theme]");
if (!btn) return;
applyColorTheme(btn.getAttribute("data-appearance-theme"));
syncAppearanceUi();
});
appearanceScaleMinusEl?.addEventListener("click", () => {
bumpUiFontScale(-1);
syncAppearanceUi();
});
appearanceScalePlusEl?.addEventListener("click", () => {
bumpUiFontScale(1);
syncAppearanceUi();
});
appearanceScaleResetEl?.addEventListener("click", () => {
applyColorTheme("modern");
applyUiFontScale(UI_FONT_SCALE_DEFAULT);
syncAppearanceUi();
showToast("外观已恢复默认");
});
window.addEventListener("storage", (e) => {
if (APPEARANCE_STORAGE_KEYS.includes(e.key)) syncAppearanceUi();
});
renderAppearanceThemes();
syncAppearanceUi();
renderPanelHeader();
loadAll().catch((err) => {
showToast(`加载设置失败: ${err.message}`, true);
+1
View File
@@ -23,6 +23,7 @@
<a href="/modules.html">技能</a>
<a href="/libraries.html">库</a>
<a href="/stats.html" aria-current="page">Token</a>
<a href="/presets.html">预设</a>
<a href="/settings.html">设置</a>
</nav>
</header>
-44
View File
@@ -3241,50 +3241,6 @@ body[data-lifecycle="play"] .msg-play-aux .msg-body {
letter-spacing: 0.02em;
color: var(--muted);
}
.more-font-scale {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 10px 8px;
}
.more-font-btn {
box-sizing: border-box;
width: 28px;
height: 28px;
padding: 0;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--surface);
color: var(--text);
font: inherit;
font-size: 16px;
line-height: 1;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
}
.more-font-btn:hover:not(:disabled) {
background: var(--bg);
border-color: var(--accent);
color: var(--accent);
}
.more-font-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.more-font-btn:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 1px;
}
.more-font-value {
min-width: 2.5em;
text-align: center;
font-size: 13px;
font-variant-numeric: tabular-nums;
font-weight: 600;
color: var(--text);
}
.more-menu-item {
display: block;
width: 100%;
+7 -116
View File
@@ -1,13 +1,6 @@
/* Apply the conversation workspace theme before first paint, then mount the
* same theme controls in every secondary-page top bar. */
/* Apply the workspace theme + UI font scale before first paint.
* The controls live in 设置 → 外观 (see settings.js); pages only read storage. */
(function () {
var themes = [
{ id: "paper", icon: "📖", name: "仿纸暖黄", description: "经典护眼" },
{ id: "modern", icon: "✨", name: "现代清爽", description: "干净明快" },
{ id: "dark", icon: "🌙", name: "夜间暗色", description: "黑底灰字" },
{ id: "green", icon: "🍃", name: "豆沙绿", description: "自然疗愈" },
];
function normalizeTheme(value) {
return /^(paper|modern|dark|green)$/.test(value || "") ? value : "modern";
}
@@ -24,9 +17,6 @@
var next = normalizeTheme(theme);
document.documentElement.setAttribute("data-color-theme", next);
document.documentElement.setAttribute("data-theme", next);
try {
localStorage.setItem("wa-color-theme", next);
} catch (_) {}
return next;
}
@@ -34,9 +24,6 @@
var next = normalizeScale(scale);
document.documentElement.setAttribute("data-ui-font-scale", String(next));
document.documentElement.style.setProperty("--ui-font-scale", String(next / 100));
try {
localStorage.setItem("wa-ui-font-scale", String(next));
} catch (_) {}
return next;
}
@@ -47,105 +34,9 @@
/* Storage may be unavailable; CSS defaults remain usable. */
}
function mountThemeMenu() {
var top = document.querySelector(".st-top");
if (!top || top.querySelector(".st-theme-menu")) return;
var actions = document.createElement("div");
actions.className = "st-top-actions";
actions.innerHTML =
'<details class="st-theme-menu">' +
'<summary class="st-theme-summary" title="主题与字号" aria-label="主题与字号">' +
'<span class="st-hamburger" aria-hidden="true"></span>' +
"</summary>" +
'<div class="st-theme-panel">' +
'<div class="st-theme-label">主题</div>' +
themes
.map(function (item) {
return (
'<button type="button" class="st-theme-item" data-st-theme="' +
item.id +
'">' +
'<span class="st-theme-icon" aria-hidden="true">' +
item.icon +
"</span>" +
'<span class="st-theme-copy"><span class="st-theme-name">' +
item.name +
'</span><span class="st-theme-desc">' +
item.description +
"</span></span>" +
"</button>"
);
})
.join("") +
'<div class="st-theme-separator"></div>' +
'<div class="st-theme-label">字号</div>' +
'<div class="st-font-scale" role="group" aria-label="字号">' +
'<button type="button" data-st-scale="-1" aria-label="缩小字号">−</button>' +
'<span class="st-font-value" aria-live="polite"></span>' +
'<button type="button" data-st-scale="1" aria-label="放大字号">+</button>' +
"</div></div></details>";
top.appendChild(actions);
var menu = actions.querySelector(".st-theme-menu");
var value = actions.querySelector(".st-font-value");
function sync() {
var active = normalizeTheme(
document.documentElement.getAttribute("data-color-theme"),
);
var scale = normalizeScale(
document.documentElement.getAttribute("data-ui-font-scale"),
);
actions.querySelectorAll("[data-st-theme]").forEach(function (button) {
var selected = button.getAttribute("data-st-theme") === active;
button.classList.toggle("is-active", selected);
button.setAttribute("aria-pressed", String(selected));
});
value.textContent = String(scale);
var minus = actions.querySelector('[data-st-scale="-1"]');
var plus = actions.querySelector('[data-st-scale="1"]');
minus.disabled = scale <= 80;
plus.disabled = scale >= 150;
}
actions.addEventListener("click", function (event) {
var themeButton = event.target.closest("[data-st-theme]");
if (themeButton) {
applyTheme(themeButton.getAttribute("data-st-theme"));
sync();
return;
}
var scaleButton = event.target.closest("[data-st-scale]");
if (scaleButton) {
var current = normalizeScale(
document.documentElement.getAttribute("data-ui-font-scale"),
);
applyScale(current + Number(scaleButton.getAttribute("data-st-scale")) * 5);
sync();
}
});
document.addEventListener("click", function (event) {
if (menu.open && !menu.contains(event.target)) menu.removeAttribute("open");
});
document.addEventListener("keydown", function (event) {
if (event.key === "Escape" && menu.open) {
menu.removeAttribute("open");
menu.querySelector("summary").focus();
}
});
window.addEventListener("storage", function (event) {
if (event.key === "wa-color-theme") applyTheme(event.newValue);
if (event.key === "wa-ui-font-scale") applyScale(event.newValue);
sync();
});
sync();
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", mountThemeMenu, { once: true });
} else {
mountThemeMenu();
}
/* Keep tabs in sync when 设置 · 外观 changes the stored values. */
window.addEventListener("storage", function (event) {
if (event.key === "wa-color-theme") applyTheme(event.newValue);
if (event.key === "wa-ui-font-scale") applyScale(event.newValue);
});
})();