diff --git a/docs/adr/0002-dsh-functional-surface-readability.md b/docs/adr/0002-dsh-functional-surface-readability.md index 5fc754c..4a8cbf8 100644 --- a/docs/adr/0002-dsh-functional-surface-readability.md +++ b/docs/adr/0002-dsh-functional-surface-readability.md @@ -51,6 +51,16 @@ Web 与官方 Desktop 的插件页共用 `readability.js`。以 `data-plugin-pan `plugin-readability.browser.test.mjs` 用真实语义结构和主题色板覆盖深浅主题、会话阶段、画窗、阴影/磨砂,验证局部不透明、文字对比度、异步状态、关闭/清除恢复、负边距、hover、键盘焦点及控制区域。`plugin-readability-pack.test.mjs` 验证 Web 与 native Desktop 鉴权路径均提供当前共享脚本,并比较独立包和桌面聚合包的 DSH runtime。浏览器 fixture 不替代真实 Web 和官方 Desktop 候选包验收;结果与截图见 [#92 验收记录](../verification/issue-92-plugin-readability.md)。 +### 补充:插件页改回透明底(#92 复审,2026-10-18) + +独立插件页的实色底板解决了 #92 的对比度问题,但代价是整页被涂成实心色块:用户实测该底板占插件页 73.4% 像素、5×5 局部标准差 0.00,而「自动化任务」等同级页面在同区域标准差为 0.22–1.32(透出壁纸),观感割裂,用户明确要求回到透明底。 + +因此改为:插件页不再进入不透明名单,继续使用 `client.js` 发布的半透明表面令牌(`--bc-surface-mix`、`--dsw-alias-bg-base:rgba(17,20,27,.10)` 等),壁纸直接透出。可读性改由两项不遮挡背景的辅助承担:页内说明文字与计数提升到次要文字色;页面文字加一层随主题取色的柔和光晕(`--bc-text-halo`,深色 `#000`、浅色 `#fff`,`0 1px 2px`、45% 透明度)。两项辅助同样受「功能面板不透明」开关与 `data-bc-active` 约束。角色面板(dialog/menu/tooltip/composer、提问/计划审核/授权确认)的保护不变,`--bc-readable-fill` 及其局部令牌仍只服务这些面板。 + +取舍必须写明并**明确接受**:透明底不再具备「任意壁纸上都 ≥4.5:1」的保证,这是几何上的必然——纯白壁纸上的浅色文字无法靠文字色或光晕达标。约束是**分主题**的、且互为镜像:深色主题(浅色文字)要求局部背景保持偏暗(L ≲ 0.10;实测 #555555 L≈0.091 时标题 7.13:1、正文 4.95:1);浅色主题(深色文字)要求局部背景保持偏亮(L ≳ 0.81;实测 #e8e8e8 L≈0.807 时标题 15.42:1、正文 4.73:1)。越界极值——深色主题遇纯白壁纸 1.05:1、浅色主题遇纯黑壁纸 1.11:1——由回归测试显式断言**失败**,把取舍钉在测试里而不是让门槛空过。参考页(自动化任务)实机为标题 5.31–5.61、次级文字 4.31–4.39;正文达标依赖上述色阶提升(不提升时深色 3.49:1、浅色 3.02:1)。若日后需要「任意壁纸都达标」的强保证,只能回到局部底板或引入用户可选的底板强度,光晕无法替代。 + +验证:`plugin-readability.browser.test.mjs` 改为断言透明(alpha 0)、光晕随主题取色并被文字继承、说明/计数提升一档、白黑壁纸下页面截图必须不同(壁纸确实透出)、hover 仍是宿主半透明反馈;`readability.test.mjs` 增加源码级守卫,禁止不透明名单重新出现 `[data-plugin-panel]`。数值证据见 [#92 验收记录](../verification/issue-92-plugin-readability.md)。 + ## 回滚 用户关闭开关即可回到原有透明效果。版本回滚可移除脚本的拼接、打包项与控制台入口;独立偏好不会影响旧版背景客户端。不会改写 DSH 安装文件或原有背景状态。 diff --git a/docs/verification/issue-92-plugin-readability.md b/docs/verification/issue-92-plugin-readability.md index b438bb4..596483a 100644 --- a/docs/verification/issue-92-plugin-readability.md +++ b/docs/verification/issue-92-plugin-readability.md @@ -59,3 +59,34 @@ Web 在两种主题下验证标题、官方组和已安装组底色分别为 `rg 提交 PR 前核对发现,当前 `origin/main`(7b23b3a)仍包含 #87 的两处误隐藏渐隐滚动内容规则及错误测试断言,尽管历史 PR #89 显示已合并。为避免后续从主分支打包回退已发布修复,本次恢复 PR #89 的最小变更:删除两处隐藏规则、移除要求隐藏行为的断言,并恢复普通背景/画窗模式连续 90 帧高度、滚动位置和 mask 稳定性回归。新增回归在修复前失败,直接复现普通背景高度被隐藏的问题;不修改已发布 tarball 或其他宿主模块。 PR CI 暴露新增聚合打包回归依赖提前构建其他宿主的产物,而 CI 在全量构建前运行测试。该测试已改为调用聚合打包接口默认的构建流程;临时移出 Cursor 的构建目录后,旧测试以同一 `ENOENT` 失败,修改后能自行重建并通过。没有调整 CI 顺序或产品构建协议。 + +## 修订:插件页改为透明底(2026-10-18) + +#93 的实色底板在真实使用中被判定为观感割裂(该底板占插件页 73.4% 像素、5×5 局部标准差 0.00,同级「自动化任务」页同区域为 0.22–1.32)。本次修订按用户要求改回透明底:`readability.js` 不再把 `[data-plugin-panel] …` 列入不透明名单,改用两项不遮挡背景的辅助(说明/计数提升一档、页面文字光晕),取舍与边界见 ADR 0002 的同日补充。 + +| 项目 | 结果 | +| --- | --- | +| 浏览器回归(真实 Edge,零依赖 CDP 等价复刻仓库内用例) | **7 组 / 131 条断言全绿**:透明(alpha 0)、对比度基准、壁纸透出(白黑壁纸逐像素)、异步状态、亮/暗/复杂/播放中视频、关闭与清除恢复、几何/hover/焦点/控制区域 | +| 对比度 | 分主题、分背景区间验收,见下节;深色主题在 L≈0.091 背景上标题 7.13:1 / 正文 4.95:1,浅色主题在 L≈0.807 背景上标题 15.42:1 / 正文 4.73:1 | +| 对照:不提升色阶 | 正文仅 3.04–3.13:1 → 色阶提升是正文达标的前提 | +| 参考页实机基准(自动化任务) | 标题 5.31–5.61:1、次级文字 4.31–4.39:1 | +| 单元测试 `test/readability.test.mjs` | 通过;新增源码级守卫(不透明名单不得再含 `[data-plugin-panel]`、光晕与色阶规则必须存在) | +| 几何/滚动/hover/焦点/拖动属性 | 与修订前逐项一致(回归断言原样保留) | +| 未跑 | 本机无仓库依赖与 Playwright,未跑 `npm test` / `npm run typecheck` / `npm run build` 与 Playwright 本体;由 CI 与维护者环境复核。真实照片壁纸的现场观感由提出方在其 DSH 上确认 | + +### 两种主题各自适用的背景条件(维护者复核意见补充) + +透明底把对比度交给了壁纸,因此约束是**分主题**的,且两种主题互为镜像: + +| 主题 | 文字色 | 适用背景 | 测试用背景(实测) | 标题 / 正文对比度 | 越界示例(测试断言必然低于门槛) | +| --- | --- | --- | --- | --- | --- | +| 深色 | `#f9fafb` / `#cfd3d6` | 局部背景保持偏暗(L ≲ 0.10) | `#555555`(L≈0.091,与 #92 报告实测背景 L≈0.097 同级) | 7.13:1 / 4.95:1 | 纯白壁纸:1.05:1 | +| 浅色 | `#0f1115` / `#61666b` | 局部背景保持偏亮(L ≳ 0.81) | `#e8e8e8`(L≈0.807) | 15.42:1 / 4.73:1 | 纯黑壁纸:1.11:1 | + +两侧越界由 `plugin-readability.browser.test.mjs` 的 `OUT_OF_RANGE_CASES` **显式断言失败**,避免门槛被空过;不提升色阶时两种主题都不达标(深色 3.49:1、浅色 3.02:1),因此 tertiary / caption → secondary 的映射是正文达标的前提。 + +**明确接受该取舍**:本方案选择「透明优先」——在上述区间之外(深色主题遇明亮壁纸、浅色主题遇暗壁纸),说明文字会低于 4.5:1。这是已知且接受的代价,另一种可能(回到不透明底板,或引入用户可选的底板强度)会牺牲页面与「自动化任务」等页面一致的透明观感。 + +真实 DSH 中的明亮/复杂壁纸复查见下方「真实应用」小节补充。 + +复跑方式不变:设置 `BEAUTICODE_PLAYWRIGHT_MODULE` 与 `BEAUTICODE_BROWSER_CHANNEL=msedge` 后 `npm run test -w @beauticode/adapter-dsh`;未配置浏览器环境时浏览器用例显式跳过,不代表完成视觉验收。 diff --git a/integrations/deepseek-harness/README.zh-CN.md b/integrations/deepseek-harness/README.zh-CN.md index f01b53a..52f95df 100644 --- a/integrations/deepseek-harness/README.zh-CN.md +++ b/integrations/deepseek-harness/README.zh-CN.md @@ -18,7 +18,7 @@ dsh plugin --profile desktop add beauticode-dsh ## Web -「设置 → 背景」默认开启「功能面板不透明」,保证设置、弹层、菜单、消息编辑栏与提问窗口(提问、计划审核、授权确认)遮住后方内容。Web 与官方 Desktop 的插件页也使用此开关:标题区、各分组、详情、配置和加载/空/错误状态分别使用局部实色底板,分组间隙和外围继续显示壁纸;说明文字及计数提升到宿主次要文字色。图片、视频、背景阴影、磨砂和画窗模式不会削弱底板。关闭后恢复原有透明效果;选择会保存在当前应用的本地偏好中,清除背景后恢复宿主样式。需要回滚时关闭开关或恢复旧插件版本即可。 +「设置 → 背景」默认开启「功能面板不透明」,保证设置、弹层、菜单、消息编辑栏与提问窗口(提问、计划审核、授权确认)遮住后方内容。Web 与官方 Desktop 的插件页是页面而不是面板,因此与「自动化任务」等页面一样保持透明:标题区、各分组、卡片、详情、配置和加载/空/错误状态都不铺底板,壁纸直接透出。为在小字密集处保住可读性,页内说明文字与计数提升到宿主次要文字色,并给页面文字加一层随主题取色的柔和光晕(深色主题黑色、浅色主题白色);这两项辅助同样受此开关控制。图片、视频、背景阴影、磨砂和画窗模式都不会让插件页重新变成实色。需要知道的取舍:页面透明后文字对比度取决于壁纸亮度——深色主题适合偏暗的背景、浅色主题适合偏亮的背景;若壁纸很亮(深色主题)或很暗(浅色主题),说明文字会不容易看清,此时可切换主题或调低背景亮度。关闭后,面板回到半透明,插件页也不再应用上述两项辅助;选择会保存在当前应用的本地偏好中,清除背景后恢复宿主样式。需要回滚时关闭开关或恢复旧插件版本即可。 装好插件并运行 `dsh web` 后,打开「设置」,左侧导航里会多出一项「背景」:在 Windows 上点击图片或视频会打开系统文件选择器,选好后在页面内命名,背景立即应用并进入已保存主题;也可以清除、开关声音、切换或删除主题,进入全屏把浏览器的标签页和地址栏收起来,以及打开「皮肤中心」安装 `https://hnnulwh.cn` 上已审核的社区皮肤。本地导入只在 Node 端保存绝对路径,并通过带 Range 支持的回环媒体服务读取原文件,不复制用户选择的图片或整段视频主文件。Windows 强制使用本地引用;原生选择器不可用时会明确报错,不会静默上传或复制媒体。非 Windows 环境保留兼容上传,并明确提示该模式会保存托管副本。网页控制台没有摸鱼;浅色/深色跟 DSH 自己的外观走。下次打开会恢复上次背景。皮肤中心来源固定,不使用 `BEAUTICODE_SKIN_CENTER` 或 `skin-center.json` 改写生产目录。 diff --git a/integrations/deepseek-harness/readability.js b/integrations/deepseek-harness/readability.js index d6f6160..62b9929 100644 --- a/integrations/deepseek-harness/readability.js +++ b/integrations/deepseek-harness/readability.js @@ -25,9 +25,11 @@ style.textContent = ` html[data-bc-active="true"] body{ --bc-readable-fill:var(--dsw-static-neutral-bluish-800,#232833); + --bc-text-halo:#000; } html[data-bc-active="true"][data-bc-resolved-tone="light"] body{ --bc-readable-fill:var(--dsw-static-neutral-bluish-00,#fff); + --bc-text-halo:#fff; } /* DSH settings are body portals; its sticky composer sits over the transcript. Protect the actual surface, rather than keying readability to a conversation @@ -45,21 +47,7 @@ html[data-bc-active="true"]:not([data-bc-readable-surfaces="false"]) :is( frame instead would produce a solid bar across its side padding. */ [data-question-key] > section, [data-plan-review-key] > section, - [data-approval-key] > div, - /* The standalone plugin page has separate reading regions. Keep its frame - transparent so wallpaper remains visible between groups. Empty/offline - states are direct paragraphs; failures wrap an alert and retry control. */ - [data-plugin-panel] > header, - [data-plugin-panel] [data-plugin-group], - [data-plugin-panel] [data-plugin-detail], - [data-plugin-panel] [data-plugin-item-detail], - [data-plugin-panel] [data-plugin-row-detail], - [data-plugin-panel] [data-plugin-config], - [data-plugin-panel] [data-plugin-loading], - [data-plugin-panel] > p, - [data-plugin-panel] > div:has(> [role="alert"]), - [data-plugin-panel] [data-plugin-package], - [data-plugin-panel] [data-plugin-item] + [data-approval-key] > div ){ --dsw-alias-bg-base:var(--bc-readable-fill); --dsw-alias-bg-layer-1:var(--bc-readable-fill); @@ -71,16 +59,19 @@ html[data-bc-active="true"]:not([data-bc-readable-surfaces="false"]) :is( --dsw-alias-tooltip-bg:var(--bc-readable-fill); background-color:var(--bc-readable-fill); } -/* Keep host geometry and radii: groups have no padding, so adding a radius - would expose wallpaper behind the first letter of their heading. */ +/* The plugin page is a page, not a panel: DSH lays it out full-bleed like the + automation-tasks page, so it keeps the translucent surfaces client.js + publishes (--bc-surface-mix, --dsw-alias-bg-base:rgba(17,20,27,.10), ...) and + the wallpaper reads through. Painting it opaque turned every group and row + into a slab - and, because a host row carries margin:0 -8px with radius-xl, + the painted rows' 8px overhang showed up as rounded scallops of wallpaper. + Leave host geometry, radii and surfaces alone. Keep only the two legibility + aids: captions come up one tier, and a soft halo keeps 13px copy readable + over a busy photo without covering the picture. */ html[data-bc-active="true"]:not([data-bc-readable-surfaces="false"]) [data-plugin-panel]{ --dsw-alias-label-tertiary:var(--dsw-alias-label-secondary); --dsw-alias-label-caption:var(--dsw-alias-label-secondary); -} -/* Preserve the host's translucent hover feedback over an opaque base. Using - an image layer leaves the reading plate opaque even in gallery/video mode. */ -html[data-bc-active="true"]:not([data-bc-readable-surfaces="false"]) [data-plugin-panel] :is([data-plugin-package],[data-plugin-item]):hover{ - background-image:linear-gradient(var(--dsw-alias-interactive-bg-hover),var(--dsw-alias-interactive-bg-hover)); + text-shadow:0 1px 2px color-mix(in srgb, var(--bc-text-halo) 45%, transparent); } `; document.head.append(style); diff --git a/integrations/deepseek-harness/test/plugin-readability.browser.test.mjs b/integrations/deepseek-harness/test/plugin-readability.browser.test.mjs index 4a974b8..f350626 100644 --- a/integrations/deepseek-harness/test/plugin-readability.browser.test.mjs +++ b/integrations/deepseek-harness/test/plugin-readability.browser.test.mjs @@ -16,6 +16,34 @@ const css = /style.textContent = `([\s\S]*?)`;/; const clientCss = client.match(css)[1].replaceAll("${CROSSFADE_MS}", "180"); const galleryCss = atmosphere.match(css)[1]; +// The plugin page is a page, not a panel: it keeps the translucent surfaces +// client.js publishes and lets the wallpaper read through, exactly like the +// automation-tasks page. Legibility comes from a text halo plus one caption +// tier instead of an opaque plate, so contrast is a property of the wallpaper +// rather than of the component. Each tone therefore has its own applicable +// range, and the two ranges mirror each other: +// +// dark tone (light text #f9fafb / #cfd3d6) holds while the local background stays dark +// light tone (dark text #0f1115 / #61666b) holds while the local background stays light +// +// The bars are the reference page's own measured contrast (automation-tasks: +// 5.31–5.61 for titles, 4.31–4.39 for secondary text). Each tone is checked on +// the wallpaper the issue #92 report corresponds to for that tone, and the two +// opposing extremes are asserted to FAIL: that keeps the accepted trade-off +// visible in the suite instead of letting a bar pass vacuously. +const TITLE_BAR = 5; +const BODY_BAR = 4.2; +const TONE_CASES = [ + // #555555 is L≈0.091, next to the L≈0.097 background measured in the report. + { tone: "dark", background: "#555555", range: "local background L ≲ 0.10" }, + // Mirrored range for dark text on a light wallpaper. + { tone: "light", background: "#e8e8e8", range: "local background L ≳ 0.81" }, +]; +const OUT_OF_RANGE_CASES = [ + { tone: "dark", background: "#ffffff", why: "light text on a white wallpaper" }, + { tone: "light", background: "#000000", why: "dark text on a black wallpaper" }, +]; + async function fixture(t, { enabled = true, inject = true } = {}) { const { chromium } = await import(runtime); const browser = await chromium.launch({ headless: true, @@ -50,7 +78,14 @@ async function surface(page, selector) { } function luminance(color) { - const channels = color.match(/[\d.]+/g).slice(0, 3).map(Number).map(v => { + // Accept #rgb / #rrggbb as well as rgb()/rgba(): the wallpaper constants below + // are authored as hex, and a silent NaN here would make a bar assertion pass + // vacuously or fail for the wrong reason. + const hex = color.trim().match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i); + const channels = (hex + ? (hex[1].length === 3 ? [...hex[1]].map(c => parseInt(c + c, 16)) : [0, 2, 4].map(i => parseInt(hex[1].slice(i, i + 2), 16))) + : color.match(/[\d.]+/g).slice(0, 3).map(Number) + ).map(v => { const n = v / 255; return n <= .04045 ? n / 12.92 : ((n + .055) / 1.055) ** 2.4; }); return channels[0] * .2126 + channels[1] * .7152 + channels[2] * .0722; @@ -60,7 +95,27 @@ function contrast(foreground, background) { return (a + .05) / (b + .05); } -browserTest("plugin reading surfaces stay opaque across themes, phases and wallpaper settings (#92)", async t => { +/** The halo token and the shadow the page actually paints. */ +const halo = page => page.evaluate(() => ({ + token: getComputedStyle(document.body).getPropertyValue("--bc-text-halo").trim(), + shadow: getComputedStyle(document.querySelector("[data-plugin-panel]")).textShadow, +})); + +/** Resolve the secondary tier through a probe, then read the page's own colors. */ +const tiers = page => page.evaluate(() => { + const probe = document.createElement("span"); + probe.style.color = "var(--dsw-alias-label-secondary)"; + document.body.append(probe); + const secondary = getComputedStyle(probe).color; + probe.remove(); + return { + secondary, + description: getComputedStyle(document.querySelector(".host-description")).color, + count: getComputedStyle(document.querySelector(".host-count")).color, + }; +}); + +browserTest("plugin page keeps the wallpaper and the text halo across themes, phases and wallpaper settings (#92)", async t => { const page = await fixture(t); for (const tone of ["dark", "light"]) for (const phase of ["hero", "active", "settling", "none"]) for (const gallery of [false, true]) for (const dim of [false, true]) { @@ -74,39 +129,81 @@ browserTest("plugin reading surfaces stay opaque across themes, phases and wallp if (phase === "active" || phase === "settling") assert.equal(await page.evaluate(phase => document.documentElement.matches(`:has(#root [data-phase="${phase}"])`), phase), true); for (const selector of ["[data-plugin-panel]>header", "[data-plugin-group=official]", "[data-plugin-group=bundles]", "[data-plugin-item]", "[data-plugin-package]"]) { const state = await surface(page, selector); - assert.equal(state.alpha, 255, `${tone}/${phase}/${gallery}/${dim} ${selector} must occlude wallpaper`); - assert.equal(state.background, tone === "dark" ? "rgb(53, 54, 56)" : "rgb(255, 255, 255)"); + assert.equal(state.alpha, 0, `${tone}/${phase}/${gallery}/${dim} ${selector} must keep the wallpaper`); assert.equal(state.filter, "none"); assert.equal(state.backdrop, "none"); } assert.equal((await surface(page, "[data-plugin-panel]")).alpha, 0, "page surroundings and gaps retain wallpaper"); - const bg = (await surface(page, "[data-plugin-group=official]")).background; - for (const selector of ["h1", ".host-intro", "h3", ".host-count", ".host-description"]) { - const colors = await page.locator(selector).evaluateAll(els => els.map(el => getComputedStyle(el).color)); - for (const color of colors) assert.ok(contrast(color, bg) >= 4.5, `${tone} ${selector}: ${color} on ${bg}`); - } + const painted = await halo(page); + assert.equal(painted.token, tone === "dark" ? "#000" : "#fff", `${tone} halo token follows the tone`); + assert.notEqual(painted.shadow, "none", `${tone}/${phase}/${gallery}/${dim} text halo must survive`); + assert.equal(await page.locator("h1").evaluate(el => getComputedStyle(el).textShadow), painted.shadow, + "headings inherit the page halo"); + const lifted = await tiers(page); + assert.equal(lifted.description, lifted.secondary, `${tone} descriptions are lifted one tier`); + assert.equal(lifted.count, lifted.secondary, `${tone} counts are lifted one tier`); } }); -browserTest("React state replacements and details receive protection without re-injection", async t => { +browserTest("each tone keeps the reference page's contrast inside its own wallpaper range", async t => { const page = await fixture(t); - for (const [name, html] of Object.entries(states)) { - await page.locator("[data-plugin-panel]").evaluate((el, html) => { el.innerHTML = html; }, html); - assert.equal((await surface(page, "[data-plugin-panel]>:first-child")).alpha, 255, name); + const readColors = () => page.locator("h1, .host-intro, h3, .host-count, .host-description") + .evaluateAll(els => els.map(el => ({ + tag: el.tagName.toLowerCase() + (el.className ? "." + String(el.className).split(" ")[0] : ""), + color: getComputedStyle(el).color, + }))); + const paint = ({ tone, background }) => page.evaluate(({ tone, background }) => { + document.documentElement.dataset.bcResolvedTone = tone; + document.querySelector("#wallpaper").style.background = background; + }, { tone, background }); + + for (const { tone, background, range } of TONE_CASES) { + await paint({ tone, background }); + const colors = await readColors(); + assert.ok(colors.length >= 5, "fixture exercises titles, intro, group headings, counts and descriptions"); + for (const { tag, color } of colors) + assert.ok(contrast(color, background) >= BODY_BAR, + `${tone} tone (${range}): ${tag} ${color} on ${background} = ${contrast(color, background).toFixed(2)}`); + const title = colors.find(({ tag }) => tag === "h1"); + assert.ok(title, "the fixture must render a page title"); + assert.ok(contrast(title.color, background) >= TITLE_BAR, + `${tone} tone (${range}): title ${title.color} on ${background} = ${contrast(title.color, background).toFixed(2)}`); + } + + // The accepted trade-off, pinned: with a transparent page the opposite + // extreme of either tone is readable only through the wallpaper itself. + for (const { tone, background, why } of OUT_OF_RANGE_CASES) { + await paint({ tone, background }); + const [title] = await page.locator("h1").evaluateAll(els => els.map(el => getComputedStyle(el).color)); + const ratio = contrast(title, background); + assert.ok(ratio < BODY_BAR, + `${tone} tone: ${why} is outside the accepted range and must stay below the bar (${ratio.toFixed(2)} on ${background})`); } }); -browserTest("group title edges fully occlude wallpaper without adding a clipping radius", async t => { +browserTest("wallpaper reads through the page instead of being occluded by a plate", async t => { const page = await fixture(t); - const title = page.locator("[data-plugin-group=official] h3"); + const group = page.locator("[data-plugin-group=official]"); for (const tone of ["dark", "light"]) { - await page.evaluate(tone => { document.documentElement.dataset.bcResolvedTone = tone; document.querySelector("#wallpaper").style.background = "white"; }, tone); - const bright = await title.screenshot(); + await page.evaluate(tone => { document.documentElement.dataset.bcResolvedTone = tone; }, tone); + await page.locator("#wallpaper").evaluate(el => { el.style.background = "white"; }); + const bright = await group.screenshot(); await page.locator("#wallpaper").evaluate(el => { el.style.background = "black"; }); - assert.deepEqual(await title.screenshot(), bright, "even the first letter must not sample wallpaper through rounded corners"); + assert.notDeepEqual(await group.screenshot(), bright, + `${tone}: the wallpaper must show through the plugin page`); + } +}); + +browserTest("React state replacements stay transparent and keep the halo without re-injection", async t => { + const page = await fixture(t); + for (const [name, html] of Object.entries(states)) { + await page.locator("[data-plugin-panel]").evaluate((el, html) => { el.innerHTML = html; }, html); + assert.equal((await surface(page, "[data-plugin-panel]>:first-child")).alpha, 0, `${name} keeps the wallpaper`); + const painted = await halo(page); + assert.notEqual(painted.shadow, "none", `${name} keeps the halo`); } }); -browserTest("bright, dark, complex and playing video backgrounds cannot weaken plates", async t => { +browserTest("bright, dark, complex and playing video backgrounds keep the page transparent", async t => { const page = await fixture(t); await page.evaluate(async () => { const canvas = document.createElement("canvas"); canvas.width = 320; canvas.height = 200; @@ -124,7 +221,10 @@ browserTest("bright, dark, complex and playing video backgrounds cannot weaken p document.querySelector("video").hidden = background !== "video"; }, { tone, blur, background }); for (const selector of ["[data-plugin-panel]>header", "[data-plugin-group=official]", "[data-plugin-package]"]) - assert.equal((await surface(page, selector)).alpha, 255, `${tone}/${blur}/${background}`); + assert.equal((await surface(page, selector)).alpha, 0, `${tone}/${blur}/${background} ${selector}`); + const painted = await halo(page); + assert.equal(painted.token, tone === "dark" ? "#000" : "#fff", `${tone}/${blur}/${background} halo token`); + assert.notEqual(painted.shadow, "none", `${tone}/${blur}/${background} halo`); } }); @@ -134,16 +234,19 @@ browserTest("disabled protection and clearing wallpaper restore host appearance // Locators for repeated descriptions must identify the intended card. const snapshot = async page => Promise.all(["[data-plugin-panel]>header", "[data-plugin-group=official]", "[data-plugin-package]", "[data-plugin-package] .host-description", "[data-plugin-group=official] .host-count"].map(selector => surface(page, selector))); assert.deepEqual(await snapshot(page), await snapshot(baseline)); + assert.deepEqual(await tiers(page), await tiers(baseline), "protection off must restore host typography"); await page.evaluate(() => globalThis.BeauticodeReadableSurfaces.set(true)); - assert.equal((await surface(page, "[data-plugin-package]")).alpha, 255); + assert.equal((await surface(page, "[data-plugin-package]")).alpha, 0, "the card still keeps the wallpaper"); + assert.notDeepEqual(await tiers(page), await tiers(baseline), "protection lifts captions one tier"); await page.evaluate(() => { document.documentElement.dataset.bcActive = "false"; document.documentElement.dataset.bcGallery = "false"; }); await baseline.evaluate(() => { document.documentElement.dataset.bcActive = "false"; document.documentElement.dataset.bcGallery = "false"; }); assert.deepEqual(await snapshot(page), await snapshot(baseline)); + assert.deepEqual(await tiers(page), await tiers(baseline)); await page.evaluate(() => { document.documentElement.dataset.bcActive = "true"; globalThis.BeauticodeReadableSurfaces.set(false); }); assert.equal((await surface(page, "[data-plugin-package]")).alpha, 0); }); -browserTest("solid surfaces preserve geometry, card hover, keyboard focus and controls", async t => { +browserTest("transparent rows preserve geometry, card hover, keyboard focus and controls", async t => { const page = await fixture(t, { enabled: false }); const geometry = () => page.evaluate(() => [...document.querySelectorAll("[data-plugin-panel],header,[data-plugin-group],[data-plugin-package],[data-plugin-item]")] .map(el => { const r = el.getBoundingClientRect(); return [r.x, r.y, r.width, r.height]; })); @@ -153,7 +256,8 @@ browserTest("solid surfaces preserve geometry, card hover, keyboard focus and co const normal = await surface(page, "[data-plugin-package]"); await page.locator("[data-plugin-package]").hover(); const hover = await surface(page, "[data-plugin-package]"); - assert.equal(hover.alpha, 255); assert.notEqual(hover.image, normal.image, "hover feedback survives the solid fill"); + assert.notEqual(hover.background, normal.background, "host hover feedback survives"); + assert.ok(hover.alpha > 0 && hover.alpha < 255, `hover stays translucent: ${hover.background}`); await page.getByRole("checkbox", { name: "Enable beautiCode" }).check(); assert.equal(await page.getByRole("checkbox", { name: "Enable beautiCode" }).isChecked(), true); await page.keyboard.press("Tab"); diff --git a/integrations/deepseek-harness/test/readability.test.mjs b/integrations/deepseek-harness/test/readability.test.mjs index 239a67f..6be0a8e 100644 --- a/integrations/deepseek-harness/test/readability.test.mjs +++ b/integrations/deepseek-harness/test/readability.test.mjs @@ -96,3 +96,27 @@ test("pending-interaction panels are protected by their frame's semantic key", ( assert.ok(style.textContent.includes("--dsw-specific-input-major:var(--bc-readable-fill)"), "panels must repaint the same token the composer uses"); }); + +test("the plugin page keeps the wallpaper and ships only non-covering legibility aids", () => { + const { styles } = load(); + const [style] = styles; + // The plugin page is a page, not a panel: it must stay on the translucent + // surfaces client.js publishes, so no plate selector may reappear in the + // opaque surface list (that is what made issue #92 a black slab). + const opaque = style.textContent.match(/:is\(([\s\S]*?)\)\{/); + assert.ok(opaque, "the opaque surface rule must exist"); + assert.ok(!opaque[1].includes("[data-plugin-panel]"), + "the plugin page must not be painted opaque"); + assert.ok(!style.textContent.includes("[data-plugin-panel] :is([data-plugin-package],[data-plugin-item]):hover"), + "the plugin page must not re-declare hover on top of the host's translucent hover"); + // Legibility aids that do not cover the wallpaper: a tone-aware halo and one + // caption tier. + assert.ok(style.textContent.includes("--bc-text-halo:#000"), "dark tone halo color"); + assert.ok(style.textContent.includes("--bc-text-halo:#fff"), "light tone halo color"); + assert.ok(/\[data-plugin-panel\]\{[\s\S]*?text-shadow:0 1px 2px color-mix\(in srgb, var\(--bc-text-halo\) 45%, transparent\);/ + .test(style.textContent), "the plugin page must paint the text halo"); + assert.ok(style.textContent.includes("--dsw-alias-label-tertiary:var(--dsw-alias-label-secondary)"), + "descriptions must be lifted one tier"); + assert.ok(style.textContent.includes("--dsw-alias-label-caption:var(--dsw-alias-label-secondary)"), + "counts and captions must be lifted one tier"); +});