diff --git a/calliope-web/src/lib/components/shell/ThemeChoices.svelte b/calliope-web/src/lib/components/shell/ThemeChoices.svelte
index abd152e6..e61bc4b2 100644
--- a/calliope-web/src/lib/components/shell/ThemeChoices.svelte
+++ b/calliope-web/src/lib/components/shell/ThemeChoices.svelte
@@ -57,10 +57,14 @@
>
{item.name}{#if tiles && item.id === "night"}
- 默认{/if}{tiles ? item.description.split(" · ")[0] : item.english}{#if !tiles}{item.description}{/if}{tiles && item.id === "night"
+ ? item.name + " "
+ : item.name}{#if tiles && item.id === "night"}默认{/if}{tiles ? item.tone : item.english}{#if !tiles}{item.description}{/if}
{$theme === item.id ? "✓" : ""} {
+ try {
+ const p = decodeURIComponent(new URL(req.url, "http://local").pathname);
+ if (p.startsWith("/api/")) {
+ apiLog.push(`${req.method} ${p}`);
+ let body = [];
+ if (p === "/api/settings")
+ body = {
+ queue_concurrency: 2,
+ queue_poll_timeout_sec: 1800,
+ comfyui_base_url: "http://127.0.0.1:8188",
+ projects_root: "D:\\AIGC\\projects",
+ dry_run: false,
+ };
+ else if (
+ p === "/api/projects" ||
+ p === "/api/workflows" ||
+ p === "/api/library/media" ||
+ p === "/api/agent/sessions" ||
+ p === "/api/agent/agents" ||
+ p === "/api/canvas" ||
+ p.startsWith("/api/tasks") ||
+ p.startsWith("/api/models")
+ )
+ body = [];
+ else if (p === "/api/jobs/queue-status") body = { paused: false };
+ res.setHeader("Content-Type", "application/json");
+ res.end(JSON.stringify(body));
+ return;
+ }
+ if (p.startsWith("/sample")) {
+ const bytes = await fs.readFile(SAMPLE);
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
+ res.end(bytes);
+ return;
+ }
+ let f = path.join(BUILD, p);
+ if (!path.extname(f)) f = path.join(BUILD, "index.html");
+ const bytes = await fs.readFile(f);
+ res.setHeader(
+ "Content-Type",
+ f.endsWith(".js")
+ ? "text/javascript"
+ : f.endsWith(".css")
+ ? "text/css"
+ : f.endsWith(".html")
+ ? "text/html"
+ : "application/octet-stream",
+ );
+ res.end(bytes);
+ } catch {
+ res.writeHead(404);
+ res.end();
+ }
+});
+await new Promise((r) => (server.listing = server.listen(0, "127.0.0.1", r)));
+const base = `http://127.0.0.1:${server.address().port}`;
+console.log("serving at", base);
+
+const browser = await chromium.launch({ headless: true, args: ["--no-sandbox"] });
+
+async function freshContext(width) {
+ const ctx = await browser.newContext({
+ viewport: { width, height: width === 1440 ? 900 : width === 1366 ? 768 : 720 },
+ timezoneId: "Asia/Shanghai",
+ deviceScaleFactor: 1,
+ });
+ await ctx.addInitScript(() => {
+ localStorage.setItem("aigc-lang", "zh");
+ localStorage.setItem("aigc-reduce-motion", "1");
+ window.__sse = [];
+ window.EventSource = class {
+ constructor() {
+ window.__sse.push(this);
+ setTimeout(() => this.onopen?.(), 0);
+ }
+ addEventListener(t, f) {
+ (this.handlers ??= {})[t] = f;
+ }
+ close() {}
+ };
+ });
+ await ctx.route("**/*", async (route) => {
+ const u = new URL(route.request().url());
+ if (["data:", "blob:", "file:"].includes(u.protocol)) return route.continue();
+ if (u.origin !== base) return route.abort();
+ route.continue();
+ });
+ return ctx;
+}
+
+/** Shared DOM measurement: given a card element handle's root, measure all internals. */
+const MEASURE = (cardSel) => {
+ const card = document.querySelector(cardSel);
+ if (!card) return null;
+ const num = (v) => parseFloat(v) || 0;
+ const r = (el) => {
+ const b = el.getBoundingClientRect();
+ return { x: +b.x.toFixed(3), y: +b.y.toFixed(3), w: +b.width.toFixed(3), h: +b.height.toFixed(3) };
+ };
+ const st = (el) => {
+ const s = getComputedStyle(el);
+ return {
+ rect: r(el),
+ background: s.backgroundColor,
+ backgroundImage: s.backgroundImage,
+ borderTopWidth: s.borderTopWidth,
+ borderRightWidth: s.borderRightWidth,
+ borderBottomWidth: s.borderBottomWidth,
+ borderLeftWidth: s.borderLeftWidth,
+ borderTopColor: s.borderTopColor,
+ borderRightColor: s.borderRightColor,
+ borderBottomColor: s.borderBottomColor,
+ borderLeftColor: s.borderLeftColor,
+ borderTopLeftRadius: s.borderTopLeftRadius,
+ borderTopRightRadius: s.borderTopRightRadius,
+ borderBottomLeftRadius: s.borderBottomLeftRadius,
+ borderBottomRightRadius: s.borderBottomRightRadius,
+ boxShadow: s.boxShadow,
+ paddingTop: s.paddingTop,
+ paddingRight: s.paddingRight,
+ paddingBottom: s.paddingBottom,
+ paddingLeft: s.paddingLeft,
+ opacity: s.opacity,
+ color: s.color,
+ overflow: s.overflow,
+ position: s.position,
+ };
+ };
+ const cardS = st(card);
+ const tag = card.querySelector(".theme-default, .tag");
+ const prev = card.querySelector(".preview") || card.querySelector(".tprev");
+ const rail = prev?.querySelector(".rail") || prev?.querySelector(".r");
+ const list = prev?.querySelector(".list") || prev?.querySelector(".l");
+ const items = list ? [...list.querySelectorAll("i")] : [];
+ const stage = prev?.querySelector(".stage") || prev?.querySelector(".m");
+ const art = stage?.querySelector("i") || stage?.querySelector(".art");
+ const dock = stage?.querySelector("b") || stage?.querySelector(".dk");
+ const badge = dock?.querySelector("em") || dock?.querySelector("span");
+ const textLine = prev?.querySelector(".preview-text") || prev?.querySelector(".tx");
+ const strong = card.querySelector(".copy strong") || card.querySelector("b");
+ const small = card.querySelector(".copy small") || card.querySelector("small");
+ const dot = rail ? getComputedStyle(rail, "::before") : null;
+ return {
+ card: cardS,
+ defaultTag: tag ? st(tag) : null,
+ cardClass: card.className,
+ ariaChecked: card.getAttribute("aria-checked") ?? card.getAttribute("aria-pressed"),
+ dataTheme: prev?.getAttribute("data-theme"),
+ preview: prev ? st(prev) : null,
+ rail: rail ? st(rail) : null,
+ railDot: dot
+ ? { w: dot.width, h: dot.height, bg: dot.backgroundColor, mt: dot.marginTop, mr: dot.marginRight, ml: dot.marginLeft, mb: dot.marginBottom, radius: dot.borderRadius }
+ : null,
+ list: list ? st(list) : null,
+ listItemCount: items.length,
+ listItems: items.map((i) => st(i)),
+ stage: stage ? st(stage) : null,
+ art: art ? st(art) : null,
+ dock: dock ? st(dock) : null,
+ badge: badge ? st(badge) : null,
+ textLine: textLine ? st(textLine) : null,
+ copyStrong: strong
+ ? { text: strong.textContent.trim().replace(/\s+/g, " "), fontSize: getComputedStyle(strong).fontSize, lineHeight: getComputedStyle(strong).lineHeight, fontWeight: getComputedStyle(strong).fontWeight }
+ : null,
+ copySmall: small
+ ? { text: small.textContent.trim(), fontSize: getComputedStyle(small).fontSize, lineHeight: getComputedStyle(small).lineHeight, color: getComputedStyle(small).color, fontFamily: getComputedStyle(small).fontFamily.slice(0, 40) }
+ : null,
+ };
+};
+
+const results = { groups: [], errors: [], apiCalls: [] };
+try {
+ for (const width of VIEWPORTS) {
+ const ctx = await freshContext(width);
+ const app = await ctx.newPage(), ref = await ctx.newPage();
+ for (const p of [app, ref]) p.on("pageerror", e => results.errors.push(e.message));
+ await app.goto(`${base}/settings`);
+ await app.locator(".choices.tiles").waitFor();
+ await ref.goto(`${base}/sample`);
+ await ref.evaluate(() => { go("settings"); document.documentElement.classList.add("capture", "reduce"); });
+ for (const theme of THEMES) {
+ await app.evaluate(t => { localStorage.setItem("aigc-theme", t); window.dispatchEvent(new StorageEvent("storage", {key:"aigc-theme", newValue:t})); }, theme);
+ await ref.evaluate(t => setTheme(t), theme);
+ await app.mouse.move(1200, 600); await ref.mouse.move(1200, 600);
+ await app.waitForTimeout(350);
+ const key = `${theme}-${width}`;
+ await app.screenshot({path:path.join(OUT, `${key}-full-app.png`)});
+ await ref.screenshot({path:path.join(OUT, `${key}-full-sample.png`)});
+ for (let i=0;i<4;i++) {
+ const a = `.choices.tiles .choice:nth-child(${i+1})`, b = `#themeTiles .ttile:nth-child(${i+1})`;
+ for (const state of ["normal", "selected", "hover"]) {
+ // Hold page theme fixed to test every card/state. Class changes are fixture-only;
+ // natural click/selection/persistence are exercised by verify-hel769.py separately.
+ await app.locator(a).evaluate((e,on)=>e.classList.toggle("selected",on), state === "selected");
+ await ref.locator(b).evaluate((e,on)=>e.classList.toggle("on",on), state === "selected");
+ await app.mouse.move(1200,600); await ref.mouse.move(1200,600);
+ if (state === "hover") { await app.locator(a).hover(); await ref.locator(b).hover(); }
+ await app.waitForTimeout(300);
+ const name = `${key}-card${i}-${state}`;
+ results.groups.push({name, theme, width, card:i, state, app:await app.evaluate(MEASURE,a), sample:await ref.evaluate(MEASURE,b)});
+ await app.locator(a).screenshot({path:path.join(OUT,`${name}-app.png`)});
+ await ref.locator(b).screenshot({path:path.join(OUT,`${name}-sample.png`)});
+ }
+ await app.locator(a).evaluate((e,on)=>e.classList.toggle("selected",on), THEMES[i]===theme);
+ await ref.locator(b).evaluate((e,on)=>e.classList.toggle("on",on), THEMES[i]===theme);
+ }
+ }
+ await ctx.close();
+ }
+} finally {
+ results.apiCalls = [...new Set(apiLog)];
+ await fs.writeFile(path.join(OUT,"measurements.json"), JSON.stringify(results,null,2));
+ await browser.close();
+ await new Promise(r=>server.close(r));
+}
+if(results.errors.length || results.groups.length !== 144) throw Error("Incomplete matrix or page errors");
+console.log(`Collected ${results.groups.length} card/state pairs; zero page errors; browser and server closed.`);