2
0

fix: keep native account overlays opaque

This commit is contained in:
Leon
2026-10-20 12:37:36 +08:00
parent 7d1d3742e5
commit 5d145272b7
5 changed files with 156 additions and 5 deletions

View File

@@ -34,3 +34,9 @@ DSH Web:可用 `dsh plugin --profile web add /绝对路径/测试包.tgz` 安
## Issue / PR 收尾 ## Issue / PR 收尾
已确认可收尾的 #2、#3、#6 关闭。#8、#12、#14、#15 与 PR #10、#13 在用户验证前保持开放;PR #9 留待后续处理。 已确认可收尾的 #2、#3、#6 关闭。#8、#12、#14、#15 与 PR #10、#13 在用户验证前保持开放;PR #9 留待后续处理。
## 后续:DSH 0.2.1-rc.2 账号页面
已核对[官方固定发布源码](https://github.com/deepseek-ai/deepseek-harness/blob/687f282e8625c6b8d11ff58211a90524be2e4317/packages/client/ui-settings-account/src/client/PlatformOverlay.module.css)及对应 npm 产物:账号用量/充值页覆盖层仍继承背景 token,48px 返回栏没有独立底色。皮肤新增局部规则,仅为 body 直属、带原生拖拽返回栏和 viewport 的模态覆盖层恢复配色背景。
验收:分别在浅色和黑金档打开用量/充值页,确认返回栏与加载状态背景不透明、返回按钮清晰可用;普通弹窗、菜单和聊天背景保持原行为。停用皮肤后恢复宿主背景。自动化只使用宿主结构夹具,不登录账号,也不替代原生 Desktop 验收。

View File

@@ -255,6 +255,13 @@ body[data-liang-skin="on"]:is(
top: var(--dsh-desktop-frame-height, 0px); top: var(--dsh-desktop-frame-height, 0px);
} }
/* Native account pages are full-screen body portals. Their header and loading
surface must stay opaque when the chat background token is transparent. */
body[data-liang-skin="on"] > [role="dialog"][aria-modal="true"][class*="_overlay"]:has(> header[data-window-drag] button[class*="_back"]):has(> [class*="_viewport"]) {
--dsw-alias-bg-base: var(--liang-page, #fff) !important;
background: var(--liang-page, #fff) !important;
}
/* Windows paints the sidebar fill across the host frame, covering the portrait. /* Windows paints the sidebar fill across the host frame, covering the portrait.
Match macOS's transparent frame while keeping the sidebar, caption, and Match macOS's transparent frame while keeping the sidebar, caption, and
conversation surfaces painted by their own host rules. */ conversation surfaces painted by their own host rules. */

File diff suppressed because one or more lines are too long

View File

@@ -11,7 +11,11 @@ const projectRoot = fileURLToPath(new URL("../", import.meta.url));
const packageId = "@leon___/dsh-client-liang-intensity-skin"; const packageId = "@leon___/dsh-client-liang-intensity-skin";
const viewport = { width: 1100, height: 700 }; const viewport = { width: 1100, height: 700 };
const css = await readFile(new URL("../src/client/skin.css", import.meta.url), "utf8"); const css = await readFile(new URL("../src/client/skin.css", import.meta.url), "utf8");
const compiled = await build({ absWorkingDir: projectRoot, entryPoints: ["src/client/appearance.ts"], const compiled = await build({ absWorkingDir: projectRoot,
stdin: { resolveDir: projectRoot, contents: `
export { installLiangAppearanceButton } from './src/client/appearance.ts';
export { paletteForFrame, PREVIEW_MAX_FRAME } from './src/client/logic.ts';
` },
bundle: true, write: false, format: "iife", globalName: "LiangAppearance", platform: "browser", target: "es2022" }); bundle: true, write: false, format: "iife", globalName: "LiangAppearance", platform: "browser", target: "es2022" });
async function chromeExecutable() { async function chromeExecutable() {
@@ -232,6 +236,131 @@ async function appearanceChecks(page, hash) {
assert.equal(await page.locator(".liang-appearance-choice, .liang-appearance-binding").count(), 0); assert.equal(await page.locator(".liang-appearance-choice, .liang-appearance-binding").count(), 0);
} }
// Account settings are a body portal, not a generic modal. Match the audited
// Host structure while using different build hashes and multiple class names.
async function overlayChecks(context) {
let scenarios = 0;
for (const hash of ["oldAccount42", "_44HXVa"]) {
const { page, errors } = await prepare(context, { enabled: false });
await page.evaluate(hash => {
const style = document.createElement("style");
style.textContent = `
body { --dsw-alias-label-primary: rgb(34 38 42); --dsw-alias-label-secondary: rgb(75 81 87);
--dsw-alias-border-l1: rgb(180 184 188); --dsw-radius-sm: 4px; }
[data-account-host-fixture] { position: fixed; inset: 0; z-index: 1001;
display: flex; flex-direction: column; background: var(--dsw-alias-bg-base); }
html[data-windows-titlebar] [data-account-host-fixture] { padding-top: var(--dsh-windows-titlebar-height); }
[data-account-host-fixture] > header { -webkit-app-region: initial; box-sizing: border-box;
border-bottom: .5px solid var(--dsw-alias-border-l1); flex: none; height: 48px; padding: 6px 16px; }
[data-account-host-fixture] .controls { display: flex; align-items: center; gap: 24px; padding-top: 5px; }
[data-account-host-fixture] button { box-sizing: border-box; border-radius: var(--dsw-radius-sm);
height: 28px; color: var(--dsw-alias-label-secondary); font: inherit; white-space: nowrap;
cursor: pointer; background: transparent; border: none; justify-content: center; align-items: center;
gap: 8px; padding: 0 6px; font-size: 14px; line-height: 22px; display: inline-flex; }
[data-account-host-fixture] > [class*="_viewport"] { min-height: 0;
color: var(--dsw-alias-label-secondary); flex: 1; position: relative; }
[data-native-overlay-sentinel] { position: fixed; left: -2000px; width: 100px;
height: 60px; --dsw-alias-bg-base: rgb(201 183 165);
background: rgb(201 183 165); color: rgb(35 47 59); }
[data-native-overlay-sentinel] > header { background: rgb(190 170 150); }
#nested-account { inset: auto auto 0 -2000px; width: 100px; height: 60px;
--dsw-alias-bg-base: rgb(210 190 170); --dsw-alias-label-secondary: rgb(35 47 59); }
`;
document.querySelector("#liang-styles").before(style);
const accountMarkup = (id, nested = false) => `
<div id="${id}" data-account-host-fixture role="dialog" aria-modal="true"
class="native-screen ${hash}_overlay extra-overlay">
<header data-window-drag class="${hash}_header extra-header">
<div class="controls ${hash}_controls">
<button id="${nested ? "nested-back" : "account-back"}" type="button"
class="native-control ${hash}_back extra-back" aria-label="返回">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path d="M10 3 5 8l5 5" stroke="currentColor" stroke-width="2"/>
</svg><span>返回</span>
</button>
</div>
</header>
<div class="native-scroll ${hash}_viewport extra-viewport"></div>
</div>`;
document.querySelector("#root").insertAdjacentHTML("beforeend", accountMarkup("nested-account", true));
document.body.insertAdjacentHTML("beforeend", `
<div id="ordinary-modal" role="dialog" aria-modal="true" data-native-overlay-sentinel
class="native-modal ${hash}_dialog extra-dialog">
<header data-window-drag><button class="${hash}_back">Back</button></header>
<div class="${hash}_viewport"></div>
</div>
<div id="ordinary-menu" role="menu" data-native-overlay-sentinel
class="native-menu ${hash}_overlay extra-menu"><button role="menuitem">Item</button></div>
${accountMarkup("account-overlay")}`);
window.accountBackClicks = 0;
document.querySelector("#account-back").addEventListener("click", () => window.accountBackClicks++);
window.overlayFixture = {
applyPalette(frame) {
const palette = window.LiangAppearance.paletteForFrame(frame);
const mapping = { page: "page", base: "bg-base", layer1: "layer-1", layer2: "layer-2",
layer3: "layer-3", sidebar: "sidebar", ink: "ink", secondary: "secondary",
tertiary: "tertiary", border: "border", accent: "accent", accentHover: "accent-hover",
hover: "hover", portraitOpacity: "portrait-opacity" };
for (const [key, variable] of Object.entries(mapping)) {
document.body.style.setProperty(`--liang-${variable}`, palette[key]);
}
document.body.dataset.liangStage = String(palette.stage);
return palette;
},
snapshot() {
const read = element => {
const computed = getComputedStyle(element);
return { background: computed.backgroundColor, color: computed.color,
base: computed.getPropertyValue("--dsw-alias-bg-base").trim() };
};
const overlay = document.querySelector("#account-overlay");
return { overlay: read(overlay), header: read(overlay.querySelector("header")),
back: read(overlay.querySelector("button")), viewport: read(overlay.querySelector('[class*="_viewport"]')),
sentinels: [...document.querySelectorAll('#ordinary-modal, #ordinary-modal > header, #ordinary-menu, #nested-account, #nested-account > header, #nested-back, #nested-account > [class*="_viewport"]')].map(read) };
},
};
}, hash);
const maxFrame = await page.evaluate(() => window.LiangAppearance.PREVIEW_MAX_FRAME);
for (const { name, frame, stage } of [
{ name: "light", frame: 0, stage: 0 },
{ name: "black-gold", frame: maxFrame, stage: 5 },
]) {
await page.evaluate(() => window.fixture.setEnabled(false));
const palette = await page.evaluate(frame => window.overlayFixture.applyPalette(frame), frame);
assert.equal(palette.stage, stage, "Use the actual internal maximum frame for black-gold");
const native = await page.evaluate(() => window.overlayFixture.snapshot());
await page.evaluate(() => window.fixture.setEnabled(true));
const active = await page.evaluate(() => window.overlayFixture.snapshot());
const expectedPage = `rgb(${palette.page.match(/\d+/g).join(", ")})`;
const expectedSecondary = `rgb(${palette.secondary.match(/\d+/g).join(", ")})`;
assert.equal(active.overlay.background, expectedPage, `${hash}/${name}: account overlay restores an opaque page surface`);
assert.equal(active.overlay.base, palette.page, `${hash}/${name}: background token is local to the account overlay`);
assert.equal(active.viewport.background, "rgba(0, 0, 0, 0)", "Keep the native transparent viewport");
assert.equal(active.viewport.base, palette.page, `${hash}/${name}: viewport inherits the locally restored base token`);
assert.equal(active.viewport.color, expectedSecondary, "Keep the native viewport secondary text color");
assert.equal(active.header.background, "rgba(0, 0, 0, 0)", "Keep the native transparent header");
assert.equal(active.back.background, "rgba(0, 0, 0, 0)", "Keep the native transparent back button");
assert.equal(active.back.color, expectedSecondary, `${hash}/${name}: back icon and label use the native secondary text token`);
assert.equal(await page.locator("#account-overlay > header").evaluate(element => getComputedStyle(element).height), "48px");
assert.equal(await page.locator("#account-back").evaluate(element => getComputedStyle(element).height), "28px");
const pageRgb = palette.page.match(/\d+/g).map(Number);
closeColor(await pixelAt(page, 500, 20), pageRgb, `${hash}/${name}: header paints over the opaque account surface`);
closeColor(await pixelAt(page, 1070, 430), pageRgb, `${hash}/${name}: portrait does not bleed through the account viewport`);
assert.deepEqual(active.sentinels, native.sentinels, "Account styles must not affect ordinary modals, menus, or non-body portals");
const clicks = await page.evaluate(() => window.accountBackClicks);
await page.locator("#account-back").click();
assert.equal(await page.evaluate(() => window.accountBackClicks), clicks + 1, "Native back button remains clickable");
await page.evaluate(() => window.fixture.setEnabled(false));
assert.deepEqual(await page.evaluate(() => window.overlayFixture.snapshot()), native, "Disabling the skin restores every native overlay style");
scenarios++;
console.log(`PASS account overlay ${hash}/${name}: opaque portal, native header/back, ordinary modal/menu boundaries, skin-off restore`);
}
assert.deepEqual(errors, [], `${hash}: overlay browser errors`);
await page.close();
}
return scenarios;
}
const browser = await chromium.launch({ executablePath: await chromeExecutable(), headless: true }); const browser = await chromium.launch({ executablePath: await chromeExecutable(), headless: true });
try { try {
const context = await browser.newContext({ viewport, reducedMotion: "reduce", deviceScaleFactor: 1 }); const context = await browser.newContext({ viewport, reducedMotion: "reduce", deviceScaleFactor: 1 });
@@ -262,7 +391,9 @@ try {
assert.equal(await second.page.evaluate(() => window.fixture.snapshot().enabled), true); assert.equal(await second.page.evaluate(() => window.fixture.snapshot().enabled), true);
closeColor(await pixelAt(first.page, 1070, 430), [240, 244, 248], "First window skin off"); closeColor(await pixelAt(first.page, 1070, 430), [240, 244, 248], "First window skin off");
closeColor(await pixelAt(second.page, 1070, 430), [24, 156, 72], "Second window remains visible"); closeColor(await pixelAt(second.page, 1070, 430), [24, 156, 72], "Second window remains visible");
assert.deepEqual([...first.errors, ...second.errors], []); await context.close(); assert.deepEqual([...first.errors, ...second.errors], []);
const overlayScenarios = await overlayChecks(context);
await context.close();
console.log("PASS independent windows: host geometry and appearance state stay isolated"); console.log("PASS independent windows: host geometry and appearance state stay isolated");
console.log("Client compatibility browser checks passed (11 scenarios). Native Desktop validation remains manual."); console.log(`Client compatibility browser checks passed (${cases.length + 3 + overlayScenarios} scenarios). Native Desktop validation remains manual.`);
} finally { await browser.close(); } } finally { await browser.close(); }

View File

@@ -225,6 +225,13 @@ body[data-liang-skin="on"]:is(
top: var(--dsh-desktop-frame-height, 0px); top: var(--dsh-desktop-frame-height, 0px);
} }
/* Native account pages are full-screen body portals. Their header and loading
surface must stay opaque when the chat background token is transparent. */
body[data-liang-skin="on"] > [role="dialog"][aria-modal="true"][class*="_overlay"]:has(> header[data-window-drag] button[class*="_back"]):has(> [class*="_viewport"]) {
--dsw-alias-bg-base: var(--liang-page, #fff) !important;
background: var(--liang-page, #fff) !important;
}
/* Windows paints the sidebar fill across the host frame, covering the portrait. /* Windows paints the sidebar fill across the host frame, covering the portrait.
Match macOS's transparent frame while keeping the sidebar, caption, and Match macOS's transparent frame while keeping the sidebar, caption, and
conversation surfaces painted by their own host rules. */ conversation surfaces painted by their own host rules. */