dsh-deepseek-style-theme 1.43.3 → 1.43.6

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
@@ -2,6 +2,17 @@
2
2
 
3
3
  为 DeepSeek Harness Web GUI 复刻 DeepSeek 官网视觉的主题皮肤插件。
4
4
 
5
+ ## 鸣谢
6
+
7
+ 本插件的两块核心视觉——**流体背景**与**玻璃质感**——都不是从零写出来的,它们分别来自下面两个上游项目。没有它们,这个主题不会有现在这个样子:
8
+
9
+ | 上游 | 作者 | 本插件用到了什么 |
10
+ |---|---|---|
11
+ | [dsh-theme-mineradio](https://github.com/dhicoc/dsh-theme-mineradio) | [@dhicoc](https://github.com/dhicoc) | 流体背景的**三段 GLSL 着色器**(顶点 / 流场 / 显示)是上游的逐字节副本;四分之一分辨率流场 + 双 framebuffer 乒乓的求解器结构、玻璃色散折射滤镜、光标视差、对话框悬停倾斜同样移植自上游 |
12
+ | [deepseek-harness-background](https://github.com/HaoyueQin/deepseek-harness-background) | [@HaoyueQin](https://github.com/HaoyueQin) | 液态玻璃配方的**技法来源**(半透明填充 + 竖向光泽 + 背板滤镜链的设计思路),本插件按自己的选择器与 `--dshome-glass-*` 变量重新表达,未复制源码 |
13
+
14
+ 两个项目均为 MIT 许可;完整许可原文见 [`THIRD-PARTY-NOTICES.md`](./THIRD-PARTY-NOTICES.md),随包发布,再分发时必须保留。详细的移植范围与改写点见下文「[第三方代码与署名](#第三方代码与署名)」。
15
+
5
16
  ## 特性
6
17
 
7
18
  - **流体流动背景(1.42.0+)**:全屏 WebGL2 双通道流体模拟——四分之一分辨率的流场(衰减 + 带速度的指针笔刷,两个 framebuffer 乒乓)被全分辨率的域扭曲噪声渲染器采样,带旋流迭代与三色柔性混合;按钮悬停会轻推流场、点击则荡开涟漪。配色跟随 DSTT 三色令牌(峰谷红 / 谷时蓝 / 常态绿)实时重新着色,无需重挂。**无 WebGL2 时自动回落到原来的粒子背景**(粒子实现完整保留)
package/cordis.patch.yml CHANGED
@@ -1,6 +1,6 @@
1
- # DeepSeek-Style-Theme bundle patch: inserts its public dsh.client entry into
2
- # the web plugin roster. The bundle is the official dsh plugin shape — install
3
- # it with `dsh plugin --profile <name> add <path-or-git-url>`.
4
- - insert:
5
- - id: ui-skin-deepseek-style
6
- name: 'dsh-deepseek-style-theme'
1
+ # DeepSeek-Style-Theme bundle patch: inserts its public dsh.client entry into
2
+ # the web plugin roster. The bundle is the official dsh plugin shape — install
3
+ # it with `dsh plugin --profile <name> add <path-or-git-url>`.
4
+ - insert:
5
+ - id: ui-skin-deepseek-style
6
+ name: 'dsh-deepseek-style-theme'
package/lib/client.js CHANGED
@@ -1,18 +1,22 @@
1
- window.__ModuleLoader__.load({
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",
13
- factory: (require) => {
14
- var module = { exports: {} };
15
- var exports = module.exports;
1
+ // The loader requires this bundle to register itself under the id the host asked
2
+ // for, and that id is the package NAME of whichever copy is installed -- which is
3
+ // not one value during a rename. The package moved from
4
+ // @dsh-external/dsh-deepseek-style-theme to the unscoped dsh-deepseek-style-theme
5
+ // in 1.43.1, so a profile installed before the rename asks for the old id and one
6
+ // installed after asks for the new one. A single hard-coded literal therefore
7
+ // breaks whichever half it does not match, and BOTH directions were hit for real,
8
+ // one machine each:
9
+ //
10
+ // literal old, install new -> "loaded without registering \"dsh-deepseek-style-theme\""
11
+ // literal new, install old -> "loaded without registering \"@dsh-external/...\""
12
+ //
13
+ // So register under BOTH ids with the same factory: the loader finds the one it
14
+ // wants and the other entry is simply never asked for. The registrations are
15
+ // guarded individually because a loader might reject an id it did not request,
16
+ // and that must not take the working one down with it.
17
+ const dshDeepseekStyleThemeFactory = (require) => {
18
+ var module = { exports: {} };
19
+ var exports = module.exports;
16
20
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
17
21
  const React = require("react");
18
22
 
@@ -991,18 +995,20 @@ void main() {
991
995
  const CORE_CSS = `
992
996
  :root{--ds-brand:#059669;--ds-brand-deep:#047857;--ds-brand-mid:#10B981;--ds-brand-light:#34D399;--ds-glass-blur:24px}
993
997
  html,body{background-color:transparent!important}
994
- /* The page background below is painted from OUR scheme marker
995
- (body[data-dshome-dark]), while message text takes its colour from the
996
- product's --dsw-alias-* tokens. Those are two different signals, and when
997
- they disagree — the marker is derived from the theme service and can miss a
998
- scheme change — the result is near-white text on a near-white page: every
999
- paragraph vanishes while anything carrying its own background (tool cards)
1000
- stays readable. Reproduced by clearing the marker on a dark page.
1001
- Declaring the same two label tokens under the same marker puts both sides on
1002
- ONE signal. The values are exactly what theme.overrideTokens() already
1003
- installs for each scheme, so a page where the two agree repaints identically. */
1004
- body{--dsw-alias-label-primary:#152443!important;--dsw-alias-label-secondary:#3c4a63!important}
1005
- body[data-dshome-dark]{--dsw-alias-label-primary:#eef2ff!important;--dsw-alias-label-secondary:#a9b8d8!important}
998
+ /* THE LABEL TOKENS ARE NOT PINNED HERE, AND MUST NOT BE AGAIN.
999
+ A previous attempt at the invisible-text bug declared
1000
+ body{--dsw-alias-label-primary:...!important} under our own scheme marker,
1001
+ so that background and text could never end up on opposite sides. It did fix
1002
+ that, and it broke something worse: those !important declarations outrank the
1003
+ PRODUCT's tokens, so when the user switched appearance the product changed its
1004
+ own tokens and this stylesheet kept painting the old ones. The UI looked like
1005
+ the switch was dead.
1006
+ Measured on the real page: with the theme's style tags removed the switch
1007
+ works, with them present it does not, and reverting the header z-index changes
1008
+ nothing -- i.e. it is this sheet, and it is these declarations.
1009
+ The invisible-text hazard stays open on purpose. A stuck appearance switch is
1010
+ a broken control; near-white text on a near-white page at least still reads
1011
+ once the marker agrees. Do not re-add these two rules. */
1006
1012
  body{background:radial-gradient(1200px 700px at 12% -10%,color-mix(in srgb,var(--ds-brand) 14%,transparent),transparent 60%),radial-gradient(1000px 600px at 105% 12%,color-mix(in srgb,var(--ds-brand-light) 16%,transparent),transparent 55%),radial-gradient(900px 700px at 50% 115%,color-mix(in srgb,var(--ds-brand) 10%,transparent),transparent 60%),linear-gradient(180deg,#fbfbfd 0%,#f5f6fa 100%);background-attachment:fixed}
1007
1013
  body[data-dshome-color=red]{--ds-brand:#F5222D;--ds-brand-deep:#CF1322;--ds-brand-mid:#FF4D4F;--ds-brand-light:#FF7875}
1008
1014
  body[data-dshome-color=blue]{--ds-brand:#4D6BFE;--ds-brand-deep:#3A65C2;--ds-brand-mid:#4176E6;--ds-brand-light:#73A3D2}
@@ -1639,6 +1645,18 @@ html[data-dshome-dispersion][data-dshome-glass="liquid"] body .hHd-Xa_root::befo
1639
1645
  -webkit-backdrop-filter:url(#dshome-glass-dispersion) blur(var(--dshome-liquid-blur-sidebar)) saturate(var(--dshome-liquid-saturate))!important;
1640
1646
  backdrop-filter:url(#dshome-glass-dispersion) blur(var(--dshome-liquid-blur-sidebar)) saturate(var(--dshome-liquid-saturate))!important;
1641
1647
  }
1648
+ /* Backdrop blur, switched off by the durable backdropBlur setting. Each glass
1649
+ recipe reads its radius from a variable, so turning the frosting off is one
1650
+ 0px per variable -- the translucency, the sheen and the edge refraction all
1651
+ stay exactly as they were. Written with a body step so it also out-ranks the
1652
+ html[data-dshome-glass] block the liquid radii are declared in. */
1653
+ html[data-dshome-blur="off"],
1654
+ html[data-dshome-blur="off"] body{
1655
+ --dshome-glass-blur:0px;
1656
+ --dshome-liquid-blur:0px;
1657
+ --dshome-liquid-blur-sidebar:0px;
1658
+ --dshome-liquid-blur-composer:0px;
1659
+ }
1642
1660
  @media (prefers-reduced-motion:reduce){
1643
1661
  html[data-dshome-glass="liquid"] .hHd-Xa_root::after{
1644
1662
  animation:none;
@@ -1710,6 +1728,20 @@ html[data-dshome-dispersion][data-dshome-glass="liquid"] body .hHd-Xa_root::befo
1710
1728
  * subscription is unavailable.
1711
1729
  */
1712
1730
  function createDarkSync(ctx, theme) {
1731
+ /** Last value we wrote to the body; null until the first apply(). */
1732
+ let lastDark = null;
1733
+ /**
1734
+ * Scheme-change listeners. The dark marker is a marker only if someone
1735
+ * reads it: the stylesheet matches on it, and the ambient background reads
1736
+ * it for its palette (see startAmbient). A listener that throws must not
1737
+ * cost the others their update, so each call is boxed.
1738
+ */
1739
+ const darkListeners = new Set();
1740
+ const notifyDark = (dark) => {
1741
+ for (const fn of darkListeners) {
1742
+ try { fn(dark); } catch (error) {}
1743
+ }
1744
+ };
1713
1745
  const apply = () => {
1714
1746
  let dark = false;
1715
1747
  // Two signals, and BOTH of them lie in some state -- measured, not
@@ -1733,10 +1765,19 @@ html[data-dshome-dispersion][data-dshome-glass="liquid"] body .hHd-Xa_root::befo
1733
1765
  }
1734
1766
  }
1735
1767
  if (!dark) dark = document.body.hasAttribute('data-ds-dark-theme');
1768
+ // Remember what we actually put on the body, so a later apply() that
1769
+ // lands on the same answer does not wake the background up for
1770
+ // nothing. `lastDark` is null until the first write.
1771
+ const changed = lastDark !== dark;
1772
+ lastDark = dark;
1736
1773
  if (dark) document.body.setAttribute('data-dshome-dark', '');
1737
1774
  else document.body.removeAttribute('data-dshome-dark');
1775
+ // Publish the write. The marker is what EVERY dark rule and the fluid
1776
+ // palette read, so a light/dark switch that only set the attribute left
1777
+ // the background on the scheme it was mounted with -- reported as
1778
+ // "切到深色背景还是蓝白".
1779
+ if (changed) notifyDark(dark);
1738
1780
  };
1739
- apply();
1740
1781
  let unsubscribe = null;
1741
1782
  try {
1742
1783
  unsubscribe = ctx.on('theme/change', apply);
@@ -1753,9 +1794,21 @@ html[data-dshome-dispersion][data-dshome-glass="liquid"] body .hHd-Xa_root::befo
1753
1794
  } catch (error) {
1754
1795
  mirror = null;
1755
1796
  }
1756
- return () => {
1757
- if (unsubscribe !== null) unsubscribe();
1758
- if (mirror !== null) mirror.disconnect();
1797
+ return {
1798
+ /**
1799
+ * Subscribe to light/dark *writes*. Nothing is replayed on subscribe:
1800
+ * a reader that needs the current value reads the attribute (or calls
1801
+ * dshomeDark()), which keeps one source of truth.
1802
+ */
1803
+ subscribe: (fn) => {
1804
+ darkListeners.add(fn);
1805
+ return () => darkListeners.delete(fn);
1806
+ },
1807
+ dispose: () => {
1808
+ darkListeners.clear();
1809
+ if (unsubscribe !== null) unsubscribe();
1810
+ if (mirror !== null) mirror.disconnect();
1811
+ }
1759
1812
  };
1760
1813
  }
1761
1814
 
@@ -1771,6 +1824,7 @@ html[data-dshome-dispersion][data-dshome-glass="liquid"] body .hHd-Xa_root::befo
1771
1824
  const apply = () => {
1772
1825
  root.setAttribute('data-dshome-glass', dsttGetGlass());
1773
1826
  root.setAttribute('data-dshome-composer', dsttGetComposer());
1827
+ root.setAttribute('data-dshome-blur', dsttGetBlur() ? 'on' : 'off');
1774
1828
  };
1775
1829
  apply();
1776
1830
  const unsubscribe = dsttSubscribe(apply);
@@ -1778,6 +1832,7 @@ html[data-dshome-dispersion][data-dshome-glass="liquid"] body .hHd-Xa_root::befo
1778
1832
  unsubscribe();
1779
1833
  root.removeAttribute('data-dshome-glass');
1780
1834
  root.removeAttribute('data-dshome-composer');
1835
+ root.removeAttribute('data-dshome-blur');
1781
1836
  };
1782
1837
  }
1783
1838
 
@@ -2019,25 +2074,48 @@ html[data-dshome-dispersion][data-dshome-glass="liquid"] body .hHd-Xa_root::befo
2019
2074
  };
2020
2075
  applyGlass();
2021
2076
  const unsubscribeGlass = dsttSubscribe(applyGlass);
2022
- const unsubscribe = subscribeColor((color) => {
2023
- fluid.setParams(dshomeFluidParams());
2024
- dispersion.setTint(FLUID_HUES[color] ?? FLUID_HUES.green);
2077
+ // ── palette reconciliation ────────────────────────────────────────────
2078
+ // One reader for BOTH inputs the palette depends on: the colour token on
2079
+ // <body> and the dark marker. Reported symptom: "切主题模式背景不变 /
2080
+ // 深色模式背景还是蓝白". The colour half of that was a real dead end --
2081
+ // `subscribeColor` only fires when the notified colour string CHANGES, so
2082
+ // the boot race (this canvas mounts, then the durable mode arrives over
2083
+ // the async bridge and re-resolves the colour) left the flow painted from
2084
+ // whatever the driver had guessed first, and a mode switch that resolved
2085
+ // to the same colour never notified at all. The scheme half had no
2086
+ // listener: a MutationObserver was watching the marker, but `darkSync`
2087
+ // pointedly does not set a *changed* value twice, and the marker itself
2088
+ // was the only thing written.
2089
+ // So: derive from the DOM on every signal instead of trusting the event to
2090
+ // carry it, and box every listener so one throw cannot cost the rest.
2091
+ let fluidParams = JSON.stringify(dshomeFluidParams());
2092
+ const syncFluid = () => {
2093
+ const next = dshomeFluidParams();
2094
+ const key = JSON.stringify(next);
2095
+ if (key === fluidParams) return;
2096
+ fluidParams = key;
2097
+ try { fluid.setParams(next); } catch (error) {}
2098
+ };
2099
+ const unsubscribeColor = subscribeColor((color) => {
2100
+ syncFluid();
2101
+ try { dispersion.setTint(FLUID_HUES[color] ?? FLUID_HUES.green); } catch (error) {}
2025
2102
  });
2026
- // The palette is chosen per colour SCHEME, not only per colour token:
2027
- // without this the canvas keeps whatever palette it was mounted with,
2028
- // so a page that loads light and is then switched to dark keeps a
2029
- // bright flow under a dark shell.
2030
- let darkMirror = null;
2103
+ const unsubscribeDark = darkSync === null ? null : darkSync.subscribe(syncFluid);
2104
+ // The colour driver writes the token as an attribute; watching it as well
2105
+ // keeps this correct no matter who wrote it (the peak-hour chain, a manual
2106
+ // mode pick, or a future caller that forgets to notify).
2107
+ let tokenMirror = null;
2031
2108
  try {
2032
- darkMirror = new MutationObserver(() => { fluid.setParams(dshomeFluidParams()); });
2033
- darkMirror.observe(document.body, { attributes: true, attributeFilter: ['data-dshome-dark'] });
2109
+ tokenMirror = new MutationObserver(syncFluid);
2110
+ tokenMirror.observe(document.body, { attributes: true, attributeFilter: ['data-dshome-color'] });
2034
2111
  } catch (error) {
2035
- darkMirror = null;
2112
+ tokenMirror = null;
2036
2113
  }
2037
2114
  return () => {
2038
- unsubscribe();
2115
+ unsubscribeColor();
2116
+ if (unsubscribeDark !== null) unsubscribeDark();
2039
2117
  unsubscribeGlass();
2040
- if (darkMirror !== null) darkMirror.disconnect();
2118
+ if (tokenMirror !== null) tokenMirror.disconnect();
2041
2119
  dispersion.dispose();
2042
2120
  stopParallax();
2043
2121
  stopSpotter();
@@ -2159,7 +2237,20 @@ html[data-dshome-dispersion][data-dshome-glass="liquid"] body .hHd-Xa_root::befo
2159
2237
  const colors = palette();
2160
2238
  for (const p of parts) p.c = colors[(Math.random() * colors.length) | 0];
2161
2239
  };
2240
+ // Two inputs again: the colour token (notified) and the dark marker
2241
+ // (written by darkSync). The particle palette is per scheme, so a
2242
+ // light/dark switch has to re-roll the colours too -- otherwise the
2243
+ // fallback background keeps the scheme it was mounted with, exactly the
2244
+ // bug the fluid path had.
2162
2245
  const unsubscribeColor = subscribeColor(refreshColors);
2246
+ const unsubscribeDark = darkSync === null ? null : darkSync.subscribe(refreshColors);
2247
+ let tokenMirror = null;
2248
+ try {
2249
+ tokenMirror = new MutationObserver(refreshColors);
2250
+ tokenMirror.observe(document.body, { attributes: true, attributeFilter: ['data-dshome-color'] });
2251
+ } catch (error) {
2252
+ tokenMirror = null;
2253
+ }
2163
2254
 
2164
2255
  window.addEventListener('resize', resize);
2165
2256
  resize();
@@ -2171,6 +2262,8 @@ html[data-dshome-dispersion][data-dshome-glass="liquid"] body .hHd-Xa_root::befo
2171
2262
  cancelAnimationFrame(rafId);
2172
2263
  window.removeEventListener('resize', resize);
2173
2264
  unsubscribeColor();
2265
+ if (unsubscribeDark !== null) unsubscribeDark();
2266
+ if (tokenMirror !== null) tokenMirror.disconnect();
2174
2267
  canvas.remove();
2175
2268
  };
2176
2269
  }
@@ -3420,7 +3513,7 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
3420
3513
  * plugin's own loopback channel instead; every reader (the color driver,
3421
3514
  * the settings tab) subscribes to this mirror.
3422
3515
  */
3423
- const dsttState = { mode: "peakvalley-redblue", fluidBrush: false, glassStyle: DSTT_GLASS_DEFAULT, composer: DSTT_COMPOSER_DEFAULT, listeners: new Set() };
3516
+ const dsttState = { mode: "peakvalley-redblue", fluidBrush: false, glassStyle: DSTT_GLASS_DEFAULT, composer: DSTT_COMPOSER_DEFAULT, blur: true, listeners: new Set() };
3424
3517
  // Fields the user has chosen in this session. A host half that predates a
3425
3518
  // field ignores it and answers without it -- or with its own default -- so
3426
3519
  // without this a fresh choice gets overwritten by the next sync and the
@@ -3432,6 +3525,26 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
3432
3525
  if (dsttState.composer === "origin") return "wide";
3433
3526
  return DSTT_COMPOSER.includes(dsttState.composer) ? dsttState.composer : DSTT_COMPOSER_DEFAULT;
3434
3527
  };
3528
+ const dsttGetBlur = () => dsttState.blur !== false;
3529
+ /**
3530
+ * Turn the glass frosting on or off. Same shape as the other CSS-only
3531
+ * preferences: apply locally first, then let the host persist it, and treat
3532
+ * its answer as confirmation only.
3533
+ */
3534
+ async function dsttSetBlur(ctx, next) {
3535
+ dsttState.blur = next === true;
3536
+ dsttTouched.add("blur");
3537
+ dsttNotify();
3538
+ try {
3539
+ const result = await bridgeCall(DSTT_SET, { mode: dsttState.mode, backdropBlur: dsttState.blur });
3540
+ if (result !== undefined && result !== null && result.ok === true) {
3541
+ return result.value.backdropBlur === dsttState.blur;
3542
+ }
3543
+ } catch (error) {
3544
+ // Fall through: the local choice stands, it just was not stored.
3545
+ }
3546
+ return false;
3547
+ }
3435
3548
  /**
3436
3549
  * Persist the composer's refraction width through the bridge.
3437
3550
  *
@@ -3527,6 +3640,9 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
3527
3640
  return await response.json();
3528
3641
  }
3529
3642
 
3643
+ /** Fallback used when the host half is unavailable. */
3644
+ const dsttGetBlurFallback = () => dsttState.blur !== false;
3645
+
3530
3646
  /** Pull the durable mode from the host half once. */
3531
3647
  async function dsttSync(ctx) {
3532
3648
  try {
@@ -3541,6 +3657,7 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
3541
3657
  // which is what made a fresh choice look like it snapped back.
3542
3658
  if (!dsttTouched.has("glassStyle") && DSTT_GLASS_STYLES.includes(result.value.glassStyle)) dsttState.glassStyle = result.value.glassStyle;
3543
3659
  if (!dsttTouched.has("composer") && DSTT_COMPOSER.includes(result.value.composerRefraction)) dsttState.composer = result.value.composerRefraction;
3660
+ if (!dsttTouched.has("blur") && typeof result.value.backdropBlur === "boolean") dsttState.blur = result.value.backdropBlur;
3544
3661
  dsttNotify();
3545
3662
  }
3546
3663
  } catch (error) {}
@@ -3705,10 +3822,18 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
3705
3822
 
3706
3823
  anchor();
3707
3824
 
3825
+ // The durable mode crosses an async bridge (dsttSync), so the anchor above
3826
+ // may have resolved the colour from the DEFAULT mode -- its 常态绿/常态蓝
3827
+ // token then never appears, and the background keeps the first guess. One
3828
+ // extra read after the sync has had its turn fixes the boot race, and it
3829
+ // is idempotent, so it cannot fight a later answer.
3830
+ const bootTimer = setTimeout(anchor, 0);
3831
+
3708
3832
  try { unsubscribe = dsttSubscribe(anchor); } catch (error) { unsubscribe = null; }
3709
3833
 
3710
3834
  return () => {
3711
3835
  stopped = true;
3836
+ clearTimeout(bootTimer);
3712
3837
  if (chainTimer !== 0) clearTimeout(chainTimer);
3713
3838
  if (unsubscribe !== null) unsubscribe();
3714
3839
  document.removeEventListener('visibilitychange', onVisibility);
@@ -3801,6 +3926,11 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
3801
3926
  if (dstt === undefined || typeof dstt.setComposer !== 'function' || next === composerNow) return;
3802
3927
  dstt.setComposer(next);
3803
3928
  };
3929
+ const blurOn = dstt !== undefined && typeof dstt.blur === 'function' ? dstt.blur() !== false : true;
3930
+ const toggleBlur = () => {
3931
+ if (dstt === undefined || typeof dstt.setBlur !== 'function') return;
3932
+ dstt.setBlur(!blurOn);
3933
+ };
3804
3934
 
3805
3935
  return React.createElement('div', { className: 'dshome-dstt' },
3806
3936
  React.createElement('h3', null,
@@ -3867,6 +3997,19 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
3867
3997
  className: 'dshome-dstt-seg-btn' + (composerNow === choice ? ' dshome-on' : ''),
3868
3998
  onClick: () => pickComposer(choice)
3869
3999
  }, isZh ? DSTT_COMPOSER_LABELS[choice].zh : DSTT_COMPOSER_LABELS[choice].en)))),
4000
+ React.createElement('div', { className: 'dshome-dstt-row' },
4001
+ React.createElement('div', { className: 'dshome-dstt-row-body' },
4002
+ React.createElement('div', { className: 'dshome-dstt-row-title' }, isZh ? '背景模糊(磨砂)' : 'Backdrop blur'),
4003
+ React.createElement('div', { className: 'dshome-dstt-row-hint' }, isZh
4004
+ ? '玻璃面板后面那层虚化。关闭后玻璃变成纯半透明:通透度、顶部光泽、边缘折射都保留,只是不再把背后的东西磨掉。'
4005
+ : 'The frosting behind every glass pane. Off leaves the translucency, the sheen and the edge refraction intact and only stops blurring what is behind the glass.')),
4006
+ React.createElement('button', {
4007
+ type: 'button',
4008
+ role: 'switch',
4009
+ 'aria-checked': blurOn,
4010
+ className: 'dshome-dstt-seg-btn' + (blurOn ? ' dshome-on' : ''),
4011
+ onClick: toggleBlur
4012
+ }, blurOn ? (isZh ? '已开启' : 'On') : (isZh ? '已关闭' : 'Off'))),
3870
4013
  React.createElement('div', { className: 'dshome-dstt-status', 'data-peak': timeSliced && peakNow },
3871
4014
  '当前:' + colorName + ' · ' + status));
3872
4015
  }
@@ -3894,8 +4037,18 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
3894
4037
 
3895
4038
  // Dark marker: owned attribute driven by the theme service, never by
3896
4039
  // the product's attribute name. Must precede the patch blocks so the
3897
- // dark selectors match from the first paint.
3898
- ctx.effect(() => createDarkSync(ctx, theme), 'deepseek-style-theme: dark sync');
4040
+ // dark selectors match from the first paint. The handle is kept in scope
4041
+ // because the ambient background subscribes to its writes: the marker
4042
+ // alone was not enough, since nothing re-read it when the scheme changed.
4043
+ let darkSync = null;
4044
+ ctx.effect(() => {
4045
+ const handle = createDarkSync(ctx, theme);
4046
+ darkSync = handle;
4047
+ return () => {
4048
+ if (darkSync === handle) darkSync = null;
4049
+ handle.dispose();
4050
+ };
4051
+ }, 'deepseek-style-theme: dark sync');
3899
4052
 
3900
4053
  // Glass-recipe marker: same idea, and it has to be on <html> before the
3901
4054
  // first paint of the glass surfaces or the pane flashes the other recipe.
@@ -3986,6 +4139,8 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
3986
4139
  setGlass: (next) => dsttSetGlass(ctx, next),
3987
4140
  composer: dsttGetComposer,
3988
4141
  setComposer: (next) => dsttSetComposer(ctx, next),
4142
+ blur: dsttGetBlur,
4143
+ setBlur: (next) => dsttSetBlur(ctx, next),
3989
4144
  subscribe: dsttSubscribe
3990
4145
  },
3991
4146
  locale: ctx.get('locale')
@@ -3997,5 +4152,13 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
3997
4152
  exports.apply = apply;
3998
4153
  exports.inject = ['theme', 'slots', 'locale', 'workspaces', 'connection'];
3999
4154
  return module.exports;
4000
- }
4001
- });
4155
+ };
4156
+
4157
+ // Both ids, same factory. See the note at the top of this file for why one
4158
+ // literal cannot work during the rename.
4159
+ try {
4160
+ window.__ModuleLoader__.load({ id: "dsh-deepseek-style-theme", factory: dshDeepseekStyleThemeFactory });
4161
+ } catch (error) {}
4162
+ try {
4163
+ window.__ModuleLoader__.load({ id: "@dsh-external/dsh-deepseek-style-theme", factory: dshDeepseekStyleThemeFactory });
4164
+ } catch (error) {}
package/lib/index.js CHANGED
@@ -134,7 +134,12 @@ const DsttSettingsSchema = z === null ? null : z.object({
134
134
  z.const("wide"),
135
135
  z.const("origin"),
136
136
  z.const("off")
137
- ]).default(COMPOSER_REFRACTION_DEFAULT)
137
+ ]).default(COMPOSER_REFRACTION_DEFAULT),
138
+ // The `blur()` term in every glass surface's backdrop chain. Turning it off
139
+ // keeps the translucency, the sheen and the edge refraction and drops only
140
+ // the frosting -- implemented as four variables driven to 0px, so nothing
141
+ // else about the recipes moves.
142
+ backdropBlur: z.boolean().default(true)
138
143
  });
139
144
 
140
145
  /** Map a legacy 1.37.x mode id onto a four-mode id, or null when nothing to do. */
@@ -585,7 +590,8 @@ function dsttRead(ctx) {
585
590
  mode: DSTT_MODES.includes(stored) ? stored : DSTT_MODE_DEFAULT,
586
591
  fluidBrush: descriptor.value.fluidBrush === true,
587
592
  glassStyle: dsttGlassValue(ctx),
588
- composerRefraction: dsttComposerValue(ctx)
593
+ composerRefraction: dsttComposerValue(ctx),
594
+ backdropBlur: descriptor.value.backdropBlur !== false
589
595
  }
590
596
  };
591
597
  }
@@ -597,7 +603,7 @@ function dsttRead(ctx) {
597
603
  * persist a choice without a second round trip. None of them is mandatory the
598
604
  * way `mode` is — an older client that only knows about the mode keeps working.
599
605
  */
600
- async function dsttWrite(ctx, mode, fluidBrush, glassStyle, composerRefraction) {
606
+ async function dsttWrite(ctx, mode, fluidBrush, glassStyle, composerRefraction, backdropBlur) {
601
607
  if (!DSTT_MODES.includes(mode)) {
602
608
  return failure("bad-request", `mode must be one of ${DSTT_MODES.join("/")}`, { issues: [] });
603
609
  }
@@ -607,6 +613,7 @@ async function dsttWrite(ctx, mode, fluidBrush, glassStyle, composerRefraction)
607
613
  if (typeof fluidBrush === "boolean") ops.push({ op: "set", path: ["fluidBrush"], value: fluidBrush });
608
614
  if (wantsGlass) ops.push({ op: "set", path: ["glassStyle"], value: glassStyle });
609
615
  if (wantsComposer) ops.push({ op: "set", path: ["composerRefraction"], value: composerRefraction });
616
+ if (typeof backdropBlur === "boolean") ops.push({ op: "set", path: ["backdropBlur"], value: backdropBlur });
610
617
  const settings = ctx.get("settings");
611
618
  if (settings === undefined || settings === null || typeof settings.mutate !== "function") {
612
619
  return failure("internal", "settings service unavailable", {});
@@ -622,11 +629,18 @@ async function dsttWrite(ctx, mode, fluidBrush, glassStyle, composerRefraction)
622
629
  mode,
623
630
  fluidBrush: typeof fluidBrush === "boolean" ? fluidBrush === true : dsttBrushValue(ctx),
624
631
  glassStyle: wantsGlass ? glassStyle : dsttGlassValue(ctx),
625
- composerRefraction: wantsComposer ? composerRefraction : dsttComposerValue(ctx)
632
+ composerRefraction: wantsComposer ? composerRefraction : dsttComposerValue(ctx),
633
+ backdropBlur: typeof backdropBlur === "boolean" ? backdropBlur : dsttBlurValue(ctx)
626
634
  }
627
635
  };
628
636
  }
629
637
 
638
+ /** The stored backdrop-blur preference, defaulting to on. */
639
+ function dsttBlurValue(ctx) {
640
+ const descriptor = settingsDescriptor(ctx, DSTT_SETTINGS_NS);
641
+ return descriptor === null ? true : descriptor.value.backdropBlur !== false;
642
+ }
643
+
630
644
  /**
631
645
  * The stored composer-refraction width, defaulting to the 8px band. `origin` was
632
646
  * this option's first id: it is still accepted by the schema so a settings file
@@ -704,7 +718,7 @@ function apply(ctx) {
704
718
  if (endpoint === DSTT_GET) return dsttRead(ctx);
705
719
  if (endpoint === DSTT_SET) {
706
720
  const payloadObject = payload === null || payload === undefined || typeof payload !== "object" ? {} : payload;
707
- return await dsttWrite(ctx, payloadObject.mode, payloadObject.fluidBrush, payloadObject.glassStyle, payloadObject.composerRefraction);
721
+ return await dsttWrite(ctx, payloadObject.mode, payloadObject.fluidBrush, payloadObject.glassStyle, payloadObject.composerRefraction, payloadObject.backdropBlur);
708
722
  }
709
723
  if (endpoint === FILE_OPEN || endpoint === FILE_REVEAL) {
710
724
  const target = localPathOf(payload);
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": "1.43.3",
4
+ "version": "1.43.6",
5
5
  "type": "module",
6
6
  "keywords": [
7
7
  "deepseek",