dsh-deepseek-style-theme 2.0.82 → 2.0.85
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/lib/client.js +341 -103
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# DeepSeek Style Theme
|
|
1
|
+
# DeepSeek Style Theme
|
|
2
2
|
|
|
3
3
|
为 DeepSeek Harness Web GUI 复刻 DeepSeek 官网视觉的主题皮肤插件。
|
|
4
4
|
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
- **标题栏**:默认透明,悬停变毛玻璃;栏内按钮统一为真圆角(覆盖产品全局 corner-shape:superellipse 造成的方角观感),会话标题小胶囊化
|
|
25
25
|
- **悬停动画**:中/E、「打开方式」、「对话 / 轨迹」标签悬停时上浮 + 品牌色辉光;中/E 短标签「中/E」平滑展开为「中文/EN」(max-width 过渡,非 display 硬切);「打开方式」展开菜单时箭头翻转,切换标签时选中项弹出
|
|
26
26
|
- **DeepSeek 品牌链接**:点击侧边栏 DeepSeek 标识跳转 `https://www.deepseek.com/`
|
|
27
|
-
- **轨迹视图**:「对话 /
|
|
27
|
+
- **轨迹视图**:「对话 / 轨迹」标签在标题栏悬停(`.dshome-swap`)或轨迹视图存在时显示,**不是常驻**——会话视图下实测 `[class*="wSkVaW_tabs"]` 默认 `display:none`(2.0.84 按实机报告 B4 勘误)
|
|
28
28
|
- **运行中子代理面板**:有子代理启动时,右下角浮出玻璃面板,实时列出正在运行的子代理——名称、已运行时长、token 用量与流动进度条;点击条目直接跳转到该子代理会话,可折叠收起
|
|
29
29
|
- **跨平台**:打开工作区目录时按系统调用文件管理器(Windows Explorer / macOS Finder / Linux 默认文件管理器)
|
|
30
30
|
- **模型目录同步(1.40.0+)**:每次插件启动向 DeepSeek 端点询问一次模型列表,与 `llm-deepseek` 目录比对后按需对齐——「选择器里有哪些模型」由接口说了算,但**绝不臆造能力位、绝不清空你的条目**,移除项会逐个记日志;可用 `catalogSync: auto | add | off` 控制(详见下文)
|
|
@@ -318,7 +318,8 @@ dsh plugin --profile web remove dsh-deepseek-style-theme
|
|
|
318
318
|
| `symptom-probe-selftest.mjs` | 上面那份探针的自测(19 项):在一个合成页面上跑(我们自己的 canvas + 带超宽子元素的消息列 + 每 60 ms 改一次 transform 的卡片 + CDP 派发真实鼠标移动),断言环境报告的字段形状、`run()` 的采样与逐字段 diff、五个开关的实际效果(含 `clip` 在 `overflow-y:auto` 旁会计算成 `hidden` 这条 CSS Overflow 3 规则) | `chrome --headless=new --remote-debugging-port=9333 --user-data-dir=%TEMP%\dsh-probe-chrome about:blank`,然后 `node tools/symptom-probe-selftest.mjs` |
|
|
319
319
|
| `bridge-smoke.mjs` | 宿主端私有 RPC 通道:协议、错误路径,以及完整围栏(非回环对端 / 缺 `Host` / DNS rebinding / 跨站来源 / `Origin` 不匹配 / 非 JSON 类型 / UNC 路径 / `file.*` 端点识别与校验,24 项检查;加 `--open` 为 25 项,会真的用系统默认应用打开一个临时文件,**会在桌面弹出窗口**,默认不启用) | `node tools/bridge-smoke.mjs [已安装的 lib/index.js] [--open]` |
|
|
320
320
|
| `dstt-schema-smoke.mjs` | DSTT 设置 schema 与写入路径的一致性:**每个能写进 `settings.yaml` 的值都必须过 `register()` 的校验**(1.43.2 那次 schema 缺 `wide` 的发布阻塞就靠它守)。含背景方式四档的往返、未知 id 拒写、自定义背景的归一化(控制字符→空格、去首尾、长度上限)、动态背景开关的读写与布尔回读,以及「客户端枚举 = 宿主枚举」的跨文件检查(29 项) | `node tools/dstt-schema-smoke.mjs [lib/index.js]` |
|
|
321
|
-
| `bg-recipes-smoke.mjs` | 客户端源码层的数据与契约断言(2.0.73+,随每版增补):① 必须含纯白 + 近白、② 只有两个颜色参与、`bold` 浅色无 `#FFFFFF`、③ 的基底/让位规则与选择器守卫、壁纸引擎标记与「提示而非自动切换」、桌面壁纸端点与 `WallpaperStyle` 映射、`darkSync` 作用域、两条反引号守卫(整表扫描)、发送按钮 hover 不再位移、流场 16F
|
|
321
|
+
| `bg-recipes-smoke.mjs` | 客户端源码层的数据与契约断言(2.0.73+,随每版增补):① 必须含纯白 + 近白、② 只有两个颜色参与、`bold` 浅色无 `#FFFFFF`、③ 的基底/让位规则与选择器守卫、壁纸引擎标记与「提示而非自动切换」、桌面壁纸端点与 `WallpaperStyle` 映射、`darkSync` 作用域、两条反引号守卫(整表扫描)、发送按钮 hover 不再位移、流场 16F 探测与回退、悬停倾斜的抬升 1.01 与 `TILT_CONTROLS` 不含文字录入、进入控件时冻结而非释放、释放只在离开卡片矩形、两套玻璃配方都生效(2.0.82)、横向裁切覆盖消息列/对话框/转写区、**8 个 CSS 契约名必须已声明 + 模板插值不得早于 const 声明(2.0.84:`ATTR` 被误删、`USER_BUBBLE` 提前插值都属于这一类,`node --check` 看不见)**(73 项) | `node tools/bg-recipes-smoke.mjs [lib/client.js]` |
|
|
322
|
+
| `parse-smoke.mjs` | **每个文件都必须能解析**(2.0.84+):`lib/` 与 `tools/` 下每个 `.js/.mjs` 过一遍 `node --check`。看着多余,其实不是——已经有三次"注释里的反引号把模板字符串提前结束"的事故(`lib/client.js` 两次、`tools/gui-probe.mjs` 一次,后者 Node 的报错还不带文件名)。纯解析门,不做 lint | `node tools/parse-smoke.mjs` |
|
|
322
323
|
| `fluid-precision-test.mjs` | **流场存储精度对照实验**(2.0.81+):把 `lib/client.js` 里的真实 `VERTEX_SHADER` / `FLOW_SHADER` 抠出来,在无头 Chrome 里对同一场景各跑 240 步,分别存 RGBA8 与 RGBA16F,读回统计"取值分布 / 落在 1/255 网格上的层级 / max / mean",并判定 16F 是否真的摆脱了量化(5 项)。**需要 Chromium 系浏览器**,且 Chrome 在受限进程里起不来,请从普通终端跑 | `node tools/fluid-precision-test.mjs [--chrome <路径>] [--keep]` |
|
|
323
324
|
| `catalog-sync-smoke.mjs` | 模型目录同步的全部分支:一致 / 可描述漂移 / 聚合网关目录 / 只追加 / `off` / baseURL 解析顺序 / 无密钥 / 端点故障 / 命名空间未就绪 / 版本冲突(27 项检查,全用替身,无需凭据与网络) | `node tools/catalog-sync-smoke.mjs [已安装的 lib/index.js]` |
|
|
324
325
|
| `catalog-sync-live.mjs` | 用**真实端点 + 真实密钥**跑一遍同步:A 场景(目录已一致)应 0 写入,B 场景(人为制造漂移)应恰好 1 次写入并复原条目;写入被拦下,不落盘 | `DEEPSEEK_API_KEY=... node tools/catalog-sync-live.mjs [--profile web] [--drift-id deepseek-v4-pro]` |
|
package/lib/client.js
CHANGED
|
@@ -499,6 +499,15 @@ void main() {
|
|
|
499
499
|
let previous = 0;
|
|
500
500
|
const step = 1e3 / 30;
|
|
501
501
|
const frame = (now) => {
|
|
502
|
+
// Self-cleanup: a canvas that has left the DOM must not keep
|
|
503
|
+
// simulating. This is the backstop for a mount that threw midway --
|
|
504
|
+
// B1 in the 2026-09-16 report leaked one full-resolution simulator per
|
|
505
|
+
// 动态背景 flip because the disposer never came back.
|
|
506
|
+
if (!canvas.isConnected) {
|
|
507
|
+
running = false;
|
|
508
|
+
cancelAnimationFrame(raf);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
502
511
|
raf = requestAnimationFrame(frame);
|
|
503
512
|
if (now - previous < step) return;
|
|
504
513
|
previous = now - (now - previous) % step;
|
|
@@ -690,6 +699,21 @@ void main() {
|
|
|
690
699
|
var FILTER_ID = "dshome-glass-dispersion";
|
|
691
700
|
var TINT_ID = "dshome-glass-dispersion-tint";
|
|
692
701
|
var REFRACT_ID = "dshome-glass-dispersion-refract";
|
|
702
|
+
/**
|
|
703
|
+
* The opt-in marker on <html> that the stylesheets chain `url(#…)` behind.
|
|
704
|
+
*
|
|
705
|
+
* This declaration was DELETED in d862487 by accident and stayed missing
|
|
706
|
+
* from 1.43.6 through 2.0.82, which is the whole of B1 in the 2026-09-16 bug
|
|
707
|
+
* report: `startGlassDispersion()` threw a ReferenceError on its first line
|
|
708
|
+
* (`root.hasAttribute(ATTR)`), so the refraction never mounted on any
|
|
709
|
+
* version -- and because `startAmbient()` calls into this function, the
|
|
710
|
+
* ambient mount died with it, taking the disposer with it (the 动态背景
|
|
711
|
+
* toggle stopped unmounting, and each flip leaked a full-resolution fluid
|
|
712
|
+
* simulator). Two lessons are encoded here: the constant is declared next to
|
|
713
|
+
* the ids it belongs to, and the ambient mount no longer swallows a throw
|
|
714
|
+
* silently (see its try/catch and the caller's console.error).
|
|
715
|
+
*/
|
|
716
|
+
var ATTR = "data-dshome-dispersion";
|
|
693
717
|
var TALL_FILTER_ID = "dshome-glass-dispersion-tall";
|
|
694
718
|
var TALL_TINT_ID = "dshome-glass-dispersion-tall-tint";
|
|
695
719
|
var TALL_REFRACT_ID = "dshome-glass-dispersion-tall-refract";
|
|
@@ -1222,10 +1246,47 @@ body[data-dshome-dark] .dshome-sa-item.dshome-sa-stopping .dshome-sa-bar{backgro
|
|
|
1222
1246
|
@media (max-width:760px){.dshome-sa{right:10px;bottom:10px;width:min(286px,calc(100vw - 20px))}}
|
|
1223
1247
|
`;
|
|
1224
1248
|
|
|
1249
|
+
// ── USER_BUBBLE: the one selector both recipes glass user bubbles with ────
|
|
1250
|
+
// Declared HERE, above PATCH_BLOCKS and GLASS_CSS, because both interpolate
|
|
1251
|
+
// it while they are being built: as a `const` declared further down it threw
|
|
1252
|
+
// `Cannot access 'USER_BUBBLE' before initialization` the moment the factory
|
|
1253
|
+
// ran, which took the entire theme down (no style tags at all). Same failure
|
|
1254
|
+
// class as B1 -- an identifier that resolves to nothing at activation time --
|
|
1255
|
+
// and the reason tools/factory-smoke.mjs exists.
|
|
1256
|
+
//
|
|
1257
|
+
// Two properties are load-bearing, and both were learned the hard way
|
|
1258
|
+
// (B5 / B2 of the 2026-09-16 bug report):
|
|
1259
|
+
//
|
|
1260
|
+
// 1. It is ONE compound selector — no top-level comma. Used as a prefix in
|
|
1261
|
+
// `html[data-dshome-glass="liquid"] :is(USER_BUBBLE){…}` rules, a comma
|
|
1262
|
+
// list is a trap: the prefix binds only to the branch it touches and every
|
|
1263
|
+
// later branch escapes the gate entirely. That is exactly what happened.
|
|
1264
|
+
// A second branch, meant to cover a just-typed bubble, also matched the
|
|
1265
|
+
// product's TOOLTIP (`_bubble_<hash>_<n>`, `position:fixed`), and the
|
|
1266
|
+
// theme's `position:relative` turned that floating tooltip into an
|
|
1267
|
+
// in-flow element — 78px of width inside the composer, 48px inside a
|
|
1268
|
+
// message action bar — so hovering the send or copy button shoved the
|
|
1269
|
+
// button out from under the cursor, the tooltip unmounted, the button
|
|
1270
|
+
// came back, and the loop fed itself at up to 60Hz. That WAS the user's
|
|
1271
|
+
// 「按钮跳动」. Every usage is wrapped in `:is(…)` now, so a future second
|
|
1272
|
+
// branch cannot escape the gate again.
|
|
1273
|
+
// 2. The tooltip must be excluded by SHAPE. Message bubbles are
|
|
1274
|
+
// `<hash>_bubble` (`Sixlwa_bubble`, `oRe1gG_bubble`); the tooltip is the
|
|
1275
|
+
// leading-underscore form `_bubble_…`, which is what the first `:not()`
|
|
1276
|
+
// keys on. `:not([role="tooltip"])` is belt-and-braces for the day the
|
|
1277
|
+
// product starts labelling it.
|
|
1278
|
+
//
|
|
1279
|
+
// The gap the deleted branch covered — a message typed while the agent is
|
|
1280
|
+
// still working sits outside any flow-kind container until the turn picks it
|
|
1281
|
+
// up — is knowingly left unglazed for that moment: a transient unglazed
|
|
1282
|
+
// bubble is a cosmetic wait, a teleporting button is not.
|
|
1283
|
+
const USER_BUBBLE = ':is([data-chat-flow-kind="user"],[data-chat-flow-kind="steering"]) [class*="bubble" i]:not([class*="_bubble_"]):not([role="tooltip"])';
|
|
1284
|
+
|
|
1225
1285
|
// PATCH_BLOCKS: every rule group that targets product build-time class
|
|
1226
1286
|
// names. `anchor` is the class that must exist in the DOM before the
|
|
1227
1287
|
// block is injected; each block degrades independently when its surface
|
|
1228
|
-
// renames in a product rebuild
|
|
1288
|
+
// renames in a product rebuild, and `anchor` may be a list of
|
|
1289
|
+
// alternatives when a surface has already been renamed once.
|
|
1229
1290
|
const PATCH_BLOCKS = [
|
|
1230
1291
|
// Frame / sidebar / details columns.
|
|
1231
1292
|
{ anchor: ".pI_x6G_frame", css: `
|
|
@@ -1314,7 +1375,7 @@ body[data-dshome-dark] .wSkVaW_tabActive{box-shadow:0 3px 12px color-mix(in srgb
|
|
|
1314
1375
|
/* Header chips: the product's global corner-shape:superellipse(1.5) squares off
|
|
1315
1376
|
even pill radii, so every control in the bar is restored to a true round end
|
|
1316
1377
|
(and the session-title crumb, 12px on a 28px box, becomes a real pill). */
|
|
1317
|
-
.wSkVaW_header .SVAs4q_label,.wSkVaW_header .cubgiG_seat,.wSkVaW_header .CAgGvG_split,.wSkVaW_header .dshome-locale,.wSkVaW_header [data-sidebar-right-expand],.wSkVaW_header
|
|
1378
|
+
.wSkVaW_header .SVAs4q_label,.wSkVaW_header .cubgiG_seat,.wSkVaW_header .CAgGvG_split,.wSkVaW_header .dshome-locale,.wSkVaW_header [data-sidebar-right-expand],.wSkVaW_header [class*="_moreButton"],.wSkVaW_header .wSkVaW_crumb{corner-shape:round}
|
|
1318
1379
|
.wSkVaW_header .wSkVaW_crumb{border-radius:999px}
|
|
1319
1380
|
/* Open-in-app split button: hover lift with a brand tint, and the chevron turns
|
|
1320
1381
|
over while its menu is open. */
|
|
@@ -1328,13 +1389,15 @@ body[data-dshome-dark] .wSkVaW_tabActive{box-shadow:0 3px 12px color-mix(in srgb
|
|
|
1328
1389
|
body[data-dshome-dark] .wSkVaW_header .CAgGvG_split:hover{border-color:color-mix(in srgb,var(--ds-brand) 52%,transparent);background:color-mix(in srgb,var(--ds-brand) 16%,transparent);box-shadow:0 4px 16px color-mix(in srgb,var(--ds-brand) 30%,transparent)}
|
|
1329
1390
|
body[data-dshome-dark] .wSkVaW_header .CAgGvG_split:hover .CAgGvG_main,body[data-dshome-dark] .wSkVaW_header .CAgGvG_split:hover .CAgGvG_chevron{background:color-mix(in srgb,var(--ds-brand) 24%,transparent);color:#fff}
|
|
1330
1391
|
` },
|
|
1331
|
-
// Session-log button (hover expand).
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1392
|
+
// Session-log button (hover expand). The product renamed it to
|
|
1393
|
+
// `nL4_yW_moreButton` on the current build (B2, 2026-09-16), so anchor and
|
|
1394
|
+
// rules both key on the suffix, the way `[class*="_fade" i]` already does.
|
|
1395
|
+
{ anchor: '[class*="_moreButton"]', css: `
|
|
1396
|
+
:is([class*="_moreButton"]){min-width:0!important;width:28px;height:28px;padding:0!important;border-radius:50%!important;border:none!important;background:color-mix(in srgb,var(--ds-brand) 8%,transparent)!important;color:var(--ds-brand)!important;overflow:hidden;transition:width .2s ease,padding .2s ease,border-radius .2s ease}
|
|
1397
|
+
:is([class*="_moreButton"]) span{max-width:0;opacity:0;overflow:hidden;transition:max-width .2s ease,opacity .2s ease}
|
|
1398
|
+
.wSkVaW_header:hover :is([class*="_moreButton"]){width:auto;padding:0 12px!important;border-radius:18px!important}
|
|
1399
|
+
.wSkVaW_header:hover :is([class*="_moreButton"]) span{max-width:90px;opacity:1}
|
|
1400
|
+
body[data-dshome-dark] :is([class*="_moreButton"]){color:var(--ds-brand-light)!important}
|
|
1338
1401
|
` },
|
|
1339
1402
|
// Glass cards with spinning gradient border — upgraded to liquid glass:
|
|
1340
1403
|
// specular top edge, saturated blur, brighter inner rim.
|
|
@@ -1386,17 +1449,21 @@ body[data-dshome-dark] .uV2eYG_card::before{background:conic-gradient(from var(-
|
|
|
1386
1449
|
{ anchor: ".wSkVaW_scrollBody", css: `
|
|
1387
1450
|
[class*="scrollBody"],[class*="composerSeat"],[class*="viewArea"]{overflow-x:clip}
|
|
1388
1451
|
` },
|
|
1389
|
-
// Empty-state headline + preview badge.
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1452
|
+
// Empty-state headline + preview badge. The product renamed
|
|
1453
|
+
// `pXSMma_headlineText` to `pXSMma_headline` (B2, 2026-09-16), which
|
|
1454
|
+
// silently killed this block and left the headline at 26px/500 with no
|
|
1455
|
+
// gradient. Keyed on the suffix now, along with the two brand marks that
|
|
1456
|
+
// live in the same family.
|
|
1457
|
+
{ anchor: '[class*="_headline"]', css: `
|
|
1458
|
+
:is([class*="_headline"]){font-size:34px;line-height:42px;font-weight:700;letter-spacing:-.01em;background:linear-gradient(120deg,#152443 0%,var(--ds-brand-deep) 55%,var(--ds-brand) 100%);-webkit-background-clip:text;background-clip:text;color:transparent}
|
|
1459
|
+
body[data-dshome-dark] :is([class*="_headline"]){background:linear-gradient(120deg,#fff 0%,var(--ds-brand-light) 60%,var(--ds-brand) 100%);-webkit-background-clip:text;background-clip:text}
|
|
1460
|
+
:is([class*="_previewBadge"]){border-radius:999px!important;background:color-mix(in srgb,var(--ds-brand) 10%,transparent)!important;border:1px solid color-mix(in srgb,var(--ds-brand) 25%,transparent)!important;color:var(--ds-brand)!important}
|
|
1461
|
+
:is([class*="_fish"]){color:var(--ds-brand)!important}
|
|
1395
1462
|
` },
|
|
1396
1463
|
// Chat bubbles — liquid glass panes.
|
|
1397
|
-
{ anchor: ".
|
|
1398
|
-
|
|
1399
|
-
body[data-dshome-dark]
|
|
1464
|
+
{ anchor: ".wSkVaW_scrollBody", css: `
|
|
1465
|
+
:is(${USER_BUBBLE}){background:rgba(255,255,255,.6)!important;-webkit-backdrop-filter:blur(16px);backdrop-filter:blur(16px);border:1px solid rgba(0,0,0,.05);box-shadow:0 4px 16px rgba(13,30,60,.06)}
|
|
1466
|
+
body[data-dshome-dark] :is(${USER_BUBBLE}){background:rgba(21,42,86,.65)!important;border:1px solid rgba(255,255,255,.1);box-shadow:0 4px 16px rgba(0,0,0,.25)}
|
|
1400
1467
|
` },
|
|
1401
1468
|
// Trajectory view bar.
|
|
1402
1469
|
{ anchor: ".fV0t5q_root", css: `
|
|
@@ -1470,7 +1537,7 @@ body[data-dshome-dark]{
|
|
|
1470
1537
|
}
|
|
1471
1538
|
html .hHd-Xa_root::before,
|
|
1472
1539
|
html .uV2eYG_card::after,
|
|
1473
|
-
html
|
|
1540
|
+
html :is(${USER_BUBBLE}),
|
|
1474
1541
|
html .fV0t5q_root,
|
|
1475
1542
|
html [data-composer-card],
|
|
1476
1543
|
html .VOzbGW_panel,
|
|
@@ -1489,27 +1556,27 @@ html .dshome-file-menu{
|
|
|
1489
1556
|
--dshome-spec-x/y (written per element by startSpecularParallax). */
|
|
1490
1557
|
html [data-dshome-spot]:hover{--dshome-glass-spec:1;}
|
|
1491
1558
|
html [data-composer-card]:hover{--dshome-glass-spec:1;--dshome-glass-sheen:.12;}
|
|
1492
|
-
html
|
|
1559
|
+
html :is(${USER_BUBBLE}):hover,html .dshome-sa:hover,html .dshome-file-menu:hover{
|
|
1493
1560
|
background-color:rgb(255 255 255 / calc(var(--dshome-glass-sheen) + .16))!important;
|
|
1494
1561
|
}
|
|
1495
|
-
body[data-dshome-dark]
|
|
1562
|
+
body[data-dshome-dark] :is(${USER_BUBBLE}):hover,
|
|
1496
1563
|
body[data-dshome-dark] .dshome-sa:hover,
|
|
1497
1564
|
body[data-dshome-dark] .dshome-file-menu:hover{
|
|
1498
1565
|
background-color:rgb(255 255 255 / calc(var(--dshome-glass-sheen) + .06))!important;
|
|
1499
1566
|
}
|
|
1500
1567
|
html[data-dshome-dispersion] .hHd-Xa_root::before,
|
|
1501
|
-
html[data-dshome-dispersion]
|
|
1568
|
+
html[data-dshome-dispersion] :is(${USER_BUBBLE}),
|
|
1502
1569
|
html[data-dshome-dispersion] .VOzbGW_panel{
|
|
1503
1570
|
-webkit-backdrop-filter:url(#dshome-glass-dispersion) blur(var(--dshome-glass-blur)) saturate(var(--dshome-glass-saturate))!important;
|
|
1504
1571
|
backdrop-filter:url(#dshome-glass-dispersion) blur(var(--dshome-glass-blur)) saturate(var(--dshome-glass-saturate))!important;
|
|
1505
1572
|
}
|
|
1506
1573
|
@supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){
|
|
1507
|
-
html .hHd-Xa_root::before,html .uV2eYG_card::after,html
|
|
1574
|
+
html .hHd-Xa_root::before,html .uV2eYG_card::after,html :is(${USER_BUBBLE}),html [data-composer-card],html .VOzbGW_panel,html .dshome-sa,html .dshome-file-menu{
|
|
1508
1575
|
background-color:rgba(255,255,255,.86)!important;
|
|
1509
1576
|
}
|
|
1510
1577
|
body[data-dshome-dark] .hHd-Xa_root::before,
|
|
1511
1578
|
body[data-dshome-dark] .uV2eYG_card::after,
|
|
1512
|
-
body[data-dshome-dark]
|
|
1579
|
+
body[data-dshome-dark] :is(${USER_BUBBLE}),
|
|
1513
1580
|
body[data-dshome-dark] [data-composer-card],
|
|
1514
1581
|
body[data-dshome-dark] .VOzbGW_panel,
|
|
1515
1582
|
body[data-dshome-dark] .dshome-sa,
|
|
@@ -1560,14 +1627,9 @@ html[data-dshome-dispersion] .VOzbGW_panel{
|
|
|
1560
1627
|
// a transcript can contain ONLY steering bubbles — anchoring the selector
|
|
1561
1628
|
// on "user" alone silently skipped every bubble on the page.
|
|
1562
1629
|
//
|
|
1563
|
-
//
|
|
1564
|
-
//
|
|
1565
|
-
//
|
|
1566
|
-
// two anchored selectors miss it and it renders unglazed until the turn
|
|
1567
|
-
// consumes it. Rather than enumerate the container it lands in, this side
|
|
1568
|
-
// takes any bubble that is NOT part of an assistant step — which is the
|
|
1569
|
-
// only place a bubble must not be glassed.
|
|
1570
|
-
const LIQUID_BUBBLE = ':is([data-chat-flow-kind="user"],[data-chat-flow-kind="steering"]) [class*="bubble" i],[class*="bubble" i]:not([data-chat-flow-kind="assistant-step"] *)';
|
|
1630
|
+
// (USER_BUBBLE itself is declared above PATCH_BLOCKS: this sheet and the
|
|
1631
|
+
// patch blocks both interpolate it, so it has to exist before either is
|
|
1632
|
+
// built.)
|
|
1571
1633
|
const LIQUID_CSS = `
|
|
1572
1634
|
@property --dshome-liquid-sweep{syntax:"<angle>";inherits:false;initial-value:0deg}
|
|
1573
1635
|
@keyframes dshome-liquid-sweep{from{--dshome-liquid-sweep:0deg}to{--dshome-liquid-sweep:360deg}}
|
|
@@ -1635,7 +1697,7 @@ body[data-dshome-dark]{
|
|
|
1635
1697
|
html[data-dshome-glass="liquid"] body .hHd-Xa_root::before,
|
|
1636
1698
|
html[data-dshome-glass="liquid"] body [data-composer-card],
|
|
1637
1699
|
html[data-dshome-glass="liquid"] body .wSkVaW_header:hover::before,
|
|
1638
|
-
html[data-dshome-glass="liquid"] body ${
|
|
1700
|
+
html[data-dshome-glass="liquid"] body :is(${USER_BUBBLE}){
|
|
1639
1701
|
background-color:var(--dshome-liquid-fill)!important;
|
|
1640
1702
|
background-image:
|
|
1641
1703
|
radial-gradient(360px circle at calc(50% + var(--dshome-spec-x,0) * 50%) calc(50% + var(--dshome-spec-y,0) * 50%), rgb(255 255 255 / calc(var(--dshome-glass-spec) * .24)), transparent 60%),
|
|
@@ -1688,8 +1750,11 @@ html[data-dshome-glass="liquid"] body .hHd-Xa_root::before{
|
|
|
1688
1750
|
radial-gradient(360px circle at calc(50% + var(--dshome-spec-x,0) * 50%) calc(50% + var(--dshome-spec-y,0) * 50%), rgb(255 255 255 / calc(var(--dshome-glass-spec) * .24)), transparent 60%),
|
|
1689
1751
|
linear-gradient(180deg, rgba(255,255,255,var(--dshome-liquid-sheen)), rgba(255,255,255,.015) 34%, rgba(255,255,255,0))!important;
|
|
1690
1752
|
}
|
|
1691
|
-
/* The message bubble is the one surface that is not already positioned.
|
|
1692
|
-
|
|
1753
|
+
/* The message bubble is the one surface that is not already positioned. The
|
|
1754
|
+
tooltip is not in this list any more -- that was B5: making a
|
|
1755
|
+
position:fixed tooltip relative turned it into an in-flow 78px block that
|
|
1756
|
+
shoved the send button out from under the cursor. See USER_BUBBLE. */
|
|
1757
|
+
html[data-dshome-glass="liquid"] :is(${USER_BUBBLE}){
|
|
1693
1758
|
position:relative;
|
|
1694
1759
|
transition:background-color .18s ease,box-shadow .18s ease;
|
|
1695
1760
|
}
|
|
@@ -1701,10 +1766,12 @@ html[data-dshome-glass="liquid"] [data-composer-card]{
|
|
|
1701
1766
|
transition:transform .24s cubic-bezier(.2,.8,.2,1),background-color .18s ease,box-shadow .18s ease!important;
|
|
1702
1767
|
will-change:transform;
|
|
1703
1768
|
}
|
|
1704
|
-
html[data-dshome-glass="liquid"] ${
|
|
1769
|
+
html[data-dshome-glass="liquid"] :is(${USER_BUBBLE}):hover{
|
|
1705
1770
|
background-color:rgb(255 255 255 / .18)!important;
|
|
1706
1771
|
}
|
|
1707
|
-
|
|
1772
|
+
/* Liquid's dark hover used to sit OUTSIDE the gate, so it also repainted the
|
|
1773
|
+
frosted recipe's bubbles with liquid's values. Gated like its siblings. */
|
|
1774
|
+
html[data-dshome-glass="liquid"] body[data-dshome-dark] :is(${USER_BUBBLE}):hover{
|
|
1708
1775
|
background-color:rgb(255 255 255 / .10)!important;
|
|
1709
1776
|
}
|
|
1710
1777
|
html[data-dshome-glass="liquid"] body .hHd-Xa_root::after{
|
|
@@ -1741,7 +1808,7 @@ html[data-dshome-glass="liquid"] .hHd-Xa_root::after{
|
|
|
1741
1808
|
continuous background a horizontal displacement has nothing to show, which
|
|
1742
1809
|
the user accepted. */
|
|
1743
1810
|
html[data-dshome-dispersion][data-dshome-glass="liquid"] body .wSkVaW_header:hover::before,
|
|
1744
|
-
html[data-dshome-dispersion][data-dshome-glass="liquid"] body ${
|
|
1811
|
+
html[data-dshome-dispersion][data-dshome-glass="liquid"] body :is(${USER_BUBBLE}){
|
|
1745
1812
|
-webkit-backdrop-filter:url(#dshome-glass-dispersion) blur(var(--dshome-liquid-blur)) saturate(var(--dshome-liquid-saturate))!important;
|
|
1746
1813
|
backdrop-filter:url(#dshome-glass-dispersion) blur(var(--dshome-liquid-blur)) saturate(var(--dshome-liquid-saturate))!important;
|
|
1747
1814
|
}
|
|
@@ -1798,56 +1865,90 @@ html[data-dshome-blur="off"] body{
|
|
|
1798
1865
|
html[data-dshome-glass="liquid"] body .hHd-Xa_root::before,
|
|
1799
1866
|
html[data-dshome-glass="liquid"] body [data-composer-card],
|
|
1800
1867
|
html[data-dshome-glass="liquid"] body .wSkVaW_header:hover::before,
|
|
1801
|
-
html[data-dshome-glass="liquid"] body ${
|
|
1868
|
+
html[data-dshome-glass="liquid"] body :is(${USER_BUBBLE}){
|
|
1802
1869
|
background-color:rgba(255,255,255,.86)!important;
|
|
1803
1870
|
}
|
|
1804
1871
|
body[data-dshome-dark] .hHd-Xa_root::before,
|
|
1805
1872
|
body[data-dshome-dark] [data-composer-card],
|
|
1806
1873
|
body[data-dshome-dark] .wSkVaW_header:hover::before,
|
|
1807
|
-
body[data-dshome-dark] ${
|
|
1874
|
+
html[data-dshome-glass="liquid"] body[data-dshome-dark] :is(${USER_BUBBLE}){
|
|
1808
1875
|
background-color:rgba(13,30,60,.92)!important;
|
|
1809
1876
|
}
|
|
1810
1877
|
}
|
|
1811
1878
|
`;
|
|
1812
1879
|
|
|
1813
1880
|
/**
|
|
1814
|
-
* Inject `css`
|
|
1815
|
-
*
|
|
1816
|
-
*
|
|
1817
|
-
*
|
|
1818
|
-
*
|
|
1881
|
+
* Inject `css` while `anchor` (a selector, or a list of alternatives --
|
|
1882
|
+
* several product surfaces have been renamed at least once) matches the DOM.
|
|
1883
|
+
*
|
|
1884
|
+
* The wait is NOT time-boxed. It used to give up after 30s, which quietly
|
|
1885
|
+
* broke every surface the user opens late: B3 in the 2026-09-16 report
|
|
1886
|
+
* measured the same code injecting the session-row patch when the sidebar was
|
|
1887
|
+
* expanded at ~4s, and never injecting it when the sidebar was expanded after
|
|
1888
|
+
* 30s (the default is collapsed, so for most sessions the patch simply never
|
|
1889
|
+
* existed). Panels behind a click -- settings, the subagent panel, the
|
|
1890
|
+
* trajectory view -- were all in that set.
|
|
1891
|
+
*
|
|
1892
|
+
* The observer therefore lives as long as the plugin does, and the cost is
|
|
1893
|
+
* bounded instead of unbounded: re-checks are throttled to one per animation
|
|
1894
|
+
* frame, a batch that finds nothing schedules nothing, and the observer
|
|
1895
|
+
* disconnects the moment the block lands. A block that never lands costs one
|
|
1896
|
+
* `querySelector` per frame at worst, and logs once, with the anchors it tried.
|
|
1819
1897
|
*/
|
|
1820
1898
|
function installGuarded(anchor, css) {
|
|
1899
|
+
const selectors = Array.isArray(anchor) ? anchor : [anchor];
|
|
1900
|
+
const label = selectors.join('|');
|
|
1821
1901
|
let dispose = null;
|
|
1822
1902
|
const applyNow = () => {
|
|
1823
1903
|
if (dispose !== null) return true;
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1904
|
+
for (const selector of selectors) {
|
|
1905
|
+
if (document.querySelector(selector) === null) continue;
|
|
1906
|
+
dispose = injectStyle(css, 'patch' + selectors[0]);
|
|
1907
|
+
return true;
|
|
1908
|
+
}
|
|
1909
|
+
return false;
|
|
1827
1910
|
};
|
|
1828
1911
|
if (applyNow()) {
|
|
1829
1912
|
return () => { if (dispose !== null) dispose(); };
|
|
1830
1913
|
}
|
|
1831
|
-
let
|
|
1832
|
-
let
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1914
|
+
let queued = false;
|
|
1915
|
+
let missed = false;
|
|
1916
|
+
let mo = null;
|
|
1917
|
+
const schedule = () => {
|
|
1918
|
+
if (queued || dispose !== null) return;
|
|
1919
|
+
queued = true;
|
|
1920
|
+
try {
|
|
1921
|
+
requestAnimationFrame(() => {
|
|
1922
|
+
queued = false;
|
|
1923
|
+
if (applyNow() && mo !== null) {
|
|
1924
|
+
mo.disconnect();
|
|
1925
|
+
mo = null;
|
|
1926
|
+
}
|
|
1927
|
+
});
|
|
1928
|
+
} catch (error) {
|
|
1929
|
+
queued = false;
|
|
1930
|
+
}
|
|
1931
|
+
};
|
|
1932
|
+
mo = new MutationObserver(schedule);
|
|
1837
1933
|
try {
|
|
1838
1934
|
mo.observe(document.body, { childList: true, subtree: true });
|
|
1839
1935
|
} catch (error) {
|
|
1840
1936
|
mo = null;
|
|
1841
1937
|
}
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1938
|
+
// One diagnostic line per block, on first sight of a shell that has
|
|
1939
|
+
// rendered and still does not carry any of the alternatives.
|
|
1940
|
+
const announce = () => {
|
|
1941
|
+
if (missed || dispose !== null) return;
|
|
1942
|
+
missed = true;
|
|
1943
|
+
console.info('[' + PLUGIN_ID + '] patch waiting: none of ' + label + ' in the DOM yet (renamed product surface? the guard keeps watching)');
|
|
1944
|
+
};
|
|
1945
|
+
const timer = setTimeout(() => {
|
|
1946
|
+
if (missed || dispose !== null) return;
|
|
1947
|
+
announce();
|
|
1948
|
+
}, 15000);
|
|
1848
1949
|
return () => {
|
|
1950
|
+
if (timer !== 0) clearTimeout(timer);
|
|
1849
1951
|
if (mo !== null) mo.disconnect();
|
|
1850
|
-
clearTimeout(timer);
|
|
1851
1952
|
if (dispose !== null) dispose();
|
|
1852
1953
|
};
|
|
1853
1954
|
}
|
|
@@ -2225,7 +2326,7 @@ html[data-dshome-blur="off"] body{
|
|
|
2225
2326
|
* `.gdEzaW_bubble` this list used to carry matches nothing on the current
|
|
2226
2327
|
* build, which is how the old entry went dead without anyone noticing.
|
|
2227
2328
|
*/
|
|
2228
|
-
const SPOT_SURFACES = `${
|
|
2329
|
+
const SPOT_SURFACES = `${USER_BUBBLE},.uV2eYG_card,.dshome-sa,.dshome-file-menu,.hHd-Xa_root,[data-composer-card]`;
|
|
2229
2330
|
function startSpecularSpotter() {
|
|
2230
2331
|
const onOver = (event) => {
|
|
2231
2332
|
const target = event.target;
|
|
@@ -2277,6 +2378,19 @@ html[data-dshome-blur="off"] body{
|
|
|
2277
2378
|
// recipe. Both recipes tilt now, and because the liquid sheet used to be
|
|
2278
2379
|
// the only place carrying `transition: transform`, the motion carries
|
|
2279
2380
|
// its own transition (TILT_TRANSITION) so a recipe cannot lose it.
|
|
2381
|
+
// * 2.0.85 makes ENGAGEMENT GEOMETRIC and the gates honest. Engagement used
|
|
2382
|
+
// to need a `pointerover` whose target was inside the card; that is a
|
|
2383
|
+
// chain of assumptions about product markup (a wrapper with role=button,
|
|
2384
|
+
// a swapped card node, a portal, an overlay) which a theme cannot verify,
|
|
2385
|
+
// and failing it looks exactly like a broken effect -- reported as
|
|
2386
|
+
// "把我的对话框随着鼠标位置而变化的效果补回来". `spotAt()` decides from the
|
|
2387
|
+
// card's own rectangle on every pointermove, so a missed event costs one
|
|
2388
|
+
// frame instead of the whole effect; `controlAt()` remains the single hit
|
|
2389
|
+
// test, and only to answer "is a control under the pointer". The two
|
|
2390
|
+
// SILENT gates are reported now: `__dshomeTilt()` includes `reducedMotion`
|
|
2391
|
+
// (an OS setting that disables the effect and looks like a bug) and
|
|
2392
|
+
// `spots` (0 means the composer no longer matches TILT_SELECTOR), and the
|
|
2393
|
+
// reduced-motion case logs one info line instead of vanishing.
|
|
2280
2394
|
const TILT_SELECTOR = '[data-composer-card],.uV2eYG_card';
|
|
2281
2395
|
const TILT_MAX = 0.0175;
|
|
2282
2396
|
const TILT_PERSPECTIVE = 800;
|
|
@@ -2306,13 +2420,28 @@ html[data-dshome-blur="off"] body{
|
|
|
2306
2420
|
const tilted = new WeakSet();
|
|
2307
2421
|
const settle = new Map();
|
|
2308
2422
|
const reduced = typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
2309
|
-
|
|
2310
|
-
//
|
|
2311
|
-
//
|
|
2312
|
-
//
|
|
2423
|
+
// Readable from the page: the effect is guarded several times over, so "it
|
|
2424
|
+
// is missing" is otherwise a guess about which guard ate it.
|
|
2425
|
+
// `__dshomeTilt()` answers that in one call -- including the two SILENT
|
|
2426
|
+
// gates, which is why `reduced` and `spots` are in here at all: an OS-level
|
|
2427
|
+
// "reduce motion" or a composer whose markup no longer matches
|
|
2428
|
+
// TILT_SELECTOR both look exactly like a broken effect.
|
|
2313
2429
|
try {
|
|
2314
|
-
window.__dshomeTilt = () => ({
|
|
2430
|
+
window.__dshomeTilt = () => ({
|
|
2431
|
+
recipe: dsttGetGlass(),
|
|
2432
|
+
reducedMotion: reduced,
|
|
2433
|
+
spots: document.querySelectorAll(TILT_SELECTOR).length,
|
|
2434
|
+
leaning: current !== null,
|
|
2435
|
+
frozen,
|
|
2436
|
+
engagements,
|
|
2437
|
+
sinceRelease: releasedAt === -Infinity ? null : Math.round(performance.now() - releasedAt)
|
|
2438
|
+
});
|
|
2315
2439
|
} catch (error) {}
|
|
2440
|
+
if (reduced) {
|
|
2441
|
+
// Never silent: this is the one gate a user cannot see in any setting.
|
|
2442
|
+
console.info('[dsh-deepseek-style-theme] composer tilt disabled: the system asks for reduced motion (prefers-reduced-motion: reduce)');
|
|
2443
|
+
return () => {};
|
|
2444
|
+
}
|
|
2316
2445
|
const release = (spot) => {
|
|
2317
2446
|
const pending = settle.get(spot);
|
|
2318
2447
|
if (pending !== undefined) {
|
|
@@ -2351,21 +2480,48 @@ html[data-dshome-blur="off"] body{
|
|
|
2351
2480
|
tilted.add(spot);
|
|
2352
2481
|
});
|
|
2353
2482
|
};
|
|
2483
|
+
/**
|
|
2484
|
+
* Which tilt spot contains a point, by RECTANGLE -- deliberately not by
|
|
2485
|
+
* walking `event.target` up to a selector. Engagement used to depend on a
|
|
2486
|
+
* `pointerover` whose target was inside the card, and that is a chain of
|
|
2487
|
+
* assumptions about the product's markup (a wrapper with role=button, a
|
|
2488
|
+
* swapped-in card node, a portal, an overlay) that a theme cannot verify.
|
|
2489
|
+
* The reported symptom of getting it wrong is indistinguishable from a
|
|
2490
|
+
* broken effect: the box just never leans. Geometry is the one thing the
|
|
2491
|
+
* theme can measure for itself.
|
|
2492
|
+
*/
|
|
2493
|
+
const spotAt = (clientX, clientY) => {
|
|
2494
|
+
for (const spot of document.querySelectorAll(TILT_SELECTOR)) {
|
|
2495
|
+
const rect = spot.getBoundingClientRect();
|
|
2496
|
+
if (rect.width <= 0 || rect.height <= 0) continue;
|
|
2497
|
+
if (clientX < rect.left || clientX > rect.right || clientY < rect.top || clientY > rect.bottom) continue;
|
|
2498
|
+
return { spot, rect };
|
|
2499
|
+
}
|
|
2500
|
+
return null;
|
|
2501
|
+
};
|
|
2502
|
+
/** The topmost element at a point, used only to decide "is this a control". */
|
|
2503
|
+
const controlAt = (clientX, clientY) => {
|
|
2504
|
+
let node = null;
|
|
2505
|
+
try { node = document.elementFromPoint(clientX, clientY); } catch (error) { node = null; }
|
|
2506
|
+
if (node === null || typeof node.closest !== 'function') return false;
|
|
2507
|
+
return node.closest(TILT_CONTROLS) !== null;
|
|
2508
|
+
};
|
|
2354
2509
|
const onOver = (event) => {
|
|
2510
|
+
// Kept for the one thing it does better than a move: it fires the
|
|
2511
|
+
// moment the pointer arrives, before any movement. Everything it
|
|
2512
|
+
// decides is re-derived from geometry, so a missed event costs one
|
|
2513
|
+
// frame, not the effect.
|
|
2355
2514
|
const target = event.target;
|
|
2356
2515
|
if (target === null || typeof target.closest !== 'function') return;
|
|
2357
2516
|
const spot = target.closest(TILT_SELECTOR);
|
|
2358
2517
|
if (spot === null) return;
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
if (
|
|
2518
|
+
const rect = spot.getBoundingClientRect();
|
|
2519
|
+
if (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom) return;
|
|
2520
|
+
if (controlAt(event.clientX, event.clientY)) {
|
|
2362
2521
|
if (current === spot) frozen = true;
|
|
2363
2522
|
return;
|
|
2364
2523
|
}
|
|
2365
|
-
// The cooldown gates a fresh engagement only, never an ongoing lean.
|
|
2366
2524
|
if (!tilted.has(spot) && performance.now() - releasedAt < TILT_COOLDOWN_MS) return;
|
|
2367
|
-
const rect = spot.getBoundingClientRect();
|
|
2368
|
-
if (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom) return;
|
|
2369
2525
|
const pending = settle.get(spot);
|
|
2370
2526
|
if (pending !== undefined) {
|
|
2371
2527
|
clearTimeout(pending);
|
|
@@ -2376,22 +2532,38 @@ html[data-dshome-blur="off"] body{
|
|
|
2376
2532
|
paint(spot, rect, event.clientX, event.clientY);
|
|
2377
2533
|
};
|
|
2378
2534
|
const onMove = (event) => {
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2535
|
+
const hit = spotAt(event.clientX, event.clientY);
|
|
2536
|
+
if (hit === null) {
|
|
2537
|
+
// Off the card entirely: release only the spot that was leaning.
|
|
2538
|
+
if (current !== null) {
|
|
2539
|
+
release(current);
|
|
2540
|
+
current = null;
|
|
2541
|
+
}
|
|
2542
|
+
return;
|
|
2543
|
+
}
|
|
2544
|
+
const { spot, rect } = hit;
|
|
2545
|
+
if (controlAt(event.clientX, event.clientY)) {
|
|
2546
|
+
// A control is under the pointer: hold the lean exactly where it is
|
|
2547
|
+
// (releasing here is what used to feed the ~8 Hz hover loop).
|
|
2548
|
+
if (current === spot) frozen = true;
|
|
2383
2549
|
return;
|
|
2384
2550
|
}
|
|
2551
|
+
// The cooldown gates a fresh engagement only, never an ongoing lean.
|
|
2552
|
+
if (!tilted.has(spot) && current !== spot && performance.now() - releasedAt < TILT_COOLDOWN_MS) return;
|
|
2553
|
+
if (current !== spot) {
|
|
2554
|
+
if (current !== null) release(current);
|
|
2555
|
+
const pending = settle.get(spot);
|
|
2556
|
+
if (pending !== undefined) {
|
|
2557
|
+
clearTimeout(pending);
|
|
2558
|
+
settle.delete(spot);
|
|
2559
|
+
}
|
|
2560
|
+
current = spot;
|
|
2561
|
+
}
|
|
2385
2562
|
frozen = false;
|
|
2386
|
-
paint(
|
|
2563
|
+
paint(spot, rect, event.clientX, event.clientY);
|
|
2387
2564
|
};
|
|
2388
2565
|
const onOut = (event) => {
|
|
2389
2566
|
if (current === null) return;
|
|
2390
|
-
const target = event.target;
|
|
2391
|
-
if (target !== null && typeof target.closest === 'function') {
|
|
2392
|
-
const spot = target.closest(TILT_SELECTOR);
|
|
2393
|
-
if (spot !== null && spot !== current) return;
|
|
2394
|
-
}
|
|
2395
2567
|
const rect = current.getBoundingClientRect();
|
|
2396
2568
|
if (event.clientX >= rect.left && event.clientX <= rect.right && event.clientY >= rect.top && event.clientY <= rect.bottom) return;
|
|
2397
2569
|
release(current);
|
|
@@ -2430,7 +2602,7 @@ html[data-dshome-blur="off"] body{
|
|
|
2430
2602
|
if (!supportsWebgl2()) return startParticles();
|
|
2431
2603
|
// Which build is running, readable from the page (see README). One line in
|
|
2432
2604
|
// the console answers "is the bundle I just installed actually live?".
|
|
2433
|
-
try { window.__dshomeBuild = '2.0.
|
|
2605
|
+
try { window.__dshomeBuild = '2.0.85 glass-extras'; } catch (error) {}
|
|
2434
2606
|
const canvas = document.createElement('canvas');
|
|
2435
2607
|
canvas.setAttribute('data-dsh-deepseek-canvas', '');
|
|
2436
2608
|
const style = canvas.style;
|
|
@@ -2472,17 +2644,6 @@ html[data-dshome-blur="off"] body{
|
|
|
2472
2644
|
// `attachFluidInteractions` is therefore NOT wired — it is kept in the
|
|
2473
2645
|
// file, unmodified, because re-enabling button ripples is one call, but
|
|
2474
2646
|
// nothing in this theme uses it today.
|
|
2475
|
-
const stopSpotter = startSpecularSpotter();
|
|
2476
|
-
const stopParallax = startSpecularParallax();
|
|
2477
|
-
const dispersion = startGlassDispersion({ hue: FLUID_HUES[dshomeColor()] });
|
|
2478
|
-
// Both recipes share one filter pair, so the recipe selects the scale:
|
|
2479
|
-
// 104 for liquid (visible edge refraction) and the untouched 60 for
|
|
2480
|
-
// frosted, which is what the pane has always used.
|
|
2481
|
-
const applyGlass = () => {
|
|
2482
|
-
dispersion.setRefraction(dsttGetGlass() === 'liquid' ? LIQUID_REF_SCALE : DEFAULT_REF_SCALE);
|
|
2483
|
-
};
|
|
2484
|
-
applyGlass();
|
|
2485
|
-
const unsubscribeGlass = dsttSubscribe(applyGlass);
|
|
2486
2647
|
// ── palette reconciliation ────────────────────────────────────────────
|
|
2487
2648
|
// One reader for BOTH inputs the palette depends on: the colour token on
|
|
2488
2649
|
// <body> and the dark marker. Reported symptom: "切主题模式背景不变 /
|
|
@@ -2507,7 +2668,6 @@ html[data-dshome-blur="off"] body{
|
|
|
2507
2668
|
};
|
|
2508
2669
|
const unsubscribeColor = subscribeColor((color) => {
|
|
2509
2670
|
syncFluid();
|
|
2510
|
-
try { dispersion.setTint(FLUID_HUES[color] ?? FLUID_HUES.green); } catch (error) {}
|
|
2511
2671
|
});
|
|
2512
2672
|
const unsubscribeDark = darkSync === null ? null : darkSync.subscribe(syncFluid);
|
|
2513
2673
|
// The colour driver writes the token as an attribute; watching it as well
|
|
@@ -2523,19 +2683,65 @@ html[data-dshome-blur="off"] body{
|
|
|
2523
2683
|
return () => {
|
|
2524
2684
|
unsubscribeColor();
|
|
2525
2685
|
if (unsubscribeDark !== null) unsubscribeDark();
|
|
2686
|
+
if (tokenMirror !== null) tokenMirror.disconnect();
|
|
2687
|
+
clearInterval(fluidSync);
|
|
2688
|
+
fluid.dispose();
|
|
2689
|
+
canvas.remove();
|
|
2690
|
+
};
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2693
|
+
/**
|
|
2694
|
+
* The cursor-following half of the glass: the specular spotter (which marks
|
|
2695
|
+
* the surfaces the highlight may land on), the parallax that writes
|
|
2696
|
+
* `--dshome-spec-x/y` for the hovered surface, and the SVG edge refraction.
|
|
2697
|
+
*
|
|
2698
|
+
* These used to be mounted INSIDE {@link startAmbient}, which tied them to
|
|
2699
|
+
* the fluid layer: turning 「动态背景」 off -- which is exactly what the user
|
|
2700
|
+
* did to stop the iGPU squares -- or choosing ③ 自选背景, or landing on an
|
|
2701
|
+
* engine without WebGL2 (the particle fallback returns before them), silently
|
|
2702
|
+
* took the dialog's cursor-following highlight and its edge refraction with
|
|
2703
|
+
* it. Reported as "把我的对话框随着鼠标位置而变化的效果补回来". They belong to
|
|
2704
|
+
* the glass, not to the flow field, so they mount on their own and survive
|
|
2705
|
+
* every background recipe.
|
|
2706
|
+
*/
|
|
2707
|
+
function startGlassExtras() {
|
|
2708
|
+
const stopSpotter = startSpecularSpotter();
|
|
2709
|
+
const stopParallax = startSpecularParallax();
|
|
2710
|
+
const dispersion = startGlassDispersion({ hue: FLUID_HUES[dshomeColor()] });
|
|
2711
|
+
// Both recipes share one filter pair, so the recipe selects the scale:
|
|
2712
|
+
// 104 for liquid (visible edge refraction) and the untouched 60 for
|
|
2713
|
+
// frosted, which is what the pane has always used.
|
|
2714
|
+
const applyGlass = () => {
|
|
2715
|
+
dispersion.setRefraction(dsttGetGlass() === 'liquid' ? LIQUID_REF_SCALE : DEFAULT_REF_SCALE);
|
|
2716
|
+
};
|
|
2717
|
+
applyGlass();
|
|
2718
|
+
const unsubscribeGlass = dsttSubscribe(applyGlass);
|
|
2719
|
+
// The tint follows the DSTT colour token. Same belt-and-braces as the
|
|
2720
|
+
// fluid palette: subscribe AND watch the attribute the driver writes,
|
|
2721
|
+
// because the colour driver writes it before this mount exists.
|
|
2722
|
+
const syncTint = () => {
|
|
2723
|
+
try { dispersion.setTint(FLUID_HUES[dshomeColor()] ?? FLUID_HUES.green); } catch (error) {}
|
|
2724
|
+
};
|
|
2725
|
+
const unsubscribeColor = subscribeColor(syncTint);
|
|
2726
|
+
let tokenMirror = null;
|
|
2727
|
+
try {
|
|
2728
|
+
tokenMirror = new MutationObserver(syncTint);
|
|
2729
|
+
tokenMirror.observe(document.body, { attributes: true, attributeFilter: ['data-dshome-color'] });
|
|
2730
|
+
} catch (error) {
|
|
2731
|
+
tokenMirror = null;
|
|
2732
|
+
}
|
|
2733
|
+
return () => {
|
|
2734
|
+
unsubscribeColor();
|
|
2526
2735
|
unsubscribeGlass();
|
|
2527
2736
|
if (tokenMirror !== null) tokenMirror.disconnect();
|
|
2528
2737
|
dispersion.dispose();
|
|
2529
2738
|
stopParallax();
|
|
2530
2739
|
stopSpotter();
|
|
2531
|
-
clearInterval(fluidSync);
|
|
2532
|
-
fluid.dispose();
|
|
2533
|
-
canvas.remove();
|
|
2534
2740
|
};
|
|
2535
2741
|
}
|
|
2536
2742
|
|
|
2537
2743
|
function startParticles() {
|
|
2538
|
-
try { window.__dshomeBuild = '2.0.
|
|
2744
|
+
try { window.__dshomeBuild = '2.0.85 glass-extras'; } catch (error) {}
|
|
2539
2745
|
const canvas = document.createElement('canvas');
|
|
2540
2746
|
canvas.setAttribute('data-dsh-deepseek-canvas', '');
|
|
2541
2747
|
const style = canvas.style;
|
|
@@ -2607,6 +2813,11 @@ html[data-dshome-blur="off"] body{
|
|
|
2607
2813
|
|
|
2608
2814
|
function step() {
|
|
2609
2815
|
if (!running) return;
|
|
2816
|
+
// Same self-cleanup as the fluid loop: no canvas in the DOM, no work.
|
|
2817
|
+
if (!canvas.isConnected) {
|
|
2818
|
+
running = false;
|
|
2819
|
+
return;
|
|
2820
|
+
}
|
|
2610
2821
|
g.clearRect(0, 0, W, H);
|
|
2611
2822
|
const base = linkBase();
|
|
2612
2823
|
for (let i = 0; i < parts.length; i++) {
|
|
@@ -3937,7 +4148,10 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
|
|
|
3937
4148
|
// set they asked to keep. Ids match the host schema's `backgroundMode`
|
|
3938
4149
|
// enum; the free-text value for 方式3 is `customBackground`.
|
|
3939
4150
|
const DSTT_BACKGROUNDS = ["classic", "white", "custom", "bold"];
|
|
3940
|
-
|
|
4151
|
+
// `bold` by default: ① classic averages (soft lead + white + near-white) into a
|
|
4152
|
+
// wash the eye reads as blue-white whatever the colour token says, so it is a
|
|
4153
|
+
// poor first impression -- it stays selectable for anyone who wants it.
|
|
4154
|
+
const DSTT_BACKGROUND_DEFAULT = "bold";
|
|
3941
4155
|
const DSTT_BACKGROUND_LABELS = {
|
|
3942
4156
|
classic: { zh: "① 主色+纯白+近白", en: "1 · Lead + white + near-white" },
|
|
3943
4157
|
white: { zh: "② 主色+纯白", en: "2 · Lead + white" },
|
|
@@ -4578,7 +4792,7 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
|
|
|
4578
4792
|
React.createElement('div', { className: 'dshome-dstt-row-title' }, isZh ? '背景方式' : 'Background recipe'),
|
|
4579
4793
|
React.createElement('div', { className: 'dshome-dstt-row-hint' }, isZh
|
|
4580
4794
|
? '① 主色+纯白+近白(默认,旧版那套同色系柔和观感);② 主色+纯白(只有两个色参与,最干净);③ 自选背景:我们的流体与主题底色全部让位(没别的在画时是纯白 / 纯黑);装了 dsh-plugin-wallpaper-engine 且它正在渲染时,连纯白也不画,背景交给它。颜色只驱动按钮之类。输入框可填图片 URL / 任意 CSS 值,或点「桌面壁纸」用本机壁纸;浓三色是 1.43.12 那套,先留着。'
|
|
4581
|
-
: '1 Lead + white + near-white (
|
|
4795
|
+
: 'Bold (default: the lead tone carries real chroma, so all six backgrounds read at a glance). 1 Lead + white + near-white (the soft single-family look this theme shipped before). 2 Lead + white (only two colours participate, the cleanest). 3 Custom background: our fluid and our themed background both stand down (flat white / black when nothing else paints); while dsh-plugin-wallpaper-engine is rendering we do not even paint that, and the background is its layer. The colour drives the buttons and such only. Type an image URL / any CSS value, or press Desktop for the Windows wallpaper.'),
|
|
4582
4796
|
React.createElement('div', { className: 'dshome-dstt-seg dshome-dstt-seg-row', role: 'radiogroup', 'aria-label': isZh ? '背景方式' : 'Background recipe' },
|
|
4583
4797
|
DSTT_BACKGROUNDS.map((id) => React.createElement('button', {
|
|
4584
4798
|
key: id,
|
|
@@ -4811,7 +5025,19 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
|
|
|
4811
5025
|
try { stop(); } catch (error) {}
|
|
4812
5026
|
return;
|
|
4813
5027
|
}
|
|
4814
|
-
try {
|
|
5028
|
+
try {
|
|
5029
|
+
dispose = startAmbient();
|
|
5030
|
+
} catch (error) {
|
|
5031
|
+
// NEVER silent. A throw inside the mount used to be swallowed right
|
|
5032
|
+
// here, which is how one undeclared identifier (ATTR, deleted in
|
|
5033
|
+
// d862487) hid for nineteen versions: no refraction anywhere, a
|
|
5034
|
+
// 动态背景 toggle that stopped unmounting, and one leaked
|
|
5035
|
+
// simulator per flip. Sweep the orphan canvas too -- the fluid
|
|
5036
|
+
// loop also self-cancels when its canvas leaves the DOM.
|
|
5037
|
+
console.error('[dsh-deepseek-style-theme] ambient background failed to mount:', error);
|
|
5038
|
+
for (const stray of document.querySelectorAll('canvas[data-dsh-deepseek-canvas]')) stray.remove();
|
|
5039
|
+
dispose = () => {};
|
|
5040
|
+
}
|
|
4815
5041
|
};
|
|
4816
5042
|
syncAmbient();
|
|
4817
5043
|
const unsubscribe = dsttSubscribe(syncAmbient);
|
|
@@ -4822,6 +5048,18 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
|
|
|
4822
5048
|
if (stop !== null) { try { stop(); } catch (error) {} }
|
|
4823
5049
|
};
|
|
4824
5050
|
}, 'deepseek-style-theme: fluid background');
|
|
5051
|
+
// The cursor-following half of the glass mounts ON ITS OWN, with no
|
|
5052
|
+
// dependence on the fluid layer: see startGlassExtras for why (turning
|
|
5053
|
+
// 「动态背景」 off, or ③ 自选背景, or a machine without WebGL2 used to take
|
|
5054
|
+
// the dialog's cursor-following highlight and the edge refraction with it).
|
|
5055
|
+
ctx.effect(() => {
|
|
5056
|
+
try {
|
|
5057
|
+
return startGlassExtras();
|
|
5058
|
+
} catch (error) {
|
|
5059
|
+
console.error('[dsh-deepseek-style-theme] glass extras failed to mount:', error);
|
|
5060
|
+
return () => {};
|
|
5061
|
+
}
|
|
5062
|
+
}, 'deepseek-style-theme: glass extras (specular + dispersion)');
|
|
4825
5063
|
// The composer's hover tilt is independent of the background: it must
|
|
4826
5064
|
// survive a machine without WebGL2, where startAmbient() falls back to
|
|
4827
5065
|
// the 2D particle field.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-deepseek-style-theme",
|
|
3
3
|
"description": "DeepSeek official-home style theme: WebGL fluid-flow background, liquid glass with SVG edge refraction, glass sidebar and composer, frosted header, running-subagent progress panel, and DeepSeek brand link for the dsh web GUI",
|
|
4
|
-
"version": "2.0.
|
|
4
|
+
"version": "2.0.85",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"deepseek",
|