dsh-deepseek-style-theme 2.0.84 → 2.0.85

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/lib/client.js +155 -38
  2. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -2378,6 +2378,19 @@ html[data-dshome-blur="off"] body{
2378
2378
  // recipe. Both recipes tilt now, and because the liquid sheet used to be
2379
2379
  // the only place carrying `transition: transform`, the motion carries
2380
2380
  // its own transition (TILT_TRANSITION) so a recipe cannot lose it.
2381
+ // * 2.0.85 makes ENGAGEMENT GEOMETRIC and the gates honest. Engagement used
2382
+ // to need a `pointerover` whose target was inside the card; that is a
2383
+ // chain of assumptions about product markup (a wrapper with role=button,
2384
+ // a swapped card node, a portal, an overlay) which a theme cannot verify,
2385
+ // and failing it looks exactly like a broken effect -- reported as
2386
+ // "把我的对话框随着鼠标位置而变化的效果补回来". `spotAt()` decides from the
2387
+ // card's own rectangle on every pointermove, so a missed event costs one
2388
+ // frame instead of the whole effect; `controlAt()` remains the single hit
2389
+ // test, and only to answer "is a control under the pointer". The two
2390
+ // SILENT gates are reported now: `__dshomeTilt()` includes `reducedMotion`
2391
+ // (an OS setting that disables the effect and looks like a bug) and
2392
+ // `spots` (0 means the composer no longer matches TILT_SELECTOR), and the
2393
+ // reduced-motion case logs one info line instead of vanishing.
2381
2394
  const TILT_SELECTOR = '[data-composer-card],.uV2eYG_card';
2382
2395
  const TILT_MAX = 0.0175;
2383
2396
  const TILT_PERSPECTIVE = 800;
@@ -2407,13 +2420,28 @@ html[data-dshome-blur="off"] body{
2407
2420
  const tilted = new WeakSet();
2408
2421
  const settle = new Map();
2409
2422
  const reduced = typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
2410
- if (reduced) return () => {};
2411
- // Readable from the page: the effect is guarded twice, so "it is missing"
2412
- // is otherwise a guess about which guard ate it. `__dshomeTilt()` answers
2413
- // that in one call (and reports the recipe it is running under).
2423
+ // Readable from the page: the effect is guarded several times over, so "it
2424
+ // is missing" is otherwise a guess about which guard ate it.
2425
+ // `__dshomeTilt()` answers that in one call -- including the two SILENT
2426
+ // gates, which is why `reduced` and `spots` are in here at all: an OS-level
2427
+ // "reduce motion" or a composer whose markup no longer matches
2428
+ // TILT_SELECTOR both look exactly like a broken effect.
2414
2429
  try {
2415
- window.__dshomeTilt = () => ({ recipe: dsttGetGlass(), leaning: current !== null, frozen, engagements, sinceRelease: releasedAt === -Infinity ? null : Math.round(performance.now() - releasedAt) });
2430
+ window.__dshomeTilt = () => ({
2431
+ recipe: dsttGetGlass(),
2432
+ reducedMotion: reduced,
2433
+ spots: document.querySelectorAll(TILT_SELECTOR).length,
2434
+ leaning: current !== null,
2435
+ frozen,
2436
+ engagements,
2437
+ sinceRelease: releasedAt === -Infinity ? null : Math.round(performance.now() - releasedAt)
2438
+ });
2416
2439
  } catch (error) {}
2440
+ if (reduced) {
2441
+ // Never silent: this is the one gate a user cannot see in any setting.
2442
+ console.info('[dsh-deepseek-style-theme] composer tilt disabled: the system asks for reduced motion (prefers-reduced-motion: reduce)');
2443
+ return () => {};
2444
+ }
2417
2445
  const release = (spot) => {
2418
2446
  const pending = settle.get(spot);
2419
2447
  if (pending !== undefined) {
@@ -2452,21 +2480,48 @@ html[data-dshome-blur="off"] body{
2452
2480
  tilted.add(spot);
2453
2481
  });
2454
2482
  };
2483
+ /**
2484
+ * Which tilt spot contains a point, by RECTANGLE -- deliberately not by
2485
+ * walking `event.target` up to a selector. Engagement used to depend on a
2486
+ * `pointerover` whose target was inside the card, and that is a chain of
2487
+ * assumptions about the product's markup (a wrapper with role=button, a
2488
+ * swapped-in card node, a portal, an overlay) that a theme cannot verify.
2489
+ * The reported symptom of getting it wrong is indistinguishable from a
2490
+ * broken effect: the box just never leans. Geometry is the one thing the
2491
+ * theme can measure for itself.
2492
+ */
2493
+ const spotAt = (clientX, clientY) => {
2494
+ for (const spot of document.querySelectorAll(TILT_SELECTOR)) {
2495
+ const rect = spot.getBoundingClientRect();
2496
+ if (rect.width <= 0 || rect.height <= 0) continue;
2497
+ if (clientX < rect.left || clientX > rect.right || clientY < rect.top || clientY > rect.bottom) continue;
2498
+ return { spot, rect };
2499
+ }
2500
+ return null;
2501
+ };
2502
+ /** The topmost element at a point, used only to decide "is this a control". */
2503
+ const controlAt = (clientX, clientY) => {
2504
+ let node = null;
2505
+ try { node = document.elementFromPoint(clientX, clientY); } catch (error) { node = null; }
2506
+ if (node === null || typeof node.closest !== 'function') return false;
2507
+ return node.closest(TILT_CONTROLS) !== null;
2508
+ };
2455
2509
  const onOver = (event) => {
2510
+ // Kept for the one thing it does better than a move: it fires the
2511
+ // moment the pointer arrives, before any movement. Everything it
2512
+ // decides is re-derived from geometry, so a missed event costs one
2513
+ // frame, not the effect.
2456
2514
  const target = event.target;
2457
2515
  if (target === null || typeof target.closest !== 'function') return;
2458
2516
  const spot = target.closest(TILT_SELECTOR);
2459
2517
  if (spot === null) return;
2460
- // A control is under the pointer: hold the lean exactly where it is.
2461
- // Releasing here is what used to feed the ~8 Hz hover loop.
2462
- if (target.closest(TILT_CONTROLS) !== null) {
2518
+ const rect = spot.getBoundingClientRect();
2519
+ if (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom) return;
2520
+ if (controlAt(event.clientX, event.clientY)) {
2463
2521
  if (current === spot) frozen = true;
2464
2522
  return;
2465
2523
  }
2466
- // The cooldown gates a fresh engagement only, never an ongoing lean.
2467
2524
  if (!tilted.has(spot) && performance.now() - releasedAt < TILT_COOLDOWN_MS) return;
2468
- const rect = spot.getBoundingClientRect();
2469
- if (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom) return;
2470
2525
  const pending = settle.get(spot);
2471
2526
  if (pending !== undefined) {
2472
2527
  clearTimeout(pending);
@@ -2477,22 +2532,38 @@ html[data-dshome-blur="off"] body{
2477
2532
  paint(spot, rect, event.clientX, event.clientY);
2478
2533
  };
2479
2534
  const onMove = (event) => {
2480
- if (current === null) return;
2481
- const target = event.target;
2482
- if (target !== null && typeof target.closest === 'function' && target.closest(TILT_CONTROLS) !== null) {
2483
- frozen = true;
2535
+ const hit = spotAt(event.clientX, event.clientY);
2536
+ if (hit === null) {
2537
+ // Off the card entirely: release only the spot that was leaning.
2538
+ if (current !== null) {
2539
+ release(current);
2540
+ current = null;
2541
+ }
2542
+ return;
2543
+ }
2544
+ const { spot, rect } = hit;
2545
+ if (controlAt(event.clientX, event.clientY)) {
2546
+ // A control is under the pointer: hold the lean exactly where it is
2547
+ // (releasing here is what used to feed the ~8 Hz hover loop).
2548
+ if (current === spot) frozen = true;
2484
2549
  return;
2485
2550
  }
2551
+ // The cooldown gates a fresh engagement only, never an ongoing lean.
2552
+ if (!tilted.has(spot) && current !== spot && performance.now() - releasedAt < TILT_COOLDOWN_MS) return;
2553
+ if (current !== spot) {
2554
+ if (current !== null) release(current);
2555
+ const pending = settle.get(spot);
2556
+ if (pending !== undefined) {
2557
+ clearTimeout(pending);
2558
+ settle.delete(spot);
2559
+ }
2560
+ current = spot;
2561
+ }
2486
2562
  frozen = false;
2487
- paint(current, current.getBoundingClientRect(), event.clientX, event.clientY);
2563
+ paint(spot, rect, event.clientX, event.clientY);
2488
2564
  };
2489
2565
  const onOut = (event) => {
2490
2566
  if (current === null) return;
2491
- const target = event.target;
2492
- if (target !== null && typeof target.closest === 'function') {
2493
- const spot = target.closest(TILT_SELECTOR);
2494
- if (spot !== null && spot !== current) return;
2495
- }
2496
2567
  const rect = current.getBoundingClientRect();
2497
2568
  if (event.clientX >= rect.left && event.clientX <= rect.right && event.clientY >= rect.top && event.clientY <= rect.bottom) return;
2498
2569
  release(current);
@@ -2531,7 +2602,7 @@ html[data-dshome-blur="off"] body{
2531
2602
  if (!supportsWebgl2()) return startParticles();
2532
2603
  // Which build is running, readable from the page (see README). One line in
2533
2604
  // the console answers "is the bundle I just installed actually live?".
2534
- try { window.__dshomeBuild = '2.0.84 bug-report-fixes'; } catch (error) {}
2605
+ try { window.__dshomeBuild = '2.0.85 glass-extras'; } catch (error) {}
2535
2606
  const canvas = document.createElement('canvas');
2536
2607
  canvas.setAttribute('data-dsh-deepseek-canvas', '');
2537
2608
  const style = canvas.style;
@@ -2573,17 +2644,6 @@ html[data-dshome-blur="off"] body{
2573
2644
  // `attachFluidInteractions` is therefore NOT wired — it is kept in the
2574
2645
  // file, unmodified, because re-enabling button ripples is one call, but
2575
2646
  // nothing in this theme uses it today.
2576
- const stopSpotter = startSpecularSpotter();
2577
- const stopParallax = startSpecularParallax();
2578
- const dispersion = startGlassDispersion({ hue: FLUID_HUES[dshomeColor()] });
2579
- // Both recipes share one filter pair, so the recipe selects the scale:
2580
- // 104 for liquid (visible edge refraction) and the untouched 60 for
2581
- // frosted, which is what the pane has always used.
2582
- const applyGlass = () => {
2583
- dispersion.setRefraction(dsttGetGlass() === 'liquid' ? LIQUID_REF_SCALE : DEFAULT_REF_SCALE);
2584
- };
2585
- applyGlass();
2586
- const unsubscribeGlass = dsttSubscribe(applyGlass);
2587
2647
  // ── palette reconciliation ────────────────────────────────────────────
2588
2648
  // One reader for BOTH inputs the palette depends on: the colour token on
2589
2649
  // <body> and the dark marker. Reported symptom: "切主题模式背景不变 /
@@ -2608,7 +2668,6 @@ html[data-dshome-blur="off"] body{
2608
2668
  };
2609
2669
  const unsubscribeColor = subscribeColor((color) => {
2610
2670
  syncFluid();
2611
- try { dispersion.setTint(FLUID_HUES[color] ?? FLUID_HUES.green); } catch (error) {}
2612
2671
  });
2613
2672
  const unsubscribeDark = darkSync === null ? null : darkSync.subscribe(syncFluid);
2614
2673
  // The colour driver writes the token as an attribute; watching it as well
@@ -2624,19 +2683,65 @@ html[data-dshome-blur="off"] body{
2624
2683
  return () => {
2625
2684
  unsubscribeColor();
2626
2685
  if (unsubscribeDark !== null) unsubscribeDark();
2686
+ if (tokenMirror !== null) tokenMirror.disconnect();
2687
+ clearInterval(fluidSync);
2688
+ fluid.dispose();
2689
+ canvas.remove();
2690
+ };
2691
+ }
2692
+
2693
+ /**
2694
+ * The cursor-following half of the glass: the specular spotter (which marks
2695
+ * the surfaces the highlight may land on), the parallax that writes
2696
+ * `--dshome-spec-x/y` for the hovered surface, and the SVG edge refraction.
2697
+ *
2698
+ * These used to be mounted INSIDE {@link startAmbient}, which tied them to
2699
+ * the fluid layer: turning 「动态背景」 off -- which is exactly what the user
2700
+ * did to stop the iGPU squares -- or choosing ③ 自选背景, or landing on an
2701
+ * engine without WebGL2 (the particle fallback returns before them), silently
2702
+ * took the dialog's cursor-following highlight and its edge refraction with
2703
+ * it. Reported as "把我的对话框随着鼠标位置而变化的效果补回来". They belong to
2704
+ * the glass, not to the flow field, so they mount on their own and survive
2705
+ * every background recipe.
2706
+ */
2707
+ function startGlassExtras() {
2708
+ const stopSpotter = startSpecularSpotter();
2709
+ const stopParallax = startSpecularParallax();
2710
+ const dispersion = startGlassDispersion({ hue: FLUID_HUES[dshomeColor()] });
2711
+ // Both recipes share one filter pair, so the recipe selects the scale:
2712
+ // 104 for liquid (visible edge refraction) and the untouched 60 for
2713
+ // frosted, which is what the pane has always used.
2714
+ const applyGlass = () => {
2715
+ dispersion.setRefraction(dsttGetGlass() === 'liquid' ? LIQUID_REF_SCALE : DEFAULT_REF_SCALE);
2716
+ };
2717
+ applyGlass();
2718
+ const unsubscribeGlass = dsttSubscribe(applyGlass);
2719
+ // The tint follows the DSTT colour token. Same belt-and-braces as the
2720
+ // fluid palette: subscribe AND watch the attribute the driver writes,
2721
+ // because the colour driver writes it before this mount exists.
2722
+ const syncTint = () => {
2723
+ try { dispersion.setTint(FLUID_HUES[dshomeColor()] ?? FLUID_HUES.green); } catch (error) {}
2724
+ };
2725
+ const unsubscribeColor = subscribeColor(syncTint);
2726
+ let tokenMirror = null;
2727
+ try {
2728
+ tokenMirror = new MutationObserver(syncTint);
2729
+ tokenMirror.observe(document.body, { attributes: true, attributeFilter: ['data-dshome-color'] });
2730
+ } catch (error) {
2731
+ tokenMirror = null;
2732
+ }
2733
+ return () => {
2734
+ unsubscribeColor();
2627
2735
  unsubscribeGlass();
2628
2736
  if (tokenMirror !== null) tokenMirror.disconnect();
2629
2737
  dispersion.dispose();
2630
2738
  stopParallax();
2631
2739
  stopSpotter();
2632
- clearInterval(fluidSync);
2633
- fluid.dispose();
2634
- canvas.remove();
2635
2740
  };
2636
2741
  }
2637
2742
 
2638
2743
  function startParticles() {
2639
- try { window.__dshomeBuild = '2.0.84 bug-report-fixes'; } catch (error) {}
2744
+ try { window.__dshomeBuild = '2.0.85 glass-extras'; } catch (error) {}
2640
2745
  const canvas = document.createElement('canvas');
2641
2746
  canvas.setAttribute('data-dsh-deepseek-canvas', '');
2642
2747
  const style = canvas.style;
@@ -4943,6 +5048,18 @@ body[data-dshome-dark] .${CLS}{background:rgba(14,30,60,.94);box-shadow:0 18px 4
4943
5048
  if (stop !== null) { try { stop(); } catch (error) {} }
4944
5049
  };
4945
5050
  }, 'deepseek-style-theme: fluid background');
5051
+ // The cursor-following half of the glass mounts ON ITS OWN, with no
5052
+ // dependence on the fluid layer: see startGlassExtras for why (turning
5053
+ // 「动态背景」 off, or ③ 自选背景, or a machine without WebGL2 used to take
5054
+ // the dialog's cursor-following highlight and the edge refraction with it).
5055
+ ctx.effect(() => {
5056
+ try {
5057
+ return startGlassExtras();
5058
+ } catch (error) {
5059
+ console.error('[dsh-deepseek-style-theme] glass extras failed to mount:', error);
5060
+ return () => {};
5061
+ }
5062
+ }, 'deepseek-style-theme: glass extras (specular + dispersion)');
4946
5063
  // The composer's hover tilt is independent of the background: it must
4947
5064
  // survive a machine without WebGL2, where startAmbient() falls back to
4948
5065
  // the 2D particle field.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-deepseek-style-theme",
3
3
  "description": "DeepSeek official-home style theme: WebGL fluid-flow background, liquid glass with SVG edge refraction, glass sidebar and composer, frosted header, running-subagent progress panel, and DeepSeek brand link for the dsh web GUI",
4
- "version": "2.0.84",
4
+ "version": "2.0.85",
5
5
  "type": "module",
6
6
  "keywords": [
7
7
  "deepseek",