dsh-deepseek-style-theme 1.43.1
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/LICENSE +21 -0
- package/README.md +248 -0
- package/THIRD-PARTY-NOTICES.md +95 -0
- package/cordis.patch.yml +6 -0
- package/lib/client.js +3775 -0
- package/lib/index.js +824 -0
- package/package.json +59 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,3775 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@dsh-external/dsh-deepseek-style-theme",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
const React = require("react");
|
|
8
|
+
|
|
9
|
+
// ── design-standard conformance (创造模式 preset) ────────────────────
|
|
10
|
+
// The product shell ships build-time hashed class names (e.g.
|
|
11
|
+
// `wSkVaW_header`). They are not API and change on every product rebuild,
|
|
12
|
+
// so this theme treats every rule that targets them as a guarded patch
|
|
13
|
+
// layer: each block is injected only while its anchor class actually
|
|
14
|
+
// exists in the DOM (a 30s wait covers late-rendered shells), and a block
|
|
15
|
+
// whose anchor never appears is skipped with a one-line diagnostic
|
|
16
|
+
// instead of silently shipping dead CSS.
|
|
17
|
+
// Dark styling keys off our own `data-dshome-dark` attribute, driven by
|
|
18
|
+
// the theme service's `active.colorScheme` — never the product's
|
|
19
|
+
// attribute name — so a product-side rename cannot break dark mode.
|
|
20
|
+
// UI that the product exposes through a Slot (the locale toggle) stays on
|
|
21
|
+
// the Slot; only surfaces with no Slot use DOM patches.
|
|
22
|
+
//
|
|
23
|
+
// Markup sinks: `innerHTML` appears exactly five times in this file and
|
|
24
|
+
// every one of them assigns a module-level or call-site `const` SVG
|
|
25
|
+
// literal. It is this file's only markup sink, so anything derived from
|
|
26
|
+
// page data — session titles, file paths, workspace names — must go
|
|
27
|
+
// through `textContent`/`title`/`setAttribute`, never `innerHTML`. An
|
|
28
|
+
// interpolated value there would be DOM XSS.
|
|
29
|
+
const PLUGIN_ID = "@dsh-external/dsh-deepseek-style-theme";
|
|
30
|
+
|
|
31
|
+
// ─── fluid background + liquid-glass refraction ───────────────────────
|
|
32
|
+
// Ported from dsh-theme-mineradio v2.3.8 (MIT, Copyright (c) 2026 John Wu).
|
|
33
|
+
// See the licence header on the block below; the notice must be preserved.
|
|
34
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
35
|
+
// deepseek-style-theme — fluid shader + glass dispersion port
|
|
36
|
+
//
|
|
37
|
+
// The fluid shader (GLSL programs, the WebGL2 ping-pong fluid solver, the
|
|
38
|
+
// pointer/click interaction feeds) and the glass chromatic-dispersion filter
|
|
39
|
+
// plus the specular-highlight cursor parallax below are ported from
|
|
40
|
+
// `dsh-theme-mineradio` v2.3.8 — MIT, Copyright (c) 2026 John Wu.
|
|
41
|
+
// The upstream MIT licence notice MUST be preserved wherever this code ships.
|
|
42
|
+
//
|
|
43
|
+
// Only naming and external dependencies were adapted; the three GLSL shader
|
|
44
|
+
// sources (VERTEX_SHADER / FLOW_SHADER / DISPLAY_SHADER) are byte-for-byte
|
|
45
|
+
// copies of upstream, including `#version 300 es`, every precision qualifier
|
|
46
|
+
// and every in-shader comment.
|
|
47
|
+
//
|
|
48
|
+
// External dependencies found in the upstream module and what was done:
|
|
49
|
+
//
|
|
50
|
+
// INLINED VERBATIM (they sit outside the ported ranges upstream and are
|
|
51
|
+
// small pure helpers the ported code cannot run without):
|
|
52
|
+
// • `uv(canvas, clientX, clientY)` — upstream fluid-interactions.ts
|
|
53
|
+
// line 916, used by
|
|
54
|
+
// attachFluidInteractions.
|
|
55
|
+
// • `hslToRgb01(h, s, l)` — upstream glass-dispersion.ts
|
|
56
|
+
// line 2409, used by tintMatrix.
|
|
57
|
+
// • `filterMarkup(id, refractId, tintId, map)` — upstream glass-dispersion.ts
|
|
58
|
+
// line 2443, used by
|
|
59
|
+
// startGlassDispersion to build
|
|
60
|
+
// the SVG <filter> pair.
|
|
61
|
+
// • `closestSpot(target)`, `visualRect(spot)`,
|
|
62
|
+
// `inside(visual, clientX, clientY)` — upstream spot-core.ts
|
|
63
|
+
// lines 1711-1732, used by
|
|
64
|
+
// startSpecularParallax.
|
|
65
|
+
//
|
|
66
|
+
// CONSTANTS TAKEN FROM ADJACENT UPSTREAM DECLARATIONS (needed to make the
|
|
67
|
+
// requested subset self-contained; upstream declares them beside it):
|
|
68
|
+
// • TINT_ID / REFRACT_ID / TALL_FILTER_ID / TALL_TINT_ID /
|
|
69
|
+
// TALL_REFRACT_ID / DEFAULT_REF_SCALE / EDGE_DX / TINT_SAT / TINT_LIGHT
|
|
70
|
+
// — values unchanged, only FILTER_ID's value renamed.
|
|
71
|
+
// • SPOT_ATTR / SPOT_SELECTOR (upstream spot-core.ts lines 1707-1710)
|
|
72
|
+
// — renamed to the `dshome-` prefix.
|
|
73
|
+
//
|
|
74
|
+
// RENAMED (no longer refers to the upstream plugin):
|
|
75
|
+
// • FILTER_ID "mineradio-glass-dispersion" → "dshome-glass-dispersion"
|
|
76
|
+
// • every other SVG filter/tint/refract id → "dshome-glass-dispersion-*"
|
|
77
|
+
// • ATTR "data-dsh-dispersion" → "data-dshome-dispersion"
|
|
78
|
+
// • SPOT_ATTR "data-dsh-aqua-spot" → "data-dshome-spot"
|
|
79
|
+
// • SPEC_X / SPEC_Y --dsh-aqua-spec-x/y → --dshome-spec-x/y
|
|
80
|
+
// • console tags "ui-aqua fluid shader" / "ui-aqua fluid link" →
|
|
81
|
+
// "deepseek-style-theme fluid shader" /
|
|
82
|
+
// "deepseek-style-theme fluid link"
|
|
83
|
+
//
|
|
84
|
+
// DELIBERATELY NOT PORTED (upstream settings/theme-store/React layer). Each
|
|
85
|
+
// was replaced by a parameter or a local default:
|
|
86
|
+
// • `this.settings.dispersionHue` (dispersionTintHue()) — upstream read the
|
|
87
|
+
// user's hue knob and an extracted-wallpaper hue. REPLACED by the `hue`
|
|
88
|
+
// argument of startGlassDispersion({ hue }), defaulting to the upstream
|
|
89
|
+
// default DEFAULT_TINT_HUE = 44. The caller owns the hue source.
|
|
90
|
+
// • `this.settings.dispersionRefract` — upstream called
|
|
91
|
+
// handle.setRefraction(n) on every settings change. The caller now calls
|
|
92
|
+
// the returned handle's setRefraction(n) itself.
|
|
93
|
+
// • `this.settings.fluidHue` / `fluidDepth` / `rainbow` / `stationOffset`
|
|
94
|
+
// and the helper `fluidToneColors()` (upstream line 125) plus
|
|
95
|
+
// `HUE_BASE` — NONE of these are ported. attachFluidShader takes a
|
|
96
|
+
// complete params object; SITE_FLUID_PARAMS is exported as the upstream
|
|
97
|
+
// default (its own three palette colours), so the caller supplies its
|
|
98
|
+
// own palette by spreading SITE_FLUID_PARAMS and overriding color1..3.
|
|
99
|
+
// • `this.settings.audioReact` / the mic analyser (createAudioReactivity)
|
|
100
|
+
// — not ported. The audio coupling survives as the ported handle method
|
|
101
|
+
// `setAudioLow(level)`; the caller feeds it, or ignores it.
|
|
102
|
+
// • upstream's React component and the `[data-dsh-aqua-fluid-canvas]` /
|
|
103
|
+
// `[data-dsh-aqua-ambient]` mount lookups — the caller passes the canvas
|
|
104
|
+
// element and the { main, mainCanvas } handle explicitly.
|
|
105
|
+
//
|
|
106
|
+
// DOM hooks the ported code still reads (product/platform markup, intentionally
|
|
107
|
+
// NOT renamed): `[data-composer-card]` and `[data-dsh-stats]` inside
|
|
108
|
+
// visualRect(), and the `button` closest-match in attachFluidInteractions.
|
|
109
|
+
//
|
|
110
|
+
// Indentation note: this fragment is tab-indented at a base of 2 tabs to match
|
|
111
|
+
// the target factory body. Line indentation INSIDE the three GLSL template
|
|
112
|
+
// literals is string data and was left exactly as upstream, per the verbatim
|
|
113
|
+
// requirement; tab-indenting those lines would have altered the shipped shader
|
|
114
|
+
// text.
|
|
115
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
var SITE_FLUID_PARAMS = {
|
|
118
|
+
mouseRadius: 0.22,
|
|
119
|
+
mouseStrength: 1.1,
|
|
120
|
+
decay: 0.96,
|
|
121
|
+
distortBoost: 1.35,
|
|
122
|
+
noiseBoost: 0,
|
|
123
|
+
swirlBoost: 0.45,
|
|
124
|
+
speed: 14,
|
|
125
|
+
distortion: 20,
|
|
126
|
+
swirl: 12,
|
|
127
|
+
swirlIterations: 8,
|
|
128
|
+
scale: 0.5,
|
|
129
|
+
rotation: -5,
|
|
130
|
+
proportion: 50,
|
|
131
|
+
softness: 100,
|
|
132
|
+
shapeScale: 10,
|
|
133
|
+
offsetX: 0,
|
|
134
|
+
offsetY: 65,
|
|
135
|
+
color1: "#8AA3D6",
|
|
136
|
+
color2: "#FFFFFF",
|
|
137
|
+
color3: "#FFFFFF"
|
|
138
|
+
};
|
|
139
|
+
var VERTEX_SHADER = `#version 300 es
|
|
140
|
+
in vec4 a_position;
|
|
141
|
+
out vec2 vUv;
|
|
142
|
+
void main() {
|
|
143
|
+
vUv = a_position.xy * 0.5 + 0.5;
|
|
144
|
+
gl_Position = a_position;
|
|
145
|
+
}
|
|
146
|
+
`;
|
|
147
|
+
var FLOW_SHADER = `#version 300 es
|
|
148
|
+
precision mediump float;
|
|
149
|
+
in vec2 vUv;
|
|
150
|
+
uniform sampler2D u_prev;
|
|
151
|
+
uniform vec2 u_mouse;
|
|
152
|
+
uniform vec2 u_velocity;
|
|
153
|
+
uniform float u_brushRadius;
|
|
154
|
+
uniform float u_brushStrength;
|
|
155
|
+
uniform float u_decay;
|
|
156
|
+
out vec4 fragColor;
|
|
157
|
+
|
|
158
|
+
void main() {
|
|
159
|
+
vec4 prev = texture(u_prev, vUv);
|
|
160
|
+
|
|
161
|
+
prev.r *= u_decay;
|
|
162
|
+
prev.gb = mix(vec2(0.5), prev.gb, u_decay);
|
|
163
|
+
|
|
164
|
+
float dist = distance(vUv, u_mouse);
|
|
165
|
+
|
|
166
|
+
float influence = exp(-dist * dist / (u_brushRadius * u_brushRadius * 0.5));
|
|
167
|
+
influence = max(0.0, influence - 0.01);
|
|
168
|
+
|
|
169
|
+
float speed = length(u_velocity);
|
|
170
|
+
float presenceStrength = u_brushStrength * 0.3;
|
|
171
|
+
float velBonus = min(speed * 3.0, 0.7) * u_brushStrength;
|
|
172
|
+
float totalStrength = presenceStrength + velBonus;
|
|
173
|
+
|
|
174
|
+
prev.r = max(prev.r, influence * totalStrength);
|
|
175
|
+
float blendAmt = influence * min(totalStrength, 0.4) * 0.3;
|
|
176
|
+
prev.g = mix(prev.g, clamp(u_velocity.x * 2.0 + 0.5, 0.0, 1.0), blendAmt);
|
|
177
|
+
prev.b = mix(prev.b, clamp(u_velocity.y * 2.0 + 0.5, 0.0, 1.0), blendAmt);
|
|
178
|
+
|
|
179
|
+
fragColor = prev;
|
|
180
|
+
}
|
|
181
|
+
`;
|
|
182
|
+
var DISPLAY_SHADER = `#version 300 es
|
|
183
|
+
precision mediump float;
|
|
184
|
+
in vec2 vUv;
|
|
185
|
+
uniform float u_time;
|
|
186
|
+
uniform float u_pixelRatio;
|
|
187
|
+
uniform vec2 u_resolution;
|
|
188
|
+
uniform float u_scale;
|
|
189
|
+
uniform float u_rotation;
|
|
190
|
+
uniform vec4 u_color1, u_color2, u_color3;
|
|
191
|
+
uniform float u_colorCount;
|
|
192
|
+
uniform float u_proportion;
|
|
193
|
+
uniform float u_softness;
|
|
194
|
+
uniform float u_shape;
|
|
195
|
+
uniform float u_shapeScale;
|
|
196
|
+
uniform float u_distortion;
|
|
197
|
+
uniform float u_swirl;
|
|
198
|
+
uniform float u_swirlIterations;
|
|
199
|
+
uniform vec2 u_offset;
|
|
200
|
+
uniform sampler2D u_flowmap;
|
|
201
|
+
uniform float u_distortBoost;
|
|
202
|
+
uniform float u_noiseBoost;
|
|
203
|
+
uniform float u_swirlBoost;
|
|
204
|
+
out vec4 fragColor;
|
|
205
|
+
|
|
206
|
+
#define TWO_PI 6.28318530718
|
|
207
|
+
#define PI 3.14159265358979323846
|
|
208
|
+
|
|
209
|
+
vec2 rotate(vec2 uv, float th) { return mat2(cos(th), sin(th), -sin(th), cos(th)) * uv; }
|
|
210
|
+
float random(vec2 st) { return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453123); }
|
|
211
|
+
float noise(vec2 st) {
|
|
212
|
+
vec2 i = floor(st); vec2 f = fract(st);
|
|
213
|
+
float a = random(i), b = random(i + vec2(1,0)), c = random(i + vec2(0,1)), d = random(i + vec2(1,1));
|
|
214
|
+
vec2 u = f*f*(3.0-2.0*f);
|
|
215
|
+
return mix(mix(a,b,u.x), mix(c,d,u.x), u.y);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
vec3 blend_multi(float mixer, float softness) {
|
|
219
|
+
float edge = 1.0 - softness;
|
|
220
|
+
vec3 col = u_color1.rgb;
|
|
221
|
+
if (u_colorCount > 1.5) { col = mix(col, u_color2.rgb, smoothstep(0.0 + 0.35*edge, 0.7 - 0.35*edge, mixer)); }
|
|
222
|
+
if (u_colorCount > 2.5) { col = mix(col, u_color3.rgb, smoothstep(0.3 + 0.35*edge, 1.0 - 0.35*edge, mixer)); }
|
|
223
|
+
return col;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
void main() {
|
|
227
|
+
vec2 uv = gl_FragCoord.xy / u_resolution.xy;
|
|
228
|
+
float t = .5 * u_time;
|
|
229
|
+
float ns = .0005 + .006 * u_scale;
|
|
230
|
+
uv -= .5; uv *= (ns * u_resolution); uv = rotate(uv, u_rotation * .5 * PI);
|
|
231
|
+
uv /= u_pixelRatio; uv += .5; uv += u_offset;
|
|
232
|
+
|
|
233
|
+
vec2 fragUV = gl_FragCoord.xy / u_resolution.xy;
|
|
234
|
+
vec4 flow = texture(u_flowmap, fragUV);
|
|
235
|
+
float influence = flow.r;
|
|
236
|
+
vec2 flowDir = (flow.gb - 0.5) * 2.0;
|
|
237
|
+
|
|
238
|
+
float n1 = noise(uv + t), n2 = noise(uv*2. - t);
|
|
239
|
+
float angle = n1 * TWO_PI;
|
|
240
|
+
|
|
241
|
+
float totalDistortion = u_distortion + influence * u_distortBoost;
|
|
242
|
+
uv.x += 4. * totalDistortion * n2 * cos(angle);
|
|
243
|
+
uv.y += 4. * totalDistortion * n2 * sin(angle);
|
|
244
|
+
|
|
245
|
+
uv += flowDir * influence * 0.15;
|
|
246
|
+
|
|
247
|
+
if (influence > 0.001) {
|
|
248
|
+
float localNoise = noise(uv * 2.0 + t * 1.5);
|
|
249
|
+
uv += influence * u_noiseBoost * vec2(cos(localNoise * TWO_PI), sin(localNoise * TWO_PI));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
float iters = ceil(clamp(u_swirlIterations, 1., 30.));
|
|
253
|
+
float swirlAmt = clamp(u_swirl, 0., 2.) + influence * u_swirlBoost;
|
|
254
|
+
for (float i = 1.; i <= 30.0; i++) {
|
|
255
|
+
if (i > iters) break;
|
|
256
|
+
uv.x += swirlAmt / i * cos(t + i*1.5*uv.y);
|
|
257
|
+
uv.y += swirlAmt / i * cos(t + i*1.*uv.x);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
float proportion = clamp(u_proportion, 0., 1.);
|
|
261
|
+
vec2 cuv = uv * (.5 + 3.5 * u_shapeScale);
|
|
262
|
+
float shape = .5 + .5 * sin(cuv.x) * cos(cuv.y);
|
|
263
|
+
float mixer = shape + .48 * sign(proportion - .5) * pow(abs(proportion - .5), .5);
|
|
264
|
+
vec3 col = blend_multi(mixer, clamp(u_softness, 0., 1.));
|
|
265
|
+
fragColor = vec4(col, 1.0);
|
|
266
|
+
}
|
|
267
|
+
`;
|
|
268
|
+
function hexToRgb(value) {
|
|
269
|
+
const hex = value.replace("#", "");
|
|
270
|
+
return [
|
|
271
|
+
parseInt(hex.slice(0, 2), 16) / 255,
|
|
272
|
+
parseInt(hex.slice(2, 4), 16) / 255,
|
|
273
|
+
parseInt(hex.slice(4, 6), 16) / 255
|
|
274
|
+
];
|
|
275
|
+
}
|
|
276
|
+
function attachFluidShader(canvas, params) {
|
|
277
|
+
const gl = canvas.getContext("webgl2", {
|
|
278
|
+
alpha: true,
|
|
279
|
+
premultipliedAlpha: false,
|
|
280
|
+
powerPreference: "low-power"
|
|
281
|
+
});
|
|
282
|
+
if (gl === null) {
|
|
283
|
+
return {
|
|
284
|
+
setParams: () => {
|
|
285
|
+
},
|
|
286
|
+
stir: () => {
|
|
287
|
+
},
|
|
288
|
+
setAudioLow: () => {
|
|
289
|
+
},
|
|
290
|
+
setRunning: () => {
|
|
291
|
+
},
|
|
292
|
+
dispose: () => {
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
const compile = (type, source) => {
|
|
297
|
+
const shader = gl.createShader(type);
|
|
298
|
+
if (shader === null) return null;
|
|
299
|
+
gl.shaderSource(shader, source);
|
|
300
|
+
gl.compileShader(shader);
|
|
301
|
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
302
|
+
console.error("deepseek-style-theme fluid shader:", gl.getShaderInfoLog(shader));
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
return shader;
|
|
306
|
+
};
|
|
307
|
+
const link = (fragment) => {
|
|
308
|
+
const vertex = compile(gl.VERTEX_SHADER, VERTEX_SHADER);
|
|
309
|
+
const frag = compile(gl.FRAGMENT_SHADER, fragment);
|
|
310
|
+
if (vertex === null || frag === null) return null;
|
|
311
|
+
const program = gl.createProgram();
|
|
312
|
+
if (program === null) return null;
|
|
313
|
+
gl.attachShader(program, vertex);
|
|
314
|
+
gl.attachShader(program, frag);
|
|
315
|
+
gl.linkProgram(program);
|
|
316
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
317
|
+
console.error("deepseek-style-theme fluid link:", gl.getProgramInfoLog(program));
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
return program;
|
|
321
|
+
};
|
|
322
|
+
const flowProgram = link(FLOW_SHADER);
|
|
323
|
+
const displayProgram = link(DISPLAY_SHADER);
|
|
324
|
+
if (flowProgram === null || displayProgram === null) {
|
|
325
|
+
return {
|
|
326
|
+
setParams: () => {
|
|
327
|
+
},
|
|
328
|
+
stir: () => {
|
|
329
|
+
},
|
|
330
|
+
setAudioLow: () => {
|
|
331
|
+
},
|
|
332
|
+
setRunning: () => {
|
|
333
|
+
},
|
|
334
|
+
dispose: () => {
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
const flow = {
|
|
339
|
+
prev: gl.getUniformLocation(flowProgram, "u_prev"),
|
|
340
|
+
mouse: gl.getUniformLocation(flowProgram, "u_mouse"),
|
|
341
|
+
velocity: gl.getUniformLocation(flowProgram, "u_velocity"),
|
|
342
|
+
brushRadius: gl.getUniformLocation(flowProgram, "u_brushRadius"),
|
|
343
|
+
brushStrength: gl.getUniformLocation(flowProgram, "u_brushStrength"),
|
|
344
|
+
decay: gl.getUniformLocation(flowProgram, "u_decay")
|
|
345
|
+
};
|
|
346
|
+
const display = {
|
|
347
|
+
time: gl.getUniformLocation(displayProgram, "u_time"),
|
|
348
|
+
pixelRatio: gl.getUniformLocation(displayProgram, "u_pixelRatio"),
|
|
349
|
+
resolution: gl.getUniformLocation(displayProgram, "u_resolution"),
|
|
350
|
+
scale: gl.getUniformLocation(displayProgram, "u_scale"),
|
|
351
|
+
rotation: gl.getUniformLocation(displayProgram, "u_rotation"),
|
|
352
|
+
offset: gl.getUniformLocation(displayProgram, "u_offset"),
|
|
353
|
+
color1: gl.getUniformLocation(displayProgram, "u_color1"),
|
|
354
|
+
color2: gl.getUniformLocation(displayProgram, "u_color2"),
|
|
355
|
+
color3: gl.getUniformLocation(displayProgram, "u_color3"),
|
|
356
|
+
colorCount: gl.getUniformLocation(displayProgram, "u_colorCount"),
|
|
357
|
+
proportion: gl.getUniformLocation(displayProgram, "u_proportion"),
|
|
358
|
+
softness: gl.getUniformLocation(displayProgram, "u_softness"),
|
|
359
|
+
shape: gl.getUniformLocation(displayProgram, "u_shape"),
|
|
360
|
+
shapeScale: gl.getUniformLocation(displayProgram, "u_shapeScale"),
|
|
361
|
+
distortion: gl.getUniformLocation(displayProgram, "u_distortion"),
|
|
362
|
+
swirl: gl.getUniformLocation(displayProgram, "u_swirl"),
|
|
363
|
+
swirlIterations: gl.getUniformLocation(displayProgram, "u_swirlIterations"),
|
|
364
|
+
flowmap: gl.getUniformLocation(displayProgram, "u_flowmap"),
|
|
365
|
+
distortBoost: gl.getUniformLocation(displayProgram, "u_distortBoost"),
|
|
366
|
+
noiseBoost: gl.getUniformLocation(displayProgram, "u_noiseBoost"),
|
|
367
|
+
swirlBoost: gl.getUniformLocation(displayProgram, "u_swirlBoost")
|
|
368
|
+
};
|
|
369
|
+
const quadBuffer = gl.createBuffer();
|
|
370
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuffer);
|
|
371
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]), gl.STATIC_DRAW);
|
|
372
|
+
const bindQuad = (program) => {
|
|
373
|
+
const position = gl.getAttribLocation(program, "a_position");
|
|
374
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuffer);
|
|
375
|
+
gl.enableVertexAttribArray(position);
|
|
376
|
+
gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);
|
|
377
|
+
};
|
|
378
|
+
const makeTarget = (width2, height2, initial2) => {
|
|
379
|
+
const tex = gl.createTexture();
|
|
380
|
+
if (tex === null) throw new Error("deepseek-style-theme fluid: texture allocation failed");
|
|
381
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
382
|
+
if (initial2 !== void 0) {
|
|
383
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width2, height2, 0, gl.RGBA, gl.UNSIGNED_BYTE, initial2);
|
|
384
|
+
} else {
|
|
385
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width2, height2, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
|
|
386
|
+
}
|
|
387
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
388
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
389
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
390
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
391
|
+
const fbo = gl.createFramebuffer();
|
|
392
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
|
|
393
|
+
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);
|
|
394
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
395
|
+
return { fbo, tex };
|
|
396
|
+
};
|
|
397
|
+
let width = 0;
|
|
398
|
+
let height = 0;
|
|
399
|
+
let flowWidth = 0;
|
|
400
|
+
let flowHeight = 0;
|
|
401
|
+
let flip = false;
|
|
402
|
+
let current = { ...params };
|
|
403
|
+
let audioLow = 0;
|
|
404
|
+
const pointer = { x: 0.5, y: 0.5, smoothX: 0.5, smoothY: 0.5, vx: 0, vy: 0, svx: 0, svy: 0 };
|
|
405
|
+
const dprCap = Math.min(window.devicePixelRatio || 1, 1.5);
|
|
406
|
+
width = Math.round(canvas.clientWidth * dprCap);
|
|
407
|
+
height = Math.round(canvas.clientHeight * dprCap);
|
|
408
|
+
canvas.width = width;
|
|
409
|
+
canvas.height = height;
|
|
410
|
+
flowWidth = Math.round(width / 4);
|
|
411
|
+
flowHeight = Math.round(height / 4);
|
|
412
|
+
const initial = new Uint8Array(flowWidth * flowHeight * 4);
|
|
413
|
+
for (let i = 0; i < flowWidth * flowHeight; i += 1) {
|
|
414
|
+
initial[4 * i] = 0;
|
|
415
|
+
initial[4 * i + 1] = 128;
|
|
416
|
+
initial[4 * i + 2] = 128;
|
|
417
|
+
initial[4 * i + 3] = 255;
|
|
418
|
+
}
|
|
419
|
+
let targetA = makeTarget(flowWidth, flowHeight, initial);
|
|
420
|
+
let targetB = makeTarget(flowWidth, flowHeight, initial);
|
|
421
|
+
const coarse = window.matchMedia("(hover: none), (pointer: coarse)").matches;
|
|
422
|
+
const ua = navigator;
|
|
423
|
+
const windows = ua.userAgentData ? ua.userAgentData.platform === "Windows" : navigator.userAgent.includes("Windows");
|
|
424
|
+
const onMouseMove = (event) => {
|
|
425
|
+
const rect = canvas.getBoundingClientRect();
|
|
426
|
+
pointer.x = (event.clientX - rect.left) / rect.width;
|
|
427
|
+
pointer.y = 1 - (event.clientY - rect.top) / rect.height;
|
|
428
|
+
};
|
|
429
|
+
// Local adaptation: upstream always feeds the cursor into the flow field
|
|
430
|
+
// (except on touch and Windows). DSTT gates it behind its own
|
|
431
|
+
// `fluidBrush` setting, which ships OFF — the brush is the most
|
|
432
|
+
// expensive part of the simulation and leaves a visible wake trailing
|
|
433
|
+
// the pointer across the whole background.
|
|
434
|
+
if (!coarse && !windows && params.mouseFeed !== false) window.addEventListener("mousemove", onMouseMove);
|
|
435
|
+
const start = performance.now();
|
|
436
|
+
let raf = 0;
|
|
437
|
+
let running = false;
|
|
438
|
+
let previous = 0;
|
|
439
|
+
const step = 1e3 / 30;
|
|
440
|
+
const frame = (now) => {
|
|
441
|
+
raf = requestAnimationFrame(frame);
|
|
442
|
+
if (now - previous < step) return;
|
|
443
|
+
previous = now - (now - previous) % step;
|
|
444
|
+
const ratio = Math.min(window.devicePixelRatio || 1, 1.5);
|
|
445
|
+
const nextWidth = Math.round(canvas.clientWidth * ratio);
|
|
446
|
+
const nextHeight = Math.round(canvas.clientHeight * ratio);
|
|
447
|
+
if (nextWidth > 0 && nextHeight > 0 && (nextWidth !== width || nextHeight !== height)) {
|
|
448
|
+
width = nextWidth;
|
|
449
|
+
height = nextHeight;
|
|
450
|
+
canvas.width = width;
|
|
451
|
+
canvas.height = height;
|
|
452
|
+
}
|
|
453
|
+
const p = current;
|
|
454
|
+
const s = pointer;
|
|
455
|
+
s.svx *= 0.94;
|
|
456
|
+
s.svy *= 0.94;
|
|
457
|
+
s.smoothX += (s.x - s.smoothX) * 0.12;
|
|
458
|
+
s.smoothY += (s.y - s.smoothY) * 0.12;
|
|
459
|
+
s.svx += ((s.x - s.smoothX) * 0.5 - s.svx) * 0.15;
|
|
460
|
+
s.svy += ((s.y - s.smoothY) * 0.5 - s.svy) * 0.15;
|
|
461
|
+
const read = flip ? targetA : targetB;
|
|
462
|
+
const write = flip ? targetB : targetA;
|
|
463
|
+
flip = !flip;
|
|
464
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, write.fbo);
|
|
465
|
+
gl.viewport(0, 0, flowWidth, flowHeight);
|
|
466
|
+
gl.useProgram(flowProgram);
|
|
467
|
+
bindQuad(flowProgram);
|
|
468
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
469
|
+
gl.bindTexture(gl.TEXTURE_2D, read.tex);
|
|
470
|
+
gl.uniform1i(flow.prev, 0);
|
|
471
|
+
gl.uniform2f(flow.mouse, s.smoothX, s.smoothY);
|
|
472
|
+
gl.uniform2f(flow.velocity, s.svx, s.svy);
|
|
473
|
+
gl.uniform1f(flow.brushRadius, p.mouseRadius);
|
|
474
|
+
gl.uniform1f(flow.brushStrength, p.mouseStrength);
|
|
475
|
+
gl.uniform1f(flow.decay, p.decay);
|
|
476
|
+
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
|
477
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
478
|
+
gl.viewport(0, 0, width, height);
|
|
479
|
+
gl.useProgram(displayProgram);
|
|
480
|
+
bindQuad(displayProgram);
|
|
481
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
482
|
+
gl.bindTexture(gl.TEXTURE_2D, write.tex);
|
|
483
|
+
gl.uniform1i(display.flowmap, 0);
|
|
484
|
+
const time = (performance.now() - start) * 1e-3 * (p.speed * (1 + audioLow * 1.3) / 100);
|
|
485
|
+
gl.uniform1f(display.time, time);
|
|
486
|
+
gl.uniform1f(display.pixelRatio, window.devicePixelRatio || 1);
|
|
487
|
+
gl.uniform2f(display.resolution, width, height);
|
|
488
|
+
gl.uniform1f(display.scale, p.scale);
|
|
489
|
+
gl.uniform1f(display.rotation, p.rotation / 90);
|
|
490
|
+
gl.uniform2f(display.offset, p.offsetX / 100, p.offsetY / 100);
|
|
491
|
+
const c1 = hexToRgb(p.color1 || "#2E58A4");
|
|
492
|
+
const c2 = hexToRgb(p.color2 || "#D2E2EE");
|
|
493
|
+
const c3 = hexToRgb(p.color3 || "#FFFFFF");
|
|
494
|
+
gl.uniform4f(display.color1, c1[0], c1[1], c1[2], 1);
|
|
495
|
+
gl.uniform4f(display.color2, c2[0], c2[1], c2[2], 1);
|
|
496
|
+
gl.uniform4f(display.color3, c3[0], c3[1], c3[2], 1);
|
|
497
|
+
gl.uniform1f(display.colorCount, 3);
|
|
498
|
+
gl.uniform1f(display.proportion, p.proportion / 100);
|
|
499
|
+
gl.uniform1f(display.softness, p.softness / 100);
|
|
500
|
+
gl.uniform1f(display.shape, 0);
|
|
501
|
+
gl.uniform1f(display.shapeScale, p.shapeScale / 100);
|
|
502
|
+
gl.uniform1f(display.distortion, p.distortion / 100);
|
|
503
|
+
gl.uniform1f(display.swirl, p.swirl / 50);
|
|
504
|
+
gl.uniform1f(display.swirlIterations, p.swirlIterations);
|
|
505
|
+
gl.uniform1f(display.distortBoost, p.distortBoost * (1 + audioLow * 0.85));
|
|
506
|
+
gl.uniform1f(display.noiseBoost, p.noiseBoost);
|
|
507
|
+
gl.uniform1f(display.swirlBoost, p.swirlBoost * (1 + audioLow * 0.6));
|
|
508
|
+
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
|
509
|
+
};
|
|
510
|
+
const handle = {
|
|
511
|
+
setParams: (next) => {
|
|
512
|
+
current = { ...next };
|
|
513
|
+
if (!running) {
|
|
514
|
+
frame(performance.now());
|
|
515
|
+
cancelAnimationFrame(raf);
|
|
516
|
+
}
|
|
517
|
+
},
|
|
518
|
+
stir: (x, y, vx, vy) => {
|
|
519
|
+
pointer.x += (x - pointer.x) * 0.35;
|
|
520
|
+
pointer.y += (y - pointer.y) * 0.35;
|
|
521
|
+
pointer.svx += (vx - pointer.svx) * 0.3;
|
|
522
|
+
pointer.svy += (vy - pointer.svy) * 0.3;
|
|
523
|
+
},
|
|
524
|
+
setAudioLow: (level) => {
|
|
525
|
+
audioLow = Math.max(0, Math.min(1, level));
|
|
526
|
+
},
|
|
527
|
+
setRunning: (on) => {
|
|
528
|
+
if (on) {
|
|
529
|
+
if (running) return;
|
|
530
|
+
running = true;
|
|
531
|
+
previous = 0;
|
|
532
|
+
raf = requestAnimationFrame(frame);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
if (!running) return;
|
|
536
|
+
running = false;
|
|
537
|
+
cancelAnimationFrame(raf);
|
|
538
|
+
},
|
|
539
|
+
dispose: () => {
|
|
540
|
+
cancelAnimationFrame(raf);
|
|
541
|
+
window.removeEventListener("mousemove", onMouseMove);
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
|
|
545
|
+
frame(performance.now());
|
|
546
|
+
cancelAnimationFrame(raf);
|
|
547
|
+
return handle;
|
|
548
|
+
}
|
|
549
|
+
running = true;
|
|
550
|
+
raf = requestAnimationFrame(frame);
|
|
551
|
+
return handle;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Inlined from upstream `src/client/fluid-interactions.ts` (line 916):
|
|
555
|
+
// canvas-relative pointer position, flipped to GL bottom-left origin.
|
|
556
|
+
function uv(canvas, clientX, clientY) {
|
|
557
|
+
const rect = canvas.getBoundingClientRect();
|
|
558
|
+
return {
|
|
559
|
+
x: rect.width <= 0 ? 0.5 : (clientX - rect.left) / rect.width,
|
|
560
|
+
y: rect.height <= 0 ? 0.5 : 1 - (clientY - rect.top) / rect.height
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
function attachFluidInteractions(targets) {
|
|
564
|
+
const { main, mainCanvas } = targets;
|
|
565
|
+
const lastStir = /* @__PURE__ */ new WeakMap();
|
|
566
|
+
const ripples = /* @__PURE__ */ new Set();
|
|
567
|
+
const stirButton = (button, strength) => {
|
|
568
|
+
const now = performance.now();
|
|
569
|
+
const previous = lastStir.get(button) ?? 0;
|
|
570
|
+
if (now - previous < 160) return;
|
|
571
|
+
lastStir.set(button, now);
|
|
572
|
+
const rect = button.getBoundingClientRect();
|
|
573
|
+
const point = uv(mainCanvas, rect.left + rect.width / 2, rect.top + rect.height / 2);
|
|
574
|
+
main.stir(point.x, point.y, 0, -strength);
|
|
575
|
+
};
|
|
576
|
+
const ripple = (cx, cy) => {
|
|
577
|
+
const rect = mainCanvas.getBoundingClientRect();
|
|
578
|
+
if (rect.width <= 0 || rect.height <= 0) return;
|
|
579
|
+
const ux = (cx - rect.left) / rect.width;
|
|
580
|
+
const uy = 1 - (cy - rect.top) / rect.height;
|
|
581
|
+
const start = performance.now();
|
|
582
|
+
const duration = 1500;
|
|
583
|
+
const maxRadius = 120;
|
|
584
|
+
const count = 8;
|
|
585
|
+
const step = () => {
|
|
586
|
+
const t = performance.now() - start;
|
|
587
|
+
if (t > duration) return;
|
|
588
|
+
const k = t / duration;
|
|
589
|
+
const radius = maxRadius * k * k;
|
|
590
|
+
const strength = 0.05 * (1 - k);
|
|
591
|
+
const spin = 0.4 * k;
|
|
592
|
+
for (let i = 0; i < count; i += 1) {
|
|
593
|
+
const angle = i / count * Math.PI * 2 + spin;
|
|
594
|
+
const px = ux + radius * Math.cos(angle) / rect.width;
|
|
595
|
+
const py = uy + radius * Math.sin(angle) / rect.height;
|
|
596
|
+
main.stir(px, py, Math.cos(angle) * strength, -Math.sin(angle) * strength);
|
|
597
|
+
}
|
|
598
|
+
const id2 = requestAnimationFrame(step);
|
|
599
|
+
ripples.add(id2);
|
|
600
|
+
};
|
|
601
|
+
const id = requestAnimationFrame(step);
|
|
602
|
+
ripples.add(id);
|
|
603
|
+
};
|
|
604
|
+
const onPointerOver = (event) => {
|
|
605
|
+
const button = event.target?.closest?.("button");
|
|
606
|
+
if (button !== void 0 && button !== null) stirButton(button, 0.04);
|
|
607
|
+
};
|
|
608
|
+
const onClick = (event) => {
|
|
609
|
+
const button = event.target?.closest?.("button");
|
|
610
|
+
if (button === void 0 || button === null) return;
|
|
611
|
+
const now = performance.now();
|
|
612
|
+
const previous = lastStir.get(button) ?? 0;
|
|
613
|
+
if (now - previous < 500) return;
|
|
614
|
+
lastStir.set(button, now);
|
|
615
|
+
const rect = button.getBoundingClientRect();
|
|
616
|
+
ripple(rect.left + rect.width / 2, rect.top + rect.height / 2);
|
|
617
|
+
};
|
|
618
|
+
document.addEventListener("pointerover", onPointerOver, { capture: true });
|
|
619
|
+
document.addEventListener("click", onClick, { capture: true });
|
|
620
|
+
return () => {
|
|
621
|
+
for (const id of ripples) cancelAnimationFrame(id);
|
|
622
|
+
ripples.clear();
|
|
623
|
+
document.removeEventListener("pointerover", onPointerOver, { capture: true });
|
|
624
|
+
document.removeEventListener("click", onClick, { capture: true });
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// ── glass chromatic dispersion ───────────────────────────────────────
|
|
629
|
+
var FILTER_ID = "dshome-glass-dispersion";
|
|
630
|
+
var TINT_ID = "dshome-glass-dispersion-tint";
|
|
631
|
+
var REFRACT_ID = "dshome-glass-dispersion-refract";
|
|
632
|
+
var TALL_FILTER_ID = "dshome-glass-dispersion-tall";
|
|
633
|
+
var TALL_TINT_ID = "dshome-glass-dispersion-tall-tint";
|
|
634
|
+
var TALL_REFRACT_ID = "dshome-glass-dispersion-tall-refract";
|
|
635
|
+
var ATTR = "data-dshome-dispersion";
|
|
636
|
+
var DEFAULT_REF_SCALE = 60;
|
|
637
|
+
// The liquid recipe drives the same feDisplacementMap harder: at 60 the
|
|
638
|
+
// bend is there but only just legible once the blur sits on top of it, and
|
|
639
|
+
// "you can see the edge refract" is the whole point of that recipe. Kept as
|
|
640
|
+
// a separate constant so the frosted recipe keeps receiving 60.
|
|
641
|
+
var LIQUID_REF_SCALE = 104;
|
|
642
|
+
var EDGE_DX = -14;
|
|
643
|
+
var TINT_OPACITY = 0.45;
|
|
644
|
+
var TINT_SAT = 0.85;
|
|
645
|
+
var TINT_LIGHT = 0.6;
|
|
646
|
+
var DEFAULT_TINT_HUE = 44;
|
|
647
|
+
function buildDisplacementMapDataUrl(width = 400, height = 92, radius = 50) {
|
|
648
|
+
const w = Math.max(240, Math.round(width));
|
|
649
|
+
const h = Math.max(48, Math.round(height));
|
|
650
|
+
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>`;
|
|
656
|
+
return `data:image/svg+xml,${encodeURIComponent(svg2)}`;
|
|
657
|
+
}
|
|
658
|
+
// Inlined from upstream `src/client/glass-dispersion.ts` (line 2409) —
|
|
659
|
+
// required by tintMatrix below.
|
|
660
|
+
function hslToRgb01(h, s, l) {
|
|
661
|
+
const hue = (h % 360 + 360) % 360 / 360;
|
|
662
|
+
if (s === 0) return [l, l, l];
|
|
663
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
664
|
+
const p = 2 * l - q;
|
|
665
|
+
const channel = (t) => {
|
|
666
|
+
let tt = t;
|
|
667
|
+
if (tt < 0) tt += 1;
|
|
668
|
+
if (tt > 1) tt -= 1;
|
|
669
|
+
if (tt < 1 / 6) return p + (q - p) * 6 * tt;
|
|
670
|
+
if (tt < 1 / 2) return q;
|
|
671
|
+
if (tt < 2 / 3) return p + (q - p) * (2 / 3 - tt) * 6;
|
|
672
|
+
return p;
|
|
673
|
+
};
|
|
674
|
+
return [channel(hue + 1 / 3), channel(hue), channel(hue - 1 / 3)];
|
|
675
|
+
}
|
|
676
|
+
function tintMatrix(hue, opacity) {
|
|
677
|
+
const [r, g, b] = hslToRgb01(hue, TINT_SAT, TINT_LIGHT);
|
|
678
|
+
const lr = (0.2126 * opacity).toFixed(4);
|
|
679
|
+
const lg = (0.7152 * opacity).toFixed(4);
|
|
680
|
+
const lb = (0.0722 * opacity).toFixed(4);
|
|
681
|
+
return `0 0 0 0 ${r.toFixed(4)} 0 0 0 0 ${g.toFixed(4)} 0 0 0 0 ${b.toFixed(4)} ${lr} ${lg} ${lb} 0 0`;
|
|
682
|
+
}
|
|
683
|
+
function supportsSvgBackdropFilter() {
|
|
684
|
+
try {
|
|
685
|
+
const ua = navigator.userAgent || "";
|
|
686
|
+
if (/Safari/.test(ua) && !/Chrome/.test(ua) || /Firefox/.test(ua)) return false;
|
|
687
|
+
const div = document.createElement("div");
|
|
688
|
+
div.style.backdropFilter = `url(#${FILTER_ID})`;
|
|
689
|
+
return div.style.backdropFilter !== "";
|
|
690
|
+
} catch {
|
|
691
|
+
return false;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
// Inlined from upstream `src/client/glass-dispersion.ts` (line 2443) —
|
|
695
|
+
// 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>`;
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Mount the two SVG backdrop filters and set `ATTR` on <html> so the
|
|
701
|
+
* stylesheet can opt surfaces into the dispersion. Returns a handle whose
|
|
702
|
+
* `setTint(hue)` / `setRefraction(scale)` accept values from the caller
|
|
703
|
+
* (upstream read them from its own settings store).
|
|
704
|
+
* @param {{ hue?: number, refraction?: number }} [options]
|
|
705
|
+
* @returns {{ setTint: (hue: number) => void, setRefraction: (scale: number) => void, dispose: () => void }}
|
|
706
|
+
*/
|
|
707
|
+
function startGlassDispersion(options) {
|
|
708
|
+
const hue = options !== undefined && typeof options.hue === "number" ? options.hue : DEFAULT_TINT_HUE;
|
|
709
|
+
const root = document.documentElement;
|
|
710
|
+
if (root.hasAttribute(ATTR) || document.getElementById(FILTER_ID) !== null) {
|
|
711
|
+
return { setTint() {
|
|
712
|
+
}, setRefraction() {
|
|
713
|
+
}, dispose() {
|
|
714
|
+
} };
|
|
715
|
+
}
|
|
716
|
+
if (!supportsSvgBackdropFilter()) {
|
|
717
|
+
return { setTint() {
|
|
718
|
+
}, setRefraction() {
|
|
719
|
+
}, dispose() {
|
|
720
|
+
} };
|
|
721
|
+
}
|
|
722
|
+
const mapWide = buildDisplacementMapDataUrl(400, 92, 50);
|
|
723
|
+
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>`;
|
|
725
|
+
const container = document.createElement("div");
|
|
726
|
+
container.setAttribute("aria-hidden", "true");
|
|
727
|
+
container.style.cssText = "position:absolute;width:0;height:0;overflow:hidden;";
|
|
728
|
+
container.innerHTML = markup;
|
|
729
|
+
document.body.appendChild(container);
|
|
730
|
+
const tint = container.querySelectorAll("feColorMatrix");
|
|
731
|
+
const refract = container.querySelectorAll("feDisplacementMap");
|
|
732
|
+
root.setAttribute(ATTR, "");
|
|
733
|
+
const handle = {
|
|
734
|
+
setTint(nextHue) {
|
|
735
|
+
const values = tintMatrix(nextHue, TINT_OPACITY);
|
|
736
|
+
tint.forEach((node) => node.setAttribute("values", values));
|
|
737
|
+
},
|
|
738
|
+
setRefraction(scale) {
|
|
739
|
+
const next = String(Math.max(0, Math.min(140, Number(scale) || 0)));
|
|
740
|
+
refract.forEach((node) => node.setAttribute("scale", next));
|
|
741
|
+
},
|
|
742
|
+
dispose() {
|
|
743
|
+
root.removeAttribute(ATTR);
|
|
744
|
+
container.remove();
|
|
745
|
+
}
|
|
746
|
+
};
|
|
747
|
+
if (options !== undefined && typeof options.refraction === "number") handle.setRefraction(options.refraction);
|
|
748
|
+
handle.setTint(hue);
|
|
749
|
+
return handle;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// ── specular-highlight cursor parallax ───────────────────────────────
|
|
753
|
+
// SPOT_ATTR / SPEC_X / SPEC_Y/ SPOT_SELECTOR renamed to the `dshome-`
|
|
754
|
+
// prefix. The caller decides which elements carry `data-dshome-spot`;
|
|
755
|
+
// declare them as `data-dshome-spot` in the stylesheet/DOM layer.
|
|
756
|
+
var SPOT_ATTR = "data-dshome-spot";
|
|
757
|
+
var SPOT_SELECTOR = `[${SPOT_ATTR}]`;
|
|
758
|
+
var SPEC_X = "--dshome-spec-x";
|
|
759
|
+
var SPEC_Y = "--dshome-spec-y";
|
|
760
|
+
// Inlined from upstream `src/client/spot-core.ts` (lines 1711-1713).
|
|
761
|
+
function closestSpot(target) {
|
|
762
|
+
return target instanceof Element ? target.closest(SPOT_SELECTOR) : null;
|
|
763
|
+
}
|
|
764
|
+
// Inlined from upstream `src/client/spot-core.ts` (lines 1717-1729). The
|
|
765
|
+
// `[data-composer-card]` / `[data-dsh-stats]` selectors are product markup
|
|
766
|
+
// and are deliberately left unrenamed; a mount that lacks them simply
|
|
767
|
+
// falls back to the spot's own border box.
|
|
768
|
+
function visualRect(spot) {
|
|
769
|
+
if (spot.querySelector("[data-composer-card]") !== null) {
|
|
770
|
+
const card = spot.querySelector("[data-composer-card]");
|
|
771
|
+
const r0 = card.getBoundingClientRect();
|
|
772
|
+
const stats = spot.querySelector("[data-dsh-stats]");
|
|
773
|
+
if (stats === null) return r0;
|
|
774
|
+
const r1 = stats.getBoundingClientRect();
|
|
775
|
+
const left = Math.min(r0.left, r1.left);
|
|
776
|
+
const top = Math.min(r0.top, r1.top);
|
|
777
|
+
return new DOMRect(left, top, Math.max(r0.right, r1.right) - left, Math.max(r0.bottom, r1.bottom) - top);
|
|
778
|
+
}
|
|
779
|
+
return spot.getBoundingClientRect();
|
|
780
|
+
}
|
|
781
|
+
// Inlined from upstream `src/client/spot-core.ts` (lines 1730-1732).
|
|
782
|
+
function inside(visual, clientX, clientY) {
|
|
783
|
+
return clientX >= visual.left && clientX <= visual.right && clientY >= visual.top && clientY <= visual.bottom;
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Write normalised cursor coordinates into `--dshome-spec-x` /
|
|
787
|
+
* `--dshome-spec-y` on the hovered `[data-dshome-spot]` element.
|
|
788
|
+
* @returns {() => void} disposer that detaches every listener and clears vars.
|
|
789
|
+
*/
|
|
790
|
+
function startSpecularParallax() {
|
|
791
|
+
let current = null;
|
|
792
|
+
let visual = null;
|
|
793
|
+
let raf = 0;
|
|
794
|
+
const clear = () => {
|
|
795
|
+
if (current === null) return;
|
|
796
|
+
current.style.removeProperty(SPEC_X);
|
|
797
|
+
current.style.removeProperty(SPEC_Y);
|
|
798
|
+
current = null;
|
|
799
|
+
visual = null;
|
|
800
|
+
};
|
|
801
|
+
const paint = (clientX, clientY) => {
|
|
802
|
+
if (raf !== 0) return;
|
|
803
|
+
raf = requestAnimationFrame(() => {
|
|
804
|
+
raf = 0;
|
|
805
|
+
if (current === null || visual === null) return;
|
|
806
|
+
if (!inside(visual, clientX, clientY)) {
|
|
807
|
+
clear();
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
const sx = Math.min(1, Math.max(-1, (clientX - visual.left) / visual.width * 2 - 1));
|
|
811
|
+
const sy = Math.min(1, Math.max(-1, (clientY - visual.top) / visual.height * 2 - 1));
|
|
812
|
+
current.style.setProperty(SPEC_X, sx.toFixed(4));
|
|
813
|
+
current.style.setProperty(SPEC_Y, sy.toFixed(4));
|
|
814
|
+
});
|
|
815
|
+
};
|
|
816
|
+
const onMove = (event) => {
|
|
817
|
+
const spot = closestSpot(event.target);
|
|
818
|
+
if (spot === null || spot !== current) return;
|
|
819
|
+
paint(event.clientX, event.clientY);
|
|
820
|
+
};
|
|
821
|
+
const onOver = (event) => {
|
|
822
|
+
const spot = closestSpot(event.target);
|
|
823
|
+
if (spot === null) return;
|
|
824
|
+
const rect = visualRect(spot);
|
|
825
|
+
if (!inside(rect, event.clientX, event.clientY)) return;
|
|
826
|
+
current = spot;
|
|
827
|
+
visual = rect;
|
|
828
|
+
paint(event.clientX, event.clientY);
|
|
829
|
+
};
|
|
830
|
+
const onOut = (event) => {
|
|
831
|
+
const spot = closestSpot(event.target);
|
|
832
|
+
if (spot === null || spot !== current) return;
|
|
833
|
+
if (visual !== null && inside(visual, event.clientX, event.clientY)) return;
|
|
834
|
+
clear();
|
|
835
|
+
};
|
|
836
|
+
document.addEventListener("pointermove", onMove, { passive: true });
|
|
837
|
+
document.addEventListener("pointerover", onOver, { passive: true });
|
|
838
|
+
document.addEventListener("pointerout", onOut, { passive: true });
|
|
839
|
+
return () => {
|
|
840
|
+
document.removeEventListener("pointermove", onMove);
|
|
841
|
+
document.removeEventListener("pointerover", onOver);
|
|
842
|
+
document.removeEventListener("pointerout", onOut);
|
|
843
|
+
if (raf !== 0) cancelAnimationFrame(raf);
|
|
844
|
+
clear();
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
849
|
+
// PUBLIC API exposed by this fragment (all declarations are inert until called;
|
|
850
|
+
// nothing here runs at load time):
|
|
851
|
+
//
|
|
852
|
+
// SITE_FLUID_PARAMS : object
|
|
853
|
+
// Upstream's default fluid parameter set (mouse radius/strength, decay,
|
|
854
|
+
// distortion, swirl, scale, offsets and three palette colours). Spread it
|
|
855
|
+
// and override `color1`/`color2`/`color3` (and any knob) to theme the
|
|
856
|
+
// fluid: `{ ...SITE_FLUID_PARAMS, color1: '#8AA3D6' }`.
|
|
857
|
+
//
|
|
858
|
+
// attachFluidShader(canvas, params) -> handle
|
|
859
|
+
// canvas: HTMLCanvasElement in the DOM and sized by CSS. params: a full
|
|
860
|
+
// parameter object (see SITE_FLUID_PARAMS). Returns:
|
|
861
|
+
// setParams(next) -> void swap params; repaints once if paused
|
|
862
|
+
// stir(x, y, vx, vy) -> void inject a velocity impulse at uv (0..1)
|
|
863
|
+
// setAudioLow(level) -> void 0..1 audio boost for speed/boost knobs
|
|
864
|
+
// setRunning(on) -> void start/stop the rAF loop
|
|
865
|
+
// dispose() -> void cancel rAF + drop the mousemove listener
|
|
866
|
+
// Degrades to no-op methods when WebGL2 is unavailable or a shader fails to
|
|
867
|
+
// compile/link. Never throws for that reason.
|
|
868
|
+
//
|
|
869
|
+
// attachFluidInteractions({ main, mainCanvas }) -> dispose
|
|
870
|
+
// Wires document-level pointerover/click capture so hovering a <button>
|
|
871
|
+
// nudges the fluid and clicking one emits a ripple. `main` is the handle
|
|
872
|
+
// from attachFluidShader; `mainCanvas` is the same canvas element passed
|
|
873
|
+
// there. Returns a () => void that cancels in-flight ripples and detaches
|
|
874
|
+
// both listeners.
|
|
875
|
+
//
|
|
876
|
+
// startGlassDispersion(options?) -> { setTint(hue), setRefraction(scale), dispose() }
|
|
877
|
+
// options: { hue?: number, refraction?: number } — both optional.
|
|
878
|
+
// `hue` (default 44) tints the dispersion edge; `refraction` (default 60,
|
|
879
|
+
// clamped 0..140) is the displacement scale. Injects one hidden <svg> with
|
|
880
|
+
// a wide and a tall filter and sets `data-dshome-dispersion` on <html> so
|
|
881
|
+
// CSS can apply `backdrop-filter: url(#dshome-glass-dispersion)`. Returns
|
|
882
|
+
// no-op methods (idempotent, safe to call twice) when the document already
|
|
883
|
+
// has one or the engine does not support SVG backdrop filters.
|
|
884
|
+
//
|
|
885
|
+
// startSpecularParallax() -> dispose
|
|
886
|
+
// Tracks the cursor over any element carrying `data-dshome-spot` and
|
|
887
|
+
// writes normalised -1..1 coordinates to `--dshome-spec-x` /
|
|
888
|
+
// `--dshome-spec-y` on that element each animation frame. Requires the
|
|
889
|
+
// caller to mark spot elements with `data-dshome-spot` (SPOT_ATTR) and to
|
|
890
|
+
// consume the two custom properties in CSS. Returns a () => void that
|
|
891
|
+
// detaches all three document listeners, cancels the pending frame and
|
|
892
|
+
// clears both properties.
|
|
893
|
+
//
|
|
894
|
+
// hexToRgb(value) -> [r, g, b] (0..1) — internal helper, exported for reuse.
|
|
895
|
+
// buildDisplacementMapDataUrl(w?, h?, r?) -> string — SVG data URL.
|
|
896
|
+
// tintMatrix(hue, opacity) -> string — feColorMatrix values string.
|
|
897
|
+
// supportsSvgBackdropFilter() -> boolean — engine capability probe.
|
|
898
|
+
// FILTER_ID / TALL_FILTER_ID / ATTR / SPOT_ATTR / SPEC_X / SPEC_Y — the
|
|
899
|
+
// ids and attribute/var names the shipped CSS must match.
|
|
900
|
+
//
|
|
901
|
+
// Typical mount:
|
|
902
|
+
// const fluid = attachFluidShader(canvas, { ...SITE_FLUID_PARAMS, color1: c1 });
|
|
903
|
+
// const stopInteractions = attachFluidInteractions({ main: fluid, mainCanvas: canvas });
|
|
904
|
+
// const dispersion = startGlassDispersion({ hue: 44, refraction: 60 });
|
|
905
|
+
// const stopParallax = startSpecularParallax();
|
|
906
|
+
// Typical dispose: stopParallax(); dispersion.dispose(); stopInteractions(); fluid.dispose();
|
|
907
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
908
|
+
|
|
909
|
+
// CORE_CSS: ours only (`:root` tokens, `html/body`, keyframes, stable
|
|
910
|
+
// `[role=menu]`, `.dshome-*`). No product class names — always injected.
|
|
911
|
+
// Color model: `data-dshome-color` ∈ {green(base), red(鲜红 peak), blue}
|
|
912
|
+
// re-tints every var(--ds-brand*) consumer.
|
|
913
|
+
const CORE_CSS = `
|
|
914
|
+
:root{--ds-brand:#059669;--ds-brand-deep:#047857;--ds-brand-mid:#10B981;--ds-brand-light:#34D399;--ds-glass-blur:24px}
|
|
915
|
+
html,body{background-color:transparent!important}
|
|
916
|
+
/* The page background below is painted from OUR scheme marker
|
|
917
|
+
(body[data-dshome-dark]), while message text takes its colour from the
|
|
918
|
+
product's --dsw-alias-* tokens. Those are two different signals, and when
|
|
919
|
+
they disagree — the marker is derived from the theme service and can miss a
|
|
920
|
+
scheme change — the result is near-white text on a near-white page: every
|
|
921
|
+
paragraph vanishes while anything carrying its own background (tool cards)
|
|
922
|
+
stays readable. Reproduced by clearing the marker on a dark page.
|
|
923
|
+
Declaring the same two label tokens under the same marker puts both sides on
|
|
924
|
+
ONE signal. The values are exactly what theme.overrideTokens() already
|
|
925
|
+
installs for each scheme, so a page where the two agree repaints identically. */
|
|
926
|
+
body{--dsw-alias-label-primary:#152443!important;--dsw-alias-label-secondary:#3c4a63!important}
|
|
927
|
+
body[data-dshome-dark]{--dsw-alias-label-primary:#eef2ff!important;--dsw-alias-label-secondary:#a9b8d8!important}
|
|
928
|
+
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}
|
|
929
|
+
body[data-dshome-color=red]{--ds-brand:#F5222D;--ds-brand-deep:#CF1322;--ds-brand-mid:#FF4D4F;--ds-brand-light:#FF7875}
|
|
930
|
+
body[data-dshome-color=blue]{--ds-brand:#4D6BFE;--ds-brand-deep:#3A65C2;--ds-brand-mid:#4176E6;--ds-brand-light:#73A3D2}
|
|
931
|
+
body[data-dshome-dark]{--ds-brand:#10B981;--ds-brand-deep:#059669;--ds-brand-mid:#34D399;--ds-brand-light:#6EE7B7;background:radial-gradient(1100px 650px at 8% -8%,#0a3a28 0,transparent 62%),radial-gradient(950px 600px at 104% 10%,#0d4d35 0,transparent 58%),radial-gradient(800px 600px at 50% 118%,#06291c 0,transparent 60%),linear-gradient(180deg,#081b14 0%,#0a241a 55%,#081b14 100%);background-attachment:fixed}
|
|
932
|
+
body[data-dshome-dark][data-dshome-color=red]{--ds-brand:#FF4D4F;--ds-brand-deep:#F5222D;--ds-brand-mid:#FF7875;--ds-brand-light:#FFA39E;background:radial-gradient(1100px 650px at 8% -8%,#5c0f14 0,transparent 62%),radial-gradient(950px 600px at 104% 10%,#7d1a20 0,transparent 58%),radial-gradient(800px 600px at 50% 118%,#3f0a0e 0,transparent 60%),linear-gradient(180deg,#260709 0%,#330a0d 55%,#260709 100%);background-attachment:fixed}
|
|
933
|
+
body[data-dshome-dark][data-dshome-color=blue]{--ds-brand:#5D79FF;--ds-brand-deep:#4D6BFE;--ds-brand-mid:#7A96FF;--ds-brand-light:#9DB9FF;background:radial-gradient(1100px 650px at 8% -8%,#1a3870 0,transparent 62%),radial-gradient(950px 600px at 104% 10%,#2d5f9e 0,transparent 58%),radial-gradient(800px 600px at 50% 118%,#0d1f4a 0,transparent 60%),linear-gradient(180deg,#0a1a3a 0%,#0d1f4a 55%,#0a1a3a 100%);background-attachment:fixed}
|
|
934
|
+
@property --dsh-border-angle{syntax:"<angle>";initial-value:0deg;inherits:false}
|
|
935
|
+
@keyframes dsh-border-spin{to{--dsh-border-angle:360deg}}
|
|
936
|
+
@keyframes dshome-view-enter{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}
|
|
937
|
+
@keyframes dshome-pop-in{from{opacity:0;transform:translateY(10px) scale(.97)}to{opacity:1;transform:none}}
|
|
938
|
+
@keyframes dshome-fade-in{from{opacity:0}to{opacity:1}}
|
|
939
|
+
@keyframes dshome-tab-in{from{opacity:0;transform:translateX(-8px)}to{opacity:1;transform:none}}
|
|
940
|
+
@keyframes dshome-modal-in{from{opacity:0;transform:translateY(24px) scale(.94)}to{opacity:1;transform:none}}
|
|
941
|
+
[role=menu]{animation:dshome-pop-in .22s cubic-bezier(.2,.8,.2,1)}
|
|
942
|
+
.dshome-ws-open-wrap{padding:2px 0}
|
|
943
|
+
.dshome-ws-open{display:flex;align-items:center;gap:8px;width:100%;min-width:170px;padding:6px 10px;border:none;border-radius:8px;background:transparent;color:var(--dsw-alias-label-primary);font-size:13px;line-height:18px;font-family:inherit;text-align:left;cursor:pointer;transition:background .15s ease,color .15s ease}
|
|
944
|
+
.dshome-ws-open:hover{background:color-mix(in srgb,var(--ds-brand) 12%,transparent)!important;color:var(--ds-brand-deep)!important}
|
|
945
|
+
.dshome-ws-open-icon{display:inline-flex;flex:none;color:var(--ds-brand)}
|
|
946
|
+
.dshome-ws-open-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
947
|
+
body[data-dshome-dark] .dshome-ws-open{color:#eef2ff}
|
|
948
|
+
body[data-dshome-dark] .dshome-ws-open:hover{background:color-mix(in srgb,var(--ds-brand) 22%,transparent)!important;color:#fff!important}
|
|
949
|
+
body[data-dshome-dark] .dshome-ws-open-icon{color:var(--ds-brand-light)}
|
|
950
|
+
.dshome-dstt{max-width:720px;color:var(--dsw-alias-label-primary);flex-direction:column;gap:14px;display:flex}
|
|
951
|
+
.dshome-dstt h3{margin:0;font-size:16px;font-weight:500;line-height:24px;display:flex;align-items:center;gap:8px}
|
|
952
|
+
.dshome-dstt-tag{flex:none;border:1px solid color-mix(in srgb,var(--ds-brand) 35%,transparent);background:color-mix(in srgb,var(--ds-brand) 12%,transparent);color:var(--ds-brand);border-radius:999px;padding:1px 8px;font-size:11px;line-height:18px;font-weight:600}
|
|
953
|
+
body[data-dshome-dark] .dshome-dstt-tag{color:var(--ds-brand-light)}
|
|
954
|
+
.dshome-dstt p{margin:0;font-size:13px;line-height:20px;color:var(--dsw-alias-label-secondary)}
|
|
955
|
+
.dshome-dstt-row{display:flex;align-items:center;gap:12px;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;padding:12px 14px;background:var(--dsw-alias-bg-layer-1)}
|
|
956
|
+
.dshome-dstt-row-body{flex:1;min-width:0}
|
|
957
|
+
.dshome-dstt-row-title{font-size:14px;line-height:22px;font-weight:500}
|
|
958
|
+
.dshome-dstt-row-hint{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}
|
|
959
|
+
.dshome-dstt-status{flex:none;font-size:12px;line-height:18px;padding:4px 10px;border-radius:999px;display:inline-flex;align-items:center;gap:6px}
|
|
960
|
+
.dshome-dstt-status[data-peak=true]{background:color-mix(in srgb,var(--ds-brand) 14%,transparent);color:var(--ds-brand-deep);border:1px solid color-mix(in srgb,var(--ds-brand) 30%,transparent)}
|
|
961
|
+
.dshome-dstt-status[data-peak=false]{background:color-mix(in srgb,var(--ds-brand) 8%,transparent);color:var(--ds-brand);border:1px solid color-mix(in srgb,var(--ds-brand) 22%,transparent)}
|
|
962
|
+
body[data-dshome-dark] .dshome-dstt-status[data-peak=true]{color:var(--ds-brand-light)}
|
|
963
|
+
body[data-dshome-dark] .dshome-dstt-status[data-peak=false]{color:var(--ds-brand-light)}
|
|
964
|
+
.dshome-dstt-seg{display:inline-flex;align-items:center;gap:2px;flex:none;border:1px solid color-mix(in srgb,var(--ds-brand) 25%,transparent);border-radius:999px;background:color-mix(in srgb,var(--ds-brand) 8%,transparent);padding:2px}
|
|
965
|
+
.dshome-dstt-seg-btn{height:26px;padding:0 12px;border:none;border-radius:999px;background:transparent;color:var(--ds-brand);font-size:12px;font-weight:500;cursor:pointer;font-family:inherit;line-height:26px;transition:background .15s ease,color .15s ease}
|
|
966
|
+
.dshome-dstt-seg-btn:hover{background:color-mix(in srgb,var(--ds-brand) 12%,transparent)}
|
|
967
|
+
.dshome-dstt-seg-btn.dshome-on{background:linear-gradient(135deg,var(--ds-brand),var(--ds-brand-mid));color:#fff}
|
|
968
|
+
.dshome-dstt-seg-btn:disabled{opacity:.5;cursor:default}
|
|
969
|
+
.dshome-dstt-seg-btn:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}
|
|
970
|
+
`;
|
|
971
|
+
|
|
972
|
+
// Locale toggle styling. The two header-hover dependent rules live in the
|
|
973
|
+
// `.wSkVaW_root` patch block below, so the toggle stays styled (short
|
|
974
|
+
// labels) even if the header class renames.
|
|
975
|
+
// The product sets `corner-shape:superellipse(1.5)` on `*` (its
|
|
976
|
+
// corner-shape.css), which flattens even a 999px radius into a rounded
|
|
977
|
+
// rectangle; `corner-shape:round` restores true round ends here.
|
|
978
|
+
// Label bloom: the short 中/E label grows into 中文/EN with an animated
|
|
979
|
+
// max-width instead of the old `display` hard swap.
|
|
980
|
+
const ACTIONS_CSS = `
|
|
981
|
+
.dshome-locale{display:inline-flex;align-items:center;height:28px;border:1px solid color-mix(in srgb,var(--ds-brand) 25%,transparent);border-radius:999px;background:color-mix(in srgb,var(--ds-brand) 8%,transparent);overflow:hidden;flex:none;corner-shape:round;transition:transform .28s cubic-bezier(.2,.8,.2,1),box-shadow .28s ease,border-color .22s ease,background .22s ease}
|
|
982
|
+
.dshome-locale:hover{transform:translateY(-1px) scale(1.03);border-color:color-mix(in srgb,var(--ds-brand) 48%,transparent);background:color-mix(in srgb,var(--ds-brand) 14%,transparent);box-shadow:0 4px 14px color-mix(in srgb,var(--ds-brand) 26%,transparent)}
|
|
983
|
+
body[data-dshome-dark] .dshome-locale{border-color:color-mix(in srgb,var(--ds-brand) 35%,transparent);background:color-mix(in srgb,var(--ds-brand) 16%,transparent)}
|
|
984
|
+
body[data-dshome-dark] .dshome-locale:hover{border-color:color-mix(in srgb,var(--ds-brand) 55%,transparent);background:color-mix(in srgb,var(--ds-brand) 24%,transparent);box-shadow:0 4px 16px color-mix(in srgb,var(--ds-brand) 34%,transparent)}
|
|
985
|
+
.dshome-locale button{height:28px;padding:0 11px;border:none;background:transparent;color:var(--ds-brand);font-size:12px;font-weight:500;cursor:pointer;font-family:inherit;line-height:28px;transition:background .22s ease,color .22s ease,transform .18s cubic-bezier(.2,.8,.2,1)}
|
|
986
|
+
.dshome-locale button:not(.dshome-on):hover{background:color-mix(in srgb,var(--ds-brand) 14%,transparent);color:var(--ds-brand-deep)}
|
|
987
|
+
.dshome-locale button:active{transform:scale(.92)}
|
|
988
|
+
.dshome-locale button.dshome-on{background:linear-gradient(135deg,var(--ds-brand),var(--ds-brand-mid));color:#fff;animation:dshome-locale-pop .34s cubic-bezier(.2,.8,.2,1)}
|
|
989
|
+
@keyframes dshome-locale-pop{0%{transform:scale(.86)}60%{transform:scale(1.05)}to{transform:none}}
|
|
990
|
+
.dshome-locale .dshome-short,.dshome-locale .dshome-full{display:inline-block;overflow:hidden;white-space:nowrap;transition:max-width .3s cubic-bezier(.2,.8,.2,1),opacity .22s ease,transform .3s cubic-bezier(.2,.8,.2,1)}
|
|
991
|
+
.dshome-locale .dshome-short{max-width:2.2em;opacity:1;transform:none}
|
|
992
|
+
.dshome-locale .dshome-full{max-width:0;opacity:0;transform:translateX(-6px)}
|
|
993
|
+
body[data-dshome-dark] .dshome-locale button{color:var(--ds-brand-light)}
|
|
994
|
+
body[data-dshome-dark] .dshome-locale button:not(.dshome-on):hover{background:color-mix(in srgb,var(--ds-brand) 22%,transparent);color:#fff}
|
|
995
|
+
body[data-dshome-dark] .dshome-locale button.dshome-on{background:linear-gradient(135deg,color-mix(in srgb,var(--ds-brand) 72%,#fff),var(--ds-brand));color:#fff}
|
|
996
|
+
`;
|
|
997
|
+
|
|
998
|
+
const SUBAGENT_PANEL_CSS = `
|
|
999
|
+
.dshome-sa{position:fixed;right:16px;bottom:16px;z-index:70;width:286px;max-width:calc(100vw - 32px);box-sizing:border-box;display:flex;flex-direction:column;gap:2px;padding:10px 12px 8px;border-radius:16px;border:1px solid rgba(255,255,255,.55);background:linear-gradient(165deg,rgba(255,255,255,.46),rgba(255,255,255,.22));box-shadow:0 12px 40px rgba(13,30,60,.14),inset 0 1px 0 rgba(255,255,255,.85),inset 0 0 0 .5px rgba(255,255,255,.5);-webkit-backdrop-filter:blur(24px) saturate(1.6);backdrop-filter:blur(24px) saturate(1.6);font-family:inherit;transition:opacity .22s ease,transform .22s ease;animation:dshome-sa-in .3s cubic-bezier(.2,.8,.2,1)}
|
|
1000
|
+
.dshome-sa[hidden]{display:none!important}
|
|
1001
|
+
.dshome-sa.dshome-sa-leave{opacity:0;transform:translateY(10px);pointer-events:none}
|
|
1002
|
+
body[data-dshome-dark] .dshome-sa{background:linear-gradient(165deg,rgba(21,42,86,.52),rgba(10,26,58,.3));border-color:color-mix(in srgb,var(--ds-brand) 24%,transparent);box-shadow:0 12px 40px rgba(0,0,0,.4),inset 0 1px 0 rgba(255,255,255,.12),inset 0 0 0 .5px color-mix(in srgb,var(--ds-brand) 16%,transparent)}
|
|
1003
|
+
@supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){.dshome-sa{background:linear-gradient(165deg,rgba(255,255,255,.84),rgba(255,255,255,.68))}body[data-dshome-dark] .dshome-sa{background:linear-gradient(165deg,rgba(13,30,60,.88),rgba(10,26,58,.78))}}
|
|
1004
|
+
@keyframes dshome-sa-in{from{opacity:0;transform:translateY(14px) scale(.97)}to{opacity:1;transform:none}}
|
|
1005
|
+
.dshome-sa-head{display:flex;flex-direction:row;align-items:center;gap:7px;min-height:26px}
|
|
1006
|
+
.dshome-sa-pulse{position:relative;flex:none;width:8px;height:8px;border-radius:50%;background:var(--ds-brand);animation:dshome-sa-pulse 1.8s ease-out infinite}
|
|
1007
|
+
@keyframes dshome-sa-pulse{0%{box-shadow:0 0 0 0 color-mix(in srgb,var(--ds-brand) 45%,transparent)}70%{box-shadow:0 0 0 7px color-mix(in srgb,var(--ds-brand) 0%,transparent)}100%{box-shadow:0 0 0 0 color-mix(in srgb,var(--ds-brand) 0%,transparent)}}
|
|
1008
|
+
.dshome-sa-title{flex:1;min-width:0;font-size:12.5px;line-height:18px;font-weight:600;color:var(--dsw-alias-label-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1009
|
+
.dshome-sa-collapse{flex:none;display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;padding:0;border:none;border-radius:50%;background:rgba(255,255,255,.5);color:var(--ds-brand);cursor:pointer;box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--ds-brand) 18%,transparent);transition:transform .2s ease,background .15s ease}
|
|
1010
|
+
.dshome-sa-collapse:hover{background:rgba(255,255,255,.75)}
|
|
1011
|
+
body[data-dshome-dark] .dshome-sa-collapse{background:color-mix(in srgb,var(--ds-brand) 18%,transparent);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--ds-brand) 25%,transparent)}
|
|
1012
|
+
body[data-dshome-dark] .dshome-sa-collapse:hover{background:color-mix(in srgb,var(--ds-brand) 30%,transparent)}
|
|
1013
|
+
.dshome-sa-collapse svg{display:block}
|
|
1014
|
+
.dshome-sa-collapsed .dshome-sa-collapse{transform:rotate(180deg)}
|
|
1015
|
+
.dshome-sa-list{display:flex;flex-direction:column;gap:2px;max-height:312px;overflow-y:auto;overflow-x:hidden;scrollbar-gutter:stable;padding:2px 0}
|
|
1016
|
+
.dshome-sa-collapsed .dshome-sa-list{display:none}
|
|
1017
|
+
.dshome-sa-item{position:relative;display:flex;flex-direction:row;align-items:flex-start;gap:8px;width:100%;min-width:0;box-sizing:border-box;padding:7px 8px;border:none;border-radius:10px;background:transparent;color:inherit;font-family:inherit;text-align:left;cursor:pointer;transition:background .15s ease}
|
|
1018
|
+
.dshome-sa-item:hover{background:linear-gradient(135deg,rgba(255,255,255,.65),rgba(255,255,255,.35));box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--ds-brand) 14%,transparent)}
|
|
1019
|
+
body[data-dshome-dark] .dshome-sa-item:hover{background:linear-gradient(135deg,color-mix(in srgb,var(--ds-brand) 22%,transparent),color-mix(in srgb,var(--ds-brand) 10%,transparent));box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--ds-brand) 22%,transparent)}
|
|
1020
|
+
.dshome-sa-dot{flex:none;width:6px;height:6px;margin-top:7px;border-radius:50%;background:linear-gradient(135deg,var(--ds-brand),var(--ds-brand-light));animation:dshome-sa-blink 1.2s ease-in-out infinite}
|
|
1021
|
+
@keyframes dshome-sa-blink{0%,100%{opacity:1}50%{opacity:.35}}
|
|
1022
|
+
.dshome-sa-stop{flex:none;width:18px;height:18px;margin-top:4px;padding:0;display:inline-flex;align-items:center;justify-content:center;border:none;border-radius:6px;background:transparent;color:var(--dsw-alias-label-tertiary);cursor:pointer;opacity:0;pointer-events:none;transition:opacity .15s ease,background .15s ease,color .15s ease}
|
|
1023
|
+
.dshome-sa-item:hover .dshome-sa-stop,.dshome-sa-item:focus-within .dshome-sa-stop{opacity:1;pointer-events:auto}
|
|
1024
|
+
.dshome-sa-stop:hover{background:rgba(239,68,68,.12);color:#ef4444}
|
|
1025
|
+
body[data-dshome-dark] .dshome-sa-stop:hover{background:rgba(255,107,107,.16);color:#ff6b6b}
|
|
1026
|
+
.dshome-sa-stop:disabled{opacity:.55;cursor:default}
|
|
1027
|
+
.dshome-sa-stop-busy svg{animation:dshome-sa-blink .8s ease-in-out infinite}
|
|
1028
|
+
.dshome-sa-stop-denied{background:rgba(239,68,68,.18)!important;color:#ef4444!important}
|
|
1029
|
+
body[data-dshome-dark] .dshome-sa-stop-denied{background:rgba(255,107,107,.2)!important;color:#ff6b6b!important}
|
|
1030
|
+
.dshome-sa-sweep{position:absolute;top:0;bottom:0;left:0;width:0;border-radius:10px;background:linear-gradient(90deg,rgba(239,68,68,.14),rgba(239,68,68,.24));pointer-events:none;z-index:0}
|
|
1031
|
+
body[data-dshome-dark] .dshome-sa-sweep{background:linear-gradient(90deg,rgba(255,107,107,.16),rgba(255,107,107,.28))}
|
|
1032
|
+
.dshome-sa-item>.dshome-sa-dot,.dshome-sa-item>.dshome-sa-body,.dshome-sa-item>.dshome-sa-stop{position:relative;z-index:1}
|
|
1033
|
+
.dshome-sa-item.dshome-sa-stopping .dshome-sa-bar{background:rgba(239,68,68,.85)}
|
|
1034
|
+
.dshome-sa-item.dshome-sa-stopping .dshome-sa-bar-fill{display:none}
|
|
1035
|
+
body[data-dshome-dark] .dshome-sa-item.dshome-sa-stopping .dshome-sa-bar{background:rgba(255,107,107,.8)}
|
|
1036
|
+
.dshome-sa-item.dshome-sa-stopping .dshome-sa-stop{opacity:0;pointer-events:none}
|
|
1037
|
+
.dshome-sa-item.dshome-sa-item-exit{opacity:0;transform:translateX(8px);transition:opacity .2s ease,transform .2s ease}
|
|
1038
|
+
.dshome-sa-body{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px}
|
|
1039
|
+
.dshome-sa-name{font-size:13px;line-height:18px;font-weight:600;color:var(--dsw-alias-label-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1040
|
+
.dshome-sa-meta{font-size:11.5px;line-height:16px;color:var(--dsw-alias-label-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
1041
|
+
.dshome-sa-bar{position:relative;height:3px;border-radius:999px;background:color-mix(in srgb,var(--ds-brand) 12%,transparent);overflow:hidden}
|
|
1042
|
+
.dshome-sa-bar-fill{position:absolute;inset:0;width:42%;border-radius:999px;background:linear-gradient(90deg,transparent,var(--ds-brand),transparent);animation:dshome-sa-slide 1.5s ease-in-out infinite}
|
|
1043
|
+
@keyframes dshome-sa-slide{0%{transform:translateX(-110%)}100%{transform:translateX(350%)}}
|
|
1044
|
+
@media (max-width:760px){.dshome-sa{right:10px;bottom:10px;width:min(286px,calc(100vw - 20px))}}
|
|
1045
|
+
`;
|
|
1046
|
+
|
|
1047
|
+
// PATCH_BLOCKS: every rule group that targets product build-time class
|
|
1048
|
+
// names. `anchor` is the class that must exist in the DOM before the
|
|
1049
|
+
// block is injected; each block degrades independently when its surface
|
|
1050
|
+
// renames in a product rebuild.
|
|
1051
|
+
const PATCH_BLOCKS = [
|
|
1052
|
+
// Frame / sidebar / details columns.
|
|
1053
|
+
{ anchor: ".pI_x6G_frame", css: `
|
|
1054
|
+
.pI_x6G_frame,.pI_x6G_sidebarCol,.pI_x6G_detailsCol,.pI_x6G_centerCol{background:transparent!important}
|
|
1055
|
+
.pI_x6G_sidebarCol{border-right:none!important}
|
|
1056
|
+
.pI_x6G_detailsCol{border-left:none!important}
|
|
1057
|
+
` },
|
|
1058
|
+
// Sidebar glass root — Apple liquid glass pane: layered translucent
|
|
1059
|
+
// gradient + blur/saturate/brightness/contrast + specular edges.
|
|
1060
|
+
{ anchor: ".hHd-Xa_root", css: `
|
|
1061
|
+
.hHd-Xa_root{position:relative;background:transparent!important}
|
|
1062
|
+
.hHd-Xa_root::before{content:"";position:absolute;inset:0;z-index:-1;background:rgba(255,255,255,.5);-webkit-backdrop-filter:blur(var(--ds-glass-blur));backdrop-filter:blur(var(--ds-glass-blur));border-right:1px solid rgba(0,0,0,.06);pointer-events:none}
|
|
1063
|
+
body[data-dshome-dark] .hHd-Xa_root::before{background:rgba(10,26,58,.48);border-right:1px solid rgba(255,255,255,.08)}
|
|
1064
|
+
.hHd-Xa_brand{color:var(--ds-brand);cursor:pointer}
|
|
1065
|
+
body[data-dshome-dark] .hHd-Xa_brand{color:#fff}
|
|
1066
|
+
.hHd-Xa_newSession{border:none!important;background:linear-gradient(135deg,var(--ds-brand) 0%,var(--ds-brand-mid) 100%)!important;color:#fff!important;border-radius:999px!important;box-shadow:0 4px 14px color-mix(in srgb,var(--ds-brand) 35%,transparent);transition:transform .15s ease,box-shadow .15s ease,background .15s ease}
|
|
1067
|
+
.hHd-Xa_newSession:hover{background:linear-gradient(135deg,color-mix(in srgb,var(--ds-brand) 72%,#fff) 0%,var(--ds-brand) 100%)!important;box-shadow:0 6px 18px color-mix(in srgb,var(--ds-brand) 45%,transparent);transform:translateY(-1px)}
|
|
1068
|
+
` },
|
|
1069
|
+
// Session / project rows and icon buttons.
|
|
1070
|
+
{ anchor: ".YDXeBa_sessionRow", css: `
|
|
1071
|
+
.YDXeBa_sessionRow,.YDXeBa_projectRow{background:rgba(255,255,255,.35)!important;border:1px solid transparent;border-radius:10px!important;margin:1px 0;transition:background .15s ease,border-color .15s ease,transform .15s ease}
|
|
1072
|
+
.YDXeBa_sessionRow:hover,.YDXeBa_projectRow:hover{background:rgba(255,255,255,.6)!important;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);transform:translateX(2px)}
|
|
1073
|
+
.YDXeBa_sessionRow.YDXeBa_selected{background:linear-gradient(135deg,color-mix(in srgb,var(--ds-brand) 16%,transparent),color-mix(in srgb,var(--ds-brand-mid) 12%,transparent))!important;border-color:color-mix(in srgb,var(--ds-brand) 25%,transparent);color:var(--ds-brand-deep)}
|
|
1074
|
+
body[data-dshome-dark] .YDXeBa_sessionRow,body[data-dshome-dark] .YDXeBa_projectRow{background:rgba(21,42,86,.4)!important}
|
|
1075
|
+
body[data-dshome-dark] .YDXeBa_sessionRow:hover,body[data-dshome-dark] .YDXeBa_projectRow:hover{background:rgba(30,52,100,.6)!important}
|
|
1076
|
+
body[data-dshome-dark] .YDXeBa_sessionRow.YDXeBa_selected{background:linear-gradient(135deg,color-mix(in srgb,var(--ds-brand) 28%,transparent),color-mix(in srgb,var(--ds-brand) 20%,transparent))!important;border-color:color-mix(in srgb,var(--ds-brand) 35%,transparent);color:#eef2ff}
|
|
1077
|
+
.YDXeBa_iconButton{transition:background .15s ease,color .15s ease,transform .15s ease,opacity .15s ease}
|
|
1078
|
+
.YDXeBa_iconButton:hover{transform:scale(1.1)}
|
|
1079
|
+
` },
|
|
1080
|
+
// Chat view: header (incl. locale hover swap), tabs, crumbs, title.
|
|
1081
|
+
// Hover pill upgraded to a liquid-glass capsule.
|
|
1082
|
+
// 0.1.5-rc.1 draws the header divider as a real `border-bottom` on
|
|
1083
|
+
// `.wSkVaW_header` (1.37 used an `::after` pseudo-element), so the
|
|
1084
|
+
// floating capsule grew a stray hairline under it. Neutralise the
|
|
1085
|
+
// border here and keep the legacy `::after` hide for older builds.
|
|
1086
|
+
// The header divider would otherwise be a 0.5px `--dsw-alias-border-l3`
|
|
1087
|
+
// line spanning exactly this absolutely positioned bar (24px inset).
|
|
1088
|
+
// `titleRow`/`titleCluster` are flattened with `display:contents`, so
|
|
1089
|
+
// every direct header child needs an explicit `order`; 0.1.5-rc.1 added
|
|
1090
|
+
// `headerCorner` (the right-sidebar expand button) and, left at the
|
|
1091
|
+
// default `order:0`, it tied with the tabs and landed at the far left,
|
|
1092
|
+
// where the first tab pill overlapped it. It now trails the utilities
|
|
1093
|
+
// block, i.e. sits right of the 中/EN locale toggle.
|
|
1094
|
+
{ anchor: ".wSkVaW_root", css: `
|
|
1095
|
+
.wSkVaW_root{position:relative;background:transparent!important}
|
|
1096
|
+
.wSkVaW_scrollBody{padding-top:56px}
|
|
1097
|
+
.wSkVaW_viewArea{padding:0 18px}
|
|
1098
|
+
.wSkVaW_viewArea{animation:dshome-view-enter .45s ease}
|
|
1099
|
+
/* Header z-index: the product's column-resize grips (.wSkVaW_widthHandle) are
|
|
1100
|
+
40px-wide full-height strips at z-index 8, while this bar sat at 5 — so the
|
|
1101
|
+
cursor anywhere inside the title bar's own box turned into col-resize and the
|
|
1102
|
+
bar's controls were unreachable. 9 puts the bar above the grips; the grips
|
|
1103
|
+
stay grabbable everywhere the bar is not (above y=8, below y=46). */
|
|
1104
|
+
.wSkVaW_header{position:absolute;top:8px;left:24px;right:24px;z-index:9;min-height:36px;padding:4px 12px;display:flex;flex-direction:row;align-items:center;gap:8px;justify-content:flex-start;border:0!important;border-radius:0;background:transparent!important;transition:border-radius .3s ease}
|
|
1105
|
+
.wSkVaW_header::before{content:"";position:absolute;inset:0;z-index:-1;background:transparent;border:1px solid transparent;border-radius:inherit;-webkit-backdrop-filter:none;backdrop-filter:none;transition:background .3s ease,border-color .3s ease,box-shadow .3s ease}
|
|
1106
|
+
.wSkVaW_header:hover{border-radius:999px}
|
|
1107
|
+
.wSkVaW_titleRow{display:contents}
|
|
1108
|
+
.wSkVaW_titleCluster{display:contents}
|
|
1109
|
+
.wSkVaW_crumbs{order:2;flex:none;min-width:0}
|
|
1110
|
+
.wSkVaW_tabs{order:0;flex:none;align-items:center;display:none;gap:8px;margin:0;padding:0}
|
|
1111
|
+
.wSkVaW_headerActions{order:1;flex:none;align-items:center;display:flex;gap:8px}
|
|
1112
|
+
.wSkVaW_headerUtilities{order:3;flex:none;align-items:center;display:flex;gap:8px;margin-left:auto}
|
|
1113
|
+
.wSkVaW_headerCorner{order:4;flex:none;align-items:center;display:flex;margin-left:0;margin-right:0}
|
|
1114
|
+
.wSkVaW_header.dshome-swap .wSkVaW_tabs{display:flex;animation:dshome-tab-in .22s ease}
|
|
1115
|
+
.wSkVaW_root:has(.fV0t5q_root) .wSkVaW_tabs{display:flex!important}
|
|
1116
|
+
.wSkVaW_header:hover::before{background:rgba(255,255,255,.45);border-color:rgba(0,0,0,.06);-webkit-backdrop-filter:blur(20px);backdrop-filter:blur(20px);box-shadow:0 4px 16px rgba(0,0,0,.05)}
|
|
1117
|
+
body[data-dshome-dark] .wSkVaW_header:hover::before{background:rgba(13,30,60,.45);border-color:rgba(255,255,255,.1);box-shadow:0 4px 20px rgba(0,0,0,.3)}
|
|
1118
|
+
.wSkVaW_header:after{display:none!important}
|
|
1119
|
+
.SVAs4q_label{background:color-mix(in srgb,var(--ds-brand) 10%,transparent)!important;border:1px solid color-mix(in srgb,var(--ds-brand) 22%,transparent);border-radius:999px!important;padding:0 10px!important;height:28px;line-height:26px;color:var(--ds-brand)!important}
|
|
1120
|
+
body[data-dshome-dark] .SVAs4q_label{color:var(--ds-brand-light)!important}
|
|
1121
|
+
.wSkVaW_tab{border:1px solid color-mix(in srgb,var(--ds-brand) 25%,transparent);background:color-mix(in srgb,var(--ds-brand) 8%,transparent);border-radius:999px;corner-shape:round;padding:5px 16px;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:16px;font-weight:500;transition:color .22s ease,background .22s ease,border-color .22s ease,box-shadow .24s ease,transform .24s cubic-bezier(.2,.8,.2,1)}
|
|
1122
|
+
.wSkVaW_tab:hover{color:var(--dsw-alias-label-primary);background:color-mix(in srgb,var(--ds-brand) 16%,transparent);border-color:color-mix(in srgb,var(--ds-brand) 42%,transparent);box-shadow:0 4px 12px color-mix(in srgb,var(--ds-brand) 22%,transparent);transform:translateY(-1px)}
|
|
1123
|
+
.wSkVaW_tab:active{transform:translateY(0) scale(.96)}
|
|
1124
|
+
.wSkVaW_tab::after{display:none!important}
|
|
1125
|
+
.wSkVaW_tabActive{background:linear-gradient(135deg,var(--ds-brand),var(--ds-brand-mid));color:#fff;border-color:transparent;box-shadow:0 3px 10px color-mix(in srgb,var(--ds-brand) 26%,transparent);animation:dshome-tab-pop .34s cubic-bezier(.2,.8,.2,1)}
|
|
1126
|
+
.wSkVaW_tabActive:hover{color:#fff}
|
|
1127
|
+
body[data-dshome-dark] .wSkVaW_tab{border-color:color-mix(in srgb,var(--ds-brand) 32%,transparent);background:color-mix(in srgb,var(--ds-brand) 14%,transparent)}
|
|
1128
|
+
body[data-dshome-dark] .wSkVaW_tab:hover{background:color-mix(in srgb,var(--ds-brand) 24%,transparent);box-shadow:0 4px 14px color-mix(in srgb,var(--ds-brand) 34%,transparent)}
|
|
1129
|
+
body[data-dshome-dark] .wSkVaW_tabActive{box-shadow:0 3px 12px color-mix(in srgb,var(--ds-brand) 40%,transparent)}
|
|
1130
|
+
@keyframes dshome-tab-pop{0%{transform:translateY(-1px) scale(.88)}60%{transform:translateY(-1px) scale(1.05)}to{transform:none}}
|
|
1131
|
+
.wSkVaW_root:has(.fV0t5q_root) .wSkVaW_header:hover{border-radius:0}
|
|
1132
|
+
.wSkVaW_root:has(.fV0t5q_root) .wSkVaW_header:hover::before{background:transparent;border-color:transparent;-webkit-backdrop-filter:none;backdrop-filter:none;box-shadow:none}
|
|
1133
|
+
.wSkVaW_composerSeat{background:transparent!important}
|
|
1134
|
+
.wSkVaW_header:hover .dshome-locale .dshome-short{max-width:0;opacity:0;transform:translateX(6px)}
|
|
1135
|
+
.wSkVaW_header:hover .dshome-locale .dshome-full{max-width:3em;opacity:1;transform:none}
|
|
1136
|
+
/* Header chips: the product's global corner-shape:superellipse(1.5) squares off
|
|
1137
|
+
even pill radii, so every control in the bar is restored to a true round end
|
|
1138
|
+
(and the session-title crumb, 12px on a 28px box, becomes a real pill). */
|
|
1139
|
+
.wSkVaW_header .SVAs4q_label,.wSkVaW_header .cubgiG_seat,.wSkVaW_header .CAgGvG_split,.wSkVaW_header .dshome-locale,.wSkVaW_header [data-sidebar-right-expand],.wSkVaW_header .nL4_yW_sessionLogButton,.wSkVaW_header .wSkVaW_crumb{corner-shape:round}
|
|
1140
|
+
.wSkVaW_header .wSkVaW_crumb{border-radius:999px}
|
|
1141
|
+
/* Open-in-app split button: hover lift with a brand tint, and the chevron turns
|
|
1142
|
+
over while its menu is open. */
|
|
1143
|
+
.wSkVaW_header .CAgGvG_split{transition:transform .28s cubic-bezier(.2,.8,.2,1),box-shadow .28s ease,border-color .22s ease,background .22s ease}
|
|
1144
|
+
.wSkVaW_header .CAgGvG_split:hover{transform:translateY(-1px);border-color:color-mix(in srgb,var(--ds-brand) 40%,transparent);background:color-mix(in srgb,var(--ds-brand) 8%,transparent);box-shadow:0 4px 14px color-mix(in srgb,var(--ds-brand) 22%,transparent)}
|
|
1145
|
+
.wSkVaW_header .CAgGvG_split:active{transform:translateY(0) scale(.97)}
|
|
1146
|
+
.wSkVaW_header .CAgGvG_main,.wSkVaW_header .CAgGvG_chevron{transition:background .22s ease,color .22s ease}
|
|
1147
|
+
.wSkVaW_header .CAgGvG_split:hover .CAgGvG_main,.wSkVaW_header .CAgGvG_split:hover .CAgGvG_chevron{background:color-mix(in srgb,var(--ds-brand) 12%,transparent);color:var(--ds-brand-deep)}
|
|
1148
|
+
.wSkVaW_header .CAgGvG_chevron svg{transition:transform .3s cubic-bezier(.2,.8,.2,1)}
|
|
1149
|
+
.wSkVaW_header .CAgGvG_chevron[aria-expanded=true] svg{transform:rotate(180deg)}
|
|
1150
|
+
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)}
|
|
1151
|
+
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}
|
|
1152
|
+
` },
|
|
1153
|
+
// Session-log button (hover expand).
|
|
1154
|
+
{ anchor: ".nL4_yW_sessionLogButton", css: `
|
|
1155
|
+
.nL4_yW_sessionLogButton{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}
|
|
1156
|
+
.nL4_yW_sessionLogButton span{max-width:0;opacity:0;overflow:hidden;transition:max-width .2s ease,opacity .2s ease}
|
|
1157
|
+
.wSkVaW_header:hover .nL4_yW_sessionLogButton{width:auto;padding:0 12px!important;border-radius:18px!important}
|
|
1158
|
+
.wSkVaW_header:hover .nL4_yW_sessionLogButton span{max-width:90px;opacity:1}
|
|
1159
|
+
body[data-dshome-dark] .nL4_yW_sessionLogButton{color:var(--ds-brand-light)!important}
|
|
1160
|
+
` },
|
|
1161
|
+
// Glass cards with spinning gradient border — upgraded to liquid glass:
|
|
1162
|
+
// specular top edge, saturated blur, brighter inner rim.
|
|
1163
|
+
{ anchor: ".uV2eYG_card", css: `
|
|
1164
|
+
.uV2eYG_card{position:relative;background:rgba(255,255,255,.42)!important;border:1.5px solid rgba(255,255,255,.55)!important;box-shadow:0 8px 32px rgba(13,30,60,.1),inset 0 1px 1px hsla(0,0%,100%,.8);border-radius:22px!important}
|
|
1165
|
+
body[data-dshome-dark] .uV2eYG_card{background:rgba(13,30,60,.55)!important;border:1.5px solid rgba(255,255,255,.16)!important;box-shadow:0 10px 40px rgba(0,0,0,.35),inset 0 1px 1px rgba(255,255,255,.06)}
|
|
1166
|
+
.uV2eYG_card::after{content:"";position:absolute;inset:0;z-index:-1;border-radius:22px;-webkit-backdrop-filter:blur(28px);backdrop-filter:blur(28px);pointer-events:none}
|
|
1167
|
+
.uV2eYG_card::before{content:"";position:absolute;inset:-1px;border-radius:inherit;padding:1px;background:conic-gradient(from var(--dsh-border-angle),color-mix(in srgb,var(--ds-brand) 0%,transparent),color-mix(in srgb,var(--ds-brand) 45%,transparent),color-mix(in srgb,var(--ds-brand-light) 20%,transparent),color-mix(in srgb,var(--ds-brand) 45%,transparent),color-mix(in srgb,var(--ds-brand) 0%,transparent));-webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);-webkit-mask-composite:xor;mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);mask-composite:exclude;pointer-events:none;animation:dsh-border-spin 6s linear infinite;z-index:0}
|
|
1168
|
+
body[data-dshome-dark] .uV2eYG_card::before{background:conic-gradient(from var(--dsh-border-angle),rgba(255,255,255,0),color-mix(in srgb,var(--ds-brand) 55%,transparent),color-mix(in srgb,var(--ds-brand-light) 25%,transparent),color-mix(in srgb,var(--ds-brand) 55%,transparent),rgba(255,255,255,0))}
|
|
1169
|
+
.uV2eYG_card>*{position:relative;z-index:1}
|
|
1170
|
+
.uV2eYG_primary{background:linear-gradient(135deg,var(--ds-brand) 0%,var(--ds-brand-mid) 100%)!important;border:none!important;color:#fff!important;box-shadow:0 4px 14px color-mix(in srgb,var(--ds-brand) 35%,transparent);transition:box-shadow .15s ease,transform .15s ease}
|
|
1171
|
+
.uV2eYG_primary:hover{box-shadow:0 6px 18px color-mix(in srgb,var(--ds-brand) 45%,transparent);transform:translateY(-1px)}
|
|
1172
|
+
` },
|
|
1173
|
+
// Empty-state headline + preview badge.
|
|
1174
|
+
{ anchor: ".pXSMma_headlineText", css: `
|
|
1175
|
+
.pXSMma_headlineText{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}
|
|
1176
|
+
body[data-dshome-dark] .pXSMma_headlineText{background:linear-gradient(120deg,#fff 0%,var(--ds-brand-light) 60%,var(--ds-brand) 100%);-webkit-background-clip:text;background-clip:text}
|
|
1177
|
+
.pXSMma_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}
|
|
1178
|
+
.pXSMma_fish{color:var(--ds-brand)!important}
|
|
1179
|
+
` },
|
|
1180
|
+
// Chat bubbles — liquid glass panes.
|
|
1181
|
+
{ anchor: ".gdEzaW_bubble", css: `
|
|
1182
|
+
.gdEzaW_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)}
|
|
1183
|
+
body[data-dshome-dark] .gdEzaW_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)}
|
|
1184
|
+
` },
|
|
1185
|
+
// Trajectory view bar.
|
|
1186
|
+
{ anchor: ".fV0t5q_root", css: `
|
|
1187
|
+
.fV0t5q_root{background:rgba(255,255,255,.45)!important;border-bottom:1px solid rgba(0,0,0,.06)!important}
|
|
1188
|
+
body[data-dshome-dark] .fV0t5q_root{background:rgba(13,30,60,.45)!important;border-bottom-color:rgba(255,255,255,.08)!important}
|
|
1189
|
+
` },
|
|
1190
|
+
// Timeline root/plot/span.
|
|
1191
|
+
{ anchor: "._1p9O6q_root", css: `
|
|
1192
|
+
._1p9O6q_root{background:rgba(255,255,255,.3)!important;border-bottom:1px solid rgba(0,0,0,.05)!important}
|
|
1193
|
+
._1p9O6q_plot{background:transparent!important}
|
|
1194
|
+
body[data-dshome-dark] ._1p9O6q_root{background:rgba(13,30,60,.3)!important}
|
|
1195
|
+
._1p9O6q_span[data-timeline-span=assistant]{background:var(--ds-brand)!important}
|
|
1196
|
+
._1p9O6q_span[data-timeline-span=user]{background:var(--ds-brand-light)!important}
|
|
1197
|
+
` },
|
|
1198
|
+
// Progress panel entrance.
|
|
1199
|
+
{ anchor: ".Nqubda_panel", css: `
|
|
1200
|
+
.Nqubda_panel{animation:dshome-pop-in .25s cubic-bezier(.2,.8,.2,1)}
|
|
1201
|
+
` },
|
|
1202
|
+
// Settings overlay / modal — near-opaque glass with specular edge.
|
|
1203
|
+
{ anchor: ".VOzbGW_panel", css: `
|
|
1204
|
+
.VOzbGW_mask{animation:dshome-fade-in .2s ease}
|
|
1205
|
+
.VOzbGW_panel{background:rgba(255,255,255,.97)!important;animation:dshome-modal-in .4s cubic-bezier(.2,.8,.2,1)!important}
|
|
1206
|
+
body[data-dshome-dark] .VOzbGW_panel{background:rgba(13,30,60,.96)!important}
|
|
1207
|
+
.VOzbGW_overlay{position:fixed!important;top:0!important;left:0!important;width:100vw!important;height:100vh!important;inset:0!important}
|
|
1208
|
+
` }
|
|
1209
|
+
];
|
|
1210
|
+
|
|
1211
|
+
// LIQUID GLASS — the "wet glass" recipe, imitated from
|
|
1212
|
+
// deepseek-harness-background (MIT, Copyright (c) 2026 HaoyueQin): a
|
|
1213
|
+
// translucent fill, a vertical sheen gradient that is brightest along the
|
|
1214
|
+
// top edge and gone by ~38%, one shared backdrop chain
|
|
1215
|
+
// (blur + saturate + brightness + contrast), an inset top highlight and a
|
|
1216
|
+
// hairline ring, with hover only brightening the fill. Every knob is a
|
|
1217
|
+
// --dshome-glass-* property so the theme stays one recipe.
|
|
1218
|
+
//
|
|
1219
|
+
// The specular highlight is composed as an EXTRA background layer rather
|
|
1220
|
+
// than a pseudo-element: `--dshome-spec-x/y` (centre-relative, -1..1) are
|
|
1221
|
+
// written by startSpecular() on the hovered surface and
|
|
1222
|
+
// `--dshome-glass-spec` fades it, so a surface that already spends its
|
|
1223
|
+
// ::after on backdrop blur still gets a moving highlight.
|
|
1224
|
+
//
|
|
1225
|
+
// Refraction is a deliberately narrower layer: `backdrop-filter: url(#…)`
|
|
1226
|
+
// with the feDisplacementMap filter from dsh-theme-mineradio bends the
|
|
1227
|
+
// backdrop at the edges of the few LARGE surfaces only. Chromium is the
|
|
1228
|
+
// only engine that supports an SVG filter inside backdrop-filter and it
|
|
1229
|
+
// costs a filter pass per element, so the rule is gated on the
|
|
1230
|
+
// `data-dshome-dispersion` attribute that startGlassDispersion() stamps on
|
|
1231
|
+
// <html> only where it really mounted — every other browser silently keeps
|
|
1232
|
+
// the plain chain.
|
|
1233
|
+
const GLASS_CSS = `
|
|
1234
|
+
:root{
|
|
1235
|
+
--dshome-glass-blur:16px;
|
|
1236
|
+
--dshome-glass-saturate:1.42;
|
|
1237
|
+
--dshome-glass-brightness:1;
|
|
1238
|
+
--dshome-glass-contrast:1.01;
|
|
1239
|
+
--dshome-glass-sheen:.14;
|
|
1240
|
+
--dshome-glass-sheen-mid:.05;
|
|
1241
|
+
--dshome-glass-edge:rgba(255,255,255,.62);
|
|
1242
|
+
--dshome-glass-edge-low:rgba(255,255,255,.14);
|
|
1243
|
+
--dshome-glass-ring:rgba(255,255,255,.34);
|
|
1244
|
+
--dshome-glass-shadow:0 12px 40px rgba(0,0,0,.12);
|
|
1245
|
+
--dshome-glass-spec:0;
|
|
1246
|
+
}
|
|
1247
|
+
body[data-dshome-dark]{
|
|
1248
|
+
--dshome-glass-sheen:.16;
|
|
1249
|
+
--dshome-glass-sheen-mid:.06;
|
|
1250
|
+
--dshome-glass-edge:rgba(255,255,255,.30);
|
|
1251
|
+
--dshome-glass-edge-low:rgba(255,255,255,.07);
|
|
1252
|
+
--dshome-glass-ring:rgba(255,255,255,.18);
|
|
1253
|
+
--dshome-glass-shadow:0 14px 44px rgba(0,0,0,.45);
|
|
1254
|
+
}
|
|
1255
|
+
html .hHd-Xa_root::before,
|
|
1256
|
+
html .uV2eYG_card::after,
|
|
1257
|
+
html .gdEzaW_bubble,
|
|
1258
|
+
html .fV0t5q_root,
|
|
1259
|
+
html [data-composer-card],
|
|
1260
|
+
html .VOzbGW_panel,
|
|
1261
|
+
html .dshome-sa,
|
|
1262
|
+
html .dshome-file-menu{
|
|
1263
|
+
background-image:
|
|
1264
|
+
radial-gradient(340px 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) * .30)), transparent 62%),
|
|
1265
|
+
linear-gradient(180deg, rgba(255,255,255,var(--dshome-glass-sheen)), rgba(255,255,255,var(--dshome-glass-sheen-mid)) 38%, rgba(255,255,255,.01))!important;
|
|
1266
|
+
-webkit-backdrop-filter:blur(var(--dshome-glass-blur)) saturate(var(--dshome-glass-saturate)) brightness(var(--dshome-glass-brightness)) contrast(var(--dshome-glass-contrast))!important;
|
|
1267
|
+
backdrop-filter:blur(var(--dshome-glass-blur)) saturate(var(--dshome-glass-saturate)) brightness(var(--dshome-glass-brightness)) contrast(var(--dshome-glass-contrast))!important;
|
|
1268
|
+
box-shadow:inset 0 1px 0 var(--dshome-glass-edge),inset 0 -1px 0 var(--dshome-glass-edge-low),inset 0 0 0 .5px var(--dshome-glass-ring),var(--dshome-glass-shadow);
|
|
1269
|
+
transition:background-color .18s ease,box-shadow .18s ease;
|
|
1270
|
+
}
|
|
1271
|
+
/* The specular layer is multiplied by --dshome-glass-spec, so it has to be
|
|
1272
|
+
switched on: while the cursor is over a spot surface the highlight follows
|
|
1273
|
+
--dshome-spec-x/y (written per element by startSpecularParallax). */
|
|
1274
|
+
html [data-dshome-spot]:hover{--dshome-glass-spec:1;}
|
|
1275
|
+
html [data-composer-card]:hover{--dshome-glass-spec:1;--dshome-glass-sheen:.12;}
|
|
1276
|
+
html .gdEzaW_bubble:hover,html .dshome-sa:hover,html .dshome-file-menu:hover{
|
|
1277
|
+
background-color:rgb(255 255 255 / calc(var(--dshome-glass-sheen) + .16))!important;
|
|
1278
|
+
}
|
|
1279
|
+
body[data-dshome-dark] .gdEzaW_bubble:hover,
|
|
1280
|
+
body[data-dshome-dark] .dshome-sa:hover,
|
|
1281
|
+
body[data-dshome-dark] .dshome-file-menu:hover{
|
|
1282
|
+
background-color:rgb(255 255 255 / calc(var(--dshome-glass-sheen) + .06))!important;
|
|
1283
|
+
}
|
|
1284
|
+
html[data-dshome-dispersion] .hHd-Xa_root::before,
|
|
1285
|
+
html[data-dshome-dispersion] .gdEzaW_bubble,
|
|
1286
|
+
html[data-dshome-dispersion] .VOzbGW_panel{
|
|
1287
|
+
-webkit-backdrop-filter:url(#dshome-glass-dispersion) blur(var(--dshome-glass-blur)) saturate(var(--dshome-glass-saturate))!important;
|
|
1288
|
+
backdrop-filter:url(#dshome-glass-dispersion) blur(var(--dshome-glass-blur)) saturate(var(--dshome-glass-saturate))!important;
|
|
1289
|
+
}
|
|
1290
|
+
@supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){
|
|
1291
|
+
html .hHd-Xa_root::before,html .uV2eYG_card::after,html .gdEzaW_bubble,html [data-composer-card],html .VOzbGW_panel,html .dshome-sa,html .dshome-file-menu{
|
|
1292
|
+
background-color:rgba(255,255,255,.86)!important;
|
|
1293
|
+
}
|
|
1294
|
+
body[data-dshome-dark] .hHd-Xa_root::before,
|
|
1295
|
+
body[data-dshome-dark] .uV2eYG_card::after,
|
|
1296
|
+
body[data-dshome-dark] .gdEzaW_bubble,
|
|
1297
|
+
body[data-dshome-dark] [data-composer-card],
|
|
1298
|
+
body[data-dshome-dark] .VOzbGW_panel,
|
|
1299
|
+
body[data-dshome-dark] .dshome-sa,
|
|
1300
|
+
body[data-dshome-dark] .dshome-file-menu{
|
|
1301
|
+
background-color:rgba(13,30,60,.92)!important;
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
`;
|
|
1305
|
+
|
|
1306
|
+
/** Append a style tag; returns a disposer that removes it. */
|
|
1307
|
+
function injectStyle(css, label) {
|
|
1308
|
+
const tag = document.createElement('style');
|
|
1309
|
+
tag.dataset.plugin = PLUGIN_ID;
|
|
1310
|
+
tag.dataset.pluginCss = PLUGIN_ID + '/' + label;
|
|
1311
|
+
tag.textContent = css;
|
|
1312
|
+
document.head.appendChild(tag);
|
|
1313
|
+
return () => tag.remove();
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
// LIQUID GLASS — the second recipe, selected by the durable `glassStyle`
|
|
1317
|
+
// setting and gated entirely on `html[data-dshome-glass="liquid"]`. The
|
|
1318
|
+
// frosted block above is deliberately left byte-identical: switching the
|
|
1319
|
+
// setting back has to reproduce the look the user approved, so every rule
|
|
1320
|
+
// in here is additive and nothing above is edited.
|
|
1321
|
+
//
|
|
1322
|
+
// What makes it "liquid" rather than "frosted":
|
|
1323
|
+
// * the fill drops from a .42 white wash to a barely-there .10, and the
|
|
1324
|
+
// top sheen from .14 to .07, so the pane reads as glass over the fluid
|
|
1325
|
+
// rather than as a white card;
|
|
1326
|
+
// * the backdrop chain leans on saturation (1.75) instead of brightness,
|
|
1327
|
+
// which is what makes the colours behind the pane bloom;
|
|
1328
|
+
// * the shared feDisplacementMap runs at a much larger scale (see
|
|
1329
|
+
// LIQUID_REF_SCALE below), so the edge refraction is actually visible;
|
|
1330
|
+
// * a conic-gradient highlight sweeps the perimeter on its own, driven by
|
|
1331
|
+
// a registered <angle> property rather than by the pointer.
|
|
1332
|
+
//
|
|
1333
|
+
// Surfaces: exactly the four the user named — the sidebar shell, the
|
|
1334
|
+
// composer, the title bar and the user's own messages. Refraction is one
|
|
1335
|
+
// SVG filter pass per element in Chromium, so nothing else is added.
|
|
1336
|
+
//
|
|
1337
|
+
// The sweep lives on ::after because the product has already claimed
|
|
1338
|
+
// ::before on the sidebar and the title bar (measured, not assumed); the
|
|
1339
|
+
// composer claims both, which is why its edge highlight stays the
|
|
1340
|
+
// brand-tinted conic border the patch block already animates.
|
|
1341
|
+
// A user-authored message is not one flow kind but two: the message that
|
|
1342
|
+
// opens a turn is `data-chat-flow-kind="user"`, and a note typed while the
|
|
1343
|
+
// agent is already working is `"steering"`. Measured on the current build,
|
|
1344
|
+
// a transcript can contain ONLY steering bubbles — anchoring the selector
|
|
1345
|
+
// 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]';
|
|
1347
|
+
const LIQUID_CSS = `
|
|
1348
|
+
@property --dshome-liquid-sweep{syntax:"<angle>";inherits:false;initial-value:0deg}
|
|
1349
|
+
@keyframes dshome-liquid-sweep{from{--dshome-liquid-sweep:0deg}to{--dshome-liquid-sweep:360deg}}
|
|
1350
|
+
html[data-dshome-glass="liquid"]{
|
|
1351
|
+
--dshome-liquid-blur:11px;
|
|
1352
|
+
--dshome-liquid-saturate:1.75;
|
|
1353
|
+
--dshome-liquid-brightness:1.05;
|
|
1354
|
+
--dshome-liquid-contrast:1.02;
|
|
1355
|
+
--dshome-liquid-fill:rgb(255 255 255 / .14);
|
|
1356
|
+
/* The sidebar is the largest pane on screen, so a fill that reads as "barely
|
|
1357
|
+
there" on the composer leaves it washed out against the fluid; it carries a
|
|
1358
|
+
little more body of its own, and more of it again in light mode where the
|
|
1359
|
+
pane has to read as white glass rather than as tinted fluid. */
|
|
1360
|
+
--dshome-liquid-fill-sidebar:rgb(255 255 255 / .30);
|
|
1361
|
+
/* The sidebar is the boundary with the conversation pane, and it blurs harder
|
|
1362
|
+
than everything else on purpose: at the shared 11px the flow behind it stays
|
|
1363
|
+
legible and the pane never separates from the transcript. */
|
|
1364
|
+
--dshome-liquid-blur-sidebar:24px;
|
|
1365
|
+
/* One extra veil on top of the sidebar's glass. The pane sits over the fluid
|
|
1366
|
+
at roughly the same tone as the conversation it borders, so without this
|
|
1367
|
+
lift the two read as one surface. It is white in light mode. */
|
|
1368
|
+
--dshome-liquid-veil-sidebar:rgb(255 255 255 / .18);
|
|
1369
|
+
--dshome-liquid-sheen:.07;
|
|
1370
|
+
--dshome-liquid-edge:rgba(255,255,255,.60);
|
|
1371
|
+
--dshome-liquid-edge-low:rgba(255,255,255,.10);
|
|
1372
|
+
--dshome-liquid-ring:rgba(255,255,255,.26);
|
|
1373
|
+
--dshome-liquid-sweep-hi:rgba(255,255,255,.85);
|
|
1374
|
+
--dshome-liquid-sweep-lo:rgba(255,255,255,.28);
|
|
1375
|
+
--dshome-liquid-shadow:0 18px 54px rgba(9,20,44,.16);
|
|
1376
|
+
}
|
|
1377
|
+
body[data-dshome-dark]{
|
|
1378
|
+
--dshome-liquid-fill:rgb(255 255 255 / .07);
|
|
1379
|
+
/* Night mode goes the other way: the pane is a dark slab over a dark flow, so
|
|
1380
|
+
it is tinted towards the shell colour instead of towards white — dark
|
|
1381
|
+
enough to read as black glass, light enough to keep the flow visible. */
|
|
1382
|
+
--dshome-liquid-fill-sidebar:rgb(6 14 32 / .32);
|
|
1383
|
+
/* Night mode's veil is BLACK, the same idea inverted: a dark pane over a dark
|
|
1384
|
+
flow is lifted off it by deepening the pane, not by washing it grey. */
|
|
1385
|
+
--dshome-liquid-veil-sidebar:rgb(0 0 0 / .22);
|
|
1386
|
+
--dshome-liquid-sheen:.05;
|
|
1387
|
+
--dshome-liquid-edge:rgba(255,255,255,.34);
|
|
1388
|
+
--dshome-liquid-edge-low:rgba(255,255,255,.05);
|
|
1389
|
+
--dshome-liquid-ring:rgba(255,255,255,.14);
|
|
1390
|
+
--dshome-liquid-sweep-hi:rgba(255,255,255,.55);
|
|
1391
|
+
--dshome-liquid-sweep-lo:rgba(255,255,255,.16);
|
|
1392
|
+
--dshome-liquid-shadow:0 20px 60px rgba(0,0,0,.5);
|
|
1393
|
+
}
|
|
1394
|
+
/* The title bar is deliberately NOT always-on: the user's note on it is
|
|
1395
|
+
"rounded corners, and only once the mouse is over it" — the header becomes a
|
|
1396
|
+
pill on :hover (the patch block already sets border-radius:999px, and
|
|
1397
|
+
::before inherits it), so the glass is scoped to that same hover state
|
|
1398
|
+
instead of turning the strip into a permanent bar. */
|
|
1399
|
+
/* The selectors carry an extra body step on purpose. The patch blocks are
|
|
1400
|
+
injected lazily when their anchor appears — later than this sheet — and their
|
|
1401
|
+
dark variants (body[data-dshome-dark] .uV2eYG_card) tie with a
|
|
1402
|
+
html[attr] .surface selector on specificity, at which point source order
|
|
1403
|
+
hands the pane back to the old fill. The extra type selector breaks the tie. */
|
|
1404
|
+
html[data-dshome-glass="liquid"] body .hHd-Xa_root::before,
|
|
1405
|
+
html[data-dshome-glass="liquid"] body [data-composer-card],
|
|
1406
|
+
html[data-dshome-glass="liquid"] body .wSkVaW_header:hover::before,
|
|
1407
|
+
html[data-dshome-glass="liquid"] body ${LIQUID_BUBBLE}{
|
|
1408
|
+
background-color:var(--dshome-liquid-fill)!important;
|
|
1409
|
+
background-image:
|
|
1410
|
+
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%),
|
|
1411
|
+
linear-gradient(180deg, rgba(255,255,255,var(--dshome-liquid-sheen)), rgba(255,255,255,.015) 34%, rgba(255,255,255,0))!important;
|
|
1412
|
+
-webkit-backdrop-filter:blur(var(--dshome-liquid-blur)) saturate(var(--dshome-liquid-saturate)) brightness(var(--dshome-liquid-brightness)) contrast(var(--dshome-liquid-contrast))!important;
|
|
1413
|
+
backdrop-filter:blur(var(--dshome-liquid-blur)) saturate(var(--dshome-liquid-saturate)) brightness(var(--dshome-liquid-brightness)) contrast(var(--dshome-liquid-contrast))!important;
|
|
1414
|
+
box-shadow:inset 0 1px 0 var(--dshome-liquid-edge),inset 0 -1px 0 var(--dshome-liquid-edge-low),inset 0 0 0 .5px var(--dshome-liquid-ring),var(--dshome-liquid-shadow)!important;
|
|
1415
|
+
}
|
|
1416
|
+
/* The composer is the one surface the old recipe paints TWICE: the patch block
|
|
1417
|
+
blurs .uV2eYG_card::after (and the frosted list paints it too), and the card
|
|
1418
|
+
itself is in both lists, so the backdrop was blurred twice over — which is what
|
|
1419
|
+
turned the pane into a smear. Under liquid the card element IS the glass, so
|
|
1420
|
+
the redundant layer is switched off rather than re-tuned. */
|
|
1421
|
+
html[data-dshome-glass="liquid"] [data-composer-card]::after{
|
|
1422
|
+
-webkit-backdrop-filter:none!important;
|
|
1423
|
+
backdrop-filter:none!important;
|
|
1424
|
+
background-image:none!important;
|
|
1425
|
+
box-shadow:none!important;
|
|
1426
|
+
}
|
|
1427
|
+
/* The sidebar pane carries its own fill and its own shape: rounded only on the
|
|
1428
|
+
edge that faces the conversation, so it reads as a panel clipped to the
|
|
1429
|
+
window rather than as a full-bleed column. The hairline ring in the shared
|
|
1430
|
+
box-shadow above follows the radius. */
|
|
1431
|
+
html[data-dshome-glass="liquid"] body .hHd-Xa_root::before{
|
|
1432
|
+
background-color:var(--dshome-liquid-fill-sidebar)!important;
|
|
1433
|
+
border-radius:0 22px 22px 0;
|
|
1434
|
+
/* The veil rides on top of the two layers the shared rule sets, so the
|
|
1435
|
+
sidebar keeps its cursor-follow highlight and its sheen underneath. */
|
|
1436
|
+
background-image:
|
|
1437
|
+
linear-gradient(var(--dshome-liquid-veil-sidebar),var(--dshome-liquid-veil-sidebar)),
|
|
1438
|
+
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%),
|
|
1439
|
+
linear-gradient(180deg, rgba(255,255,255,var(--dshome-liquid-sheen)), rgba(255,255,255,.015) 34%, rgba(255,255,255,0))!important;
|
|
1440
|
+
}
|
|
1441
|
+
/* The message bubble is the one surface that is not already positioned. */
|
|
1442
|
+
html[data-dshome-glass="liquid"] ${LIQUID_BUBBLE}{
|
|
1443
|
+
position:relative;
|
|
1444
|
+
transition:background-color .18s ease,box-shadow .18s ease;
|
|
1445
|
+
}
|
|
1446
|
+
/* The composer leans under the cursor (see startComposerTilt). The transition
|
|
1447
|
+
is what turns the release into a settle rather than a snap: the ported
|
|
1448
|
+
routine writes the neutral transform back and only drops the inline
|
|
1449
|
+
properties 240ms later. */
|
|
1450
|
+
html[data-dshome-glass="liquid"] [data-composer-card]{
|
|
1451
|
+
transition:transform .24s cubic-bezier(.2,.8,.2,1),background-color .18s ease,box-shadow .18s ease!important;
|
|
1452
|
+
will-change:transform;
|
|
1453
|
+
}
|
|
1454
|
+
html[data-dshome-glass="liquid"] ${LIQUID_BUBBLE}:hover{
|
|
1455
|
+
background-color:rgb(255 255 255 / .18)!important;
|
|
1456
|
+
}
|
|
1457
|
+
body[data-dshome-dark] ${LIQUID_BUBBLE}:hover{
|
|
1458
|
+
background-color:rgb(255 255 255 / .10)!important;
|
|
1459
|
+
}
|
|
1460
|
+
html[data-dshome-glass="liquid"] body .hHd-Xa_root::after{
|
|
1461
|
+
border-radius:0 22px 22px 0;
|
|
1462
|
+
}
|
|
1463
|
+
/* The travelling edge highlight. mask-composite:exclude turns the filled
|
|
1464
|
+
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{
|
|
1468
|
+
content:"";position:absolute;inset:0;z-index:2;pointer-events:none;border-radius:inherit;
|
|
1469
|
+
padding:1px;
|
|
1470
|
+
background:conic-gradient(from var(--dshome-liquid-sweep),
|
|
1471
|
+
var(--dshome-liquid-sweep-lo) 0deg,var(--dshome-liquid-sweep-hi) 22deg,var(--dshome-liquid-sweep-lo) 64deg,
|
|
1472
|
+
rgba(255,255,255,.05) 140deg,var(--dshome-liquid-sweep-hi) 190deg,rgba(255,255,255,.05) 248deg,
|
|
1473
|
+
var(--dshome-liquid-sweep-lo) 360deg);
|
|
1474
|
+
-webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);
|
|
1475
|
+
-webkit-mask-composite:xor;
|
|
1476
|
+
mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);
|
|
1477
|
+
mask-composite:exclude;
|
|
1478
|
+
animation:dshome-liquid-sweep 6.5s linear infinite;
|
|
1479
|
+
}
|
|
1480
|
+
html[data-dshome-dispersion][data-dshome-glass="liquid"] body [data-composer-card],
|
|
1481
|
+
html[data-dshome-dispersion][data-dshome-glass="liquid"] body .wSkVaW_header:hover::before,
|
|
1482
|
+
html[data-dshome-dispersion][data-dshome-glass="liquid"] body ${LIQUID_BUBBLE}{
|
|
1483
|
+
-webkit-backdrop-filter:url(#dshome-glass-dispersion) blur(var(--dshome-liquid-blur)) saturate(var(--dshome-liquid-saturate))!important;
|
|
1484
|
+
backdrop-filter:url(#dshome-glass-dispersion) blur(var(--dshome-liquid-blur)) saturate(var(--dshome-liquid-saturate))!important;
|
|
1485
|
+
}
|
|
1486
|
+
/* The workspace list is faded out at its bottom by a gradient whose end stop is
|
|
1487
|
+
the sidebar-fill token — an OPAQUE colour matched to the product's own sidebar
|
|
1488
|
+
background. Over a translucent glass pane that stop no longer matches what is
|
|
1489
|
+
actually behind it, so the fade shows up as a lighter band above 设置 (the
|
|
1490
|
+
user's red box). The pane itself already separates the list from the
|
|
1491
|
+
conversation, so the fade is dropped. Scoped to the sidebar and to the
|
|
1492
|
+
product's own <hash>_fade naming so nothing else is touched. */
|
|
1493
|
+
html[data-dshome-glass="liquid"] body .hHd-Xa_root [class*="_fade" i]{
|
|
1494
|
+
background-image:none!important;
|
|
1495
|
+
}
|
|
1496
|
+
/* The sidebar's own, heavier blur. Written after the two rules above because it
|
|
1497
|
+
ties with the dispersion one on specificity and so has to come later. */
|
|
1498
|
+
html[data-dshome-glass="liquid"] body .hHd-Xa_root::before{
|
|
1499
|
+
-webkit-backdrop-filter:blur(var(--dshome-liquid-blur-sidebar)) saturate(var(--dshome-liquid-saturate)) brightness(var(--dshome-liquid-brightness)) contrast(var(--dshome-liquid-contrast))!important;
|
|
1500
|
+
backdrop-filter:blur(var(--dshome-liquid-blur-sidebar)) saturate(var(--dshome-liquid-saturate)) brightness(var(--dshome-liquid-brightness)) contrast(var(--dshome-liquid-contrast))!important;
|
|
1501
|
+
}
|
|
1502
|
+
html[data-dshome-dispersion][data-dshome-glass="liquid"] body .hHd-Xa_root::before{
|
|
1503
|
+
-webkit-backdrop-filter:url(#dshome-glass-dispersion) blur(var(--dshome-liquid-blur-sidebar)) saturate(var(--dshome-liquid-saturate))!important;
|
|
1504
|
+
backdrop-filter:url(#dshome-glass-dispersion) blur(var(--dshome-liquid-blur-sidebar)) saturate(var(--dshome-liquid-saturate))!important;
|
|
1505
|
+
}
|
|
1506
|
+
@media (prefers-reduced-motion:reduce){
|
|
1507
|
+
html[data-dshome-glass="liquid"] .hHd-Xa_root::after,
|
|
1508
|
+
html[data-dshome-glass="liquid"] ${LIQUID_BUBBLE}::after{
|
|
1509
|
+
animation:none;
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
@supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){
|
|
1513
|
+
html[data-dshome-glass="liquid"] body .hHd-Xa_root::before,
|
|
1514
|
+
html[data-dshome-glass="liquid"] body [data-composer-card],
|
|
1515
|
+
html[data-dshome-glass="liquid"] body .wSkVaW_header:hover::before,
|
|
1516
|
+
html[data-dshome-glass="liquid"] body ${LIQUID_BUBBLE}{
|
|
1517
|
+
background-color:rgba(255,255,255,.86)!important;
|
|
1518
|
+
}
|
|
1519
|
+
body[data-dshome-dark] .hHd-Xa_root::before,
|
|
1520
|
+
body[data-dshome-dark] [data-composer-card],
|
|
1521
|
+
body[data-dshome-dark] .wSkVaW_header:hover::before,
|
|
1522
|
+
body[data-dshome-dark] ${LIQUID_BUBBLE}{
|
|
1523
|
+
background-color:rgba(13,30,60,.92)!important;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
`;
|
|
1527
|
+
|
|
1528
|
+
/**
|
|
1529
|
+
* Inject `css` only while `anchor` matches the DOM. If the anchor is
|
|
1530
|
+
* absent at call time, wait up to 30s for it (covers shells that render
|
|
1531
|
+
* after the plugin activates); if it never appears, skip the block and log
|
|
1532
|
+
* one diagnostic line instead of shipping dead CSS against a renamed
|
|
1533
|
+
* product surface. Returns a disposer for the whole install.
|
|
1534
|
+
*/
|
|
1535
|
+
function installGuarded(anchor, css) {
|
|
1536
|
+
let dispose = null;
|
|
1537
|
+
const applyNow = () => {
|
|
1538
|
+
if (dispose !== null) return true;
|
|
1539
|
+
if (document.querySelector(anchor) === null) return false;
|
|
1540
|
+
dispose = injectStyle(css, 'patch' + anchor);
|
|
1541
|
+
return true;
|
|
1542
|
+
};
|
|
1543
|
+
if (applyNow()) {
|
|
1544
|
+
return () => { if (dispose !== null) dispose(); };
|
|
1545
|
+
}
|
|
1546
|
+
let timer = 0;
|
|
1547
|
+
let mo = new MutationObserver(() => {
|
|
1548
|
+
if (!applyNow()) return;
|
|
1549
|
+
mo.disconnect();
|
|
1550
|
+
clearTimeout(timer);
|
|
1551
|
+
});
|
|
1552
|
+
try {
|
|
1553
|
+
mo.observe(document.body, { childList: true, subtree: true });
|
|
1554
|
+
} catch (error) {
|
|
1555
|
+
mo = null;
|
|
1556
|
+
}
|
|
1557
|
+
timer = setTimeout(() => {
|
|
1558
|
+
if (mo !== null) mo.disconnect();
|
|
1559
|
+
if (dispose === null) {
|
|
1560
|
+
console.info('[' + PLUGIN_ID + '] patch skipped: ' + anchor + ' not found in DOM (product surface renamed?)');
|
|
1561
|
+
}
|
|
1562
|
+
}, 30000);
|
|
1563
|
+
return () => {
|
|
1564
|
+
if (mo !== null) mo.disconnect();
|
|
1565
|
+
clearTimeout(timer);
|
|
1566
|
+
if (dispose !== null) dispose();
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
/**
|
|
1571
|
+
* Own the light/dark marker: reflect the theme service's active color
|
|
1572
|
+
* scheme onto our own `data-dshome-dark` attribute, so dark styling never
|
|
1573
|
+
* depends on the product's attribute name. Falls back to mirroring the
|
|
1574
|
+
* product attribute only when the theme service or the `theme/change`
|
|
1575
|
+
* subscription is unavailable.
|
|
1576
|
+
*/
|
|
1577
|
+
function createDarkSync(ctx, theme) {
|
|
1578
|
+
const apply = () => {
|
|
1579
|
+
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') {
|
|
1586
|
+
try {
|
|
1587
|
+
const snap = theme.getTheme();
|
|
1588
|
+
dark = snap !== undefined && snap !== null &&
|
|
1589
|
+
snap.active !== undefined && snap.active !== null &&
|
|
1590
|
+
snap.active.colorScheme === 'dark';
|
|
1591
|
+
} catch (error) {
|
|
1592
|
+
dark = false;
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
if (dark) document.body.setAttribute('data-dshome-dark', '');
|
|
1596
|
+
else document.body.removeAttribute('data-dshome-dark');
|
|
1597
|
+
};
|
|
1598
|
+
apply();
|
|
1599
|
+
let unsubscribe = null;
|
|
1600
|
+
try {
|
|
1601
|
+
unsubscribe = ctx.on('theme/change', apply);
|
|
1602
|
+
} catch (error) {
|
|
1603
|
+
unsubscribe = null;
|
|
1604
|
+
}
|
|
1605
|
+
// Always mirror the product's attribute, not only when the theme service
|
|
1606
|
+
// is missing: this is the self-healing half. A missed `theme/change`
|
|
1607
|
+
// used to leave the marker stale until the next reload.
|
|
1608
|
+
let mirror = null;
|
|
1609
|
+
try {
|
|
1610
|
+
mirror = new MutationObserver(apply);
|
|
1611
|
+
mirror.observe(document.body, { attributes: true, attributeFilter: ['data-ds-dark-theme'] });
|
|
1612
|
+
} catch (error) {
|
|
1613
|
+
mirror = null;
|
|
1614
|
+
}
|
|
1615
|
+
return () => {
|
|
1616
|
+
if (unsubscribe !== null) unsubscribe();
|
|
1617
|
+
if (mirror !== null) mirror.disconnect();
|
|
1618
|
+
};
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
/**
|
|
1622
|
+
* Own the glass-recipe marker: mirror the durable `glassStyle` choice onto
|
|
1623
|
+
* <html data-dshome-glass>, which is the single switch the stylesheet gates
|
|
1624
|
+
* the liquid recipe on. The attribute is set before the first paint with the
|
|
1625
|
+
* default (`liquid`), so the only flicker is the one a user who picked
|
|
1626
|
+
* `frosted` sees during the initial loopback round trip.
|
|
1627
|
+
*/
|
|
1628
|
+
function createGlassSync(ctx) {
|
|
1629
|
+
const root = document.documentElement;
|
|
1630
|
+
const apply = () => {
|
|
1631
|
+
root.setAttribute('data-dshome-glass', dsttGetGlass());
|
|
1632
|
+
};
|
|
1633
|
+
apply();
|
|
1634
|
+
const unsubscribe = dsttSubscribe(apply);
|
|
1635
|
+
return () => {
|
|
1636
|
+
unsubscribe();
|
|
1637
|
+
root.removeAttribute('data-dshome-glass');
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
/**
|
|
1642
|
+
* Fluid palette per DSTT colour. The site ships `#8AA3D6` plus two pure
|
|
1643
|
+
* whites, which reads as one faint blue meander — legible on deepseek.com
|
|
1644
|
+
* but far too quiet to carry a colour token, so each entry here uses a
|
|
1645
|
+
* stronger lead and a genuinely different third tone: the display shader
|
|
1646
|
+
* blends all three across the noise field, so three near-identical colours
|
|
1647
|
+
* flatten the flow into a single wash.
|
|
1648
|
+
*/
|
|
1649
|
+
const FLUID_COLORS = {
|
|
1650
|
+
green: { light: ['#4FBE92', '#FFFFFF', '#D6F2E4'], dark: ['#1F6B4E', '#061423', '#0E3A2A'] },
|
|
1651
|
+
blue: { light: ['#5E82DE', '#FFFFFF', '#D8E2FA'], dark: ['#2C4A9E', '#050F26', '#122A5C'] },
|
|
1652
|
+
red: { light: ['#D4797F', '#FFFFFF', '#FAD6D8'], dark: ['#8E2F36', '#1A0A0D', '#40141A'] }
|
|
1653
|
+
};
|
|
1654
|
+
/** Edge-tint hue for the glass dispersion, following the DSTT colour. */
|
|
1655
|
+
const FLUID_HUES = { green: 152, blue: 221, red: 356 };
|
|
1656
|
+
|
|
1657
|
+
/** The active DSTT colour token. */
|
|
1658
|
+
function dshomeColor() {
|
|
1659
|
+
const value = document.body.getAttribute('data-dshome-color');
|
|
1660
|
+
return value === 'red' || value === 'blue' ? value : 'green';
|
|
1661
|
+
}
|
|
1662
|
+
function dshomeDark() {
|
|
1663
|
+
return document.body.hasAttribute('data-dshome-dark');
|
|
1664
|
+
}
|
|
1665
|
+
/** The whole fluid parameter set for the current DSTT colour and scheme. */
|
|
1666
|
+
function dshomeFluidParams() {
|
|
1667
|
+
const palette = FLUID_COLORS[dshomeColor()];
|
|
1668
|
+
const pick = dshomeDark() ? palette.dark : palette.light;
|
|
1669
|
+
// `mouseFeed` is ours, not upstream's: false keeps the cursor from
|
|
1670
|
+
// writing a wake into the flow field (see the DSTT `fluidBrush` setting).
|
|
1671
|
+
return { ...SITE_FLUID_PARAMS, color1: pick[0], color2: pick[1], color3: pick[2], mouseFeed: dsttBrush() };
|
|
1672
|
+
}
|
|
1673
|
+
/** The durable `fluidBrush` preference; off unless the host reports it on. */
|
|
1674
|
+
function dsttBrush() {
|
|
1675
|
+
try {
|
|
1676
|
+
return dsttState.fluidBrush === true;
|
|
1677
|
+
} catch (error) {
|
|
1678
|
+
return false;
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
/** WebGL2 is probed on a throwaway canvas so the real one keeps its context. */
|
|
1682
|
+
function supportsWebgl2() {
|
|
1683
|
+
try {
|
|
1684
|
+
const probe = document.createElement('canvas');
|
|
1685
|
+
const gl = probe.getContext('webgl2');
|
|
1686
|
+
if (gl === null) return false;
|
|
1687
|
+
const lose = gl.getExtension('WEBGL_lose_context');
|
|
1688
|
+
if (lose !== null) lose.loseContext();
|
|
1689
|
+
return true;
|
|
1690
|
+
} catch (error) {
|
|
1691
|
+
return false;
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
/**
|
|
1695
|
+
* Mark glass surfaces as specular spots. The ported startSpecularParallax
|
|
1696
|
+
* only tracks elements carrying the spot attribute, and stamping it on
|
|
1697
|
+
* pointerover avoids writing attributes onto product elements that React
|
|
1698
|
+
* re-renders. Registered before the parallax listener so the stamp lands on
|
|
1699
|
+
* the same event the parallax handler then reads.
|
|
1700
|
+
*/
|
|
1701
|
+
/**
|
|
1702
|
+
* Specular spot surfaces: the composer card, the sidebar shell and the
|
|
1703
|
+
* user's own messages. The transcript pane and the centre column are
|
|
1704
|
+
* deliberately NOT spots — a cursor-following highlight across that much
|
|
1705
|
+
* empty area reads as a large white patch sliding over the background.
|
|
1706
|
+
*
|
|
1707
|
+
* The message bubble is matched through the product's own
|
|
1708
|
+
* `data-chat-flow-kind="user"` marker rather than a class name: the hashed
|
|
1709
|
+
* `.gdEzaW_bubble` this list used to carry matches nothing on the current
|
|
1710
|
+
* build, which is how the old entry went dead without anyone noticing.
|
|
1711
|
+
*/
|
|
1712
|
+
const SPOT_SURFACES = `${LIQUID_BUBBLE},.uV2eYG_card,.dshome-sa,.dshome-file-menu,.hHd-Xa_root,[data-composer-card]`;
|
|
1713
|
+
function startSpecularSpotter() {
|
|
1714
|
+
const onOver = (event) => {
|
|
1715
|
+
const target = event.target;
|
|
1716
|
+
if (target === null || typeof target.closest !== 'function') return;
|
|
1717
|
+
const surface = target.closest(SPOT_SURFACES);
|
|
1718
|
+
if (surface === null) return;
|
|
1719
|
+
if (surface.getAttribute(SPOT_ATTR) === null) surface.setAttribute(SPOT_ATTR, '');
|
|
1720
|
+
};
|
|
1721
|
+
document.addEventListener('pointerover', onOver, true);
|
|
1722
|
+
return () => document.removeEventListener('pointerover', onOver, true);
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
// ── composer hover tilt ───────────────────────────────────────────────
|
|
1726
|
+
// Ported from dsh-theme-mineradio's `startSpotlight` tilt branch (MIT,
|
|
1727
|
+
// Copyright (c) 2026 John Wu), including its constants: the pane lifts by
|
|
1728
|
+
// scale(1.01) and leans up to TILT_MAX radians towards whichever side the
|
|
1729
|
+
// cursor is on, about its own centre, under an 800px perspective. The
|
|
1730
|
+
// cursor-relative offsets are the same half-range clamp, and the release
|
|
1731
|
+
// writes the neutral transform first and only drops the inline properties
|
|
1732
|
+
// SETTLE_MS later, so the stylesheet's transition animates the return
|
|
1733
|
+
// instead of snapping.
|
|
1734
|
+
//
|
|
1735
|
+
// Only the composer is tiltable here. mineradio tilts every spot it owns;
|
|
1736
|
+
// in this theme the other spots are the sidebar shell and the transcript's
|
|
1737
|
+
// message bubbles, where a whole-column lean reads as a bug rather than as
|
|
1738
|
+
// depth. It is also gated on the liquid recipe, because the frosted pane is
|
|
1739
|
+
// meant to stay observably identical to what shipped before.
|
|
1740
|
+
const TILT_SELECTOR = '[data-composer-card],.uV2eYG_card';
|
|
1741
|
+
const TILT_MAX = 0.0175;
|
|
1742
|
+
const TILT_PERSPECTIVE = 800;
|
|
1743
|
+
const TILT_SCALE = 1.01;
|
|
1744
|
+
const TILT_SETTLE_MS = 240;
|
|
1745
|
+
function startComposerTilt() {
|
|
1746
|
+
let current = null;
|
|
1747
|
+
let raf = 0;
|
|
1748
|
+
const tilted = new WeakSet();
|
|
1749
|
+
const settle = new Map();
|
|
1750
|
+
const reduced = typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
1751
|
+
if (reduced) return () => {};
|
|
1752
|
+
const release = (spot) => {
|
|
1753
|
+
const pending = settle.get(spot);
|
|
1754
|
+
if (pending !== undefined) {
|
|
1755
|
+
clearTimeout(pending);
|
|
1756
|
+
settle.delete(spot);
|
|
1757
|
+
}
|
|
1758
|
+
if (!tilted.has(spot)) return;
|
|
1759
|
+
tilted.delete(spot);
|
|
1760
|
+
spot.style.transform = 'perspective(' + TILT_PERSPECTIVE + 'px) rotateX(0rad) rotateY(0rad) scale(1)';
|
|
1761
|
+
const handle = window.setTimeout(() => {
|
|
1762
|
+
settle.delete(spot);
|
|
1763
|
+
spot.style.removeProperty('transform');
|
|
1764
|
+
spot.style.removeProperty('transform-origin');
|
|
1765
|
+
}, TILT_SETTLE_MS);
|
|
1766
|
+
settle.set(spot, handle);
|
|
1767
|
+
};
|
|
1768
|
+
const paint = (spot, rect, clientX, clientY) => {
|
|
1769
|
+
if (raf !== 0) return;
|
|
1770
|
+
raf = requestAnimationFrame(() => {
|
|
1771
|
+
raf = 0;
|
|
1772
|
+
if (current !== spot) return;
|
|
1773
|
+
if (dsttGetGlass() !== 'liquid') {
|
|
1774
|
+
release(spot);
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1777
|
+
if (clientX < rect.left || clientX > rect.right || clientY < rect.top || clientY > rect.bottom) return;
|
|
1778
|
+
const dx = Math.min(0.5, Math.max(-0.5, (clientX - rect.left) / rect.width - 0.5));
|
|
1779
|
+
const dy = Math.min(0.5, Math.max(-0.5, (clientY - rect.top) / rect.height - 0.5));
|
|
1780
|
+
spot.style.transformOrigin = (rect.width / 2) + 'px ' + (rect.height / 2) + 'px';
|
|
1781
|
+
spot.style.transform = 'perspective(' + TILT_PERSPECTIVE + 'px)'
|
|
1782
|
+
+ ' rotateX(' + (TILT_MAX * -2 * dy).toFixed(5) + 'rad)'
|
|
1783
|
+
+ ' rotateY(' + (TILT_MAX * 2 * dx).toFixed(5) + 'rad)'
|
|
1784
|
+
+ ' scale(' + TILT_SCALE + ')';
|
|
1785
|
+
tilted.add(spot);
|
|
1786
|
+
});
|
|
1787
|
+
};
|
|
1788
|
+
const onOver = (event) => {
|
|
1789
|
+
if (dsttGetGlass() !== 'liquid') return;
|
|
1790
|
+
const target = event.target;
|
|
1791
|
+
if (target === null || typeof target.closest !== 'function') return;
|
|
1792
|
+
const spot = target.closest(TILT_SELECTOR);
|
|
1793
|
+
if (spot === null) return;
|
|
1794
|
+
const rect = spot.getBoundingClientRect();
|
|
1795
|
+
if (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom) return;
|
|
1796
|
+
const pending = settle.get(spot);
|
|
1797
|
+
if (pending !== undefined) {
|
|
1798
|
+
clearTimeout(pending);
|
|
1799
|
+
settle.delete(spot);
|
|
1800
|
+
}
|
|
1801
|
+
current = spot;
|
|
1802
|
+
paint(spot, rect, event.clientX, event.clientY);
|
|
1803
|
+
};
|
|
1804
|
+
const onMove = (event) => {
|
|
1805
|
+
if (current === null) return;
|
|
1806
|
+
paint(current, current.getBoundingClientRect(), event.clientX, event.clientY);
|
|
1807
|
+
};
|
|
1808
|
+
const onOut = (event) => {
|
|
1809
|
+
if (current === null) return;
|
|
1810
|
+
const target = event.target;
|
|
1811
|
+
if (target !== null && typeof target.closest === 'function') {
|
|
1812
|
+
const spot = target.closest(TILT_SELECTOR);
|
|
1813
|
+
if (spot !== null && spot !== current) return;
|
|
1814
|
+
}
|
|
1815
|
+
const rect = current.getBoundingClientRect();
|
|
1816
|
+
if (event.clientX >= rect.left && event.clientX <= rect.right && event.clientY >= rect.top && event.clientY <= rect.bottom) return;
|
|
1817
|
+
release(current);
|
|
1818
|
+
current = null;
|
|
1819
|
+
};
|
|
1820
|
+
document.addEventListener('pointerover', onOver, { passive: true });
|
|
1821
|
+
document.addEventListener('pointermove', onMove, { passive: true });
|
|
1822
|
+
document.addEventListener('pointerout', onOut, { passive: true });
|
|
1823
|
+
return () => {
|
|
1824
|
+
document.removeEventListener('pointerover', onOver);
|
|
1825
|
+
document.removeEventListener('pointermove', onMove);
|
|
1826
|
+
document.removeEventListener('pointerout', onOut);
|
|
1827
|
+
if (raf !== 0) cancelAnimationFrame(raf);
|
|
1828
|
+
for (const id of settle.values()) clearTimeout(id);
|
|
1829
|
+
settle.clear();
|
|
1830
|
+
for (const spot of document.querySelectorAll(TILT_SELECTOR)) {
|
|
1831
|
+
spot.style.removeProperty('transform');
|
|
1832
|
+
spot.style.removeProperty('transform-origin');
|
|
1833
|
+
}
|
|
1834
|
+
current = null;
|
|
1835
|
+
};
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
/**
|
|
1839
|
+
* Ambient background layer: the DSTT flow field, a WebGL2 two-pass fluid
|
|
1840
|
+
* simulation ported from dsh-theme-mineradio, replacing the particle field.
|
|
1841
|
+
* The particles stay the fallback — {@link startParticles} remains the whole
|
|
1842
|
+
* implementation for engines without WebGL2 — so a missing WebGL2 downgrades
|
|
1843
|
+
* the background instead of blanking it.
|
|
1844
|
+
*
|
|
1845
|
+
* Every colour knob follows the DSTT colour token, so a mode switch re-tints
|
|
1846
|
+
* the flow and the glass edge tint without a remount.
|
|
1847
|
+
*/
|
|
1848
|
+
function startAmbient() {
|
|
1849
|
+
if (!supportsWebgl2()) return startParticles();
|
|
1850
|
+
const canvas = document.createElement('canvas');
|
|
1851
|
+
canvas.setAttribute('data-dsh-deepseek-canvas', '');
|
|
1852
|
+
const style = canvas.style;
|
|
1853
|
+
style.position = 'fixed';
|
|
1854
|
+
style.top = '0';
|
|
1855
|
+
style.left = '0';
|
|
1856
|
+
style.width = '100%';
|
|
1857
|
+
style.height = '100%';
|
|
1858
|
+
style.zIndex = '-1';
|
|
1859
|
+
style.pointerEvents = 'none';
|
|
1860
|
+
document.body.appendChild(canvas);
|
|
1861
|
+
|
|
1862
|
+
const fluid = attachFluidShader(canvas, dshomeFluidParams());
|
|
1863
|
+
// The fluid is deliberately autonomous: no pointer feed (the `fluidBrush`
|
|
1864
|
+
// setting, off by default) and no button stir either. The ported
|
|
1865
|
+
// `attachFluidInteractions` is therefore NOT wired — it is kept in the
|
|
1866
|
+
// file, unmodified, because re-enabling button ripples is one call, but
|
|
1867
|
+
// nothing in this theme uses it today.
|
|
1868
|
+
const stopSpotter = startSpecularSpotter();
|
|
1869
|
+
const stopParallax = startSpecularParallax();
|
|
1870
|
+
const dispersion = startGlassDispersion({ hue: FLUID_HUES[dshomeColor()] });
|
|
1871
|
+
// Both recipes share one filter pair, so the recipe selects the scale:
|
|
1872
|
+
// 104 for liquid (visible edge refraction) and the untouched 60 for
|
|
1873
|
+
// frosted, which is what the pane has always used.
|
|
1874
|
+
const applyGlass = () => {
|
|
1875
|
+
dispersion.setRefraction(dsttGetGlass() === 'liquid' ? LIQUID_REF_SCALE : DEFAULT_REF_SCALE);
|
|
1876
|
+
};
|
|
1877
|
+
applyGlass();
|
|
1878
|
+
const unsubscribeGlass = dsttSubscribe(applyGlass);
|
|
1879
|
+
const unsubscribe = subscribeColor((color) => {
|
|
1880
|
+
fluid.setParams(dshomeFluidParams());
|
|
1881
|
+
dispersion.setTint(FLUID_HUES[color] ?? FLUID_HUES.green);
|
|
1882
|
+
});
|
|
1883
|
+
// The palette is chosen per colour SCHEME, not only per colour token:
|
|
1884
|
+
// without this the canvas keeps whatever palette it was mounted with,
|
|
1885
|
+
// so a page that loads light and is then switched to dark keeps a
|
|
1886
|
+
// bright flow under a dark shell.
|
|
1887
|
+
let darkMirror = null;
|
|
1888
|
+
try {
|
|
1889
|
+
darkMirror = new MutationObserver(() => { fluid.setParams(dshomeFluidParams()); });
|
|
1890
|
+
darkMirror.observe(document.body, { attributes: true, attributeFilter: ['data-dshome-dark'] });
|
|
1891
|
+
} catch (error) {
|
|
1892
|
+
darkMirror = null;
|
|
1893
|
+
}
|
|
1894
|
+
return () => {
|
|
1895
|
+
unsubscribe();
|
|
1896
|
+
unsubscribeGlass();
|
|
1897
|
+
if (darkMirror !== null) darkMirror.disconnect();
|
|
1898
|
+
dispersion.dispose();
|
|
1899
|
+
stopParallax();
|
|
1900
|
+
stopSpotter();
|
|
1901
|
+
fluid.dispose();
|
|
1902
|
+
canvas.remove();
|
|
1903
|
+
};
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
function startParticles() {
|
|
1907
|
+
const canvas = document.createElement('canvas');
|
|
1908
|
+
canvas.setAttribute('data-dsh-deepseek-canvas', '');
|
|
1909
|
+
const style = canvas.style;
|
|
1910
|
+
style.position = 'fixed';
|
|
1911
|
+
style.top = '0';
|
|
1912
|
+
style.left = '0';
|
|
1913
|
+
style.width = '100%';
|
|
1914
|
+
style.height = '100%';
|
|
1915
|
+
style.zIndex = '-1';
|
|
1916
|
+
style.pointerEvents = 'none';
|
|
1917
|
+
style.opacity = '0.8';
|
|
1918
|
+
document.body.appendChild(canvas);
|
|
1919
|
+
|
|
1920
|
+
const g = canvas.getContext('2d');
|
|
1921
|
+
const isDark = () => document.body.hasAttribute('data-dshome-dark');
|
|
1922
|
+
const activeColor = () => {
|
|
1923
|
+
const c = document.body.getAttribute('data-dshome-color');
|
|
1924
|
+
return c === 'red' || c === 'blue' ? c : 'green';
|
|
1925
|
+
};
|
|
1926
|
+
let running = true;
|
|
1927
|
+
let rafId = 0;
|
|
1928
|
+
let W = 0;
|
|
1929
|
+
let H = 0;
|
|
1930
|
+
let dpr = 1;
|
|
1931
|
+
const COUNT = 72;
|
|
1932
|
+
const parts = [];
|
|
1933
|
+
|
|
1934
|
+
function palette() {
|
|
1935
|
+
const dark = isDark();
|
|
1936
|
+
const color = activeColor();
|
|
1937
|
+
if (color === 'red') {
|
|
1938
|
+
return dark ? ['#ff4d4f', '#f5222d', '#ff7875', '#ffa39e', '#ffffff'] : ['#f5222d', '#ff7875', '#ff4d4f', '#ffffff'];
|
|
1939
|
+
}
|
|
1940
|
+
if (color === 'blue') {
|
|
1941
|
+
return dark ? ['#5d79ff', '#4d6bfe', '#8fa8ff', '#ffffff', '#9db9ff'] : ['#4d6bfe', '#73a3d2', '#9db9ff', '#ffffff'];
|
|
1942
|
+
}
|
|
1943
|
+
return dark ? ['#34d399', '#059669', '#6ee7b7', '#a7f3d0', '#ffffff'] : ['#10b981', '#6ee7b7', '#34d399', '#ffffff'];
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
function linkBase() {
|
|
1947
|
+
const dark = isDark();
|
|
1948
|
+
const color = activeColor();
|
|
1949
|
+
if (color === 'red') return dark ? 'rgba(255,77,79,' : 'rgba(245,34,45,';
|
|
1950
|
+
if (color === 'blue') return dark ? 'rgba(93,121,255,' : 'rgba(77,107,254,';
|
|
1951
|
+
return dark ? 'rgba(52,211,153,' : 'rgba(16,185,129,';
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
function resize() {
|
|
1955
|
+
dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
1956
|
+
W = window.innerWidth;
|
|
1957
|
+
H = window.innerHeight;
|
|
1958
|
+
canvas.width = Math.round(W * dpr);
|
|
1959
|
+
canvas.height = Math.round(H * dpr);
|
|
1960
|
+
g.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
function make() {
|
|
1964
|
+
const colors = palette();
|
|
1965
|
+
return {
|
|
1966
|
+
x: Math.random() * W,
|
|
1967
|
+
y: Math.random() * H,
|
|
1968
|
+
vx: (Math.random() - 0.5) * 0.28,
|
|
1969
|
+
vy: (Math.random() - 0.5) * 0.28,
|
|
1970
|
+
r: Math.random() * 1.7 + 0.6,
|
|
1971
|
+
a: Math.random() * 0.55 + 0.15,
|
|
1972
|
+
c: colors[(Math.random() * colors.length) | 0]
|
|
1973
|
+
};
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
function step() {
|
|
1977
|
+
if (!running) return;
|
|
1978
|
+
g.clearRect(0, 0, W, H);
|
|
1979
|
+
const base = linkBase();
|
|
1980
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1981
|
+
for (let j = i + 1; j < parts.length; j++) {
|
|
1982
|
+
const a = parts[i];
|
|
1983
|
+
const b = parts[j];
|
|
1984
|
+
const dx = a.x - b.x;
|
|
1985
|
+
const dy = a.y - b.y;
|
|
1986
|
+
const d2 = dx * dx + dy * dy;
|
|
1987
|
+
if (d2 < 14400) {
|
|
1988
|
+
const alpha = (1 - Math.sqrt(d2) / 120) * 0.16;
|
|
1989
|
+
g.strokeStyle = base + alpha + ')';
|
|
1990
|
+
g.lineWidth = 1;
|
|
1991
|
+
g.beginPath();
|
|
1992
|
+
g.moveTo(a.x, a.y);
|
|
1993
|
+
g.lineTo(b.x, b.y);
|
|
1994
|
+
g.stroke();
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
for (const p of parts) {
|
|
1999
|
+
p.x += p.vx;
|
|
2000
|
+
p.y += p.vy;
|
|
2001
|
+
if (p.x < -20) p.x = W + 20;
|
|
2002
|
+
else if (p.x > W + 20) p.x = -20;
|
|
2003
|
+
if (p.y < -20) p.y = H + 20;
|
|
2004
|
+
else if (p.y > H + 20) p.y = -20;
|
|
2005
|
+
g.globalAlpha = p.a;
|
|
2006
|
+
g.fillStyle = p.c;
|
|
2007
|
+
g.beginPath();
|
|
2008
|
+
g.arc(p.x, p.y, p.r, 0, Math.PI * 2);
|
|
2009
|
+
g.fill();
|
|
2010
|
+
}
|
|
2011
|
+
g.globalAlpha = 1;
|
|
2012
|
+
rafId = requestAnimationFrame(step);
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
const refreshColors = () => {
|
|
2016
|
+
const colors = palette();
|
|
2017
|
+
for (const p of parts) p.c = colors[(Math.random() * colors.length) | 0];
|
|
2018
|
+
};
|
|
2019
|
+
const unsubscribeColor = subscribeColor(refreshColors);
|
|
2020
|
+
|
|
2021
|
+
window.addEventListener('resize', resize);
|
|
2022
|
+
resize();
|
|
2023
|
+
for (let i = 0; i < COUNT; i++) parts.push(make());
|
|
2024
|
+
rafId = requestAnimationFrame(step);
|
|
2025
|
+
|
|
2026
|
+
return () => {
|
|
2027
|
+
running = false;
|
|
2028
|
+
cancelAnimationFrame(rafId);
|
|
2029
|
+
window.removeEventListener('resize', resize);
|
|
2030
|
+
unsubscribeColor();
|
|
2031
|
+
canvas.remove();
|
|
2032
|
+
};
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
function watchSettingsSection() {
|
|
2036
|
+
let lastChild = null;
|
|
2037
|
+
const mo = new MutationObserver(() => {
|
|
2038
|
+
const options = document.querySelector('.VOzbGW_options');
|
|
2039
|
+
if (options === null) return;
|
|
2040
|
+
const child = options.firstElementChild;
|
|
2041
|
+
if (child !== lastChild) {
|
|
2042
|
+
lastChild = child;
|
|
2043
|
+
if (child !== null) {
|
|
2044
|
+
child.style.animation = 'none';
|
|
2045
|
+
void child.offsetWidth;
|
|
2046
|
+
child.style.animation = 'dshome-fade-in .25s ease';
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
});
|
|
2050
|
+
mo.observe(document.body, { subtree: true, childList: true });
|
|
2051
|
+
return () => mo.disconnect();
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
function watchBrandLink() {
|
|
2055
|
+
const onClick = (event) => {
|
|
2056
|
+
const brand = event.target && event.target.closest ? event.target.closest('.hHd-Xa_brand') : null;
|
|
2057
|
+
if (brand === null) return;
|
|
2058
|
+
event.preventDefault();
|
|
2059
|
+
event.stopPropagation();
|
|
2060
|
+
window.open('https://www.deepseek.com/', '_blank', 'noopener,noreferrer');
|
|
2061
|
+
};
|
|
2062
|
+
document.addEventListener('click', onClick, true);
|
|
2063
|
+
return () => document.removeEventListener('click', onClick, true);
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
function watchHeaderInteraction() {
|
|
2067
|
+
const EASE = 'cubic-bezier(.2,.8,.2,1)';
|
|
2068
|
+
const DUR = '.3s';
|
|
2069
|
+
const timers = [];
|
|
2070
|
+
|
|
2071
|
+
const headerOf = (node) => (node && node.closest ? node.closest('.wSkVaW_header') : null);
|
|
2072
|
+
const isTrajectory = (header) => {
|
|
2073
|
+
const root = header.closest('.wSkVaW_root');
|
|
2074
|
+
return root !== null && root.querySelector('.fV0t5q_root') !== null;
|
|
2075
|
+
};
|
|
2076
|
+
|
|
2077
|
+
const enter = (header) => {
|
|
2078
|
+
const actions = header.querySelector('.wSkVaW_headerActions');
|
|
2079
|
+
const crumbs = header.querySelector('.wSkVaW_crumbs');
|
|
2080
|
+
if (crumbs === null) return;
|
|
2081
|
+
|
|
2082
|
+
const actionsFirst = actions ? actions.getBoundingClientRect() : null;
|
|
2083
|
+
const crumbsFirst = crumbs.getBoundingClientRect();
|
|
2084
|
+
|
|
2085
|
+
header.classList.add('dshome-swap');
|
|
2086
|
+
|
|
2087
|
+
const actionsFlow = actions ? actions.getBoundingClientRect() : null;
|
|
2088
|
+
const crumbsFlow = crumbs.getBoundingClientRect();
|
|
2089
|
+
|
|
2090
|
+
if (actions && actionsFirst && actionsFlow) {
|
|
2091
|
+
const dx = actionsFirst.left - actionsFlow.left;
|
|
2092
|
+
const dy = actionsFirst.top - actionsFlow.top;
|
|
2093
|
+
actions.style.transition = 'none';
|
|
2094
|
+
actions.style.transform = 'translate(' + dx + 'px,' + dy + 'px)';
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
const cdx = crumbsFirst.left - crumbsFlow.left;
|
|
2098
|
+
const cdy = crumbsFirst.top - crumbsFlow.top;
|
|
2099
|
+
crumbs.style.transition = 'none';
|
|
2100
|
+
crumbs.style.transform = 'translate(' + cdx + 'px,' + cdy + 'px)';
|
|
2101
|
+
|
|
2102
|
+
void header.offsetWidth;
|
|
2103
|
+
|
|
2104
|
+
if (actions) {
|
|
2105
|
+
actions.style.transition = 'transform ' + DUR + ' ' + EASE;
|
|
2106
|
+
actions.style.transform = 'translate(0,0)';
|
|
2107
|
+
}
|
|
2108
|
+
crumbs.style.transition = 'transform ' + DUR + ' ' + EASE;
|
|
2109
|
+
crumbs.style.transform = 'translate(0,0)';
|
|
2110
|
+
|
|
2111
|
+
const timer = setTimeout(() => {
|
|
2112
|
+
if (actions) { actions.style.transition = ''; actions.style.transform = ''; }
|
|
2113
|
+
crumbs.style.transition = '';
|
|
2114
|
+
crumbs.style.transform = '';
|
|
2115
|
+
}, 320);
|
|
2116
|
+
timers.push(timer);
|
|
2117
|
+
};
|
|
2118
|
+
|
|
2119
|
+
const leave = (header) => {
|
|
2120
|
+
const actions = header.querySelector('.wSkVaW_headerActions');
|
|
2121
|
+
const crumbs = header.querySelector('.wSkVaW_crumbs');
|
|
2122
|
+
|
|
2123
|
+
const actionsHover = actions ? actions.getBoundingClientRect() : null;
|
|
2124
|
+
const crumbsHover = crumbs ? crumbs.getBoundingClientRect() : null;
|
|
2125
|
+
|
|
2126
|
+
header.classList.remove('dshome-swap');
|
|
2127
|
+
|
|
2128
|
+
if (actions && actionsHover) {
|
|
2129
|
+
const n = actions.getBoundingClientRect();
|
|
2130
|
+
const dx = actionsHover.left - n.left;
|
|
2131
|
+
const dy = actionsHover.top - n.top;
|
|
2132
|
+
actions.style.transition = 'none';
|
|
2133
|
+
actions.style.transform = 'translate(' + dx + 'px,' + dy + 'px)';
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
if (crumbs && crumbsHover) {
|
|
2137
|
+
const n = crumbs.getBoundingClientRect();
|
|
2138
|
+
const cdx = crumbsHover.left - n.left;
|
|
2139
|
+
const cdy = crumbsHover.top - n.top;
|
|
2140
|
+
crumbs.style.transition = 'none';
|
|
2141
|
+
crumbs.style.transform = 'translate(' + cdx + 'px,' + cdy + 'px)';
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
void header.offsetWidth;
|
|
2145
|
+
|
|
2146
|
+
if (actions) {
|
|
2147
|
+
actions.style.transition = 'transform ' + DUR + ' ' + EASE;
|
|
2148
|
+
actions.style.transform = 'translate(0,0)';
|
|
2149
|
+
}
|
|
2150
|
+
if (crumbs) {
|
|
2151
|
+
crumbs.style.transition = 'transform ' + DUR + ' ' + EASE;
|
|
2152
|
+
crumbs.style.transform = 'translate(0,0)';
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
const timer = setTimeout(() => {
|
|
2156
|
+
if (actions) { actions.style.transition = ''; actions.style.transform = ''; }
|
|
2157
|
+
if (crumbs) { crumbs.style.transition = ''; crumbs.style.transform = ''; }
|
|
2158
|
+
}, 320);
|
|
2159
|
+
timers.push(timer);
|
|
2160
|
+
};
|
|
2161
|
+
|
|
2162
|
+
const over = (event) => {
|
|
2163
|
+
const header = headerOf(event.target);
|
|
2164
|
+
if (header === null || isTrajectory(header)) return;
|
|
2165
|
+
if (header.classList.contains('dshome-swap')) return;
|
|
2166
|
+
const from = event.relatedTarget;
|
|
2167
|
+
if (from && header.contains(from)) return;
|
|
2168
|
+
enter(header);
|
|
2169
|
+
};
|
|
2170
|
+
|
|
2171
|
+
const out = (event) => {
|
|
2172
|
+
const header = headerOf(event.target);
|
|
2173
|
+
if (header === null) return;
|
|
2174
|
+
if (!header.classList.contains('dshome-swap')) return;
|
|
2175
|
+
const to = event.relatedTarget;
|
|
2176
|
+
if (to && header.contains(to)) return;
|
|
2177
|
+
leave(header);
|
|
2178
|
+
};
|
|
2179
|
+
|
|
2180
|
+
document.addEventListener('mouseover', over);
|
|
2181
|
+
document.addEventListener('mouseout', out);
|
|
2182
|
+
|
|
2183
|
+
return () => {
|
|
2184
|
+
document.removeEventListener('mouseover', over);
|
|
2185
|
+
document.removeEventListener('mouseout', out);
|
|
2186
|
+
timers.forEach((t) => clearTimeout(t));
|
|
2187
|
+
document.querySelectorAll('.wSkVaW_header.dshome-swap').forEach((h) => {
|
|
2188
|
+
h.classList.remove('dshome-swap');
|
|
2189
|
+
const a = h.querySelector('.wSkVaW_headerActions');
|
|
2190
|
+
const c = h.querySelector('.wSkVaW_crumbs');
|
|
2191
|
+
if (a) { a.style.transition = ''; a.style.transform = ''; }
|
|
2192
|
+
if (c) { c.style.transition = ''; c.style.transform = ''; }
|
|
2193
|
+
});
|
|
2194
|
+
};
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
/**
|
|
2198
|
+
* Add an "Open workspace" entry to the folder row's (workspace row) three-dot
|
|
2199
|
+
* menu. The workspace browser renders its row menus internally with no plugin
|
|
2200
|
+
* slot, so the theme patches the popup DOM: it tracks the ellipsis anchor of a
|
|
2201
|
+
* project row, and when the portaled [role=menu] appears it inserts an item
|
|
2202
|
+
* that resolves the row's workspace (title match against the workspaces
|
|
2203
|
+
* store, DOM-order fallback) and asks the host half (connection RPC) to open
|
|
2204
|
+
* the workspace's directory with the OS file manager in the foreground.
|
|
2205
|
+
* The private host channel is the primary reveal path; `workspaces.openPath`
|
|
2206
|
+
* is used only when it still exists (dsh ≤0.1.1), and a failed reveal is
|
|
2207
|
+
* reported on the console instead of failing silently.
|
|
2208
|
+
*/
|
|
2209
|
+
function watchWorkspaceMenu(ctx) {
|
|
2210
|
+
let workspaces;
|
|
2211
|
+
try {
|
|
2212
|
+
workspaces = ctx.get('workspaces');
|
|
2213
|
+
} catch (error) {
|
|
2214
|
+
workspaces = undefined;
|
|
2215
|
+
}
|
|
2216
|
+
let connection;
|
|
2217
|
+
try {
|
|
2218
|
+
connection = ctx.get('connection');
|
|
2219
|
+
} catch (error) {
|
|
2220
|
+
connection = undefined;
|
|
2221
|
+
}
|
|
2222
|
+
if (workspaces === undefined || workspaces === null || workspaces.list === undefined) return () => {};
|
|
2223
|
+
|
|
2224
|
+
const ROW = '.YDXeBa_projectRow';
|
|
2225
|
+
const ACTIONS = '.YDXeBa_rowActions';
|
|
2226
|
+
const LABEL = '.YDXeBa_title';
|
|
2227
|
+
const CLS = 'dshome-ws-open';
|
|
2228
|
+
const FOLDER_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>';
|
|
2229
|
+
|
|
2230
|
+
let label = '打开工作区';
|
|
2231
|
+
try {
|
|
2232
|
+
const locale = ctx.get('locale');
|
|
2233
|
+
if (locale !== undefined) {
|
|
2234
|
+
const active = locale.getLocale().active;
|
|
2235
|
+
if (active !== 'zh' && active !== 'zh-CN' && active !== 'zh-Hans') label = 'Open workspace';
|
|
2236
|
+
}
|
|
2237
|
+
} catch (error) {}
|
|
2238
|
+
|
|
2239
|
+
let anchorRow = null;
|
|
2240
|
+
|
|
2241
|
+
const resolveWorkspace = (row) => {
|
|
2242
|
+
try {
|
|
2243
|
+
const items = workspaces.list.getSnapshot().items;
|
|
2244
|
+
if (!Array.isArray(items)) return undefined;
|
|
2245
|
+
const titleEl = row.querySelector(LABEL);
|
|
2246
|
+
const labelText = titleEl === null ? '' : (titleEl.textContent || '').trim();
|
|
2247
|
+
if (labelText === '') return undefined;
|
|
2248
|
+
const byTitle = items.filter((w) => w !== null && w !== undefined && w.title === labelText);
|
|
2249
|
+
if (byTitle.length === 1) return byTitle[0];
|
|
2250
|
+
const rows = Array.from(document.querySelectorAll(ROW));
|
|
2251
|
+
const index = rows.indexOf(row);
|
|
2252
|
+
if (index === -1) return undefined;
|
|
2253
|
+
let ordinal = 0;
|
|
2254
|
+
for (let i = 0; i < index; i++) {
|
|
2255
|
+
const t = rows[i].querySelector(LABEL);
|
|
2256
|
+
const l = t === null ? '' : (t.textContent || '').trim();
|
|
2257
|
+
if (l !== '' && l !== 'Ungrouped') ordinal += 1;
|
|
2258
|
+
}
|
|
2259
|
+
const candidate = items[ordinal];
|
|
2260
|
+
return candidate === undefined || candidate === null ? undefined : candidate;
|
|
2261
|
+
} catch (error) {
|
|
2262
|
+
return undefined;
|
|
2263
|
+
}
|
|
2264
|
+
};
|
|
2265
|
+
|
|
2266
|
+
const track = (target) => {
|
|
2267
|
+
anchorRow = null;
|
|
2268
|
+
if (target === null || typeof target.closest !== 'function') return;
|
|
2269
|
+
const button = target.closest('button');
|
|
2270
|
+
if (button === null) return;
|
|
2271
|
+
const actions = button.closest(ACTIONS);
|
|
2272
|
+
if (actions === null) return;
|
|
2273
|
+
const row = actions.closest(ROW);
|
|
2274
|
+
if (row === null) return;
|
|
2275
|
+
if (button !== actions.querySelector('button')) return;
|
|
2276
|
+
anchorRow = row;
|
|
2277
|
+
};
|
|
2278
|
+
|
|
2279
|
+
const onClickCapture = (event) => track(event.target);
|
|
2280
|
+
const onFocusIn = (event) => track(event.target);
|
|
2281
|
+
document.addEventListener('click', onClickCapture, true);
|
|
2282
|
+
document.addEventListener('focusin', onFocusIn, true);
|
|
2283
|
+
|
|
2284
|
+
const injectItem = (menu) => {
|
|
2285
|
+
if (menu === null || menu.querySelector('.' + CLS) !== null) return;
|
|
2286
|
+
const viewport = menu.querySelector('[role="presentation"]');
|
|
2287
|
+
if (viewport === null) return;
|
|
2288
|
+
const row = anchorRow;
|
|
2289
|
+
if (row === null || !document.contains(row)) return;
|
|
2290
|
+
const workspace = resolveWorkspace(row);
|
|
2291
|
+
if (workspace === undefined) return;
|
|
2292
|
+
|
|
2293
|
+
const wrap = document.createElement('div');
|
|
2294
|
+
wrap.className = CLS + '-wrap';
|
|
2295
|
+
const item = document.createElement('button');
|
|
2296
|
+
item.type = 'button';
|
|
2297
|
+
item.className = CLS;
|
|
2298
|
+
item.setAttribute('role', 'menuitem');
|
|
2299
|
+
item.title = label;
|
|
2300
|
+
item.setAttribute('aria-label', label);
|
|
2301
|
+
const icon = document.createElement('span');
|
|
2302
|
+
icon.className = CLS + '-icon';
|
|
2303
|
+
icon.innerHTML = FOLDER_SVG;
|
|
2304
|
+
const text = document.createElement('span');
|
|
2305
|
+
text.className = CLS + '-label';
|
|
2306
|
+
text.textContent = label;
|
|
2307
|
+
item.appendChild(icon);
|
|
2308
|
+
item.appendChild(text);
|
|
2309
|
+
item.addEventListener('click', (event) => {
|
|
2310
|
+
event.preventDefault();
|
|
2311
|
+
event.stopPropagation();
|
|
2312
|
+
const path = workspace.path;
|
|
2313
|
+
// The host bridge is a same-origin webServer route (see bridgeCall).
|
|
2314
|
+
const fallback = (reason) => {
|
|
2315
|
+
if (typeof workspaces.openPath === 'function') {
|
|
2316
|
+
try {
|
|
2317
|
+
workspaces.openPath(path);
|
|
2318
|
+
return;
|
|
2319
|
+
} catch (error) {
|
|
2320
|
+
console.warn('[deepseek-style-theme] workspaces.openPath failed:', error);
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
console.warn(
|
|
2324
|
+
'[deepseek-style-theme] 打开工作区失败(' + reason + '):' +
|
|
2325
|
+
'私有 RPC 通道不可用,且当前 dsh 版本已移除 workspaces.openPath。' +
|
|
2326
|
+
'请确认插件为 1.38.4+ 并重启 dsh web(旧版宿主端因缺少 webServer 注入无法注册通道)。'
|
|
2327
|
+
);
|
|
2328
|
+
};
|
|
2329
|
+
Promise.resolve(bridgeCall('dshome/explorer.open', { path: path }))
|
|
2330
|
+
.then((result) => {
|
|
2331
|
+
if (result !== undefined && result !== null && result.ok === true) return;
|
|
2332
|
+
const detail = result !== undefined && result !== null && result.error !== undefined
|
|
2333
|
+
? String(result.error.message)
|
|
2334
|
+
: 'no result';
|
|
2335
|
+
fallback('bridge returned ' + detail);
|
|
2336
|
+
})
|
|
2337
|
+
.catch((error) => {
|
|
2338
|
+
fallback('bridge call threw: ' + (error instanceof Error ? error.message : String(error)));
|
|
2339
|
+
});
|
|
2340
|
+
try {
|
|
2341
|
+
document.body.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, cancelable: true }));
|
|
2342
|
+
} catch (error) {}
|
|
2343
|
+
});
|
|
2344
|
+
wrap.appendChild(item);
|
|
2345
|
+
viewport.insertBefore(wrap, viewport.firstChild);
|
|
2346
|
+
};
|
|
2347
|
+
|
|
2348
|
+
const observer = new MutationObserver((records) => {
|
|
2349
|
+
for (const record of records) {
|
|
2350
|
+
for (const node of record.addedNodes) {
|
|
2351
|
+
if (!(node instanceof Element)) continue;
|
|
2352
|
+
if (node.matches('[role="menu"]')) {
|
|
2353
|
+
injectItem(node);
|
|
2354
|
+
continue;
|
|
2355
|
+
}
|
|
2356
|
+
node.querySelectorAll('[role="menu"]').forEach(injectItem);
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
});
|
|
2360
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
2361
|
+
|
|
2362
|
+
return () => {
|
|
2363
|
+
observer.disconnect();
|
|
2364
|
+
document.removeEventListener('click', onClickCapture, true);
|
|
2365
|
+
document.removeEventListener('focusin', onFocusIn, true);
|
|
2366
|
+
};
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
/**
|
|
2370
|
+
* Action menu for the product's delivered/changed files
|
|
2371
|
+
* (`@deepseek-ai/dsh-client-ui-deliverables`), which renders two separate
|
|
2372
|
+
* surfaces:
|
|
2373
|
+
*
|
|
2374
|
+
* - `[data-produced-files-row]` — the "本轮文件改动" chips, one
|
|
2375
|
+
* `button[title=absolutePath]` per changed path, whose only own action
|
|
2376
|
+
* is the product's preview opener;
|
|
2377
|
+
* - `[data-presented-file]` — the declared-file cards, whose overlay button
|
|
2378
|
+
* previews and whose chevron already offers default-app open and reveal.
|
|
2379
|
+
*
|
|
2380
|
+
* Both now open this menu: the changed-file chips on a plain left click
|
|
2381
|
+
* (clicking one previously looked like nothing happened) and both surfaces
|
|
2382
|
+
* on right click. The product's own action is preserved as a menu item, so
|
|
2383
|
+
* hijacking the chip click trades nothing away. "Copy path" does not exist
|
|
2384
|
+
* anywhere in the product — the absolute path is otherwise only reachable by
|
|
2385
|
+
* reading a `title` tooltip by hand — so it is also added to the product's
|
|
2386
|
+
* own chevron menu.
|
|
2387
|
+
*
|
|
2388
|
+
* Both anchors are stable product attributes rather than build-hashed class
|
|
2389
|
+
* names, and each surface already carries the absolute path in its `title`
|
|
2390
|
+
* (a card via the product's own `resolveWorkspacePath`), so this patch never
|
|
2391
|
+
* re-derives a workspace.
|
|
2392
|
+
*/
|
|
2393
|
+
function watchFileCardMenu(ctx) {
|
|
2394
|
+
const CARD = '[data-presented-file]';
|
|
2395
|
+
// The product renders changed files through a *different* component:
|
|
2396
|
+
// ProducedFiles draws one `button[title=absolutePath]` per path inside
|
|
2397
|
+
// `[data-produced-files-row]`. Those chips are not
|
|
2398
|
+
// `[data-presented-file]` cards at all, so they need their own anchor.
|
|
2399
|
+
const PRODUCED = '[data-produced-files-row] button[title]';
|
|
2400
|
+
const MENU = '[role="menu"]';
|
|
2401
|
+
const CLS = 'dshome-file-menu';
|
|
2402
|
+
const ITEM_CLS = CLS + '-item';
|
|
2403
|
+
const TOAST_CLS = CLS + '-toast';
|
|
2404
|
+
const COPY_CLS = 'dshome-file-copy';
|
|
2405
|
+
const ICON_OPEN = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M21 14v5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5"/></svg>';
|
|
2406
|
+
const ICON_REVEAL = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>';
|
|
2407
|
+
const ICON_COPY = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
|
|
2408
|
+
const ICON_PREVIEW = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/></svg>';
|
|
2409
|
+
const CSS = `
|
|
2410
|
+
.${CLS}{position:fixed;z-index:2147483000;min-width:198px;padding:6px;border-radius:14px;background:rgba(255,255,255,.94);border:.5px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1));box-shadow:0 18px 44px rgba(15,30,60,.2);backdrop-filter:blur(18px) saturate(1.4);-webkit-backdrop-filter:blur(18px) saturate(1.4);animation:dshome-menu-pop .16s cubic-bezier(.2,.8,.2,1);transform-origin:top left}
|
|
2411
|
+
body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 44px rgba(0,0,0,.5)}
|
|
2412
|
+
.${ITEM_CLS},.${COPY_CLS}{display:flex;align-items:center;gap:9px;box-sizing:border-box;width:100%;padding:8px 10px;border:0;border-radius:9px;background:transparent;color:var(--dsw-alias-label-primary,#152443);font:inherit;font-size:13px;line-height:20px;text-align:left;cursor:pointer;transition:background-color .12s ease,transform .12s ease}
|
|
2413
|
+
.${ITEM_CLS}:hover,.${COPY_CLS}:hover{background:color-mix(in srgb,var(--ds-brand,#4d6bfe) 13%,transparent);transform:translateX(1px)}
|
|
2414
|
+
.${ITEM_CLS}:focus-visible,.${COPY_CLS}:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4d6bfe);outline-offset:-2px}
|
|
2415
|
+
.${ITEM_CLS} svg,.${COPY_CLS} svg{flex:none;width:15px;height:15px;opacity:.72}
|
|
2416
|
+
.${ITEM_CLS}>span,.${COPY_CLS}>span{display:inline-flex;align-items:center;min-width:0}
|
|
2417
|
+
.${CLS}-sep{height:1px;margin:5px 6px;background:var(--dsw-alias-border-l1,rgba(0,0,0,.06))}
|
|
2418
|
+
.${TOAST_CLS}{position:fixed;z-index:2147483001;left:50%;bottom:56px;transform:translateX(-50%);padding:8px 15px;border-radius:999px;background:rgba(20,36,70,.93);color:#fff;font-size:12px;line-height:18px;pointer-events:none;animation:dshome-fade-in .18s ease}
|
|
2419
|
+
@keyframes dshome-menu-pop{from{opacity:0;transform:scale(.96) translateY(-4px)}to{opacity:1;transform:none}}
|
|
2420
|
+
`;
|
|
2421
|
+
|
|
2422
|
+
const isZh = () => {
|
|
2423
|
+
try {
|
|
2424
|
+
const locale = ctx.get('locale');
|
|
2425
|
+
if (locale === undefined || locale === null) return true;
|
|
2426
|
+
const active = locale.getLocale().active;
|
|
2427
|
+
return active === 'zh' || active === 'zh-CN' || active === 'zh-Hans';
|
|
2428
|
+
} catch (error) {
|
|
2429
|
+
return true;
|
|
2430
|
+
}
|
|
2431
|
+
};
|
|
2432
|
+
const labels = () => (isZh() ? {
|
|
2433
|
+
open: '用默认应用打开',
|
|
2434
|
+
reveal: '打开所在文件夹',
|
|
2435
|
+
copy: '复制路径',
|
|
2436
|
+
preview: '在侧边栏预览',
|
|
2437
|
+
previewProduced: '打开预览',
|
|
2438
|
+
copied: '已复制路径',
|
|
2439
|
+
copyDenied: '浏览器拒绝了剪贴板访问,请手动复制',
|
|
2440
|
+
failed: '操作失败(宿主端未响应)'
|
|
2441
|
+
} : {
|
|
2442
|
+
open: 'Open with default app',
|
|
2443
|
+
reveal: 'Open containing folder',
|
|
2444
|
+
copy: 'Copy path',
|
|
2445
|
+
preview: 'Preview in sidebar',
|
|
2446
|
+
previewProduced: 'Open preview',
|
|
2447
|
+
copied: 'Path copied',
|
|
2448
|
+
copyDenied: 'Clipboard access was denied — copy it manually',
|
|
2449
|
+
failed: 'Action failed (no response from the host)'
|
|
2450
|
+
});
|
|
2451
|
+
|
|
2452
|
+
/** The absolute path the product already resolved onto a card. */
|
|
2453
|
+
const pathOf = (card) => {
|
|
2454
|
+
const overlay = card.querySelector('button[title]');
|
|
2455
|
+
if (overlay === null) return '';
|
|
2456
|
+
const value = overlay.getAttribute('title');
|
|
2457
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
2458
|
+
};
|
|
2459
|
+
|
|
2460
|
+
/**
|
|
2461
|
+
* Resolve a click target to the file it represents. A changed-file chip
|
|
2462
|
+
* is both the anchor and the button whose own click opens the product's
|
|
2463
|
+
* preview; a presented card carries that button as an overlay. Returns
|
|
2464
|
+
* null when the target is neither.
|
|
2465
|
+
*/
|
|
2466
|
+
const surfaceOf = (target) => {
|
|
2467
|
+
if (target === null || typeof target.closest !== 'function') return null;
|
|
2468
|
+
const chip = target.closest(PRODUCED);
|
|
2469
|
+
if (chip !== null) {
|
|
2470
|
+
const chipPath = chip.getAttribute('title');
|
|
2471
|
+
const path = typeof chipPath === 'string' ? chipPath.trim() : '';
|
|
2472
|
+
return path === '' ? null : { path: path, button: chip, produced: true };
|
|
2473
|
+
}
|
|
2474
|
+
const card = target.closest(CARD);
|
|
2475
|
+
if (card === null) return null;
|
|
2476
|
+
const overlay = card.querySelector('button[title]');
|
|
2477
|
+
if (overlay === null) return null;
|
|
2478
|
+
const overlayPath = overlay.getAttribute('title');
|
|
2479
|
+
const path = typeof overlayPath === 'string' ? overlayPath.trim() : '';
|
|
2480
|
+
return path === '' ? null : { path: path, button: overlay, produced: false };
|
|
2481
|
+
};
|
|
2482
|
+
|
|
2483
|
+
/**
|
|
2484
|
+
* Run the product's own action for a surface by re-dispatching its click.
|
|
2485
|
+
* The flag lets that synthetic click past our capture handler, which
|
|
2486
|
+
* would otherwise open this menu again instead of the product's preview.
|
|
2487
|
+
*/
|
|
2488
|
+
let replaying = false;
|
|
2489
|
+
const preview = (surface) => {
|
|
2490
|
+
replaying = true;
|
|
2491
|
+
try {
|
|
2492
|
+
surface.button.click();
|
|
2493
|
+
} finally {
|
|
2494
|
+
replaying = false;
|
|
2495
|
+
}
|
|
2496
|
+
};
|
|
2497
|
+
|
|
2498
|
+
let menu = null;
|
|
2499
|
+
let toastNode = null;
|
|
2500
|
+
const closeMenu = () => {
|
|
2501
|
+
if (menu === null) return;
|
|
2502
|
+
menu.remove();
|
|
2503
|
+
menu = null;
|
|
2504
|
+
};
|
|
2505
|
+
const toast = (text) => {
|
|
2506
|
+
if (toastNode !== null) toastNode.remove();
|
|
2507
|
+
const node = document.createElement('div');
|
|
2508
|
+
node.className = TOAST_CLS;
|
|
2509
|
+
node.setAttribute('role', 'status');
|
|
2510
|
+
node.textContent = text;
|
|
2511
|
+
document.body.appendChild(node);
|
|
2512
|
+
toastNode = node;
|
|
2513
|
+
setTimeout(() => {
|
|
2514
|
+
node.remove();
|
|
2515
|
+
if (toastNode === node) toastNode = null;
|
|
2516
|
+
}, 1800);
|
|
2517
|
+
};
|
|
2518
|
+
// localhost is a secure context, so the async clipboard exists in the
|
|
2519
|
+
// real GUI; a rejection is still reported rather than swallowed.
|
|
2520
|
+
const copyPath = (path) => {
|
|
2521
|
+
const clipboard = navigator.clipboard;
|
|
2522
|
+
if (clipboard === undefined || clipboard === null || typeof clipboard.writeText !== 'function') {
|
|
2523
|
+
return Promise.resolve(false);
|
|
2524
|
+
}
|
|
2525
|
+
return Promise.resolve(clipboard.writeText(path)).then(() => true).catch(() => false);
|
|
2526
|
+
};
|
|
2527
|
+
const invoke = (endpoint, path) => Promise.resolve(bridgeCall(endpoint, { path: path }))
|
|
2528
|
+
.then((result) => result !== undefined && result !== null && result.ok === true)
|
|
2529
|
+
.catch(() => false);
|
|
2530
|
+
|
|
2531
|
+
const buildItem = (icon, label, onPick) => {
|
|
2532
|
+
const item = document.createElement('button');
|
|
2533
|
+
item.type = 'button';
|
|
2534
|
+
item.className = ITEM_CLS;
|
|
2535
|
+
item.setAttribute('role', 'menuitem');
|
|
2536
|
+
item.title = label;
|
|
2537
|
+
item.setAttribute('aria-label', label);
|
|
2538
|
+
const glyph = document.createElement('span');
|
|
2539
|
+
glyph.innerHTML = icon;
|
|
2540
|
+
const caption = document.createElement('span');
|
|
2541
|
+
caption.textContent = label;
|
|
2542
|
+
item.appendChild(glyph);
|
|
2543
|
+
item.appendChild(caption);
|
|
2544
|
+
item.addEventListener('click', (event) => {
|
|
2545
|
+
event.preventDefault();
|
|
2546
|
+
event.stopPropagation();
|
|
2547
|
+
closeMenu();
|
|
2548
|
+
onPick();
|
|
2549
|
+
});
|
|
2550
|
+
return item;
|
|
2551
|
+
};
|
|
2552
|
+
|
|
2553
|
+
const openMenu = (surface, clientX, clientY) => {
|
|
2554
|
+
closeMenu();
|
|
2555
|
+
const path = surface.path;
|
|
2556
|
+
const text = labels();
|
|
2557
|
+
const node = document.createElement('div');
|
|
2558
|
+
node.className = CLS;
|
|
2559
|
+
node.setAttribute('role', 'menu');
|
|
2560
|
+
node.appendChild(buildItem(ICON_OPEN, text.open, () => {
|
|
2561
|
+
invoke('dshome/file.open', path).then((ok) => { if (!ok) toast(text.failed); });
|
|
2562
|
+
}));
|
|
2563
|
+
node.appendChild(buildItem(ICON_REVEAL, text.reveal, () => {
|
|
2564
|
+
invoke('dshome/file.reveal', path).then((ok) => { if (!ok) toast(text.failed); });
|
|
2565
|
+
}));
|
|
2566
|
+
node.appendChild(buildItem(ICON_COPY, text.copy, () => {
|
|
2567
|
+
copyPath(path).then((ok) => toast(ok ? text.copied : text.copyDenied));
|
|
2568
|
+
}));
|
|
2569
|
+
const separator = document.createElement('div');
|
|
2570
|
+
separator.className = CLS + '-sep';
|
|
2571
|
+
node.appendChild(separator);
|
|
2572
|
+
node.appendChild(buildItem(ICON_PREVIEW, surface.produced ? text.previewProduced : text.preview, () => {
|
|
2573
|
+
preview(surface);
|
|
2574
|
+
}));
|
|
2575
|
+
document.body.appendChild(node);
|
|
2576
|
+
// Clamp to the viewport so a card near an edge never opens a menu
|
|
2577
|
+
// the user cannot reach.
|
|
2578
|
+
const box = node.getBoundingClientRect();
|
|
2579
|
+
node.style.left = Math.max(8, Math.min(clientX, window.innerWidth - box.width - 8)) + 'px';
|
|
2580
|
+
node.style.top = Math.max(8, Math.min(clientY, window.innerHeight - box.height - 8)) + 'px';
|
|
2581
|
+
menu = node;
|
|
2582
|
+
const first = node.querySelector('.' + ITEM_CLS);
|
|
2583
|
+
if (first !== null) first.focus();
|
|
2584
|
+
};
|
|
2585
|
+
|
|
2586
|
+
const onContextMenu = (event) => {
|
|
2587
|
+
const surface = surfaceOf(event.target);
|
|
2588
|
+
if (surface === null) return;
|
|
2589
|
+
event.preventDefault();
|
|
2590
|
+
event.stopPropagation();
|
|
2591
|
+
openMenu(surface, event.clientX, event.clientY);
|
|
2592
|
+
};
|
|
2593
|
+
// A changed-file chip's only own action is the product's preview, and
|
|
2594
|
+
// in practice clicking one looks like nothing happens. Those chips
|
|
2595
|
+
// therefore open this menu on a plain left click, with the product's
|
|
2596
|
+
// own action kept as a menu item. Presented cards keep their left
|
|
2597
|
+
// click — their overlay visibly previews — and use the right click.
|
|
2598
|
+
const onClickCapture = (event) => {
|
|
2599
|
+
if (replaying || event.defaultPrevented) return;
|
|
2600
|
+
if (event.button !== undefined && event.button !== 0) return;
|
|
2601
|
+
const surface = surfaceOf(event.target);
|
|
2602
|
+
if (surface === null || !surface.produced) return;
|
|
2603
|
+
event.preventDefault();
|
|
2604
|
+
event.stopPropagation();
|
|
2605
|
+
openMenu(surface, event.clientX, event.clientY);
|
|
2606
|
+
};
|
|
2607
|
+
const onPointerDown = (event) => {
|
|
2608
|
+
if (menu === null) return;
|
|
2609
|
+
if (event.target instanceof Node && menu.contains(event.target)) return;
|
|
2610
|
+
closeMenu();
|
|
2611
|
+
};
|
|
2612
|
+
const onKeyDown = (event) => {
|
|
2613
|
+
if (event.key === 'Escape') closeMenu();
|
|
2614
|
+
};
|
|
2615
|
+
document.addEventListener('contextmenu', onContextMenu, true);
|
|
2616
|
+
document.addEventListener('click', onClickCapture, true);
|
|
2617
|
+
document.addEventListener('pointerdown', onPointerDown, true);
|
|
2618
|
+
document.addEventListener('keydown', onKeyDown, true);
|
|
2619
|
+
window.addEventListener('resize', closeMenu, true);
|
|
2620
|
+
window.addEventListener('scroll', closeMenu, true);
|
|
2621
|
+
|
|
2622
|
+
// "Copy path" is the one gesture the product lacks, so it is also added
|
|
2623
|
+
// to the product's own chevron menu, where file actions are looked for.
|
|
2624
|
+
let anchorCard = null;
|
|
2625
|
+
const track = (target) => {
|
|
2626
|
+
anchorCard = null;
|
|
2627
|
+
if (target === null || typeof target.closest !== 'function') return;
|
|
2628
|
+
const button = target.closest('button');
|
|
2629
|
+
if (button === null || button.getAttribute('aria-haspopup') !== 'menu') return;
|
|
2630
|
+
const card = button.closest(CARD);
|
|
2631
|
+
if (card === null) return;
|
|
2632
|
+
anchorCard = card;
|
|
2633
|
+
};
|
|
2634
|
+
const onTrackCapture = (event) => track(event.target);
|
|
2635
|
+
document.addEventListener('click', onTrackCapture, true);
|
|
2636
|
+
document.addEventListener('focusin', onTrackCapture, true);
|
|
2637
|
+
|
|
2638
|
+
const injectCopyItem = (node) => {
|
|
2639
|
+
if (anchorCard === null || !document.contains(anchorCard)) return;
|
|
2640
|
+
if (node.querySelector('.' + COPY_CLS) !== null) return;
|
|
2641
|
+
const card = anchorCard;
|
|
2642
|
+
const path = pathOf(card);
|
|
2643
|
+
if (path === '') return;
|
|
2644
|
+
const viewport = node.querySelector('[role="presentation"]');
|
|
2645
|
+
if (viewport === null) return;
|
|
2646
|
+
const text = labels();
|
|
2647
|
+
const item = document.createElement('button');
|
|
2648
|
+
item.type = 'button';
|
|
2649
|
+
item.className = COPY_CLS;
|
|
2650
|
+
item.setAttribute('role', 'menuitem');
|
|
2651
|
+
item.title = text.copy;
|
|
2652
|
+
item.setAttribute('aria-label', text.copy);
|
|
2653
|
+
const glyph = document.createElement('span');
|
|
2654
|
+
glyph.innerHTML = ICON_COPY;
|
|
2655
|
+
const caption = document.createElement('span');
|
|
2656
|
+
caption.textContent = text.copy;
|
|
2657
|
+
item.appendChild(glyph);
|
|
2658
|
+
item.appendChild(caption);
|
|
2659
|
+
item.addEventListener('click', (event) => {
|
|
2660
|
+
event.preventDefault();
|
|
2661
|
+
event.stopPropagation();
|
|
2662
|
+
copyPath(path).then((ok) => toast(ok ? text.copied : text.copyDenied));
|
|
2663
|
+
try {
|
|
2664
|
+
document.body.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, cancelable: true }));
|
|
2665
|
+
} catch (error) {}
|
|
2666
|
+
});
|
|
2667
|
+
viewport.appendChild(item);
|
|
2668
|
+
};
|
|
2669
|
+
|
|
2670
|
+
const observer = new MutationObserver((records) => {
|
|
2671
|
+
for (const record of records) {
|
|
2672
|
+
for (const added of record.addedNodes) {
|
|
2673
|
+
if (!(added instanceof Element)) continue;
|
|
2674
|
+
if (added.matches(MENU)) {
|
|
2675
|
+
injectCopyItem(added);
|
|
2676
|
+
continue;
|
|
2677
|
+
}
|
|
2678
|
+
added.querySelectorAll(MENU).forEach(injectCopyItem);
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
});
|
|
2682
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
2683
|
+
|
|
2684
|
+
const disposeStyle = injectStyle(CSS, 'file-card-menu.css');
|
|
2685
|
+
return () => {
|
|
2686
|
+
disposeStyle();
|
|
2687
|
+
observer.disconnect();
|
|
2688
|
+
closeMenu();
|
|
2689
|
+
if (toastNode !== null) toastNode.remove();
|
|
2690
|
+
document.removeEventListener('contextmenu', onContextMenu, true);
|
|
2691
|
+
document.removeEventListener('click', onClickCapture, true);
|
|
2692
|
+
document.removeEventListener('pointerdown', onPointerDown, true);
|
|
2693
|
+
document.removeEventListener('keydown', onKeyDown, true);
|
|
2694
|
+
document.removeEventListener('click', onTrackCapture, true);
|
|
2695
|
+
document.removeEventListener('focusin', onTrackCapture, true);
|
|
2696
|
+
window.removeEventListener('resize', closeMenu, true);
|
|
2697
|
+
window.removeEventListener('scroll', closeMenu, true);
|
|
2698
|
+
};
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2701
|
+
/**
|
|
2702
|
+
* "Running subagents" floating panel. While any subagent-origin session in
|
|
2703
|
+
* the sessions list mirror reports `running`, the panel shows every such
|
|
2704
|
+
* subagent with live progress: elapsed active time folded from the
|
|
2705
|
+
* `subagentTiming` projection (settled + open-turn span), token usage
|
|
2706
|
+
* folded from the `tokenUsage` projection, and an indeterminate shimmer
|
|
2707
|
+
* bar. Clicking a row opens that subagent session (catalog address when
|
|
2708
|
+
* retained, plain open otherwise). Reads only leaf fields of the live
|
|
2709
|
+
* list snapshot; every side effect (DOM, subscription, timers) is owned
|
|
2710
|
+
* by the returned disposer.
|
|
2711
|
+
*/
|
|
2712
|
+
function watchRunningSubagents(ctx) {
|
|
2713
|
+
let sessions;
|
|
2714
|
+
try {
|
|
2715
|
+
sessions = ctx.get('sessions');
|
|
2716
|
+
} catch (error) {
|
|
2717
|
+
sessions = undefined;
|
|
2718
|
+
}
|
|
2719
|
+
if (
|
|
2720
|
+
sessions === undefined || sessions === null ||
|
|
2721
|
+
sessions.list === undefined || sessions.list === null ||
|
|
2722
|
+
typeof sessions.list.getSnapshot !== 'function' ||
|
|
2723
|
+
typeof sessions.list.subscribe !== 'function'
|
|
2724
|
+
) {
|
|
2725
|
+
return () => {};
|
|
2726
|
+
}
|
|
2727
|
+
|
|
2728
|
+
let locale;
|
|
2729
|
+
try {
|
|
2730
|
+
locale = ctx.get('locale');
|
|
2731
|
+
} catch (error) {
|
|
2732
|
+
locale = undefined;
|
|
2733
|
+
}
|
|
2734
|
+
let connection;
|
|
2735
|
+
try {
|
|
2736
|
+
connection = ctx.get('connection');
|
|
2737
|
+
} catch (error) {
|
|
2738
|
+
connection = undefined;
|
|
2739
|
+
}
|
|
2740
|
+
const isZh = () => {
|
|
2741
|
+
try {
|
|
2742
|
+
if (locale === undefined || locale === null || locale.getLocale === undefined) return true;
|
|
2743
|
+
const active = locale.getLocale().active;
|
|
2744
|
+
return active === 'zh' || active === 'zh-CN' || active === 'zh-Hans';
|
|
2745
|
+
} catch (error) {
|
|
2746
|
+
return true;
|
|
2747
|
+
}
|
|
2748
|
+
};
|
|
2749
|
+
|
|
2750
|
+
const tokenTotal = (usage) => {
|
|
2751
|
+
if (usage === undefined || usage === null) return undefined;
|
|
2752
|
+
return (
|
|
2753
|
+
(Number(usage.uncachedInputTokens) || 0) +
|
|
2754
|
+
(Number(usage.outputTokens) || 0) +
|
|
2755
|
+
(Number(usage.cacheReadTokens) || 0) +
|
|
2756
|
+
(Number(usage.cacheWriteTokens) || 0)
|
|
2757
|
+
);
|
|
2758
|
+
};
|
|
2759
|
+
const formatTokens = (value) => {
|
|
2760
|
+
const scaled = (next) => (next >= 100 ? String(Math.round(next)) : String(Math.round(next * 10) / 10));
|
|
2761
|
+
if (value < 1000) return String(value);
|
|
2762
|
+
if (value < 1e6) return scaled(value / 1e3) + 'K';
|
|
2763
|
+
return scaled(value / 1e6) + 'M';
|
|
2764
|
+
};
|
|
2765
|
+
const formatDuration = (ms) => {
|
|
2766
|
+
const total = Math.max(0, Math.floor(ms / 1000));
|
|
2767
|
+
const seconds = total % 60;
|
|
2768
|
+
const minutes = Math.floor(total / 60) % 60;
|
|
2769
|
+
const hours = Math.floor(total / 3600);
|
|
2770
|
+
if (isZh()) {
|
|
2771
|
+
if (hours > 0) return hours + '小时' + String(minutes).padStart(2, '0') + '分';
|
|
2772
|
+
if (minutes > 0) return minutes + '分' + String(seconds).padStart(2, '0') + '秒';
|
|
2773
|
+
return seconds + '秒';
|
|
2774
|
+
}
|
|
2775
|
+
if (hours > 0) return hours + 'h ' + String(minutes).padStart(2, '0') + 'm';
|
|
2776
|
+
if (minutes > 0) return minutes + 'm ' + String(seconds).padStart(2, '0') + 's';
|
|
2777
|
+
return seconds + 's';
|
|
2778
|
+
};
|
|
2779
|
+
const runningTitle = (count) => (
|
|
2780
|
+
isZh()
|
|
2781
|
+
? count + ' 个子代理正在运行'
|
|
2782
|
+
: (count === 1 ? '1 subagent running' : count + ' subagents running')
|
|
2783
|
+
);
|
|
2784
|
+
|
|
2785
|
+
const styleTag = document.createElement('style');
|
|
2786
|
+
styleTag.dataset.plugin = PLUGIN_ID;
|
|
2787
|
+
styleTag.dataset.pluginCss = PLUGIN_ID + '/subagent-panel.css';
|
|
2788
|
+
styleTag.textContent = SUBAGENT_PANEL_CSS;
|
|
2789
|
+
document.head.appendChild(styleTag);
|
|
2790
|
+
|
|
2791
|
+
const CHEVRON_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3.5 5 6.5 8 3.5"/></svg>';
|
|
2792
|
+
|
|
2793
|
+
const panel = document.createElement('div');
|
|
2794
|
+
panel.className = 'dshome-sa';
|
|
2795
|
+
panel.hidden = true;
|
|
2796
|
+
panel.setAttribute('role', 'status');
|
|
2797
|
+
panel.setAttribute('aria-live', 'polite');
|
|
2798
|
+
|
|
2799
|
+
const head = document.createElement('div');
|
|
2800
|
+
head.className = 'dshome-sa-head';
|
|
2801
|
+
const pulse = document.createElement('span');
|
|
2802
|
+
pulse.className = 'dshome-sa-pulse';
|
|
2803
|
+
const titleEl = document.createElement('span');
|
|
2804
|
+
titleEl.className = 'dshome-sa-title';
|
|
2805
|
+
const collapseBtn = document.createElement('button');
|
|
2806
|
+
collapseBtn.type = 'button';
|
|
2807
|
+
collapseBtn.className = 'dshome-sa-collapse';
|
|
2808
|
+
collapseBtn.innerHTML = CHEVRON_SVG;
|
|
2809
|
+
head.appendChild(pulse);
|
|
2810
|
+
head.appendChild(titleEl);
|
|
2811
|
+
head.appendChild(collapseBtn);
|
|
2812
|
+
|
|
2813
|
+
const listEl = document.createElement('div');
|
|
2814
|
+
listEl.className = 'dshome-sa-list';
|
|
2815
|
+
panel.appendChild(head);
|
|
2816
|
+
panel.appendChild(listEl);
|
|
2817
|
+
document.body.appendChild(panel);
|
|
2818
|
+
|
|
2819
|
+
const timingOf = (s) => (
|
|
2820
|
+
s.projectionValues === undefined || s.projectionValues === null
|
|
2821
|
+
? undefined
|
|
2822
|
+
: s.projectionValues.subagentTiming
|
|
2823
|
+
);
|
|
2824
|
+
const startOf = (s) => {
|
|
2825
|
+
const t = timingOf(s);
|
|
2826
|
+
const since = t === undefined || t === null || t.active === undefined || t.active === null ? undefined : t.active.since;
|
|
2827
|
+
if (typeof since === 'number') return since;
|
|
2828
|
+
return typeof s.updatedAt === 'number' ? s.updatedAt : 0;
|
|
2829
|
+
};
|
|
2830
|
+
const elapsedMs = (s) => {
|
|
2831
|
+
const t = timingOf(s);
|
|
2832
|
+
if (t === undefined || t === null) return undefined;
|
|
2833
|
+
const settled = typeof t.settledMs === 'number' ? t.settledMs : 0;
|
|
2834
|
+
const since = t.active === undefined || t.active === null ? undefined : t.active.since;
|
|
2835
|
+
if (typeof since !== 'number') return settled;
|
|
2836
|
+
return settled + Math.max(0, Date.now() - since);
|
|
2837
|
+
};
|
|
2838
|
+
const metaText = (s) => {
|
|
2839
|
+
const parts = [];
|
|
2840
|
+
const elapsed = elapsedMs(s);
|
|
2841
|
+
if (elapsed !== undefined) parts.push((isZh() ? '已运行 ' : '') + formatDuration(elapsed));
|
|
2842
|
+
const tokens = tokenTotal(
|
|
2843
|
+
s.projectionValues === undefined || s.projectionValues === null
|
|
2844
|
+
? undefined
|
|
2845
|
+
: s.projectionValues.tokenUsage
|
|
2846
|
+
);
|
|
2847
|
+
if (tokens !== undefined) parts.push(formatTokens(tokens) + ' tok');
|
|
2848
|
+
if (parts.length === 0) parts.push(isZh() ? '正在运行…' : 'running…');
|
|
2849
|
+
return parts.join(' · ');
|
|
2850
|
+
};
|
|
2851
|
+
|
|
2852
|
+
const runningSubagents = () => {
|
|
2853
|
+
try {
|
|
2854
|
+
const snapshot = sessions.list.getSnapshot();
|
|
2855
|
+
if (snapshot === undefined || snapshot === null || snapshot.byId === undefined || snapshot.byId === null) return [];
|
|
2856
|
+
const items = [];
|
|
2857
|
+
for (const key of Object.keys(snapshot.byId)) {
|
|
2858
|
+
const s = snapshot.byId[key];
|
|
2859
|
+
if (s === null || s === undefined) continue;
|
|
2860
|
+
if (s.running !== true || s.origin !== 'subagent') continue;
|
|
2861
|
+
items.push(s);
|
|
2862
|
+
}
|
|
2863
|
+
items.sort((a, b) => startOf(a) - startOf(b));
|
|
2864
|
+
return items;
|
|
2865
|
+
} catch (error) {
|
|
2866
|
+
return [];
|
|
2867
|
+
}
|
|
2868
|
+
};
|
|
2869
|
+
|
|
2870
|
+
const openSubagent = (s) => {
|
|
2871
|
+
try {
|
|
2872
|
+
let address;
|
|
2873
|
+
if (typeof sessions.subagentAddress === 'function') address = sessions.subagentAddress(s.id);
|
|
2874
|
+
if (address !== undefined && address !== null && typeof sessions.openSubagent === 'function') sessions.openSubagent(address);
|
|
2875
|
+
else sessions.open(s.id);
|
|
2876
|
+
} catch (error) {
|
|
2877
|
+
try { sessions.open(s.id); } catch (inner) {}
|
|
2878
|
+
}
|
|
2879
|
+
};
|
|
2880
|
+
|
|
2881
|
+
const STOP_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10" fill="currentColor"><rect x="1" y="1" width="8" height="8" rx="2"/></svg>';
|
|
2882
|
+
|
|
2883
|
+
const stopTitle = () => (isZh() ? '停止此子代理' : 'Stop this subagent');
|
|
2884
|
+
|
|
2885
|
+
const rows = new Map();
|
|
2886
|
+
const buildRow = (s) => {
|
|
2887
|
+
const item = document.createElement('div');
|
|
2888
|
+
item.className = 'dshome-sa-item';
|
|
2889
|
+
item.setAttribute('role', 'button');
|
|
2890
|
+
item.tabIndex = 0;
|
|
2891
|
+
const sweep = document.createElement('span');
|
|
2892
|
+
sweep.className = 'dshome-sa-sweep';
|
|
2893
|
+
item.appendChild(sweep);
|
|
2894
|
+
const dot = document.createElement('span');
|
|
2895
|
+
dot.className = 'dshome-sa-dot';
|
|
2896
|
+
const body = document.createElement('span');
|
|
2897
|
+
body.className = 'dshome-sa-body';
|
|
2898
|
+
const name = document.createElement('span');
|
|
2899
|
+
name.className = 'dshome-sa-name';
|
|
2900
|
+
const meta = document.createElement('span');
|
|
2901
|
+
meta.className = 'dshome-sa-meta';
|
|
2902
|
+
const bar = document.createElement('span');
|
|
2903
|
+
bar.className = 'dshome-sa-bar';
|
|
2904
|
+
const fill = document.createElement('span');
|
|
2905
|
+
fill.className = 'dshome-sa-bar-fill';
|
|
2906
|
+
bar.appendChild(fill);
|
|
2907
|
+
body.appendChild(name);
|
|
2908
|
+
body.appendChild(meta);
|
|
2909
|
+
body.appendChild(bar);
|
|
2910
|
+
item.appendChild(dot);
|
|
2911
|
+
item.appendChild(body);
|
|
2912
|
+
|
|
2913
|
+
const stopBtn = document.createElement('button');
|
|
2914
|
+
stopBtn.type = 'button';
|
|
2915
|
+
stopBtn.className = 'dshome-sa-stop';
|
|
2916
|
+
stopBtn.innerHTML = STOP_SVG;
|
|
2917
|
+
stopBtn.title = stopTitle();
|
|
2918
|
+
stopBtn.setAttribute('aria-label', stopTitle());
|
|
2919
|
+
stopBtn.addEventListener('click', (event) => stopSubagent(s, stopBtn, event));
|
|
2920
|
+
item.appendChild(stopBtn);
|
|
2921
|
+
|
|
2922
|
+
item.addEventListener('click', () => openSubagent(s));
|
|
2923
|
+
item.addEventListener('keydown', (event) => {
|
|
2924
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
2925
|
+
event.preventDefault();
|
|
2926
|
+
openSubagent(s);
|
|
2927
|
+
}
|
|
2928
|
+
});
|
|
2929
|
+
return {
|
|
2930
|
+
root: item,
|
|
2931
|
+
nameEl: name,
|
|
2932
|
+
metaEl: meta,
|
|
2933
|
+
stopEl: stopBtn,
|
|
2934
|
+
sweepEl: sweep,
|
|
2935
|
+
summary: s,
|
|
2936
|
+
phase: 'running',
|
|
2937
|
+
progress: 0,
|
|
2938
|
+
stopStart: 0,
|
|
2939
|
+
completeStart: 0,
|
|
2940
|
+
fillFrom: 0,
|
|
2941
|
+
exitTimer: 0
|
|
2942
|
+
};
|
|
2943
|
+
};
|
|
2944
|
+
|
|
2945
|
+
const deniedTimers = new Set();
|
|
2946
|
+
const flashDenied = (btn) => {
|
|
2947
|
+
btn.classList.add('dshome-sa-stop-denied');
|
|
2948
|
+
const timer = setTimeout(() => {
|
|
2949
|
+
deniedTimers.delete(timer);
|
|
2950
|
+
btn.classList.remove('dshome-sa-stop-denied');
|
|
2951
|
+
}, 1200);
|
|
2952
|
+
deniedTimers.add(timer);
|
|
2953
|
+
};
|
|
2954
|
+
|
|
2955
|
+
const easeOutCubic = (k) => 1 - Math.pow(1 - k, 3);
|
|
2956
|
+
/**
|
|
2957
|
+
* Fake stopping progress: sprint to 80% within the first second,
|
|
2958
|
+
* then crawl asymptotically toward 95% — never 100% on its own;
|
|
2959
|
+
* termination is what completes the bar.
|
|
2960
|
+
*/
|
|
2961
|
+
const stoppingProgress = (elapsedSec) => {
|
|
2962
|
+
if (elapsedSec <= 1) return 0.8 * easeOutCubic(elapsedSec);
|
|
2963
|
+
return Math.min(0.95, 0.8 + 0.15 * (1 - Math.exp(-(elapsedSec - 1) / 3)));
|
|
2964
|
+
};
|
|
2965
|
+
|
|
2966
|
+
let sweepRaf = 0;
|
|
2967
|
+
const sweepTick = () => {
|
|
2968
|
+
const now = performance.now();
|
|
2969
|
+
let any = false;
|
|
2970
|
+
for (const row of rows.values()) {
|
|
2971
|
+
if (row.phase === 'stopping') {
|
|
2972
|
+
any = true;
|
|
2973
|
+
row.progress = stoppingProgress((now - row.stopStart) / 1000);
|
|
2974
|
+
row.sweepEl.style.width = (row.progress * 100).toFixed(2) + '%';
|
|
2975
|
+
} else if (row.phase === 'completing') {
|
|
2976
|
+
any = true;
|
|
2977
|
+
const t = (now - row.completeStart) / 1000;
|
|
2978
|
+
if (t < 0.3) {
|
|
2979
|
+
const k = easeOutCubic(Math.min(1, t / 0.3));
|
|
2980
|
+
row.progress = row.fillFrom + (1 - row.fillFrom) * k;
|
|
2981
|
+
row.sweepEl.style.width = (row.progress * 100).toFixed(2) + '%';
|
|
2982
|
+
} else if (t < 0.6) {
|
|
2983
|
+
row.sweepEl.style.width = '100%';
|
|
2984
|
+
} else {
|
|
2985
|
+
row.phase = 'exiting';
|
|
2986
|
+
row.root.classList.add('dshome-sa-item-exit');
|
|
2987
|
+
row.exitTimer = setTimeout(() => {
|
|
2988
|
+
row.root.remove();
|
|
2989
|
+
rows.delete(row.summary.id);
|
|
2990
|
+
syncTicker();
|
|
2991
|
+
}, 240);
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
if (any) sweepRaf = requestAnimationFrame(sweepTick);
|
|
2996
|
+
else sweepRaf = 0;
|
|
2997
|
+
};
|
|
2998
|
+
|
|
2999
|
+
const hasStopping = () => {
|
|
3000
|
+
for (const row of rows.values()) {
|
|
3001
|
+
if (row.phase === 'stopping' || row.phase === 'completing' || row.phase === 'exiting') return true;
|
|
3002
|
+
}
|
|
3003
|
+
return false;
|
|
3004
|
+
};
|
|
3005
|
+
|
|
3006
|
+
const startStopProgress = (row) => {
|
|
3007
|
+
row.phase = 'stopping';
|
|
3008
|
+
row.progress = 0;
|
|
3009
|
+
row.stopStart = performance.now();
|
|
3010
|
+
row.root.classList.add('dshome-sa-stopping');
|
|
3011
|
+
row.stopEl.classList.remove('dshome-sa-stop-busy');
|
|
3012
|
+
row.sweepEl.style.width = '0%';
|
|
3013
|
+
if (sweepRaf === 0) sweepRaf = requestAnimationFrame(sweepTick);
|
|
3014
|
+
};
|
|
3015
|
+
|
|
3016
|
+
/** Undo the optimistic stopping state when the cancel call is rejected. */
|
|
3017
|
+
const rollbackStop = (row, btn) => {
|
|
3018
|
+
if (btn !== undefined && btn !== null) {
|
|
3019
|
+
btn.disabled = false;
|
|
3020
|
+
btn.classList.remove('dshome-sa-stop-busy');
|
|
3021
|
+
flashDenied(btn);
|
|
3022
|
+
}
|
|
3023
|
+
if (row === undefined) return;
|
|
3024
|
+
if (rows.get(row.summary.id) !== row) return;
|
|
3025
|
+
if (row.phase !== 'stopping') return;
|
|
3026
|
+
row.phase = 'running';
|
|
3027
|
+
row.progress = 0;
|
|
3028
|
+
row.root.classList.remove('dshome-sa-stopping');
|
|
3029
|
+
row.sweepEl.style.width = '0%';
|
|
3030
|
+
};
|
|
3031
|
+
|
|
3032
|
+
const beginCompleting = (row) => {
|
|
3033
|
+
row.phase = 'completing';
|
|
3034
|
+
row.completeStart = performance.now();
|
|
3035
|
+
row.fillFrom = row.progress;
|
|
3036
|
+
if (sweepRaf === 0) sweepRaf = requestAnimationFrame(sweepTick);
|
|
3037
|
+
};
|
|
3038
|
+
|
|
3039
|
+
/**
|
|
3040
|
+
* Durable parent/child address for the subagents.interrupt RPC.
|
|
3041
|
+
* The mode comes from the child's own identity projection; a parent id
|
|
3042
|
+
* is required — generic host routing rejects subagent-owned sessions,
|
|
3043
|
+
* so the interrupt must ride the address, not sessions.cancel.
|
|
3044
|
+
*/
|
|
3045
|
+
const addressOf = (s) => {
|
|
3046
|
+
const identity = s.projectionValues === undefined || s.projectionValues === null ? undefined : s.projectionValues.subagent;
|
|
3047
|
+
let mode = 'continuable';
|
|
3048
|
+
if (identity !== undefined && identity !== null && (identity.mode === 'one-shot' || identity.mode === 'continuable')) mode = identity.mode;
|
|
3049
|
+
if (typeof s.parentId !== 'string' || s.parentId === '') return undefined;
|
|
3050
|
+
return { parentSessionId: s.parentId, childSessionId: s.id, mode };
|
|
3051
|
+
};
|
|
3052
|
+
|
|
3053
|
+
const stopSubagent = (s, btn, event) => {
|
|
3054
|
+
if (event !== undefined && event !== null) {
|
|
3055
|
+
event.preventDefault();
|
|
3056
|
+
event.stopPropagation();
|
|
3057
|
+
}
|
|
3058
|
+
const row = rows.get(s.id);
|
|
3059
|
+
if (row !== undefined && row.phase !== 'running') return;
|
|
3060
|
+
let face;
|
|
3061
|
+
try {
|
|
3062
|
+
const binding = sessions.binding(s.id);
|
|
3063
|
+
face = binding === undefined || binding === null ? undefined : binding.session;
|
|
3064
|
+
} catch (error) {
|
|
3065
|
+
face = undefined;
|
|
3066
|
+
}
|
|
3067
|
+
const address = addressOf(s);
|
|
3068
|
+
const api = connection === undefined || connection === null ? undefined : connection.api;
|
|
3069
|
+
const canInterrupt = address !== undefined && address.mode === 'continuable' && api !== undefined && api !== null && api.subagents !== undefined && api.subagents !== null && typeof api.subagents.interrupt === 'function';
|
|
3070
|
+
const canFaceCancel = face !== undefined && face !== null && typeof face.cancel === 'function';
|
|
3071
|
+
if (!canInterrupt && !canFaceCancel) {
|
|
3072
|
+
flashDenied(btn);
|
|
3073
|
+
return;
|
|
3074
|
+
}
|
|
3075
|
+
btn.disabled = true;
|
|
3076
|
+
// Optimistic feedback: the red line + sweep start the moment the
|
|
3077
|
+
// button is pressed; a rejected cancel rolls them back.
|
|
3078
|
+
if (row !== undefined) startStopProgress(row);
|
|
3079
|
+
let cancelPromise;
|
|
3080
|
+
try {
|
|
3081
|
+
cancelPromise = canInterrupt
|
|
3082
|
+
? Promise.resolve(api.subagents.interrupt(address))
|
|
3083
|
+
: Promise.resolve(face.cancel());
|
|
3084
|
+
} catch (error) {
|
|
3085
|
+
rollbackStop(row, btn);
|
|
3086
|
+
return;
|
|
3087
|
+
}
|
|
3088
|
+
cancelPromise.then((result) => {
|
|
3089
|
+
const ok = result !== undefined && result !== null && result.ok === true;
|
|
3090
|
+
if (!ok) rollbackStop(row, btn);
|
|
3091
|
+
}).catch(() => {
|
|
3092
|
+
rollbackStop(row, btn);
|
|
3093
|
+
});
|
|
3094
|
+
};
|
|
3095
|
+
|
|
3096
|
+
let hideTimer = 0;
|
|
3097
|
+
const hide = () => {
|
|
3098
|
+
if (panel.hidden || hideTimer !== 0) return;
|
|
3099
|
+
panel.classList.add('dshome-sa-leave');
|
|
3100
|
+
hideTimer = setTimeout(() => {
|
|
3101
|
+
hideTimer = 0;
|
|
3102
|
+
panel.hidden = true;
|
|
3103
|
+
panel.classList.remove('dshome-sa-leave');
|
|
3104
|
+
}, 240);
|
|
3105
|
+
};
|
|
3106
|
+
|
|
3107
|
+
let tickTimer = 0;
|
|
3108
|
+
const tick = () => {
|
|
3109
|
+
if (panel.hidden) return;
|
|
3110
|
+
for (const row of rows.values()) {
|
|
3111
|
+
if (row.summary.running !== true) continue;
|
|
3112
|
+
row.metaEl.textContent = metaText(row.summary);
|
|
3113
|
+
}
|
|
3114
|
+
};
|
|
3115
|
+
const syncTicker = () => {
|
|
3116
|
+
if (rows.size > 0 && tickTimer === 0) {
|
|
3117
|
+
tickTimer = setInterval(tick, 1000);
|
|
3118
|
+
} else if (rows.size === 0 && tickTimer !== 0) {
|
|
3119
|
+
clearInterval(tickTimer);
|
|
3120
|
+
tickTimer = 0;
|
|
3121
|
+
}
|
|
3122
|
+
};
|
|
3123
|
+
|
|
3124
|
+
let collapsed = false;
|
|
3125
|
+
const updateCollapsed = () => {
|
|
3126
|
+
panel.classList.toggle('dshome-sa-collapsed', collapsed);
|
|
3127
|
+
const label = collapsed ? (isZh() ? '展开' : 'Expand') : (isZh() ? '收起' : 'Collapse');
|
|
3128
|
+
collapseBtn.setAttribute('aria-label', label);
|
|
3129
|
+
collapseBtn.title = label;
|
|
3130
|
+
};
|
|
3131
|
+
collapseBtn.addEventListener('click', () => {
|
|
3132
|
+
collapsed = !collapsed;
|
|
3133
|
+
updateCollapsed();
|
|
3134
|
+
});
|
|
3135
|
+
|
|
3136
|
+
const render = () => {
|
|
3137
|
+
const items = runningSubagents();
|
|
3138
|
+
const stopping = hasStopping();
|
|
3139
|
+
if (items.length === 0 && !stopping) {
|
|
3140
|
+
titleEl.textContent = runningTitle(0);
|
|
3141
|
+
hide();
|
|
3142
|
+
return;
|
|
3143
|
+
}
|
|
3144
|
+
titleEl.textContent = items.length > 0 ? runningTitle(items.length) : (isZh() ? '正在停止子代理…' : 'Stopping subagent…');
|
|
3145
|
+
if (hideTimer !== 0) {
|
|
3146
|
+
clearTimeout(hideTimer);
|
|
3147
|
+
hideTimer = 0;
|
|
3148
|
+
}
|
|
3149
|
+
panel.classList.remove('dshome-sa-leave');
|
|
3150
|
+
panel.hidden = false;
|
|
3151
|
+
const seen = new Set();
|
|
3152
|
+
for (const s of items) {
|
|
3153
|
+
seen.add(s.id);
|
|
3154
|
+
let row = rows.get(s.id);
|
|
3155
|
+
if (row === undefined) {
|
|
3156
|
+
row = buildRow(s);
|
|
3157
|
+
rows.set(s.id, row);
|
|
3158
|
+
listEl.appendChild(row.root);
|
|
3159
|
+
}
|
|
3160
|
+
row.summary = s;
|
|
3161
|
+
const name = s.displayTitle || s.id;
|
|
3162
|
+
row.nameEl.textContent = name;
|
|
3163
|
+
row.root.title = name;
|
|
3164
|
+
row.metaEl.textContent = metaText(s);
|
|
3165
|
+
}
|
|
3166
|
+
for (const entry of Array.from(rows.entries())) {
|
|
3167
|
+
const id = entry[0];
|
|
3168
|
+
const row = entry[1];
|
|
3169
|
+
if (seen.has(id)) continue;
|
|
3170
|
+
if (row.phase === 'stopping') {
|
|
3171
|
+
beginCompleting(row);
|
|
3172
|
+
continue;
|
|
3173
|
+
}
|
|
3174
|
+
if (row.phase === 'completing' || row.phase === 'exiting') continue;
|
|
3175
|
+
row.root.remove();
|
|
3176
|
+
rows.delete(id);
|
|
3177
|
+
}
|
|
3178
|
+
syncTicker();
|
|
3179
|
+
};
|
|
3180
|
+
|
|
3181
|
+
const unsubscribe = sessions.list.subscribe(render);
|
|
3182
|
+
render();
|
|
3183
|
+
updateCollapsed();
|
|
3184
|
+
|
|
3185
|
+
return () => {
|
|
3186
|
+
unsubscribe();
|
|
3187
|
+
if (sweepRaf !== 0) cancelAnimationFrame(sweepRaf);
|
|
3188
|
+
if (tickTimer !== 0) clearInterval(tickTimer);
|
|
3189
|
+
if (hideTimer !== 0) clearTimeout(hideTimer);
|
|
3190
|
+
for (const row of rows.values()) {
|
|
3191
|
+
if (row.exitTimer !== 0) clearTimeout(row.exitTimer);
|
|
3192
|
+
}
|
|
3193
|
+
for (const timer of deniedTimers) clearTimeout(timer);
|
|
3194
|
+
deniedTimers.clear();
|
|
3195
|
+
panel.remove();
|
|
3196
|
+
styleTag.remove();
|
|
3197
|
+
};
|
|
3198
|
+
}
|
|
3199
|
+
|
|
3200
|
+
function LocaleToggle(props) {
|
|
3201
|
+
const locale = props.locale;
|
|
3202
|
+
const [active, setActive] = React.useState('zh');
|
|
3203
|
+
|
|
3204
|
+
React.useEffect(() => {
|
|
3205
|
+
if (locale === undefined) return;
|
|
3206
|
+
const sync = () => {
|
|
3207
|
+
try { setActive(locale.getLocale().active); } catch (e) {}
|
|
3208
|
+
};
|
|
3209
|
+
sync();
|
|
3210
|
+
return locale.subscribe(sync);
|
|
3211
|
+
}, [locale]);
|
|
3212
|
+
|
|
3213
|
+
const pick = (id) => () => {
|
|
3214
|
+
if (locale === undefined) return;
|
|
3215
|
+
try { locale.setLocale(id); } catch (e) {}
|
|
3216
|
+
};
|
|
3217
|
+
|
|
3218
|
+
const isZh = active === 'zh' || active === 'zh-CN' || active === 'zh-Hans';
|
|
3219
|
+
|
|
3220
|
+
return React.createElement('div', { className: 'dshome-locale', role: 'group', 'aria-label': 'Language' },
|
|
3221
|
+
React.createElement('button', { type: 'button', className: isZh ? 'dshome-on' : '', onClick: pick('zh') },
|
|
3222
|
+
React.createElement('span', { className: 'dshome-full' }, '中文'),
|
|
3223
|
+
React.createElement('span', { className: 'dshome-short' }, '中')),
|
|
3224
|
+
React.createElement('button', { type: 'button', className: !isZh ? 'dshome-on' : '', onClick: pick('en') },
|
|
3225
|
+
React.createElement('span', { className: 'dshome-full' }, 'EN'),
|
|
3226
|
+
React.createElement('span', { className: 'dshome-short' }, 'E'))
|
|
3227
|
+
);
|
|
3228
|
+
}
|
|
3229
|
+
|
|
3230
|
+
// ── DSTT (DeepSeekStyleTheme) settings + peak-hour color driver ──────
|
|
3231
|
+
// The host half owns the durable `deepseek-style-theme` settings
|
|
3232
|
+
// namespace; the browser half reads/writes one `mode` enum over this
|
|
3233
|
+
// plugin's private loopback channel — the settings RPC surface only
|
|
3234
|
+
// serves the core's allowlisted namespaces. A shared in-memory mirror
|
|
3235
|
+
// feeds the event-chain clock, which checks the Beijing time once, then
|
|
3236
|
+
// schedules exactly the next boundary switch — vivid red (鲜红) inside a
|
|
3237
|
+
// peak window (Beijing 9:00–12:00, 14:00–18:00), blue or green outside
|
|
3238
|
+
// per mode. One-minute granularity (ceiling) per the user's rule.
|
|
3239
|
+
const DSTT_NS = "deepseek-style-theme";
|
|
3240
|
+
const DSTT_FIELD = "mode";
|
|
3241
|
+
const DSTT_MODES = ["peakvalley-redblue", "peakvalley-redgreen", "always-green", "always-blue"];
|
|
3242
|
+
const DSTT_LABELS = {
|
|
3243
|
+
"peakvalley-redblue": { zh: "峰谷红蓝", en: "Peak R·B" },
|
|
3244
|
+
"peakvalley-redgreen": { zh: "峰谷红绿", en: "Peak R·G" },
|
|
3245
|
+
"always-green": { zh: "常态绿", en: "Always G" },
|
|
3246
|
+
"always-blue": { zh: "常态蓝", en: "Always B" }
|
|
3247
|
+
};
|
|
3248
|
+
/** DSTT private-channel endpoints (host half in lib/index.js). */
|
|
3249
|
+
const DSTT_GET = "dstt.mode.get";
|
|
3250
|
+
const DSTT_SET = "dstt.mode.set";
|
|
3251
|
+
/** Peak windows, [startHour, endHour), judged in Beijing time (UTC+8). */
|
|
3252
|
+
const PEAK_RANGES = [[9, 12], [14, 18]];
|
|
3253
|
+
// The two glass recipes the stylesheet gates on <html data-dshome-glass>.
|
|
3254
|
+
// `liquid` is the default; `frosted` is the pane this theme shipped before.
|
|
3255
|
+
const DSTT_GLASS_STYLES = ["liquid", "frosted"];
|
|
3256
|
+
const DSTT_GLASS_DEFAULT = "liquid";
|
|
3257
|
+
const DSTT_GLASS_LABELS = {
|
|
3258
|
+
liquid: { zh: "液态", en: "Liquid" },
|
|
3259
|
+
frosted: { zh: "白磨砂", en: "Frosted" }
|
|
3260
|
+
};
|
|
3261
|
+
|
|
3262
|
+
/**
|
|
3263
|
+
* Shared in-memory mirror of the durable DSTT mode. The settings domain's
|
|
3264
|
+
* RPC only serves allowlisted namespaces, so the mode travels over this
|
|
3265
|
+
* plugin's own loopback channel instead; every reader (the color driver,
|
|
3266
|
+
* the settings tab) subscribes to this mirror.
|
|
3267
|
+
*/
|
|
3268
|
+
const dsttState = { mode: "peakvalley-redblue", fluidBrush: false, glassStyle: DSTT_GLASS_DEFAULT, listeners: new Set() };
|
|
3269
|
+
const dsttGetMode = () => dsttState.mode;
|
|
3270
|
+
const dsttGetGlass = () => (DSTT_GLASS_STYLES.includes(dsttState.glassStyle) ? dsttState.glassStyle : DSTT_GLASS_DEFAULT);
|
|
3271
|
+
/** Persist the glass recipe through the bridge; resolves with success. */
|
|
3272
|
+
async function dsttSetGlass(ctx, next) {
|
|
3273
|
+
if (!DSTT_GLASS_STYLES.includes(next)) return false;
|
|
3274
|
+
try {
|
|
3275
|
+
const result = await bridgeCall(DSTT_SET, { mode: dsttState.mode, glassStyle: next });
|
|
3276
|
+
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;
|
|
3280
|
+
}
|
|
3281
|
+
} catch (error) {
|
|
3282
|
+
// Fall through to the caller's failure handling.
|
|
3283
|
+
}
|
|
3284
|
+
return false;
|
|
3285
|
+
}
|
|
3286
|
+
const dsttGetBrush = () => dsttState.fluidBrush === true;
|
|
3287
|
+
/** Persist the fluid-brush preference (off by default) through the bridge. */
|
|
3288
|
+
async function dsttSetBrush(ctx, next) {
|
|
3289
|
+
try {
|
|
3290
|
+
const result = await bridgeCall(DSTT_SET, { mode: dsttState.mode, fluidBrush: next === true });
|
|
3291
|
+
if (result !== undefined && result !== null && result.ok === true) {
|
|
3292
|
+
dsttState.fluidBrush = result.value.fluidBrush === true;
|
|
3293
|
+
dsttNotify();
|
|
3294
|
+
return true;
|
|
3295
|
+
}
|
|
3296
|
+
} catch (error) {
|
|
3297
|
+
// Fall through to the caller's failure handling.
|
|
3298
|
+
}
|
|
3299
|
+
return false;
|
|
3300
|
+
}
|
|
3301
|
+
const dsttSubscribe = (listener) => {
|
|
3302
|
+
dsttState.listeners.add(listener);
|
|
3303
|
+
return () => dsttState.listeners.delete(listener);
|
|
3304
|
+
};
|
|
3305
|
+
const dsttNotify = () => {
|
|
3306
|
+
for (const listener of dsttState.listeners) {
|
|
3307
|
+
try { listener(); } catch (e) {}
|
|
3308
|
+
}
|
|
3309
|
+
};
|
|
3310
|
+
/** Path of this plugin's private host bridge (a webServer prefix route). */
|
|
3311
|
+
const BRIDGE_PATH = '/dshome-open-workspace';
|
|
3312
|
+
|
|
3313
|
+
/**
|
|
3314
|
+
* Call the host bridge. The host half owns a plain `webServer` prefix route
|
|
3315
|
+
* (dsh 0.1.5's connection carrier cannot host this plugin's channel without
|
|
3316
|
+
* tripping cordis' webServer guard), reached with a same-origin fetch so the
|
|
3317
|
+
* browser session authenticates it like any other web-app request. Resolves
|
|
3318
|
+
* to the RpcResult-shaped JSON, throws on a transport failure.
|
|
3319
|
+
*/
|
|
3320
|
+
async function bridgeCall(endpoint, payload) {
|
|
3321
|
+
const response = await fetch(BRIDGE_PATH, {
|
|
3322
|
+
method: 'POST',
|
|
3323
|
+
headers: { 'content-type': 'application/json' },
|
|
3324
|
+
body: JSON.stringify({ endpoint: endpoint, payload: payload })
|
|
3325
|
+
});
|
|
3326
|
+
if (!response.ok) throw new Error('bridge http ' + response.status);
|
|
3327
|
+
return await response.json();
|
|
3328
|
+
}
|
|
3329
|
+
|
|
3330
|
+
/** Pull the durable mode from the host half once. */
|
|
3331
|
+
async function dsttSync(ctx) {
|
|
3332
|
+
try {
|
|
3333
|
+
const result = await bridgeCall(DSTT_GET, {});
|
|
3334
|
+
if (result !== undefined && result !== null && result.ok === true && DSTT_MODES.includes(result.value.mode)) {
|
|
3335
|
+
dsttState.mode = result.value.mode;
|
|
3336
|
+
dsttState.fluidBrush = result.value.fluidBrush === true;
|
|
3337
|
+
// 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;
|
|
3340
|
+
dsttNotify();
|
|
3341
|
+
}
|
|
3342
|
+
} catch (error) {}
|
|
3343
|
+
}
|
|
3344
|
+
/** Persist a mode choice through the host half; resolves with success. */
|
|
3345
|
+
async function dsttSetMode(ctx, mode) {
|
|
3346
|
+
if (!DSTT_MODES.includes(mode)) return false;
|
|
3347
|
+
try {
|
|
3348
|
+
const result = await bridgeCall(DSTT_SET, { mode });
|
|
3349
|
+
if (result !== undefined && result !== null && result.ok === true && DSTT_MODES.includes(result.value.mode)) {
|
|
3350
|
+
dsttState.mode = result.value.mode;
|
|
3351
|
+
dsttNotify();
|
|
3352
|
+
return true;
|
|
3353
|
+
}
|
|
3354
|
+
} catch (error) {}
|
|
3355
|
+
return false;
|
|
3356
|
+
}
|
|
3357
|
+
|
|
3358
|
+
/**
|
|
3359
|
+
* Current time shifted onto the Beijing clock. Date#getTime() is already a
|
|
3360
|
+
* UTC instant, so Beijing time is exactly `now + 8h` regardless of the
|
|
3361
|
+
* machine's own timezone — `getTimezoneOffset()` must NOT be involved
|
|
3362
|
+
* (adding it double-counts on UTC+8 machines and reads the wrong hour).
|
|
3363
|
+
* Beijing has no DST, so the +8h shift is exact.
|
|
3364
|
+
*/
|
|
3365
|
+
function beijingNow() {
|
|
3366
|
+
return new Date(Date.now() + 8 * 3600000);
|
|
3367
|
+
}
|
|
3368
|
+
|
|
3369
|
+
/** Beijing wall-clock HH:MM:SS for logs. */
|
|
3370
|
+
function beijingClock(bj) {
|
|
3371
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
3372
|
+
return pad(bj.getUTCHours()) + ':' + pad(bj.getUTCMinutes()) + ':' + pad(bj.getUTCSeconds()) + ' (UTC+8)';
|
|
3373
|
+
}
|
|
3374
|
+
|
|
3375
|
+
/**
|
|
3376
|
+
* Round a Beijing time UP to the next whole minute (ceiling on the second
|
|
3377
|
+
* field). One-minute granularity: 12:59:17 reads as 13:00 → valley, and
|
|
3378
|
+
* the moment the wall clock hits 14:00 the red takes over; 13:59:30
|
|
3379
|
+
* already counts as 14:00 (inside the window) because it rounds up.
|
|
3380
|
+
*/
|
|
3381
|
+
function ceilToMinute(beijingDate) {
|
|
3382
|
+
const rounded = new Date(beijingDate.getTime());
|
|
3383
|
+
if (rounded.getUTCSeconds() > 0 || rounded.getUTCMilliseconds() > 0) {
|
|
3384
|
+
rounded.setUTCSeconds(0, 0);
|
|
3385
|
+
rounded.setUTCMinutes(rounded.getUTCMinutes() + 1);
|
|
3386
|
+
}
|
|
3387
|
+
return rounded;
|
|
3388
|
+
}
|
|
3389
|
+
|
|
3390
|
+
/** Whether a rounded Beijing minute falls inside a peak window. Weekends
|
|
3391
|
+
* (Beijing Saturday/Sunday) are entirely off-peak. */
|
|
3392
|
+
function isPeakMinute(beijingDate) {
|
|
3393
|
+
const day = beijingDate.getUTCDay();
|
|
3394
|
+
if (day === 0 || day === 6) return false;
|
|
3395
|
+
const hour = beijingDate.getUTCHours();
|
|
3396
|
+
return PEAK_RANGES.some(([from, to]) => hour >= from && hour < to);
|
|
3397
|
+
}
|
|
3398
|
+
|
|
3399
|
+
/** Next Beijing wall-clock occurrence of `hour` (today if still ahead, else tomorrow). */
|
|
3400
|
+
function nextBoundary(beijingDate, hour) {
|
|
3401
|
+
const next = new Date(beijingDate.getTime());
|
|
3402
|
+
next.setUTCHours(hour, 0, 0, 0);
|
|
3403
|
+
if (next.getTime() <= beijingDate.getTime()) next.setUTCDate(next.getUTCDate() + 1);
|
|
3404
|
+
return next;
|
|
3405
|
+
}
|
|
3406
|
+
|
|
3407
|
+
/** The boundary that ends the peak window `beijingDate` is inside, or null. */
|
|
3408
|
+
function endOfWindow(beijingDate) {
|
|
3409
|
+
const hour = beijingDate.getUTCHours();
|
|
3410
|
+
for (const [from, to] of PEAK_RANGES) {
|
|
3411
|
+
if (hour >= from && hour < to) return nextBoundary(beijingDate, to);
|
|
3412
|
+
}
|
|
3413
|
+
return null;
|
|
3414
|
+
}
|
|
3415
|
+
|
|
3416
|
+
/** The earliest future peak-window start, or null if none (defensive). */
|
|
3417
|
+
function nextStart(beijingDate) {
|
|
3418
|
+
const candidates = PEAK_RANGES
|
|
3419
|
+
.map(([from]) => nextBoundary(beijingDate, from))
|
|
3420
|
+
.filter((d) => d.getTime() > beijingDate.getTime())
|
|
3421
|
+
.sort((a, b) => a - b);
|
|
3422
|
+
return candidates.length === 0 ? null : candidates[0];
|
|
3423
|
+
}
|
|
3424
|
+
|
|
3425
|
+
/** Color-change listeners (particles, UI re-render on switch). */
|
|
3426
|
+
const colorListeners = new Set();
|
|
3427
|
+
const subscribeColor = (fn) => {
|
|
3428
|
+
colorListeners.add(fn);
|
|
3429
|
+
return () => colorListeners.delete(fn);
|
|
3430
|
+
};
|
|
3431
|
+
const notifyColor = (color) => {
|
|
3432
|
+
for (const fn of colorListeners) {
|
|
3433
|
+
try { fn(color); } catch (e) {}
|
|
3434
|
+
}
|
|
3435
|
+
};
|
|
3436
|
+
const isTimeSliced = (mode) => mode === "peakvalley-redblue" || mode === "peakvalley-redgreen";
|
|
3437
|
+
const colorFor = (mode, peak) => {
|
|
3438
|
+
if (mode === "always-blue") return "blue";
|
|
3439
|
+
if (mode === "always-green") return "green";
|
|
3440
|
+
return peak ? "red" : (mode === "peakvalley-redgreen" ? "green" : "blue");
|
|
3441
|
+
};
|
|
3442
|
+
|
|
3443
|
+
/**
|
|
3444
|
+
* Color driver — event chain, no polling. On start it checks the
|
|
3445
|
+
* Beijing clock once, then schedules exactly the next boundary switch:
|
|
3446
|
+
* inside a window → vivid red now, record the delta to the window end,
|
|
3447
|
+
* flip back to the valley color when it elapses; outside → valley color,
|
|
3448
|
+
* flip to red at the next window start. Toggling the DSTT mode
|
|
3449
|
+
* re-anchors immediately; closing the feature tears every timer down. A
|
|
3450
|
+
* `visibilitychange` re-anchors the chain from the live clock in case a
|
|
3451
|
+
* throttled background tab drifted.
|
|
3452
|
+
*/
|
|
3453
|
+
function watchPeakHour(ctx) {
|
|
3454
|
+
let chainTimer = 0;
|
|
3455
|
+
let stopped = false;
|
|
3456
|
+
let unsubscribe = null;
|
|
3457
|
+
|
|
3458
|
+
const applyColor = () => {
|
|
3459
|
+
if (stopped) return;
|
|
3460
|
+
const mode = dsttGetMode();
|
|
3461
|
+
const peak = isTimeSliced(mode) && isPeakMinute(ceilToMinute(beijingNow()));
|
|
3462
|
+
const color = colorFor(mode, peak);
|
|
3463
|
+
if (document.body.getAttribute('data-dshome-color') !== color) {
|
|
3464
|
+
document.body.setAttribute('data-dshome-color', color);
|
|
3465
|
+
console.info('[' + PLUGIN_ID + '] color -> ' + color + (peak ? ' (peak)' : ' (valley)') + ' @ ' + beijingClock(beijingNow()));
|
|
3466
|
+
notifyColor(color);
|
|
3467
|
+
}
|
|
3468
|
+
};
|
|
3469
|
+
|
|
3470
|
+
const schedule = (ms) => {
|
|
3471
|
+
if (chainTimer !== 0) {
|
|
3472
|
+
clearTimeout(chainTimer);
|
|
3473
|
+
chainTimer = 0;
|
|
3474
|
+
}
|
|
3475
|
+
// setTimeout caps near 24.8 days; the longest span here (to
|
|
3476
|
+
// tomorrow's first window start) is far below that. The +500ms
|
|
3477
|
+
// margin re-anchors cleanly across the minute boundary.
|
|
3478
|
+
chainTimer = setTimeout(anchor, Math.max(0, ms) + 500);
|
|
3479
|
+
};
|
|
3480
|
+
|
|
3481
|
+
/** The single entry point: read the mode + Beijing clock, decide, schedule. */
|
|
3482
|
+
const anchor = () => {
|
|
3483
|
+
if (stopped) return;
|
|
3484
|
+
applyColor();
|
|
3485
|
+
const mode = dsttGetMode();
|
|
3486
|
+
if (!isTimeSliced(mode)) return;
|
|
3487
|
+
const now = ceilToMinute(beijingNow());
|
|
3488
|
+
const end = endOfWindow(now);
|
|
3489
|
+
if (end !== null) {
|
|
3490
|
+
schedule(end.getTime() - now.getTime());
|
|
3491
|
+
} else {
|
|
3492
|
+
const start = nextStart(now);
|
|
3493
|
+
if (start !== null) schedule(start.getTime() - now.getTime());
|
|
3494
|
+
}
|
|
3495
|
+
};
|
|
3496
|
+
|
|
3497
|
+
const onVisibility = () => {
|
|
3498
|
+
if (!document.hidden) anchor();
|
|
3499
|
+
};
|
|
3500
|
+
document.addEventListener('visibilitychange', onVisibility);
|
|
3501
|
+
|
|
3502
|
+
anchor();
|
|
3503
|
+
|
|
3504
|
+
try { unsubscribe = dsttSubscribe(anchor); } catch (error) { unsubscribe = null; }
|
|
3505
|
+
|
|
3506
|
+
return () => {
|
|
3507
|
+
stopped = true;
|
|
3508
|
+
if (chainTimer !== 0) clearTimeout(chainTimer);
|
|
3509
|
+
if (unsubscribe !== null) unsubscribe();
|
|
3510
|
+
document.removeEventListener('visibilitychange', onVisibility);
|
|
3511
|
+
};
|
|
3512
|
+
}
|
|
3513
|
+
|
|
3514
|
+
/**
|
|
3515
|
+
* DSTT settings tab (title: DSTT). Renders the four-way mode picker plus
|
|
3516
|
+
* a live status line. Reads/writes the durable namespace through the
|
|
3517
|
+
* host-side RPC mirror; every subscription and timer is owned by the
|
|
3518
|
+
* component.
|
|
3519
|
+
*/
|
|
3520
|
+
function DsttSection(props) {
|
|
3521
|
+
const dstt = props.dstt;
|
|
3522
|
+
const locale = props.locale;
|
|
3523
|
+
const [mode, setMode] = React.useState(() => (dstt === undefined ? 'peakvalley-redblue' : dstt.mode()));
|
|
3524
|
+
const [active, setActive] = React.useState('zh');
|
|
3525
|
+
const [, force] = React.useReducer((x) => x + 1, 0);
|
|
3526
|
+
|
|
3527
|
+
React.useEffect(() => {
|
|
3528
|
+
if (locale === undefined) return;
|
|
3529
|
+
const sync = () => {
|
|
3530
|
+
try { setActive(locale.getLocale().active); } catch (e) {}
|
|
3531
|
+
};
|
|
3532
|
+
sync();
|
|
3533
|
+
return locale.subscribe(sync);
|
|
3534
|
+
}, [locale]);
|
|
3535
|
+
|
|
3536
|
+
React.useEffect(() => {
|
|
3537
|
+
if (dstt === undefined) return () => {};
|
|
3538
|
+
const refresh = () => {
|
|
3539
|
+
try { setMode(dstt.mode()); } catch (e) {}
|
|
3540
|
+
force();
|
|
3541
|
+
};
|
|
3542
|
+
let unsubscribe = null;
|
|
3543
|
+
try { unsubscribe = dstt.subscribe(refresh); } catch (e) { unsubscribe = null; }
|
|
3544
|
+
const unsubscribeColor = subscribeColor(() => force());
|
|
3545
|
+
refresh();
|
|
3546
|
+
const timer = setInterval(() => {
|
|
3547
|
+
force();
|
|
3548
|
+
}, 30000);
|
|
3549
|
+
return () => {
|
|
3550
|
+
clearInterval(timer);
|
|
3551
|
+
if (unsubscribe !== null) unsubscribe();
|
|
3552
|
+
unsubscribeColor();
|
|
3553
|
+
};
|
|
3554
|
+
}, [dstt]);
|
|
3555
|
+
|
|
3556
|
+
const isZh = active === 'zh' || active === 'zh-CN' || active === 'zh-Hans';
|
|
3557
|
+
const nowMin = ceilToMinute(beijingNow());
|
|
3558
|
+
const peakNow = isPeakMinute(nowMin);
|
|
3559
|
+
const end = peakNow ? endOfWindow(nowMin) : null;
|
|
3560
|
+
const deltaMins = end === null ? 0 : Math.max(0, Math.round((end.getTime() - nowMin.getTime()) / 60000));
|
|
3561
|
+
const deltaText = deltaMins <= 0
|
|
3562
|
+
? ''
|
|
3563
|
+
: isZh
|
|
3564
|
+
? (deltaMins >= 60 ? Math.floor(deltaMins / 60) + '小时' + String(deltaMins % 60).padStart(2, '0') + '分' : deltaMins + '分')
|
|
3565
|
+
: (deltaMins >= 60 ? Math.floor(deltaMins / 60) + 'h ' + (deltaMins % 60) + 'm' : deltaMins + 'm');
|
|
3566
|
+
const colorNow = document.body.getAttribute('data-dshome-color') || 'green';
|
|
3567
|
+
const colorName = colorNow === 'red' ? (isZh ? '鲜红' : 'vivid red') : colorNow === 'blue' ? (isZh ? '蓝色' : 'blue') : (isZh ? '绿色' : 'green');
|
|
3568
|
+
const timeSliced = isTimeSliced(mode);
|
|
3569
|
+
const status = mode === 'always-blue'
|
|
3570
|
+
? (isZh ? '固定蓝色模式' : 'Fixed blue')
|
|
3571
|
+
: mode === 'always-green'
|
|
3572
|
+
? (isZh ? '固定绿色模式' : 'Fixed green')
|
|
3573
|
+
: peakNow
|
|
3574
|
+
? (isZh ? '高峰时段 · 鲜红' + (deltaText === '' ? '' : ' · 还剩 ' + deltaText) : 'Peak hours · vivid red' + (deltaText === '' ? '' : ' · ' + deltaText + ' left'))
|
|
3575
|
+
: (isZh ? '谷时段 · ' + (mode === 'peakvalley-redgreen' ? '绿色' : '蓝色') : 'Off-peak · ' + (mode === 'peakvalley-redgreen' ? 'green' : 'blue'));
|
|
3576
|
+
|
|
3577
|
+
const pickMode = (next) => {
|
|
3578
|
+
if (dstt === undefined || next === mode) return;
|
|
3579
|
+
dstt.set(next);
|
|
3580
|
+
};
|
|
3581
|
+
// The brush is a durable preference, not local state: flipping it asks the
|
|
3582
|
+
// host to persist it and the mirror re-renders this panel.
|
|
3583
|
+
const brushOn = dstt !== undefined && typeof dstt.brush === 'function' ? dstt.brush() === true : false;
|
|
3584
|
+
const toggleBrush = () => {
|
|
3585
|
+
if (dstt === undefined || typeof dstt.setBrush !== 'function') return;
|
|
3586
|
+
dstt.setBrush(!brushOn);
|
|
3587
|
+
};
|
|
3588
|
+
// The glass recipe is durable too, and the mirror re-renders this panel
|
|
3589
|
+
// when the host confirms the write.
|
|
3590
|
+
const glassNow = dstt !== undefined && typeof dstt.glass === 'function' ? dstt.glass() : DSTT_GLASS_DEFAULT;
|
|
3591
|
+
const pickGlass = (next) => {
|
|
3592
|
+
if (dstt === undefined || typeof dstt.setGlass !== 'function' || next === glassNow) return;
|
|
3593
|
+
dstt.setGlass(next);
|
|
3594
|
+
};
|
|
3595
|
+
|
|
3596
|
+
return React.createElement('div', { className: 'dshome-dstt' },
|
|
3597
|
+
React.createElement('h3', null,
|
|
3598
|
+
'DeepSeekStyleTheme',
|
|
3599
|
+
React.createElement('span', { className: 'dshome-dstt-tag' }, 'DSTT')),
|
|
3600
|
+
React.createElement('p', null, isZh
|
|
3601
|
+
? '谷时段默认绿色;高峰时段(北京时间 9:00–12:00、14:00–18:00,周六周日全天非高峰)自动切换为鲜红提示。峰谷模式可选谷色为蓝或绿,常态模式全天固定。'
|
|
3602
|
+
: 'Off-peak green; peak hours (Beijing 9:00–12:00, 14:00–18:00, all day off-peak on weekends) switch to vivid red. Peak/valley modes pick the valley color (blue or green); always modes stay fixed all day.'),
|
|
3603
|
+
React.createElement('div', { className: 'dshome-dstt-row' },
|
|
3604
|
+
React.createElement('div', { className: 'dshome-dstt-row-body' },
|
|
3605
|
+
React.createElement('div', { className: 'dshome-dstt-row-title' }, isZh ? '主题模式' : 'Theme mode'),
|
|
3606
|
+
React.createElement('div', { className: 'dshome-dstt-row-hint' }, isZh
|
|
3607
|
+
? '峰谷红蓝 / 峰谷红绿:高峰鲜红、谷时段蓝 / 绿;常态绿 / 常态蓝:全天固定'
|
|
3608
|
+
: 'Peak R·B / Peak R·G: red at peak, blue/green at valley; Always G/B: fixed all day')),
|
|
3609
|
+
React.createElement('div', { className: 'dshome-dstt-seg', role: 'radiogroup', 'aria-label': isZh ? '主题模式' : 'Theme mode' },
|
|
3610
|
+
DSTT_MODES.map((m) => React.createElement('button', {
|
|
3611
|
+
key: m,
|
|
3612
|
+
type: 'button',
|
|
3613
|
+
role: 'radio',
|
|
3614
|
+
'aria-checked': mode === m,
|
|
3615
|
+
className: 'dshome-dstt-seg-btn' + (mode === m ? ' dshome-on' : ''),
|
|
3616
|
+
onClick: () => pickMode(m)
|
|
3617
|
+
}, isZh ? DSTT_LABELS[m].zh : DSTT_LABELS[m].en)))),
|
|
3618
|
+
React.createElement('div', { className: 'dshome-dstt-row' },
|
|
3619
|
+
React.createElement('div', { className: 'dshome-dstt-row-body' },
|
|
3620
|
+
React.createElement('div', { className: 'dshome-dstt-row-title' }, isZh ? '流体跟随笔刷' : 'Fluid follow brush'),
|
|
3621
|
+
React.createElement('div', { className: 'dshome-dstt-row-hint' }, isZh
|
|
3622
|
+
? '开启后光标会把背景流场搅出一条跟随的尾迹。它是整个模拟里最贵的一部分,默认关闭;改完刷新页面生效。'
|
|
3623
|
+
: 'Lets the cursor stir a trail into the background flow. It is the most expensive part of the simulation, so it is off by default; reload after changing.')),
|
|
3624
|
+
React.createElement('button', {
|
|
3625
|
+
type: 'button',
|
|
3626
|
+
role: 'switch',
|
|
3627
|
+
'aria-checked': brushOn,
|
|
3628
|
+
className: 'dshome-dstt-seg-btn' + (brushOn ? ' dshome-on' : ''),
|
|
3629
|
+
onClick: toggleBrush
|
|
3630
|
+
}, brushOn ? (isZh ? '已开启' : 'On') : (isZh ? '已关闭' : 'Off'))),
|
|
3631
|
+
React.createElement('div', { className: 'dshome-dstt-row' },
|
|
3632
|
+
React.createElement('div', { className: 'dshome-dstt-row-body' },
|
|
3633
|
+
React.createElement('div', { className: 'dshome-dstt-row-title' }, isZh ? '玻璃风格' : 'Glass style'),
|
|
3634
|
+
React.createElement('div', { className: 'dshome-dstt-row-hint' }, isZh
|
|
3635
|
+
? '液态:透镜折射 + 会走圈的边缘高光,更通透(本次方向)。白磨砂:此前那套白磨砂 + 蓝变光边框,观感与从前完全一致。'
|
|
3636
|
+
: 'Liquid: lens refraction plus a highlight that travels the border, far more translucent (the current direction). Frosted: the white frosted pane with the blue shifting border, pixel-identical to what shipped before.')),
|
|
3637
|
+
React.createElement('div', { className: 'dshome-dstt-seg', role: 'radiogroup', 'aria-label': isZh ? '玻璃风格' : 'Glass style' },
|
|
3638
|
+
DSTT_GLASS_STYLES.map((style) => React.createElement('button', {
|
|
3639
|
+
key: style,
|
|
3640
|
+
type: 'button',
|
|
3641
|
+
role: 'radio',
|
|
3642
|
+
'aria-checked': glassNow === style,
|
|
3643
|
+
className: 'dshome-dstt-seg-btn' + (glassNow === style ? ' dshome-on' : ''),
|
|
3644
|
+
onClick: () => pickGlass(style)
|
|
3645
|
+
}, isZh ? DSTT_GLASS_LABELS[style].zh : DSTT_GLASS_LABELS[style].en)))),
|
|
3646
|
+
React.createElement('div', { className: 'dshome-dstt-status', 'data-peak': timeSliced && peakNow },
|
|
3647
|
+
'当前:' + colorName + ' · ' + status));
|
|
3648
|
+
}
|
|
3649
|
+
|
|
3650
|
+
function apply(ctx) {
|
|
3651
|
+
// A `document.querySelector('[data-dsh-probe]')` cleanup used to run
|
|
3652
|
+
// here. Removing an element selected by a generic attribute deletes
|
|
3653
|
+
// whatever else happens to use that attribute, so it is gone: a probe
|
|
3654
|
+
// owns the nodes it creates and removes them itself.
|
|
3655
|
+
|
|
3656
|
+
// Core styles (ours only) are always injected.
|
|
3657
|
+
ctx.effect(() => injectStyle(CORE_CSS, 'theme.css'), 'deepseek-style-theme: core css');
|
|
3658
|
+
ctx.effect(() => injectStyle(ACTIONS_CSS, 'actions.css'), 'deepseek-style-theme: actions css');
|
|
3659
|
+
|
|
3660
|
+
const theme = ctx.get('theme');
|
|
3661
|
+
|
|
3662
|
+
// DSTT: the durable mode travels over this plugin's private loopback
|
|
3663
|
+
// channel (the settings RPC surface only serves allowlisted
|
|
3664
|
+
// namespaces). Pull it once, then let the color driver and the
|
|
3665
|
+
// settings tab share the in-memory mirror.
|
|
3666
|
+
ctx.effect(() => {
|
|
3667
|
+
dsttSync(ctx);
|
|
3668
|
+
}, 'deepseek-style-theme: dstt sync');
|
|
3669
|
+
ctx.effect(() => watchPeakHour(ctx), 'deepseek-style-theme: color driver');
|
|
3670
|
+
|
|
3671
|
+
// Dark marker: owned attribute driven by the theme service, never by
|
|
3672
|
+
// the product's attribute name. Must precede the patch blocks so the
|
|
3673
|
+
// dark selectors match from the first paint.
|
|
3674
|
+
ctx.effect(() => createDarkSync(ctx, theme), 'deepseek-style-theme: dark sync');
|
|
3675
|
+
|
|
3676
|
+
// Glass-recipe marker: same idea, and it has to be on <html> before the
|
|
3677
|
+
// first paint of the glass surfaces or the pane flashes the other recipe.
|
|
3678
|
+
ctx.effect(() => createGlassSync(ctx), 'deepseek-style-theme: glass style sync');
|
|
3679
|
+
|
|
3680
|
+
// Guarded product-surface patches: each block activates only while
|
|
3681
|
+
// its anchor class exists in the DOM.
|
|
3682
|
+
for (const block of PATCH_BLOCKS) {
|
|
3683
|
+
ctx.effect(() => installGuarded(block.anchor, block.css), 'deepseek-style-theme: patch ' + block.anchor);
|
|
3684
|
+
}
|
|
3685
|
+
|
|
3686
|
+
// Liquid glass wins by SPECIFICITY, not by order: the patch blocks are
|
|
3687
|
+
// injected lazily when their anchor appears and set `background`
|
|
3688
|
+
// (shorthand) with !important, so every glass selector below is written
|
|
3689
|
+
// as `html .surface` — one type selector of extra weight — instead of
|
|
3690
|
+
// racing their insertion order.
|
|
3691
|
+
ctx.effect(() => injectStyle(GLASS_CSS, 'glass.css'), 'deepseek-style-theme: liquid glass');
|
|
3692
|
+
// The liquid recipe is a second sheet rather than an edit of the first:
|
|
3693
|
+
// every rule inside is gated on html[data-dshome-glass="liquid"], so the
|
|
3694
|
+
// frosted pane keeps rendering exactly what it rendered before.
|
|
3695
|
+
ctx.effect(() => injectStyle(LIQUID_CSS, 'liquid-glass.css'), 'deepseek-style-theme: apple liquid glass');
|
|
3696
|
+
|
|
3697
|
+
if (theme !== undefined) {
|
|
3698
|
+
const disposeTokens = theme.overrideTokens('deepseek-style-theme', {
|
|
3699
|
+
'--dsw-alias-bg-base': { light: '#f6f7fb', dark: '#0a1a3a' },
|
|
3700
|
+
'--dsw-alias-bg-layer-1': { light: 'rgba(255,255,255,0.72)', dark: 'rgba(15,32,66,0.72)' },
|
|
3701
|
+
'--dsw-alias-bg-layer-2': { light: 'rgba(255,255,255,0.85)', dark: 'rgba(21,42,86,0.8)' },
|
|
3702
|
+
'--dsw-alias-bg-overlay': { light: 'rgba(255,255,255,0.92)', dark: 'rgba(10,26,58,0.94)' },
|
|
3703
|
+
'--dsw-alias-border-l1': { light: 'rgba(0,0,0,0.06)', dark: 'rgba(255,255,255,0.08)' },
|
|
3704
|
+
'--dsw-alias-border-l2': { light: 'rgba(0,0,0,0.1)', dark: 'rgba(255,255,255,0.14)' },
|
|
3705
|
+
'--dsw-alias-brand-primary': { light: 'var(--ds-brand)', dark: 'color-mix(in srgb,var(--ds-brand) 72%,#fff)' },
|
|
3706
|
+
'--dsw-alias-label-primary': { light: '#152443', dark: '#eef2ff' },
|
|
3707
|
+
'--dsw-alias-label-secondary': { light: '#3c4a63', dark: '#a9b8d8' },
|
|
3708
|
+
'--dsw-alias-state-error-primary': { light: '#ef4444', dark: '#ff6b6b' },
|
|
3709
|
+
'--dsw-alias-state-success-primary': { light: '#22c55e', dark: '#4ed17e' },
|
|
3710
|
+
'--dsw-alias-state-warn-primary': { light: '#f59e0b', dark: '#f7ad31' },
|
|
3711
|
+
'--dsw-specific-sidebar-fill': { light: 'rgba(255,255,255,0.5)', dark: 'rgba(10,26,58,0.48)' }
|
|
3712
|
+
});
|
|
3713
|
+
ctx.effect(() => disposeTokens);
|
|
3714
|
+
}
|
|
3715
|
+
|
|
3716
|
+
ctx.effect(() => {
|
|
3717
|
+
try { return startAmbient(); } catch (error) { return () => {}; }
|
|
3718
|
+
}, 'deepseek-style-theme: fluid background');
|
|
3719
|
+
// The composer's hover tilt is independent of the background: it must
|
|
3720
|
+
// survive a machine without WebGL2, where startAmbient() falls back to
|
|
3721
|
+
// the 2D particle field.
|
|
3722
|
+
ctx.effect(() => {
|
|
3723
|
+
try { return startComposerTilt(); } catch (error) { return () => {}; }
|
|
3724
|
+
}, 'deepseek-style-theme: composer hover tilt');
|
|
3725
|
+
ctx.effect(() => {
|
|
3726
|
+
try { return watchSettingsSection(); } catch (error) { return () => {}; }
|
|
3727
|
+
});
|
|
3728
|
+
ctx.effect(() => {
|
|
3729
|
+
try { return watchBrandLink(); } catch (error) { return () => {}; }
|
|
3730
|
+
});
|
|
3731
|
+
ctx.effect(() => {
|
|
3732
|
+
try { return watchHeaderInteraction(); } catch (error) { return () => {}; }
|
|
3733
|
+
});
|
|
3734
|
+
ctx.effect(() => {
|
|
3735
|
+
try { return watchWorkspaceMenu(ctx); } catch (error) { return () => {}; }
|
|
3736
|
+
}, 'deepseek-style-theme: workspace open menu');
|
|
3737
|
+
ctx.effect(() => {
|
|
3738
|
+
try { return watchFileCardMenu(ctx); } catch (error) { return () => {}; }
|
|
3739
|
+
}, 'deepseek-style-theme: delivered file card menu');
|
|
3740
|
+
ctx.effect(() => {
|
|
3741
|
+
try { return watchRunningSubagents(ctx); } catch (error) { return () => {}; }
|
|
3742
|
+
}, 'deepseek-style-theme: running subagents panel');
|
|
3743
|
+
|
|
3744
|
+
const slots = ctx.get('slots');
|
|
3745
|
+
if (slots !== undefined) {
|
|
3746
|
+
slots.inject('conversation.session.header.utilities', () => slots.register(
|
|
3747
|
+
{ name: 'conversation.session.header.utilities', id: 'deepseek-locale', order: 90 },
|
|
3748
|
+
() => React.createElement(LocaleToggle, { locale: ctx.get('locale') })
|
|
3749
|
+
));
|
|
3750
|
+
slots.inject('settings.section', () => slots.register({
|
|
3751
|
+
name: 'settings.section',
|
|
3752
|
+
id: 'dstt',
|
|
3753
|
+
order: 100,
|
|
3754
|
+
label: () => 'DSTT',
|
|
3755
|
+
inject: () => ({
|
|
3756
|
+
dstt: {
|
|
3757
|
+
mode: dsttGetMode,
|
|
3758
|
+
set: (mode) => dsttSetMode(ctx, mode),
|
|
3759
|
+
brush: dsttGetBrush,
|
|
3760
|
+
setBrush: (next) => dsttSetBrush(ctx, next),
|
|
3761
|
+
glass: dsttGetGlass,
|
|
3762
|
+
setGlass: (next) => dsttSetGlass(ctx, next),
|
|
3763
|
+
subscribe: dsttSubscribe
|
|
3764
|
+
},
|
|
3765
|
+
locale: ctx.get('locale')
|
|
3766
|
+
})
|
|
3767
|
+
}, DsttSection));
|
|
3768
|
+
}
|
|
3769
|
+
}
|
|
3770
|
+
|
|
3771
|
+
exports.apply = apply;
|
|
3772
|
+
exports.inject = ['theme', 'slots', 'locale', 'workspaces', 'connection'];
|
|
3773
|
+
return module.exports;
|
|
3774
|
+
}
|
|
3775
|
+
});
|