dsh-deepseek-style-theme 1.43.1 → 1.43.3

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 CHANGED
@@ -112,13 +112,28 @@ dsh plugin --profile web add ./releases/dsh-deepseek-style-theme-1.43.1.tgz
112
112
 
113
113
  ### 从 npm
114
114
 
115
+ > ⚠️ **暂时不要用这一条。** npm 上的 `latest` 仍是 **1.43.1**,而那一版带着一个致命缺陷:客户端 bundle 的注册 id 还是旧 scope 的 `@dsh-external/dsh-deepseek-style-theme`,而宿主按包名 `dsh-deepseek-style-theme` 要求它自我注册,于是 **bundle 加载成功却什么都没注册**,主题的整个客户端半边失效。1.43.3 已修复。
116
+ >
117
+ > 在 1.43.3 发到 npm 之前,请用上面的 **tgz 别名地址** 或 `github:` 安装。
118
+
115
119
  ```bash
120
+ # 修复版发到 npm 之后可用;在那之前用 tgz 别名地址
116
121
  dsh plugin --profile web add dsh-deepseek-style-theme
117
122
  ```
118
123
 
119
- > 包名是**无 scope** 的 `dsh-deepseek-style-theme`。这是刻意的:`@dsh-external` 这个 scope 在 npm 上属于别人(`wulei1107`,下面挂着 `@dsh-external/dsh-vision-toolkit`),不是本项目的命名空间,也发不进去。仓库里 `cordis.patch.yml` 的 `name:` 与 `package.json` 的 `name` 必须始终一致——前者是 Loader 用来解析模块的说明符,写成解析不到的旧名会让插件装配失败。
124
+ > 包名是**无 scope** 的 `dsh-deepseek-style-theme`。这是刻意的:`@dsh-external` 这个 scope 在 npm 上属于别人(`wulei1107`,下面挂着 `@dsh-external/dsh-vision-toolkit`),不是本项目的命名空间,也发不进去。
125
+ >
126
+ > **改名的三个引用点必须同时对齐**,缺一个都会静默失效,且失效方式各不相同:
127
+ >
128
+ > | 位置 | 不对齐的后果 |
129
+ > |---|---|
130
+ > | `package.json` 的 `name` | 包装不进去 / 目录名不符 |
131
+ > | `cordis.patch.yml` 的 `name:` | Loader 解析不到模块,**插件装配失败** |
132
+ > | `lib/client.js` 里 `__ModuleLoader__.load` 的 `id` | bundle 加载成功却什么都没注册,**客户端半边失效**(1.43.1 的缺陷) |
133
+ >
134
+ > 自查:`grep '@dsh-external' lib/` 应当**一处都不命中**;命中即漏改(有意保留的兼容位置除外)。
120
135
  >
121
- > npm 是最省事的安装源:不走 `git clone`(绕开代理/证书问题),也不依赖 GitHub Release 资产(绕开 `releases/latest` 那类 URL 腐烂)。插件市场的目录条目一旦写上 `npm` 字段,市场就会优先用它而不是 tarball。
136
+ > npm 是最省事的安装源:不走 `git clone`(绕开代理/证书问题),也不依赖 GitHub Release 资产(绕开 `releases/latest` 那类 URL 腐烂)。
122
137
 
123
138
  ```bash
124
139
  # 或任意 git 仓库地址(若该仓库未提交构建产物,
package/lib/client.js CHANGED
@@ -1,5 +1,15 @@
1
1
  window.__ModuleLoader__.load({
2
- id: "@dsh-external/dsh-deepseek-style-theme",
2
+ // This id must equal the package name: the host builds the browser bundle
3
+ // URL as /plugins/<entry.id>/client.js (dsh-client-modules), and the loader
4
+ // then requires that the bundle registers itself under that same id. The
5
+ // package was renamed to the unscoped dsh-deepseek-style-theme in 1.43.1
6
+ // while this literal kept the old scope, so on any profile carrying the new
7
+ // name the bundle loaded and registered nothing --
8
+ // "loaded without registering \"dsh-deepseek-style-theme\" via __ModuleLoader__.load"
9
+ // -- which took the whole client half down. It stayed invisible on the
10
+ // development machine because that profile still installs the scoped package,
11
+ // whose bundle URL matches the stale literal.
12
+ id: "dsh-deepseek-style-theme",
3
13
  factory: (require) => {
4
14
  var module = { exports: {} };
5
15
  var exports = module.exports;
@@ -26,7 +36,7 @@ window.__ModuleLoader__.load({
26
36
  // page data — session titles, file paths, workspace names — must go
27
37
  // through `textContent`/`title`/`setAttribute`, never `innerHTML`. An
28
38
  // interpolated value there would be DOM XSS.
29
- const PLUGIN_ID = "@dsh-external/dsh-deepseek-style-theme";
39
+ const PLUGIN_ID = "dsh-deepseek-style-theme";
30
40
 
31
41
  // ─── fluid background + liquid-glass refraction ───────────────────────
32
42
  // Ported from dsh-theme-mineradio v2.3.8 (MIT, Copyright (c) 2026 John Wu).
@@ -632,7 +642,40 @@ void main() {
632
642
  var TALL_FILTER_ID = "dshome-glass-dispersion-tall";
633
643
  var TALL_TINT_ID = "dshome-glass-dispersion-tall-tint";
634
644
  var TALL_REFRACT_ID = "dshome-glass-dispersion-tall-refract";
635
- var ATTR = "data-dshome-dispersion";
645
+ // Two composer filters: `narrow` (an 8px band) and `wide` (16px).
646
+ //
647
+ // The band is sized by what the user can actually SEE, which is the top and
648
+ // bottom edges: the card's top abuts the 任务 bar and its bottom abuts the
649
+ // status line, so displacing the backdrop across those hard edges shows,
650
+ // while left and right sit over a continuous background where the same
651
+ // displacement changes nothing. The user accepted that asymmetry ("允许左右
652
+ // 无反射"), so these maps are tuned for the VERTICAL band and the horizontal
653
+ // one is left wherever it lands.
654
+ //
655
+ // Vertical band on screen = (edge + softness) / mapHeight * 1.56 * cardHeight
656
+ // -- the filter region is 156% of the surface's height (see filterMarkup) --
657
+ // so at the upstream 400x92 aspect and a 112px-tall card that is 1.90x the
658
+ // map inset: 1.7 + 2.5 gives 8px, 3.4 + 5 gives 16px.
659
+ var COMPOSER_FILTER_ID = "dshome-glass-dispersion-composer";
660
+ var COMPOSER_TINT_ID = "dshome-glass-dispersion-composer-tint";
661
+ var COMPOSER_REFRACT_ID = "dshome-glass-dispersion-composer-refract";
662
+ var WIDE_FILTER_ID = "dshome-glass-dispersion-composer-wide";
663
+ var WIDE_TINT_ID = "dshome-glass-dispersion-composer-wide-tint";
664
+ var WIDE_REFRACT_ID = "dshome-glass-dispersion-composer-wide-refract";
665
+ var COMPOSER_MAP_W = 400;
666
+ var COMPOSER_MAP_H = 92;
667
+ var COMPOSER_MAP_RADIUS = 50;
668
+ var COMPOSER_MAP_EDGE = 1.7;
669
+ var COMPOSER_MAP_SOFTNESS = 2.5;
670
+ var COMPOSER_WIDE_EDGE = 3.4;
671
+ var COMPOSER_WIDE_SOFTNESS = 5;
672
+ // Chromatic aberration, composer filters only. 0.18 splits the base
673
+ // displacement into three: red at 118%, blue at 82%, green in the middle,
674
+ // which is what makes a lens edge fringe instead of merely tinting. Kept to
675
+ // the two composer filters because it triples the displacement passes, and
676
+ // because the shared filters have to keep upstream's exact shape for the
677
+ // frosted recipe.
678
+ var COMPOSER_ABERRATION = 0.18;
636
679
  var DEFAULT_REF_SCALE = 60;
637
680
  // The liquid recipe drives the same feDisplacementMap harder: at 60 the
638
681
  // bend is there but only just legible once the blur sits on top of it, and
@@ -644,15 +687,20 @@ void main() {
644
687
  var TINT_SAT = 0.85;
645
688
  var TINT_LIGHT = 0.6;
646
689
  var DEFAULT_TINT_HUE = 44;
647
- function buildDisplacementMapDataUrl(width = 400, height = 92, radius = 50) {
690
+ // Upstream derives the inset from the map's SHORT side (7% of it, halved),
691
+ // which is only uniform if the map is square. The composer passes `edge`
692
+ // instead: one absolute inset for both axes, which comes out uniform as
693
+ // long as the map's aspect matches the filter region's (see COMPOSER_MAP_*).
694
+ // Defaults reproduce upstream's numbers exactly, so the frosted recipe's
695
+ // map stays byte for byte what it was.
696
+ function buildDisplacementMapDataUrl(width = 400, height = 92, radius = 50, edge = null, softness = 11) {
648
697
  const w = Math.max(240, Math.round(width));
649
698
  const h = Math.max(48, Math.round(height));
650
699
  const r = Math.max(12, Math.round(radius));
651
- const borderWidth = 0.07;
652
- const edge = Math.min(w, h) * (borderWidth * 0.5);
653
- const innerW = Math.max(1, w - edge * 2);
654
- const innerH = Math.max(1, h - edge * 2);
655
- const svg2 = `<svg viewBox="0 0 ${w} ${h}" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="glass-x" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="#000"/><stop offset="100%" stop-color="#f00"/></linearGradient><linearGradient id="glass-y" x1="0%" y1="0%" x2="0%" y2="100%"><stop offset="0%" stop-color="#000"/><stop offset="100%" stop-color="#00f"/></linearGradient></defs><rect x="0" y="0" width="${w}" height="${h}" fill="#000"/><rect x="0" y="0" width="${w}" height="${h}" rx="${r}" fill="url(#glass-x)"/><rect x="0" y="0" width="${w}" height="${h}" rx="${r}" fill="url(#glass-y)" style="mix-blend-mode:screen"/><rect x="${edge.toFixed(2)}" y="${edge.toFixed(2)}" width="${innerW.toFixed(2)}" height="${innerH.toFixed(2)}" rx="${r}" fill="hsl(0 0% 50% / 1)" style="filter:blur(11px)"/></svg>`;
700
+ const inset = edge === null ? Math.min(w, h) * 0.035 : Math.max(0, edge);
701
+ const innerW = Math.max(1, w - inset * 2);
702
+ const innerH = Math.max(1, h - inset * 2);
703
+ const svg2 = `<svg viewBox="0 0 ${w} ${h}" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="glass-x" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="#000"/><stop offset="100%" stop-color="#f00"/></linearGradient><linearGradient id="glass-y" x1="0%" y1="0%" x2="0%" y2="100%"><stop offset="0%" stop-color="#000"/><stop offset="100%" stop-color="#00f"/></linearGradient></defs><rect x="0" y="0" width="${w}" height="${h}" fill="#000"/><rect x="0" y="0" width="${w}" height="${h}" rx="${r}" fill="url(#glass-x)"/><rect x="0" y="0" width="${w}" height="${h}" rx="${r}" fill="url(#glass-y)" style="mix-blend-mode:screen"/><rect x="${inset.toFixed(2)}" y="${inset.toFixed(2)}" width="${innerW.toFixed(2)}" height="${innerH.toFixed(2)}" rx="${r}" fill="hsl(0 0% 50% / 1)" style="filter:blur(${softness})"/></svg>`;
656
704
  return `data:image/svg+xml,${encodeURIComponent(svg2)}`;
657
705
  }
658
706
  // Inlined from upstream `src/client/glass-dispersion.ts` (line 2409) —
@@ -693,8 +741,29 @@ void main() {
693
741
  }
694
742
  // Inlined from upstream `src/client/glass-dispersion.ts` (line 2443) —
695
743
  // builds the wide/tall SVG <filter> pair startGlassDispersion injects.
696
- function filterMarkup(id, refractId, tintId, map) {
697
- return `<filter id="${id}" color-interpolation-filters="sRGB" x="-12%" y="-28%" width="124%" height="156%"><feImage href="${map}" x="0" y="0" width="100%" height="100%" preserveAspectRatio="none" result="map"></feImage><feDisplacementMap id="${refractId}" in="SourceGraphic" in2="map" scale="${DEFAULT_REF_SCALE}" xChannelSelector="R" yChannelSelector="B" result="refracted"></feDisplacementMap><feOffset in="refracted" dx="${EDGE_DX}" dy="0" result="shifted"></feOffset><feBlend in="shifted" in2="refracted" mode="difference" result="edgeDiff"></feBlend><feColorMatrix id="${tintId}" in="edgeDiff" type="matrix" values="${tintMatrix(DEFAULT_TINT_HUE, TINT_OPACITY)}" result="edgeTint"></feColorMatrix><feBlend in="refracted" in2="edgeTint" mode="screen" result="output"></feBlend><feGaussianBlur in="output" stdDeviation="0.5"></feGaussianBlur></filter>`;
744
+ //
745
+ // `aberration` (0 = off, upstream's shape byte for byte) adds true
746
+ // chromatic aberration: the backdrop is displaced TWICE MORE at slightly
747
+ // different scales, and the result is assembled channel by channel -- R
748
+ // from the stronger displacement, B from the weaker, G from the base. That
749
+ // is what puts a red/blue fringe on a real lens edge; the tinted
750
+ // difference-blend below is only a colour wash and does not separate
751
+ // channels at all.
752
+ //
753
+ // The two extra maps carry a data-aberration factor so setRefraction() can
754
+ // rescale them in step with the base one instead of flattening all three
755
+ // to the same value.
756
+ function filterMarkup(id, refractId, tintId, map, aberration = 0) {
757
+ const chromatic = aberration <= 0 ? '' :
758
+ `<feDisplacementMap data-aberration="${(1 + aberration).toFixed(3)}" in="SourceGraphic" in2="map" scale="${DEFAULT_REF_SCALE}" xChannelSelector="R" yChannelSelector="B" result="refractR"></feDisplacementMap>` +
759
+ `<feDisplacementMap data-aberration="${(1 - aberration).toFixed(3)}" in="SourceGraphic" in2="map" scale="${DEFAULT_REF_SCALE}" xChannelSelector="R" yChannelSelector="B" result="refractB"></feDisplacementMap>` +
760
+ `<feColorMatrix in="refracted" type="matrix" values="0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0" result="onlyG"></feColorMatrix>` +
761
+ `<feColorMatrix in="refractR" type="matrix" values="1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0" result="onlyR"></feColorMatrix>` +
762
+ `<feColorMatrix in="refractB" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0" result="onlyB"></feColorMatrix>` +
763
+ `<feBlend in="onlyG" in2="onlyR" mode="screen" result="rgMerge"></feBlend>` +
764
+ `<feBlend in="rgMerge" in2="onlyB" mode="screen" result="aberrated"></feBlend>`;
765
+ const merged = chromatic === '' ? 'refracted' : 'aberrated';
766
+ return `<filter id="${id}" color-interpolation-filters="sRGB" x="-12%" y="-28%" width="124%" height="156%"><feImage href="${map}" x="0" y="0" width="100%" height="100%" preserveAspectRatio="none" result="map"></feImage><feDisplacementMap id="${refractId}" in="SourceGraphic" in2="map" scale="${DEFAULT_REF_SCALE}" xChannelSelector="R" yChannelSelector="B" result="refracted"></feDisplacementMap>${chromatic}<feOffset in="${merged}" dx="${EDGE_DX}" dy="0" result="shifted"></feOffset><feBlend in="shifted" in2="${merged}" mode="difference" result="edgeDiff"></feBlend><feColorMatrix id="${tintId}" in="edgeDiff" type="matrix" values="${tintMatrix(DEFAULT_TINT_HUE, TINT_OPACITY)}" result="edgeTint"></feColorMatrix><feBlend in="${merged}" in2="edgeTint" mode="screen" result="output"></feBlend><feGaussianBlur in="output" stdDeviation="0.5"></feGaussianBlur></filter>`;
698
767
  }
699
768
  /**
700
769
  * Mount the two SVG backdrop filters and set `ATTR` on <html> so the
@@ -721,7 +790,9 @@ void main() {
721
790
  }
722
791
  const mapWide = buildDisplacementMapDataUrl(400, 92, 50);
723
792
  const mapTall = buildDisplacementMapDataUrl(92, 400, 50);
724
- const markup = `<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" width="0" height="0" style="position:absolute;overflow:hidden"><defs>` + filterMarkup(FILTER_ID, REFRACT_ID, TINT_ID, mapWide) + filterMarkup(TALL_FILTER_ID, TALL_REFRACT_ID, TALL_TINT_ID, mapTall) + `</defs></svg>`;
793
+ const mapComposer = buildDisplacementMapDataUrl(COMPOSER_MAP_W, COMPOSER_MAP_H, COMPOSER_MAP_RADIUS, COMPOSER_MAP_EDGE, COMPOSER_MAP_SOFTNESS);
794
+ const mapComposerWide = buildDisplacementMapDataUrl(COMPOSER_MAP_W, COMPOSER_MAP_H, COMPOSER_MAP_RADIUS, COMPOSER_WIDE_EDGE, COMPOSER_WIDE_SOFTNESS);
795
+ const markup = `<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" width="0" height="0" style="position:absolute;overflow:hidden"><defs>` + filterMarkup(FILTER_ID, REFRACT_ID, TINT_ID, mapWide) + filterMarkup(TALL_FILTER_ID, TALL_REFRACT_ID, TALL_TINT_ID, mapTall) + filterMarkup(COMPOSER_FILTER_ID, COMPOSER_REFRACT_ID, COMPOSER_TINT_ID, mapComposer, COMPOSER_ABERRATION) + filterMarkup(WIDE_FILTER_ID, WIDE_REFRACT_ID, WIDE_TINT_ID, mapComposerWide, COMPOSER_ABERRATION) + `</defs></svg>`;
725
796
  const container = document.createElement("div");
726
797
  container.setAttribute("aria-hidden", "true");
727
798
  container.style.cssText = "position:absolute;width:0;height:0;overflow:hidden;";
@@ -736,8 +807,15 @@ void main() {
736
807
  tint.forEach((node) => node.setAttribute("values", values));
737
808
  },
738
809
  setRefraction(scale) {
739
- const next = String(Math.max(0, Math.min(140, Number(scale) || 0)));
740
- refract.forEach((node) => node.setAttribute("scale", next));
810
+ // The aberration maps are the same displacement at a slightly
811
+ // different scale, so they carry a factor and are rescaled in step;
812
+ // flattening them all to one value would erase the fringing.
813
+ const base = Math.max(0, Math.min(140, Number(scale) || 0));
814
+ refract.forEach((node) => {
815
+ const factor = Number(node.getAttribute("data-aberration"));
816
+ const value = Number.isFinite(factor) && factor > 0 ? base * factor : base;
817
+ node.setAttribute("scale", String(Math.max(0, Math.min(140, value))));
818
+ });
741
819
  },
742
820
  dispose() {
743
821
  root.removeAttribute(ATTR);
@@ -1343,7 +1421,15 @@ html[data-dshome-dispersion] .VOzbGW_panel{
1343
1421
  // agent is already working is `"steering"`. Measured on the current build,
1344
1422
  // a transcript can contain ONLY steering bubbles — anchoring the selector
1345
1423
  // on "user" alone silently skipped every bubble on the page.
1346
- const LIQUID_BUBBLE = ':is([data-chat-flow-kind="user"],[data-chat-flow-kind="steering"]) [class*="bubble" i]';
1424
+ //
1425
+ // The second half covers the gap the user hit: a message typed while the
1426
+ // agent is still working is painted in the transcript BEFORE it is picked
1427
+ // up, and at that moment it sits outside any flow-kind container, so the
1428
+ // two anchored selectors miss it and it renders unglazed until the turn
1429
+ // consumes it. Rather than enumerate the container it lands in, this side
1430
+ // takes any bubble that is NOT part of an assistant step — which is the
1431
+ // only place a bubble must not be glassed.
1432
+ 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"] *)';
1347
1433
  const LIQUID_CSS = `
1348
1434
  @property --dshome-liquid-sweep{syntax:"<angle>";inherits:false;initial-value:0deg}
1349
1435
  @keyframes dshome-liquid-sweep{from{--dshome-liquid-sweep:0deg}to{--dshome-liquid-sweep:360deg}}
@@ -1362,6 +1448,13 @@ html[data-dshome-glass="liquid"]{
1362
1448
  than everything else on purpose: at the shared 11px the flow behind it stays
1363
1449
  legible and the pane never separates from the transcript. */
1364
1450
  --dshome-liquid-blur-sidebar:24px;
1451
+ /* The composer blurs less than the rest on purpose. Filters in a backdrop
1452
+ chain apply left to right, so url(refract) followed by blur(N) blurs the
1453
+ refraction AFTER it happens: a band narrower than about the blur radius is
1454
+ simply erased. At 11px the purpose-built ~4px band was invisible, which is
1455
+ exactly what was reported. 8px lets a ~10px band read while still frosting
1456
+ the backdrop. */
1457
+ --dshome-liquid-blur-composer:8px;
1365
1458
  /* One extra veil on top of the sidebar's glass. The pane sits over the fluid
1366
1459
  at roughly the same tone as the conversation it borders, so without this
1367
1460
  lift the two read as one surface. It is white in light mode. */
@@ -1424,6 +1517,25 @@ html[data-dshome-glass="liquid"] [data-composer-card]::after{
1424
1517
  background-image:none!important;
1425
1518
  box-shadow:none!important;
1426
1519
  }
1520
+ /* The frosted recipe gives the composer a brand-tinted conic ring, painted by
1521
+ the patch block at inset:-1px and spun by dsh-border-spin. Under liquid the
1522
+ user asked for it to go: it sits one pixel OUTSIDE the card's own 1.5px white
1523
+ border, so the pair reads as a coloured line with a white line outside it, and
1524
+ two competing edges is what liquid glass is not. content:none removes the
1525
+ pseudo-element outright rather than painting it transparent. */
1526
+ html[data-dshome-glass="liquid"] body [data-composer-card]::before{
1527
+ content:none!important;
1528
+ background:none!important;
1529
+ animation:none!important;
1530
+ }
1531
+ /* With the ring gone the card's own 1.5px white border is the only edge left,
1532
+ and on its own it reads as a flat white outline rather than as glass — which
1533
+ is what the user saw next. Liquid's edge is the inset hairline plus the
1534
+ refraction, so the border goes transparent. The WIDTH stays: removing it
1535
+ would resize the box and shift every control inside by 3px. */
1536
+ html[data-dshome-glass="liquid"] body [data-composer-card]{
1537
+ border-color:transparent!important;
1538
+ }
1427
1539
  /* The sidebar pane carries its own fill and its own shape: rounded only on the
1428
1540
  edge that faces the conversation, so it reads as a panel clipped to the
1429
1541
  window rather than as a full-bleed column. The hairline ring in the shared
@@ -1462,9 +1574,14 @@ html[data-dshome-glass="liquid"] body .hHd-Xa_root::after{
1462
1574
  }
1463
1575
  /* The travelling edge highlight. mask-composite:exclude turns the filled
1464
1576
  conic gradient into a 1px perimeter band; padding:1px on the pseudo-element
1465
- sets that band's width without touching the product's box model. */
1466
- html[data-dshome-glass="liquid"] .hHd-Xa_root::after,
1467
- html[data-dshome-glass="liquid"] ${LIQUID_BUBBLE}::after{
1577
+ sets that band's width without touching the product's box model.
1578
+ It is drawn on the SIDEBAR ONLY. The user asked for it off the composer and
1579
+ off the message bubbles under liquid: on a small pill the band reads as a
1580
+ bright white outline rather than as light travelling a glass edge, and on the
1581
+ composer it doubled up with the card's own border. The bubbles and the
1582
+ composer keep liquid's other three marks — the translucent fill, the sheen
1583
+ and the refraction. */
1584
+ html[data-dshome-glass="liquid"] .hHd-Xa_root::after{
1468
1585
  content:"";position:absolute;inset:0;z-index:2;pointer-events:none;border-radius:inherit;
1469
1586
  padding:1px;
1470
1587
  background:conic-gradient(from var(--dshome-liquid-sweep),
@@ -1477,12 +1594,31 @@ html[data-dshome-glass="liquid"] ${LIQUID_BUBBLE}::after{
1477
1594
  mask-composite:exclude;
1478
1595
  animation:dshome-liquid-sweep 6.5s linear infinite;
1479
1596
  }
1480
- html[data-dshome-dispersion][data-dshome-glass="liquid"] body [data-composer-card],
1597
+ /* The composer's edge refraction. Three states, selected by the durable
1598
+ composerRefraction setting (see html[data-dshome-composer]):
1599
+ narrow (default) -- an 8px band top and bottom
1600
+ wide -- a 16px band top and bottom
1601
+ off -- no dispersion at all
1602
+ Left and right are left as they fall: the band is there, but over a
1603
+ continuous background a horizontal displacement has nothing to show, which
1604
+ the user accepted. */
1481
1605
  html[data-dshome-dispersion][data-dshome-glass="liquid"] body .wSkVaW_header:hover::before,
1482
1606
  html[data-dshome-dispersion][data-dshome-glass="liquid"] body ${LIQUID_BUBBLE}{
1483
1607
  -webkit-backdrop-filter:url(#dshome-glass-dispersion) blur(var(--dshome-liquid-blur)) saturate(var(--dshome-liquid-saturate))!important;
1484
1608
  backdrop-filter:url(#dshome-glass-dispersion) blur(var(--dshome-liquid-blur)) saturate(var(--dshome-liquid-saturate))!important;
1485
1609
  }
1610
+ html[data-dshome-dispersion][data-dshome-glass="liquid"][data-dshome-composer="narrow"] body [data-composer-card]{
1611
+ -webkit-backdrop-filter:url(#dshome-glass-dispersion-composer) blur(var(--dshome-liquid-blur-composer)) saturate(var(--dshome-liquid-saturate))!important;
1612
+ backdrop-filter:url(#dshome-glass-dispersion-composer) blur(var(--dshome-liquid-blur-composer)) saturate(var(--dshome-liquid-saturate))!important;
1613
+ }
1614
+ html[data-dshome-dispersion][data-dshome-glass="liquid"][data-dshome-composer="wide"] body [data-composer-card]{
1615
+ -webkit-backdrop-filter:url(#dshome-glass-dispersion-composer-wide) blur(var(--dshome-liquid-blur-composer)) saturate(var(--dshome-liquid-saturate))!important;
1616
+ backdrop-filter:url(#dshome-glass-dispersion-composer-wide) blur(var(--dshome-liquid-blur-composer)) saturate(var(--dshome-liquid-saturate))!important;
1617
+ }
1618
+ html[data-dshome-dispersion][data-dshome-glass="liquid"][data-dshome-composer="off"] body [data-composer-card]{
1619
+ -webkit-backdrop-filter:blur(var(--dshome-liquid-blur-composer)) saturate(var(--dshome-liquid-saturate))!important;
1620
+ backdrop-filter:blur(var(--dshome-liquid-blur-composer)) saturate(var(--dshome-liquid-saturate))!important;
1621
+ }
1486
1622
  /* The workspace list is faded out at its bottom by a gradient whose end stop is
1487
1623
  the sidebar-fill token — an OPAQUE colour matched to the product's own sidebar
1488
1624
  background. Over a translucent glass pane that stop no longer matches what is
@@ -1504,8 +1640,7 @@ html[data-dshome-dispersion][data-dshome-glass="liquid"] body .hHd-Xa_root::befo
1504
1640
  backdrop-filter:url(#dshome-glass-dispersion) blur(var(--dshome-liquid-blur-sidebar)) saturate(var(--dshome-liquid-saturate))!important;
1505
1641
  }
1506
1642
  @media (prefers-reduced-motion:reduce){
1507
- html[data-dshome-glass="liquid"] .hHd-Xa_root::after,
1508
- html[data-dshome-glass="liquid"] ${LIQUID_BUBBLE}::after{
1643
+ html[data-dshome-glass="liquid"] .hHd-Xa_root::after{
1509
1644
  animation:none;
1510
1645
  }
1511
1646
  }
@@ -1577,12 +1712,17 @@ html[data-dshome-dispersion][data-dshome-glass="liquid"] body .hHd-Xa_root::befo
1577
1712
  function createDarkSync(ctx, theme) {
1578
1713
  const apply = () => {
1579
1714
  let dark = false;
1580
- // The product's own attribute is authoritative when it is there: it
1581
- // is the same signal its `--dsw-alias-*` tokens are keyed on, and the
1582
- // page background is painted from this marker, so the two must not be
1583
- // allowed to drift apart.
1584
- if (document.body.hasAttribute('data-ds-dark-theme')) dark = true;
1585
- if (!dark && theme !== undefined && theme !== null && typeof theme.getTheme === 'function') {
1715
+ // Two signals, and BOTH of them lie in some state -- measured, not
1716
+ // assumed:
1717
+ // * the theme service reported `light` while the page was in dark
1718
+ // mode, so treating it as the only source painted a dark page light;
1719
+ // * `data-ds-dark-theme` was still on <body> after a switch back to
1720
+ // light, so treating IT as authoritative painted a light page dark.
1721
+ // This is the precedence that shipped through 1.43.1 and behaved in both
1722
+ // modes: ask the service first, and let the product's own attribute
1723
+ // override a "not dark" answer. Keep it, and sanity-check the signal
1724
+ // before touching it again.
1725
+ if (theme !== undefined && theme !== null && typeof theme.getTheme === 'function') {
1586
1726
  try {
1587
1727
  const snap = theme.getTheme();
1588
1728
  dark = snap !== undefined && snap !== null &&
@@ -1592,6 +1732,7 @@ html[data-dshome-dispersion][data-dshome-glass="liquid"] body .hHd-Xa_root::befo
1592
1732
  dark = false;
1593
1733
  }
1594
1734
  }
1735
+ if (!dark) dark = document.body.hasAttribute('data-ds-dark-theme');
1595
1736
  if (dark) document.body.setAttribute('data-dshome-dark', '');
1596
1737
  else document.body.removeAttribute('data-dshome-dark');
1597
1738
  };
@@ -1629,12 +1770,14 @@ html[data-dshome-dispersion][data-dshome-glass="liquid"] body .hHd-Xa_root::befo
1629
1770
  const root = document.documentElement;
1630
1771
  const apply = () => {
1631
1772
  root.setAttribute('data-dshome-glass', dsttGetGlass());
1773
+ root.setAttribute('data-dshome-composer', dsttGetComposer());
1632
1774
  };
1633
1775
  apply();
1634
1776
  const unsubscribe = dsttSubscribe(apply);
1635
1777
  return () => {
1636
1778
  unsubscribe();
1637
1779
  root.removeAttribute('data-dshome-glass');
1780
+ root.removeAttribute('data-dshome-composer');
1638
1781
  };
1639
1782
  }
1640
1783
 
@@ -3258,6 +3401,18 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
3258
3401
  liquid: { zh: "液态", en: "Liquid" },
3259
3402
  frosted: { zh: "白磨砂", en: "Frosted" }
3260
3403
  };
3404
+ // How wide the composer's edge refraction is under liquid, measured on the
3405
+ // top and bottom edges -- the only two where a displacement has anything to
3406
+ // show. The stylesheet gates the three states on <html data-dshome-composer>.
3407
+ // `origin` was this option's first id and is still accepted from a settings
3408
+ // file written before the rename; it reads back as `wide`.
3409
+ const DSTT_COMPOSER = ["narrow", "wide", "off"];
3410
+ const DSTT_COMPOSER_DEFAULT = "narrow";
3411
+ const DSTT_COMPOSER_LABELS = {
3412
+ narrow: { zh: "窄 8px", en: "Narrow 8px" },
3413
+ wide: { zh: "宽 16px", en: "Wide 16px" },
3414
+ off: { zh: "无", en: "Off" }
3415
+ };
3261
3416
 
3262
3417
  /**
3263
3418
  * Shared in-memory mirror of the durable DSTT mode. The settings domain's
@@ -3265,21 +3420,66 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
3265
3420
  * plugin's own loopback channel instead; every reader (the color driver,
3266
3421
  * the settings tab) subscribes to this mirror.
3267
3422
  */
3268
- const dsttState = { mode: "peakvalley-redblue", fluidBrush: false, glassStyle: DSTT_GLASS_DEFAULT, listeners: new Set() };
3423
+ const dsttState = { mode: "peakvalley-redblue", fluidBrush: false, glassStyle: DSTT_GLASS_DEFAULT, composer: DSTT_COMPOSER_DEFAULT, listeners: new Set() };
3424
+ // Fields the user has chosen in this session. A host half that predates a
3425
+ // field ignores it and answers without it -- or with its own default -- so
3426
+ // without this a fresh choice gets overwritten by the next sync and the
3427
+ // option appears to snap straight back to its default.
3428
+ const dsttTouched = new Set();
3269
3429
  const dsttGetMode = () => dsttState.mode;
3270
3430
  const dsttGetGlass = () => (DSTT_GLASS_STYLES.includes(dsttState.glassStyle) ? dsttState.glassStyle : DSTT_GLASS_DEFAULT);
3271
- /** Persist the glass recipe through the bridge; resolves with success. */
3431
+ const dsttGetComposer = () => {
3432
+ if (dsttState.composer === "origin") return "wide";
3433
+ return DSTT_COMPOSER.includes(dsttState.composer) ? dsttState.composer : DSTT_COMPOSER_DEFAULT;
3434
+ };
3435
+ /**
3436
+ * Persist the composer's refraction width through the bridge.
3437
+ *
3438
+ * Applied locally FIRST, before the round trip. Both this and glassStyle
3439
+ * only flip a CSS attribute, so the choice is fully usable the moment it is
3440
+ * made — and it has to be, because a running dsh keeps the host half it
3441
+ * booted with: until `dsh web` is restarted, `dstt.mode.set` ignores fields
3442
+ * it does not know and answers without them, which used to make a brand new
3443
+ * setting look broken. The cost of applying first is that a choice made
3444
+ * against a stale host is not persisted and reverts on reload, so the
3445
+ * return value still reports the truth.
3446
+ */
3447
+ async function dsttSetComposer(ctx, next) {
3448
+ if (!DSTT_COMPOSER.includes(next)) return false;
3449
+ dsttState.composer = next;
3450
+ dsttTouched.add("composer");
3451
+ dsttNotify();
3452
+ try {
3453
+ const result = await bridgeCall(DSTT_SET, { mode: dsttState.mode, composerRefraction: next });
3454
+ // The response is used ONLY to confirm. It must never be written back
3455
+ // into the mirror: a host that accepts the call but does not store the
3456
+ // field answers with its own value (the default, `narrow`), and copying
3457
+ // that in is what made the option snap straight back the moment the
3458
+ // round trip landed.
3459
+ if (result !== undefined && result !== null && result.ok === true && DSTT_COMPOSER.includes(result.value.composerRefraction)) {
3460
+ return result.value.composerRefraction === next;
3461
+ }
3462
+ } catch (error) {
3463
+ // Fall through: the local choice stands, it just was not stored.
3464
+ }
3465
+ return false;
3466
+ }
3467
+ /** Persist the glass recipe through the bridge; resolves with success.
3468
+ * Applied locally first for the same reason as dsttSetComposer above. */
3272
3469
  async function dsttSetGlass(ctx, next) {
3273
3470
  if (!DSTT_GLASS_STYLES.includes(next)) return false;
3471
+ dsttState.glassStyle = next;
3472
+ dsttTouched.add("glassStyle");
3473
+ dsttNotify();
3274
3474
  try {
3275
3475
  const result = await bridgeCall(DSTT_SET, { mode: dsttState.mode, glassStyle: next });
3476
+ // Confirmation only -- see dsttSetComposer for why the host's echo must
3477
+ // not be copied back into the mirror.
3276
3478
  if (result !== undefined && result !== null && result.ok === true && DSTT_GLASS_STYLES.includes(result.value.glassStyle)) {
3277
- dsttState.glassStyle = result.value.glassStyle;
3278
- dsttNotify();
3279
- return true;
3479
+ return result.value.glassStyle === next;
3280
3480
  }
3281
3481
  } catch (error) {
3282
- // Fall through to the caller's failure handling.
3482
+ // Fall through: the local choice stands, it just was not stored.
3283
3483
  }
3284
3484
  return false;
3285
3485
  }
@@ -3335,8 +3535,12 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
3335
3535
  dsttState.mode = result.value.mode;
3336
3536
  dsttState.fluidBrush = result.value.fluidBrush === true;
3337
3537
  // An older host half answers without a glass recipe: keep the
3338
- // default rather than clearing the attribute to nothing.
3339
- if (DSTT_GLASS_STYLES.includes(result.value.glassStyle)) dsttState.glassStyle = result.value.glassStyle;
3538
+ // default rather than clearing the attribute to nothing. And a
3539
+ // field the user has already chosen this session is never taken
3540
+ // back from the host -- a stale host answers with its own default,
3541
+ // which is what made a fresh choice look like it snapped back.
3542
+ if (!dsttTouched.has("glassStyle") && DSTT_GLASS_STYLES.includes(result.value.glassStyle)) dsttState.glassStyle = result.value.glassStyle;
3543
+ if (!dsttTouched.has("composer") && DSTT_COMPOSER.includes(result.value.composerRefraction)) dsttState.composer = result.value.composerRefraction;
3340
3544
  dsttNotify();
3341
3545
  }
3342
3546
  } catch (error) {}
@@ -3592,6 +3796,11 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
3592
3796
  if (dstt === undefined || typeof dstt.setGlass !== 'function' || next === glassNow) return;
3593
3797
  dstt.setGlass(next);
3594
3798
  };
3799
+ const composerNow = dstt !== undefined && typeof dstt.composer === 'function' ? dstt.composer() : DSTT_COMPOSER_DEFAULT;
3800
+ const pickComposer = (next) => {
3801
+ if (dstt === undefined || typeof dstt.setComposer !== 'function' || next === composerNow) return;
3802
+ dstt.setComposer(next);
3803
+ };
3595
3804
 
3596
3805
  return React.createElement('div', { className: 'dshome-dstt' },
3597
3806
  React.createElement('h3', null,
@@ -3643,6 +3852,21 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
3643
3852
  className: 'dshome-dstt-seg-btn' + (glassNow === style ? ' dshome-on' : ''),
3644
3853
  onClick: () => pickGlass(style)
3645
3854
  }, isZh ? DSTT_GLASS_LABELS[style].zh : DSTT_GLASS_LABELS[style].en)))),
3855
+ React.createElement('div', { className: 'dshome-dstt-row' },
3856
+ React.createElement('div', { className: 'dshome-dstt-row-body' },
3857
+ React.createElement('div', { className: 'dshome-dstt-row-title' }, isZh ? '对话框边缘倒影' : 'Composer edge reflection'),
3858
+ React.createElement('div', { className: 'dshome-dstt-row-hint' }, isZh
3859
+ ? '液态下对话框边缘的折射带宽(按上下量——左右背后是连续背景,同样的位移看不出变化)。窄 8px(默认)/ 宽 16px / 无,三者都是专为这张卡片做的位移图。'
3860
+ : 'Width of the lens band at the composer edge under liquid, measured top and bottom — left and right sit over a continuous backdrop where the same displacement shows nothing. Narrow 8px (default) / Wide 16px / Off; each is a map built for this card.')),
3861
+ React.createElement('div', { className: 'dshome-dstt-seg', role: 'radiogroup', 'aria-label': isZh ? '对话框边缘倒影' : 'Composer edge reflection' },
3862
+ DSTT_COMPOSER.map((choice) => React.createElement('button', {
3863
+ key: choice,
3864
+ type: 'button',
3865
+ role: 'radio',
3866
+ 'aria-checked': composerNow === choice,
3867
+ className: 'dshome-dstt-seg-btn' + (composerNow === choice ? ' dshome-on' : ''),
3868
+ onClick: () => pickComposer(choice)
3869
+ }, isZh ? DSTT_COMPOSER_LABELS[choice].zh : DSTT_COMPOSER_LABELS[choice].en)))),
3646
3870
  React.createElement('div', { className: 'dshome-dstt-status', 'data-peak': timeSliced && peakNow },
3647
3871
  '当前:' + colorName + ' · ' + status));
3648
3872
  }
@@ -3760,6 +3984,8 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
3760
3984
  setBrush: (next) => dsttSetBrush(ctx, next),
3761
3985
  glass: dsttGetGlass,
3762
3986
  setGlass: (next) => dsttSetGlass(ctx, next),
3987
+ composer: dsttGetComposer,
3988
+ setComposer: (next) => dsttSetComposer(ctx, next),
3763
3989
  subscribe: dsttSubscribe
3764
3990
  },
3765
3991
  locale: ctx.get('locale')