主题与字号从页面顶栏搬进设置页的「外观」,顶部导航加上预设入口。
顶栏那套汉堡菜单在每页都要单独挂载、样式也重复,收进设置页后只剩一处改;顺手把「整组都没有曾用型号」的配置组整组收起,不再留空卡片。
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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);
|
||||
});
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user