rans725/dsh-glass-ranswork ↗★ 2
dsh-glass-ranswork
为Web界面提供半透明毛玻璃主题及设置页 适合想要自定义DSH界面透明度、模糊度及背景图的视觉爱好者。
安裝
npx -p @deepseek-ai/dsh dsh plugin --profile web add github:rans725/dsh-glass-ranswork說明文件
閱讀完整 README ↗dsh-glass-ranswork · 自定义主题
给 DeepSeek Harness Web 界面加上「半透明 + 背景模糊」的毛玻璃外观,并在 设置 → 自定义主题 里提供一个完整的调参页(启用 / 不透明度 / 面板不透明度 / 模糊 / 饱和度 / 氛围光 / 配色 / 自定义背景图片)。
出厂默认即用户要的 80% 半透明(opacity: 20,即表面 20% 不透明);
信息面板单独一档 panelOpacity: 85,保证弹层和文件预览里的字看得清。
快速开始
升级之前先看「升级」一节。 把
.../main.tar.gz当安装地址时,pnpm 会把 那个 tarball 的 integrity 和 version 一起写进 lockfile ——main会动, 你的依赖不会跟着动。重装多少次都还是当初装下的那个版本。
-
打开 DSH → 设置 → 插件 → 安装插件;
-
粘贴地址(推荐第一种):
npm(推荐)
dsh-glass-ransworkcodeload 固定版本(不想用 npm 时)
https://codeload.github.com/rans725/dsh-glass-ranswork/tar.gz/refs/tags/v1.1.1本地目录:把仓库 zip(Code → Download ZIP)或 Release 的
.tgz解压, 填含package.json的那层目录,例如D:\Plugins\dsh-glass-ranswork。 -
装完按提示重启 DSH,打开 设置 → 自定义主题 即可调外观。
-
设置页顶部会显示当前加载的版本号(如
v1.1.1);有新版时多一行提示。
安装
本包是一个 DSH bundle(package.json 的 dsh.bundle.patch 指向
cordis.patch.yml,dsh.client 声明浏览器半侧),不需要构建、没有依赖。
方式一:npm(推荐)
在 profile 目录里直接装:
pnpm add dsh-glass-ranswork
为什么推荐 npm:装 npm 的包,插件市场能感知到新版本并推送更新提醒。 下面那些 URL 安装方式虽然能装,但永远收不到更新推送 —— 市场对 URL 安装 没有 registry 身份可比,宁可如实报「无更新」也不猜(dshmarket #768)。
从旧版 URL 方式升上来的用户:lockfile 里记的还是 URL,改 npm 后仍然收不到推送。 需要重装一次:
pnpm remove dsh-glass-ranswork && pnpm add dsh-glass-ranswork
好处是版本可以写范围(^1.1.1),升级由 pnpm 和市场的更新提醒共同覆盖。
方式二:codeload 固定发行版(不想用 npm 时)
pnpm add https://codeload.github.com/rans725/dsh-glass-ranswork/tar.gz/refs/tags/v1.1.1
好处是版本可以写死在 tag 里,升级时换 tag 号即可;坏处是不能写成 ^1.1.1
这种范围(pnpm 无从解析),所以「有新版本」的提示靠设置页顶部那一行。
方式三:codeload 直链(固定版本,不需要 Git)
https://codeload.github.com/rans725/dsh-glass-ranswork/tar.gz/refs/tags/v1.1.1
为什么不写 https://github.com/.../archive/...:
github.com只负责 302 跳到codeload.github.com;主站被墙 / 不稳时, pnpm 的 HEAD 请求会ETIMEDOUT,而真正下发 tar 的 codeload 往往是通的;- codeload 直链少一次跳转,受限网络下成功率明显更高,代码不用改一行。
版本号必须写进 URL。
refs/heads/main看着“永远最新”,但 lockfile 一旦 记录了这个 spec 的 integrity,pnpm 不会为了同一个 spec 重新解析远端。 升级 = 换成新 tag 的 URL 再装一次(见「升级」)。
为什么不推荐
github:前缀:pnpm 解析github:依赖会调用系统git; Windows 上没装 Git 就会报git ls-remote failed。上面的地址由 pnpm 直接 下载解包,不需要 Git。
方式四:Release 安装包(固定版本)
到 Releases 下载
dsh-glass-ranswork-1.1.1.tgz,安装时填它的本地路径,或直接填资源地址:
https://github.com/rans725/dsh-glass-ranswork/releases/download/v1.1.1/dsh-glass-ranswork-1.1.1.tgz
资产地址同样先经过
github.com。主站不通时,改用上面的 codeload 直链。
方式五:本地目录 / zip
两种来源都可以:
- 仓库页 Code → Download ZIP(解压后是
dsh-glass-ranswork-main/); - 或 Release 里的
dsh-glass-ranswork-1.1.1.tgz(解压后是package/)。
把含 package.json 的那一层目录放到任意位置,在安装入口填它的绝对路径,例如:
D:\Plugins\dsh-glass-ranswork
安装器会把依赖写进当前 profile 的
package.json并激活 bundle, 不需要手动改配置文件。
兼容性
- 在 DSH 0.2.1-rc.3(Windows 桌面版)上开发与验证(v1.1.1 起);
- 插件只用到公开客户端服务(
slots/locale/theme)和宿主webServer, 没有私有 API、没有原生依赖;DSH 大版本升级后如果主题 token 改名, 退化结果是「某块颜色/模糊不生效」,不会崩。 - 每次宿主升级都会重编译客户端,CSS Modules 哈希会全变。插件只在 纯装饰性的卡片前缀上依赖哈希;凡是「排除某个结构容器」的规则一律用 稳定 data 属性或结构判据(详细的坑见 v1.1.1 更新日志)。
卸载
设置 → 插件 → 卸载。插件不会改 DSH 本体;设置文件在
$DSH_HOME/glass-theme.json,壁纸在 $DSH_HOME/glass-theme-bg.,
删掉即可彻底复位。
升级
先确认当前版本:设置 → 自定义主题 顶部有 v1.1.1(旧版宿主没有这行,
可以在 profile 的 node_modules/dsh-glass-ranswork/package.json 里看)。
怎么知道有新版本
设置页顶部那行:v1.1.1 后面如果出现「有新版本 v1.2.0」的链接,点它去 Release。
宿主半侧每 6 小时静默问一次 api.github.com 的最新 Release(失败 10 分钟后重试,
全程不打扰;断网时只显示版本号)。
tarball / Release 装的
三步:
- 把 profile
package.json里的旧 spec 换成新 tag 的 URL; - 删掉
pnpm-lock.yaml里对应的dsh-glass-ranswork@...条目(或整份 lockfile); pnpm install,重启 DSH。
或者一步到位:
pnpm add https://codeload.github.com/rans725/dsh-glass-ranswork/tar.gz/refs/tags/v1.1.1
为什么“重装一次”不升级
pnpm 对 tarball 依赖记录的是 spec + integrity + version。spec 没变(还是
.../heads/main.tar.gz)时,它认为这份依赖已经解析过,直接用 lockfile 里的
旧包,不会去远端重新 HEAD。main 更新了、tag 打了,都和你的 lockfile 无关。
只有下面两种情况会真正换包:
- spec 变了(新 tag 的 URL / 新版本号);
- 你显式让 pnpm 重新解析(删 lockfile 或
pnpm install --force)。
卡住时的自查
| 现象 | 原因 | 处理 |
|---|---|---|
| 重装后版本不变 | lockfile 钉住了旧 spec | 换成新 tag 的 URL |
HEAD https://github.com/.../main.tar.gz ... ETIMEDOUT | github.com 不通 | 改用 codeload 直链 |
安装返回 changed:false,日志只有 fetch failed | 旧依赖也在拉 github.com,一个失败整次回滚 | 先从 package.json 删掉旧条目再装 |
| 设置页没有版本行 | 宿主半侧还是旧版 | 重启 DSH(浏览器半侧热更新,宿主不会) |
安装器目前只会把
fetch failed透出来,不会告诉你是哪个 URL 失败。 遇到时先看 profile 的package.json里还有没有github.com/.../archive/...的旧条目 —— 通常就是它。
设置
| 字段 | 默认 | 区间 | 说明 |
|---|---|---|---|
enabled | true | — | 关闭后完全恢复原生外观 |
opacity | 20 | 0–100 | 表面不透明度 %;20 = 80% 半透明,100 = 不透明 |
panelOpacity | 85 | 0–100 | 信息面板不透明度 %:弹层、文件预览、统计/费用面板、Modal 内容 |
blur | 32 | 0–80 | 三列骨架背后那层的模糊半径(px) |
overlayBlur | 32 | 0–80 | 浮层模糊半径(px):弹层、已归档面板、费用面板、Modal。运行时夹到 56px |
composerBlur | 24 | 0–80 | 发送框 / 任务卡片模糊半径(px)。运行时夹到 40px;设为 0 关闭这两处 |
previewOpacity | 85 | 0–100 | 文件预览 / 改动卡片不透明度 %:文件 diff、已编辑文件卡、文档预览 |
saturate | 180 | 100–200 | 背景饱和度 % |
ambient | true | — | 是否绘制氛围光背景 |
accent | aurora | aurora / ocean / sunset / mono | 氛围光配色 |
bgImage | '' | — | 背景图 token(空串 = 没设图)。只是缓存键,不是文件名 |
bgFit | cover | cover / contain / stretch / tile / center | 显示方式:填充 / 适应 / 拉伸 / 平铺 / 居中 |
bgCrop | {x:0,y:0,w:1,h:1} | 每项 0–1 | 自定义裁剪区域(占原图比例,整图 = 0,0,1,1) |
bgDim | 30 | 0–100 | 背景蒙层不透明度 %(浅色盖白、深色盖黑) |
bgGuard | true | — | 文字保护:有壁纸时给文字加光晕 + 加深次要文字(不加任何底色) |
bgAutoTone | true | — | 跟随图片明暗:暗图切深色主题、亮图切浅色,让文字颜色和图片对得上 |
bgBaseTheme | '' | light / dark / system | 内部状态:自动切换前用户的明暗偏好(空 = 没切过),删图/关开关时还原 |
bgLumaGrid | '' | — | 内部状态:上传时算好的 32×32 亮度图(base64),自动明暗按裁剪区域查表用 |
设置页常规是 7 个滑块:表面不透明度 / 面板不透明度 / 背景模糊 / 浮层模糊 / 发送框·任务模糊 / 文件预览·改动卡片 / 背景饱和度。设了背景图之后多一个 「背景蒙层」滑块、两个开关(文字保护 / 跟随图片明暗),并多出整行宽的 「自定义裁剪」区(设图前这些不显示,避免摆一个当前没有作用的控件)。
发送框与任务卡片的底色没有单独一档:它就是 --dsw-specific-input-major
(属 SURFACES),跟着「表面不透明度」走。曾经有过 composerOpacity
滑块,实现方式是给卡片刷一层 color-mix(白 N%) 的白底 —— 行内
background 会顶掉卡片自己的 token,拖「表面不透明度」时发送框纹丝不动,
所以已删除。挡住背后的字靠模糊,不靠颜色。
设置持久化在 $DSH_HOME/glass-theme.json(宿主半侧用「临时文件 + rename」原子写入;
坏文件按默认值处理,删掉即可复位)。范围校验以宿主半侧为准。
为什么分成两档不透明度
opacity 管的是装饰性骨架(frame / 侧栏 / 输入框 / 选区),透一点更好看;
panelOpacity 管的是要读字的地方。这两类不是一个可读性预算:把统计弹层、
文件预览也压到 20% 不透明,正文和表格会直接糊在氛围光上。
面板这一档覆盖的 token 见 client.js 的 PANELS:
| token | 谁在用 |
|---|---|
--dsw-specific-menu | 会话统计 / 逐轮用量弹层(宿主 Xt1eiG_panel)、所有下拉浮层 |
--dsw-alias-bg-document-preview | 文件预览里 PDF 的底色 |
--dsw-alias-bg-layer-2 / -3 | Modal 内容、Pill、轨迹列表、插件管理页、桌宠的费用弹层与花费看板(dsh-cost-panel / dsh-pet-dash-panel) |
基色不能用 --dsw-menu-surface-fill:那个值自带 alpha(#f8f9fa94),
再乘一次会变成 0.58 × 0.85 ≈ 49% 透明,比不改还透。所以 PANELS 用的是同色的
不透明静态 token(bluish-50 / bluish-750)。
宿主重启窗口期的保存
宿主半侧的 DEFAULTS 是模块加载时读进内存的,改它必须重启 DSH;
浏览器半侧走 HMR,刷新页面就生效。所以在「宿主还没重启」的窗口期里,
宿主不认识 panelOpacity:POST 上去会被旧键集丢掉,GET 回来也没有这个字段。
client.js 因此把设置镜像一份到 localStorage(键 dsh-glass-theme:settings),
只用来补齐宿主不认识的键;宿主认识的键永远以宿主为准。宿主重启后自然开始
持久化 panelOpacity,镜像里那份随即被覆盖成同一个值,不会分叉。
自定义背景图片
设置页可以选一张本机图片当壁纸,铺在所有半透明表面后面(不是盖在上面), 并像桌面壁纸设置一样有 填充 / 适应 / 拉伸 / 平铺 / 居中 五种显示方式, 外加一个自定义裁剪框和一个背景蒙层滑块。
图片存在哪:图片本体落盘成 $DSH_HOME/glass-theme-bg.,
设置里只存一个短 token(bgImage)。刻意不把图片转成 data URL 塞进设置 JSON:
glass-theme.json每次拖滑块都会被整体重写,塞几 MB 的 base64 会让每次 保存都写一个大文件,localStorage镜像也会跟着爆掉;- 图片走独立路由,浏览器可以长缓存;token 一变 URL 就变,不会拿到旧图。
路由(宿主半侧 index.js):
| 方法 | 路径 | 作用 |
|---|---|---|
GET | /glass-theme/background/ | 原样回传图片(immutable 长缓存 + nosniff) |
POST | /glass-theme/background | 上传图片(body 是原始二进制,不是 multipart) |
DELETE | /glass-theme/background | 删除图片并把 bgImage 清空 |
三条共用一个 prefix 路由(kind: 'prefix')。GET 把 token 放在路径段里,
不放在 query 里 —— 这样不依赖「exact 路由会不会忽略 query」这种实现细节。
安全边界:文件名固定(glass-theme-bg.),扩展名只来自
content-type 白名单(webp / png / jpeg / gif / avif / bmp / svg),
不存在「往任意路径写任意文件」的通道;bgImage 只接受
[A-Za-z0-9_-]{1,64},非法值在 normalize() 里退回空串。写操作同样过
DSH connection 层的来源信任检查(和设置路由一致)。
上传前会压缩:浏览器半侧用 createImageBitmap + canvas 把长边压到 3840px
以内并转成 WebP(质量 0.92),顺便剥掉 EXIF(含拍摄地点)。请求体上限 24 MiB。
裁剪坐标是比例,不是像素:bgCrop = {x,y,w,h} 全部是「占原图的比例」,
所以换分辨率、换窗口大小都不需要重算;服务端与浏览器半侧用同一套规则夹取
(w/h ≥ 5%,越界时把左上角往回推而不是把框压扁)。
渲染时用 canvas 把裁剪区域真的重画成一张新位图再交给 background-size
——用百分比位移模拟只对「铺满」成立,平铺/居中会露出整张原图。
蒙层为什么默认 30:玻璃表面默认只有 20% 不透明,直接压一张照片上去正文 会读不了。蒙层是与明暗同向的雾(浅色盖白、深色盖黑),和宿主表面的明暗 逻辑一致;它和「氛围光」是两件事,设图后氛围光仍然在,会在适应/平铺的留白处 透出来(配色选择依旧有意义)。
文字保护(bgGuard)为什么只加光晕、不加底色:壁纸下最自然的诱惑是
「把表面调实一点」,但那等于替用户改掉了他调好的透明度 —— 有人就是要把
表面压到 5%、模糊调到 0 来看壁纸。所以文字保护做两件事,背景一个像素都不动:
- 用
text-shadow在字形背后加一圈与明暗同向的光(浅色白晕、深色黑晕), 形成字幕式的局部对比;它可继承,挂在body上就能覆盖侧栏、顶栏、 发送框以及 portal 到 body 的浮层。 - 把次要文字加深/提亮一档:宿主的
--dsw-alias-label-secondary/tertiary/caption在浅色主题下是bluish-700/600/400(#61666b / #81858c / #a3a8b0)。这类浅灰字压在照片上,光晕反而帮倒忙 (灰字压在白晕上对比度更低),必须把字本身改深。按同一套调色板改成secondary→bluish-750、tertiary/caption→bluish-700(深色主题反向: 200/300/400)。只改颜色明度,不碰字号字重,不会引起重排;内联 SVG 图标 用currentColor,也会跟着一起变清楚。
不喜欢光晕可以关掉开关,或者把「背景蒙层」拉高。
跟随图片明暗(bgAutoTone)
为什么还需要它:文字颜色是跟主题模式走的(浅色主题深字、深色主题浅字), 而壁纸的明暗和模式无关。换一张暗色壁纸后,浅色主题的深色字压在暗图上, 光晕和加深 token 都救不了 —— 那是「方向错了」。
做法:把背景图(裁剪后的那块)画到 32×32 画布上,算平均相对亮度
(0.2126R + 0.7152G + 0.0722B):
| 平均亮度 | 动作 |
|---|---|
| 0.58 | 切到 light |
| 0.42–0.58 | 不切(明暗参半的图来回切会晃眼,交给文字保护兜) |
亮度从哪来(两层):
- 首选:上传时就算好。 选图片时浏览器已经把图片画进了 canvas(压缩用),
顺手把 32×32 的亮度图存成
bgLumaGrid。之后无论怎么裁剪,都只是按比例 取子矩形求平均 —— 纯查表,不解码图片,也就不会被 canvas 跨源/污染卡住。 - 兜底:现场量。 早于这个功能上传的旧图没有亮度图,就现场把图片
fetch成blob:URL(同源、canvas 一定读得出来)再解码量一次; 量不到会退避重试 6 次,并在开关下面显示状态(测得亮度 0.38 → 已切深色主题/还没量到图片亮度,会自动重试)。
- 切换走官方
ctx.theme.setTheme(),不是自己改data-ds-dark-theme; - 只动内置
light / dark / system:用户如果选了第三方主题 id,一律不碰 (setTheme('dark')会把第三方主题顶掉); - 切换前把用户原来的偏好写进
bgBaseTheme(落盘 + localStorage 镜像), 删图 / 关开关 / 卸载时还原 —— 重启后也认得; - 用户中途自己改过明暗 → 还原时发现当前偏好已经不是我们切的那个,就只清基准、 不覆盖他的选择;
- 判断只在「图片 token / 裁剪区域 / 开关」变化时做一次,不是每帧都量。
原生窗口菜单和标题栏按钮是 Electron / Windows 画的(不在 DOM 里), CSS 够不到;能改的只有它们下面那层底色(见下一节)。
右上角窗口按钮的「方块」
截图里三个系统按钮(最小化 / 最大化 / 关闭)底下有一块比标题栏亮的方块。
根因是 DSH 的 preload 用一个隐藏 probe 把「标题栏底色」
(--dsw-specific-sidebar-fill)算出来传给主进程,作为 Electron 原生
titleBarOverlay 的底色;而页面自己的标题条也铺了同一个半透明白。
同一层颜色被刷了两遍(opacity=5 时 0.05 叠 0.05 ≈ 0.0975),
右上角就比标题栏亮出一块。
修法:把那个 probe 的 background-color 改成透明(选择器只认
「body 直接子元素 + 无 class/id + 空元素 + 内联式里有 pointer-events 与
visibility」),主进程就会收到 rgba(0, 0, 0, 0),覆盖层不再画底,
露出下面同一条标题栏。按钮字形颜色不受影响(symbolColor 读的是 color)。
实现要点
三层结构(缺一不可):
-
氛围光背景 —
html[data-dsh-glass] body::before上一层固定定位渐变。 -
半透明表面 — 通过官方
ctx.theme.overrideTokens(source, tokens)把--dsw-alias-bg-*/--dsw-specific-*等表面 token 改成color-mix(in srgb, N%, transparent)。走官方 token 层,可撤销、 随主题切换自动重算明暗两套值。表面与面板两套表,各用各的百分比。 -
背景模糊 — 给主要列/面板加
backdrop-filter: blur() saturate(), 并把--dsw-menu-backdrop-filter/--dsw-mask-blur统一到同一档位。 -
浮层模糊 — 三列之上的浮层(已归档面板、桌宠费用弹层、宿主
_panel/_dialog/_float_、[role=dialog][aria-modal])单独挂backdrop-filter,用--dsh-glass-overlay-blur(由paint()按overlayBlur夹到 56px 上限)。原因:这些窗口原本多数没有模糊, 底色被panelOpacity压薄后背后正文会直接透出来,字压字看不清。两个必须记住的排除项(删了会出问题):
OUqwTW_panel(右栏 dockkit 定位容器)必须排除:backdrop-filter会为该元素建立「fixed 后代的包含块」,挂上去会让内部position:fixed的浮窗整体错位。_panelRow/_panelActive/_panelButton/_panelHint等行内小控件 也要排除:它们是子串匹配的误命中,背后没有要模糊的内容。
-
发送框 / 任务卡片模糊 — 发送框(
[data-composer-card])、审批卡、 提问卡、文件卡、账号提示卡,以及任务看板的内部卡片 (EBLgjq_task/EBLgjq_member)单独挂backdrop-filter, 用--dsh-glass-composer-blur(按composerBlur夹到 40px 上限)。- 发送框用
[data-composer-card]而不是哈希类名.RlGAzG_card: 那是宿主自己写、并在 4 处内部逻辑里当锚点用的稳定 data 属性。 - 不要把模糊挂到
--dsw-specific-input-majortoken 上:它还被 ImageLightbox 的.image/.close消费,会把图片查看器里的图片本身 变成毛玻璃。
- 发送框用
-
composer 铺底模糊(漏了就会「间隙透字」) — 宿主的
[data-composer-seat]自己铺了一层渐变底,基色是--dsw-alias-bg-base(骨架档,由opacity控制)。卡片之间那圈 间隙就是这层,没有任何元素给它挂模糊。所以:html[data-dsh-glass] [data-composer-seat] { backdrop-filter: blur(var(--dsh-glass-composer-blur, 24px)) saturate(...); }只加模糊,不加颜色。 曾经在这块写过
background: color-mix(白 N%), 把发送框的--dsw-specific-input-major顶掉了 —— 用户拖「表面不透明度」 时发送框不跟着变,看到的就是「发送框不半透明了」。paint()还会主动removeProperty清掉旧版本留下的行内background与--dsh-glass-composer-fill-*(HMR 只换 JS 不刷页面,行内样式会残留)。 -
发送框零白底(CSS 第 4e 段) — 发送框身上原本还有两层白:
composerSeat的渐变底(--dsw-alias-bg-base)和卡片自己的--dsw-specific-input-major;opacity=30时复合约 51%,看起来就是白卡。 现在两层都清成透明:html[data-dsh-glass] [data-composer-seat], html[data-dsh-glass] [data-composer-card] { background: transparent; }清
background而不是改 token:--dsw-specific-input-major还被审批卡 / 提问卡 / 文件卡消费,改 token 会把它们一起弄透明。代价是发送框只剩边框 与投影,输入文字直接压在模糊背景上。 -
代码块钉回原生基色(CSS 第 4f 段) — 代码要逐字读,不能半透明。 shiki 给
写的是行内background-color: var(--shiki-background), 所以改变量值最干净;再加一条pre { background-color: … !important }兜底不走 shiki 的:html[data-dsh-glass] body { --shiki-background: var(--dsw-static-neutral-bluish-50); } html[data-dsh-glass] pre { background-color: var(--dsw-static-neutral-bluish-50) !important; }深色分支用
--dsw-static-neutral-bluish-900。 -
交付物 / 文件改动卡片接到预览档(CSS 第 4g 段) — 宿主的
.kuvljq_card和.jmg6Sq_output用的是--dsw-alias-bg-layer-1(在SURFACES骨架档 = 表面不透明度),于是任务结尾那张文件预览卡 几乎全透、代码字糊在背景上。这两类是「要读字」的内容,改到previewOpacity这一档:html[data-dsh-glass] [class*="kuvljq_card"], html[data-dsh-glass] [class*="jmg6Sq_output"] { background-color: var(--dsh-glass-preview-light, var(--dsh-glass-panel-light, transparent)); } -
文档预览根节点自己补底色(CSS 第 4h 段) —
documentpreview的TdjKvG_preview/TdjKvG_body/TdjKvG_textDocument自己不铺底, 指望外层_tabHost_。预览浮动或内嵌在 diff 里时外层是透明的,于是 整块预览全透,只有 diff 行自己的增删底色还在(没底色的上下文行直接 透出氛围光)。给预览根补previewOpacity那一档:html[data-dsh-glass] [class*="TdjKvG_preview"], html[data-dsh-glass] [class*="TdjKvG_body"], html[data-dsh-glass] [class*="BMNXqa_frame"] { background-color: var(--dsh-glass-preview-light, var(--dsh-glass-panel-light, transparent)); }_body是必需的:侧边栏预览根上带_preview;而「鼠标悬停文件 → 看代码」 那个浮层只挂内部节点(_body),没有_preview根 —— 只写根会漏掉悬停浮层。住在 tabHost 里时会叠成 panel + preview(70% + 85% ≈ 95%),可读性优先。
-
颜色变量必须写在
body上,不能写:root(2026-10 真·根因) — 宿主的静态调色板定义在body{--dsw-static-neutral-bluish-*}, 而自定义属性里的var()是在声明它的元素上求值的。写成document.documentElement.style.setProperty('--x', 'color-mix(… var(--dsw-static-…) …)')时,:root上找不到那个 token → 整条自定义属性变成 guaranteed-invalid → 所有var(--x)静默失效 →background-color回退成 transparent。const colorHost = document.body ?? root; // 颜色变量写这里 colorHost.style.setProperty('--dsh-glass-panel-light', …); colorHost.style.setProperty('--dsh-glass-preview-light', …); // 模糊这种纯长度变量写 :root 没问题 root.style.setProperty('--dsh-glass-blur', …);这条不修,第 5 段(右栏 / tabHost)和第 4g / 4h 段(改动卡片 / 文档预览) 的底色永远不会出现,跟选择器写得对不对无关。
-
悬停预览浮层是第三套实现(CSS 第 4i 段) — 「鼠标悬停文件 → 看代码」 弹出的浮层不是文档预览,也不是改动卡片,而是 primitives 的
HoverCardvariant="preview":/* 宿主:web-frontend/dist/assets/index-*.css */ ._preview_38jqx_52 { background: var(--dsw-alias-bg-layer-1); … } /* SURFACES 20% */所以它同样全透,而且前两