dsh-deepseek-style-theme 1.43.4 → 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.
Files changed (3) hide show
  1. package/README.md +11 -0
  2. package/lib/client.js +124 -31
  3. package/package.json +1 -1
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/lib/client.js CHANGED
@@ -995,18 +995,20 @@ void main() {
995
995
  const CORE_CSS = `
996
996
  :root{--ds-brand:#059669;--ds-brand-deep:#047857;--ds-brand-mid:#10B981;--ds-brand-light:#34D399;--ds-glass-blur:24px}
997
997
  html,body{background-color:transparent!important}
998
- /* The page background below is painted from OUR scheme marker
999
- (body[data-dshome-dark]), while message text takes its colour from the
1000
- product's --dsw-alias-* tokens. Those are two different signals, and when
1001
- they disagree — the marker is derived from the theme service and can miss a
1002
- scheme change — the result is near-white text on a near-white page: every
1003
- paragraph vanishes while anything carrying its own background (tool cards)
1004
- stays readable. Reproduced by clearing the marker on a dark page.
1005
- Declaring the same two label tokens under the same marker puts both sides on
1006
- ONE signal. The values are exactly what theme.overrideTokens() already
1007
- installs for each scheme, so a page where the two agree repaints identically. */
1008
- body{--dsw-alias-label-primary:#152443!important;--dsw-alias-label-secondary:#3c4a63!important}
1009
- 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. */
1010
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}
1011
1013
  body[data-dshome-color=red]{--ds-brand:#F5222D;--ds-brand-deep:#CF1322;--ds-brand-mid:#FF4D4F;--ds-brand-light:#FF7875}
1012
1014
  body[data-dshome-color=blue]{--ds-brand:#4D6BFE;--ds-brand-deep:#3A65C2;--ds-brand-mid:#4176E6;--ds-brand-light:#73A3D2}
@@ -1726,6 +1728,20 @@ html[data-dshome-blur="off"] body{
1726
1728
  * subscription is unavailable.
1727
1729
  */
1728
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
+ };
1729
1745
  const apply = () => {
1730
1746
  let dark = false;
1731
1747
  // Two signals, and BOTH of them lie in some state -- measured, not
@@ -1749,10 +1765,19 @@ html[data-dshome-blur="off"] body{
1749
1765
  }
1750
1766
  }
1751
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;
1752
1773
  if (dark) document.body.setAttribute('data-dshome-dark', '');
1753
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);
1754
1780
  };
1755
- apply();
1756
1781
  let unsubscribe = null;
1757
1782
  try {
1758
1783
  unsubscribe = ctx.on('theme/change', apply);
@@ -1769,9 +1794,21 @@ html[data-dshome-blur="off"] body{
1769
1794
  } catch (error) {
1770
1795
  mirror = null;
1771
1796
  }
1772
- return () => {
1773
- if (unsubscribe !== null) unsubscribe();
1774
- 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
+ }
1775
1812
  };
1776
1813
  }
1777
1814
 
@@ -2037,25 +2074,48 @@ html[data-dshome-blur="off"] body{
2037
2074
  };
2038
2075
  applyGlass();
2039
2076
  const unsubscribeGlass = dsttSubscribe(applyGlass);
2040
- const unsubscribe = subscribeColor((color) => {
2041
- fluid.setParams(dshomeFluidParams());
2042
- 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) {}
2043
2102
  });
2044
- // The palette is chosen per colour SCHEME, not only per colour token:
2045
- // without this the canvas keeps whatever palette it was mounted with,
2046
- // so a page that loads light and is then switched to dark keeps a
2047
- // bright flow under a dark shell.
2048
- 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;
2049
2108
  try {
2050
- darkMirror = new MutationObserver(() => { fluid.setParams(dshomeFluidParams()); });
2051
- 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'] });
2052
2111
  } catch (error) {
2053
- darkMirror = null;
2112
+ tokenMirror = null;
2054
2113
  }
2055
2114
  return () => {
2056
- unsubscribe();
2115
+ unsubscribeColor();
2116
+ if (unsubscribeDark !== null) unsubscribeDark();
2057
2117
  unsubscribeGlass();
2058
- if (darkMirror !== null) darkMirror.disconnect();
2118
+ if (tokenMirror !== null) tokenMirror.disconnect();
2059
2119
  dispersion.dispose();
2060
2120
  stopParallax();
2061
2121
  stopSpotter();
@@ -2177,7 +2237,20 @@ html[data-dshome-blur="off"] body{
2177
2237
  const colors = palette();
2178
2238
  for (const p of parts) p.c = colors[(Math.random() * colors.length) | 0];
2179
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.
2180
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
+ }
2181
2254
 
2182
2255
  window.addEventListener('resize', resize);
2183
2256
  resize();
@@ -2189,6 +2262,8 @@ html[data-dshome-blur="off"] body{
2189
2262
  cancelAnimationFrame(rafId);
2190
2263
  window.removeEventListener('resize', resize);
2191
2264
  unsubscribeColor();
2265
+ if (unsubscribeDark !== null) unsubscribeDark();
2266
+ if (tokenMirror !== null) tokenMirror.disconnect();
2192
2267
  canvas.remove();
2193
2268
  };
2194
2269
  }
@@ -3747,10 +3822,18 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
3747
3822
 
3748
3823
  anchor();
3749
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
+
3750
3832
  try { unsubscribe = dsttSubscribe(anchor); } catch (error) { unsubscribe = null; }
3751
3833
 
3752
3834
  return () => {
3753
3835
  stopped = true;
3836
+ clearTimeout(bootTimer);
3754
3837
  if (chainTimer !== 0) clearTimeout(chainTimer);
3755
3838
  if (unsubscribe !== null) unsubscribe();
3756
3839
  document.removeEventListener('visibilitychange', onVisibility);
@@ -3954,8 +4037,18 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
3954
4037
 
3955
4038
  // Dark marker: owned attribute driven by the theme service, never by
3956
4039
  // the product's attribute name. Must precede the patch blocks so the
3957
- // dark selectors match from the first paint.
3958
- 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');
3959
4052
 
3960
4053
  // Glass-recipe marker: same idea, and it has to be on <html> before the
3961
4054
  // first paint of the glass surfaces or the pane flashes the other recipe.
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.4",
4
+ "version": "1.43.6",
5
5
  "type": "module",
6
6
  "keywords": [
7
7
  "deepseek",