@dsh-plugin/dsh-ui-experience 1.0.0-dev.43492422772 → 1.0.0-dev.43493974985

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +24 -8
  2. package/lib/client.js +60 -88
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -143,17 +143,32 @@ M3 那侧的分离要说明白:Material 3 的 `emphasized` 整体是两段首
143
143
 
144
144
  ### 滚动
145
145
 
146
- `scroll-behavior: smooth` 与脚本调用的 `scrollTo({behavior:'smooth'})` 都不受 CSS 曲线控制——时长和曲线由浏览器定,CSS 读不到,JS 也拿不到。勾选「滚动使用同一曲线」后,本层接管 `Element.prototype` 的 `scrollTo` / `scrollBy` / `scrollIntoView`,用脚本滚动实现同一套曲线。
146
+ `scroll-behavior: smooth` 与脚本调用的 `scrollTo({behavior:'smooth'})` 都不受 CSS 曲线控制——时长和曲线由浏览器定,CSS 读不到,JS 也拿不到。勾选「定位滚动使用同一曲线」后,本层接管 `Element.prototype.scrollIntoView`,用脚本滚动把**一次性定位**(轨迹行聚焦、插件卡片居中、提问卡定位)换成本页曲线。
147
147
 
148
- 三条固定的规则,且不可配置:
148
+ #### 为什么只接管 scrollIntoView
149
149
 
150
- 1. **读者一动就交还原生。** 滚轮、触摸、滚动按键直接取消脚本动画,不混合、不恢复。任何本层没写过的位置变化(拖滚动条、辅助技术滚动、容器在动画中重排)也会在下一帧取消。`pointerdown` 不在此列:页面上任何一次点击都取消,会让「输出还在滚动时点一下按钮」把跟随掐断,而原生平滑滚动不会。
151
- 2. **用效果曲线,不用位移曲线。** 位移曲线带过冲,滚动时过冲意味着内容冲过目标位置再退回来,终点错两次。曲线族照样生效,带过冲的那半永远不用于滚动。
152
- 3. **时长按距离增长。** 240px 以内固定 180ms,之后每像素加 0.2ms,封顶 460ms;再乘时长倍率,开了「时长对齐」时同样吸附到 Material 档位。滚动的手感预期和元素过渡不同:读者对「滚了多远、停在哪」有精确期待,长距离用固定时长会显得急促。
150
+ `scrollTo` / `scrollBy` 若一并接管,会破坏聊天跟随。跟随控制器先设一个目标,再等 `scrollend` 结算;在 Chrome 156 实测:
153
151
 
154
- `scrollIntoView` 的终点由对齐方式、`scroll-margin`、嵌套容器和元素自身盒模型共同决定,自己算就是「滚到错的那一行」的来源。这里的做法是:先向浏览器要一次瞬时跳转,读出每个被移动容器的落点,把它们全部放回原位,再动画到读到的落点。跳转与还原在同一个任务内完成,中间位置不会绘制。
152
+ | 写入方式 | scroll 事件 | scrollend 事件 |
153
+ | --- | --- | --- |
154
+ | 原生 `behavior:'smooth'` 一次 | 89 | **1** |
155
+ | 逐帧赋值 `scrollTop`(脚本动画) | 21 | **21** |
156
+
157
+ 浏览器会为自己发起的每一次瞬时滚动各发一次 `scrollend`,本层无法拦截。控制器把其中第一个读作「读者在中途停下」,于是在动画第一帧就放弃跟随。因此走这条循环的调用一律保持原生。
158
+
159
+ `scrollIntoView` 属于一次性定位:一次手势对应一个终点,没有消费方靠数滚动结束事件推断意图。
160
+
161
+ #### 固定的两条规则
162
+
163
+ 1. **读者优先。** 在正在滚动的容器上滚轮或触摸、或在文本框之外按滚动键,脚本滚动立即取消,不混合、不恢复;任何本层没写过的位置变化(拖滚动条、辅助技术滚动、容器在动画中重排)也会在下一帧取消。滚轮落在别的面板上不会打断——那不是这个容器的滚动。
164
+ 2. **用效果曲线,不用位移曲线。** 位移曲线带过冲,滚动过冲意味着内容冲过目标位置再退回来,终点错两次。曲线族照样生效,带过冲的那半永远不用于滚动。
165
+
166
+ #### 时长与落点
155
167
 
156
- `scrollend` 交给浏览器:逐帧赋值 `scrollTop` 是一串瞬时滚动,浏览器自己的滚动结束检测会在最后一帧落下后触发一次,依赖它结算状态的消费方(聊天记录的跟随控制器)看到的序列与原生平滑滚动一致。
168
+ - 240px 以内固定 180ms,之后每像素加 0.2ms,封顶 460ms;再乘时长倍率,开了「时长对齐」时同样吸附到 Material 档位。
169
+ - 终点由对齐方式、`scroll-margin`、嵌套容器和元素自身盒模型共同决定,自己算就是「滚到错的那一行」的来源。做法是先向浏览器要一次瞬时跳转,读出每个被移动容器的落点,把它们全部放回原位,再动画到读到的落点;跳转与还原在同一个任务内完成,中间位置不会绘制。
170
+ - 返回值:脚本路径返回一个在落点写入(或被取消)后兑现的 Promise,与原生 `scrollTo` 一致。
171
+ - 「始终减弱动效」或系统的减弱动效偏好生效时,平滑定位直接变成瞬时跳转。
157
172
 
158
173
  ### 滚动驱动动画不动
159
174
 
@@ -205,7 +220,8 @@ DSH 目前只有一处用到(提问卡片标题的溢出遮罩 `--question-cop
205
220
  - 跨源样式表里的规则无法改写,只会在日志中记录。
206
221
  - 本层只改写曲线与时长,不新增、不改写关键帧本身;DSH 没定义的动效不会因此出现。
207
222
  - CSS 的 `scroll-behavior: smooth`(全仓一处,dock 标签条)不是 transition/animation,本层不碰;只有减弱动效时随压制表置为 `auto`。
208
- - 脚本滚动由「滚动使用同一曲线」单独控制,与曲线族无关(关掉曲线族就一并停止接管)。
223
+ - 脚本滚动只覆盖 `scrollIntoView`,由「定位滚动使用同一曲线」单独控制,与曲线族无关(关掉曲线族就一并停止接管)。聊天跟随、附件横向滚动、虚拟列表定位都走 `scrollTo`/`scrollBy`,保持原生时序。
224
+ - 接管的定位滚动会按帧产生多次 `scrollend`(实测一次约 28 次)。目前没有消费方在这一族调用上依赖单次 `scrollend`;若将来 DSH 给轨迹面板加上类似的跟随控制器,需要重新评估。
209
225
  - 滚动驱动动画(`animation-timeline`)不属于时间轴动画,本层显式跳过,见上一节。
210
226
  - 浏览器半在刷新页面时重载,宿主半只在进程重启时重载。两者版本不一致时(刚升级插件但没重启 DSH),M3 动效行会保持无操作并在控制台记录一行原因,配色功能不受影响。
211
227
 
package/lib/client.js CHANGED
@@ -472,6 +472,8 @@
472
472
  var MAX_MS = 460;
473
473
  var FREE_PX = 240;
474
474
  var GROWTH_MS_PER_PX = 0.2;
475
+ var MOTION_EPSILON = 0.5;
476
+ var FOREIGN_TOLERANCE = 1;
475
477
  var SCROLL_KEYS = /* @__PURE__ */ new Set([
476
478
  "ArrowUp",
477
479
  "ArrowDown",
@@ -483,7 +485,7 @@
483
485
  "End",
484
486
  " "
485
487
  ]);
486
- var TAKEOVER_EVENTS = ["wheel", "touchstart", "keydown"];
488
+ var EDITABLE = 'input, textarea, select, [contenteditable=""], [contenteditable="true"], [contenteditable="plaintext-only"]';
487
489
  function scrollDistance(legs) {
488
490
  let distance = 0;
489
491
  for (const leg of legs) {
@@ -502,32 +504,27 @@
502
504
  left: Math.max(0, element.scrollWidth - element.clientWidth)
503
505
  };
504
506
  }
505
- function scrollable(element) {
506
- const max = maxima(element);
507
- return max.top > 0 || max.left > 0;
508
- }
509
507
  function containers(element) {
510
508
  const out = [];
511
509
  let node = element.parentElement;
512
510
  while (node !== null) {
513
- if (scrollable(node)) out.push(node);
511
+ const max = maxima(node);
512
+ if (max.top > 0 || max.left > 0) out.push(node);
514
513
  node = node.parentElement;
515
514
  }
516
515
  return out;
517
516
  }
518
517
  function createScrollMotion() {
519
- const shipped = {
520
- scrollTo: Element.prototype.scrollTo,
521
- scrollBy: Element.prototype.scrollBy,
522
- scrollIntoView: Element.prototype.scrollIntoView
523
- };
518
+ const shipped = Element.prototype.scrollIntoView;
524
519
  let settings;
525
520
  let run;
526
521
  let installed = false;
527
522
  function cancel() {
528
- if (run === void 0) return;
529
- window.cancelAnimationFrame(run.frame);
523
+ const active = run;
524
+ if (active === void 0) return;
525
+ window.cancelAnimationFrame(active.frame);
530
526
  run = void 0;
527
+ active.done();
531
528
  }
532
529
  function step(now) {
533
530
  const active = run;
@@ -539,7 +536,7 @@
539
536
  const leg = active.legs[index];
540
537
  const written = active.written[index];
541
538
  if (leg === void 0 || written === void 0) continue;
542
- if (Math.abs(leg.element.scrollTop - written.top) > 1 || Math.abs(leg.element.scrollLeft - written.left) > 1) {
539
+ if (Math.abs(leg.element.scrollTop - written.top) > FOREIGN_TOLERANCE || Math.abs(leg.element.scrollLeft - written.left) > FOREIGN_TOLERANCE) {
543
540
  cancel();
544
541
  return;
545
542
  }
@@ -552,77 +549,49 @@
552
549
  }
553
550
  if (progress >= 1) {
554
551
  run = void 0;
552
+ active.done();
555
553
  return;
556
554
  }
557
555
  active.frame = window.requestAnimationFrame(step);
558
556
  }
559
- function start(legs) {
557
+ function start(legs, done) {
560
558
  cancel();
561
559
  const active = settings;
562
- if (active === void 0) return;
560
+ if (active === void 0) {
561
+ done();
562
+ return;
563
+ }
563
564
  run = {
564
565
  legs,
565
566
  started: performance.now(),
566
567
  duration: scrollDurationMs(scrollDistance(legs), active),
567
568
  written: legs.map((leg) => ({ top: leg.element.scrollTop, left: leg.element.scrollLeft })),
568
- frame: window.requestAnimationFrame(step)
569
- };
570
- }
571
- function defer(behavior) {
572
- if (settings === void 0) return true;
573
- if (behavior !== "smooth") return true;
574
- return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
575
- }
576
- function legsFor(element, top, left) {
577
- const max = maxima(element);
578
- const toTop = top === void 0 ? element.scrollTop : Math.min(max.top, Math.max(0, top));
579
- const toLeft = left === void 0 ? element.scrollLeft : Math.min(max.left, Math.max(0, left));
580
- if (Math.abs(toTop - element.scrollTop) < 1 && Math.abs(toLeft - element.scrollLeft) < 1) return void 0;
581
- return [{ element, fromTop: element.scrollTop, fromLeft: element.scrollLeft, toTop, toLeft }];
582
- }
583
- function readOffsets(options, y, base) {
584
- if (typeof options === "number") {
585
- return { left: base.left + options, top: y === void 0 ? void 0 : base.top + y };
586
- }
587
- if (options === void 0 || options === null) return { top: void 0, left: void 0 };
588
- return {
589
- left: options.left === void 0 ? void 0 : base.left + options.left,
590
- top: options.top === void 0 ? void 0 : base.top + options.top
569
+ frame: window.requestAnimationFrame(step),
570
+ done
591
571
  };
592
572
  }
593
- function patchedScrollTo(options, y) {
594
- const behavior = typeof options === "object" && options !== null ? options.behavior : void 0;
595
- if (defer(behavior)) {
596
- ;
597
- shipped.scrollTo.call(this, options, y);
598
- return;
599
- }
600
- const request = readOffsets(options, y, { top: 0, left: 0 });
601
- const legs = legsFor(this, request.top, request.left);
602
- if (legs !== void 0) start(legs);
573
+ function wantsInstant() {
574
+ const active = settings;
575
+ if (active === void 0) return true;
576
+ if (active.reduced === "always") return true;
577
+ return active.reduced === "system" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
603
578
  }
604
- function patchedScrollBy(options, y) {
605
- const behavior = typeof options === "object" && options !== null ? options.behavior : void 0;
606
- if (defer(behavior)) {
607
- ;
608
- shipped.scrollBy.call(this, options, y);
609
- return;
579
+ function patchedScrollIntoView(...args) {
580
+ const options = args[0];
581
+ const requested = typeof options === "object" && options !== null ? options : void 0;
582
+ const active = settings;
583
+ if (active === void 0 || requested?.behavior !== "smooth") {
584
+ shipped.apply(this, args);
585
+ return void 0;
610
586
  }
611
- const request = readOffsets(options, y, { top: this.scrollTop, left: this.scrollLeft });
612
- const legs = legsFor(this, request.top, request.left);
613
- if (legs !== void 0) start(legs);
614
- }
615
- function patchedScrollIntoView(options) {
616
- const behavior = typeof options === "object" && options !== null ? options.behavior : void 0;
617
- if (defer(behavior)) {
618
- ;
619
- shipped.scrollIntoView.call(this, options);
620
- return;
587
+ const jump = { ...requested, behavior: "instant" };
588
+ if (wantsInstant()) {
589
+ shipped.call(this, jump);
590
+ return void 0;
621
591
  }
622
- const instant = { ...typeof options === "object" && options !== null ? options : {}, behavior: "instant" };
623
592
  const tracked = containers(this);
624
593
  const before = tracked.map((element) => ({ top: element.scrollTop, left: element.scrollLeft }));
625
- shipped.scrollIntoView.call(this, instant);
594
+ shipped.call(this, jump);
626
595
  const legs = [];
627
596
  for (let index = 0; index < tracked.length; index += 1) {
628
597
  const element = tracked[index];
@@ -631,39 +600,43 @@
631
600
  const landed = { top: element.scrollTop, left: element.scrollLeft };
632
601
  element.scrollTop = origin.top;
633
602
  element.scrollLeft = origin.left;
634
- if (Math.abs(landed.top - origin.top) < 1 && Math.abs(landed.left - origin.left) < 1) continue;
603
+ const moved = Math.abs(landed.top - origin.top) > MOTION_EPSILON || Math.abs(landed.left - origin.left) > MOTION_EPSILON;
604
+ if (!moved) continue;
635
605
  legs.push({ element, fromTop: origin.top, fromLeft: origin.left, toTop: landed.top, toLeft: landed.left });
636
606
  }
637
- if (legs.length > 0) start(legs);
607
+ if (legs.length === 0) return void 0;
608
+ return new Promise((resolve) => {
609
+ start(legs, resolve);
610
+ });
638
611
  }
639
612
  function onTakeover(event) {
640
- if (run === void 0) return;
641
- if (event.type !== "keydown") {
613
+ const active = run;
614
+ if (active === void 0) return;
615
+ if (event.type === "keydown") {
616
+ if (event.key === void 0 || !SCROLL_KEYS.has(event.key)) return;
617
+ const target2 = event.target;
618
+ if (target2 instanceof Element && target2.closest(EDITABLE) !== null) return;
642
619
  cancel();
643
620
  return;
644
621
  }
645
- if (event.key !== void 0 && SCROLL_KEYS.has(event.key)) cancel();
622
+ const target = event.target;
623
+ if (target === null || !active.legs.some((leg) => leg.element.contains(target))) return;
624
+ cancel();
646
625
  }
647
626
  function install() {
648
627
  if (installed) return;
649
628
  installed = true;
650
- const prototype = Element.prototype;
651
- prototype.scrollTo = patchedScrollTo;
652
- prototype.scrollBy = patchedScrollBy;
653
- prototype.scrollIntoView = patchedScrollIntoView;
654
- for (const kind of TAKEOVER_EVENTS) {
655
- window.addEventListener(kind, onTakeover, true);
629
+ Element.prototype.scrollIntoView = patchedScrollIntoView;
630
+ for (const kind of ["wheel", "touchstart", "keydown"]) {
631
+ window.addEventListener(kind, onTakeover, { capture: true, passive: true });
656
632
  }
657
633
  }
658
634
  function uninstall() {
659
635
  cancel();
660
636
  if (!installed) return;
661
637
  installed = false;
662
- const prototype = Element.prototype;
663
- prototype.scrollTo = shipped.scrollTo;
664
- prototype.scrollBy = shipped.scrollBy;
665
- prototype.scrollIntoView = shipped.scrollIntoView;
666
- for (const kind of TAKEOVER_EVENTS) {
638
+ Element.prototype.scrollIntoView = shipped;
639
+ for (const kind of ["wheel", "touchstart", "keydown"]) {
667
640
  window.removeEventListener(kind, onTakeover, true);
668
641
  }
669
642
  }
@@ -672,7 +645,6 @@
672
645
  uninstall();
673
646
  settings = next;
674
647
  if (!next.scroll || next.mode === "off") return;
675
- if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
676
648
  install();
677
649
  },
678
650
  restore: uninstall
@@ -860,8 +832,8 @@
860
832
  "motion.spatial-split-hint": "\u4F4D\u79FB\u66F2\u7EBF\u5E26\u8FC7\u51B2\uFF08\u5FEB\u4F4D\u79FB\u7EA6 9%\uFF0C\u9ED8\u8BA4\u4F4D\u79FB\u7EA6 1.4%\uFF09\uFF0C\u6548\u679C\u66F2\u7EBF\u6CA1\u6709\u3002\u5173\u95ED\u540E\u5168\u90E8\u4F7F\u7528\u6548\u679C\u66F2\u7EBF\u3002",
861
833
  "motion.quantize": "\u65F6\u957F\u5BF9\u9F50\u5230 Material \u6863\u4F4D",
862
834
  "motion.quantize-hint": "\u628A\u6539\u5199\u540E\u7684\u65F6\u957F\u5438\u9644\u5230 50/100/150/200/250/300/350/400/450/500/550/600/700/800/900/1000ms\u3002",
863
- "motion.scroll": "\u6EDA\u52A8\u4F7F\u7528\u540C\u4E00\u66F2\u7EBF",
864
- "motion.scroll-hint": "\u63A5\u7BA1\u811A\u672C\u8C03\u7528\u7684\u5E73\u6ED1\u6EDA\u52A8\uFF08\u804A\u5929\u8DDF\u968F\u3001\u8F68\u8FF9\u5B9A\u4F4D\u3001\u5361\u7247\u5C45\u4E2D\uFF09\uFF0C\u6539\u7528\u672C\u9875\u66F2\u7EBF\u3002\u8BFB\u8005\u4E00\u65E6\u6EDA\u8F6E\u3001\u89E6\u6478\u6216\u6309\u952E\uFF0C\u811A\u672C\u6EDA\u52A8\u7ACB\u5373\u505C\u6B62\u5E76\u4EA4\u8FD8\u539F\u751F\u63A7\u5236\u3002",
835
+ "motion.scroll": "\u5B9A\u4F4D\u6EDA\u52A8\u4F7F\u7528\u540C\u4E00\u66F2\u7EBF",
836
+ "motion.scroll-hint": "\u63A5\u7BA1 scrollIntoView \u7684\u5E73\u6ED1\u5B9A\u4F4D\uFF08\u8F68\u8FF9\u884C\u805A\u7126\u3001\u63D2\u4EF6\u5361\u7247\u5C45\u4E2D\uFF09\uFF0C\u6539\u7528\u672C\u9875\u66F2\u7EBF\u3002\u8BFB\u8005\u4E00\u65E6\u5728\u5BB9\u5668\u4E0A\u6EDA\u8F6E\u3001\u89E6\u6478\u6216\u6309\u6EDA\u52A8\u952E\uFF0C\u811A\u672C\u6EDA\u52A8\u7ACB\u5373\u505C\u6B62\u5E76\u4EA4\u8FD8\u539F\u751F\u63A7\u5236\u3002\u804A\u5929\u8DDF\u968F\u4E0E\u5404\u7C7B scrollTo \u6EDA\u52A8\u4FDD\u6301\u539F\u751F\uFF1A\u5B83\u4EEC\u9760\u6BCF\u6B21\u6EDA\u52A8\u53EA\u7ED3\u675F\u4E00\u6B21\u7684 scrollend \u5224\u65AD\u610F\u56FE\uFF0C\u800C\u9010\u5E27\u811A\u672C\u6EDA\u52A8\u4F1A\u6BCF\u5E27\u7ED3\u675F\u4E00\u6B21\u3002",
865
837
  "motion.reduced": "\u59CB\u7EC8\u51CF\u5F31\u52A8\u6548",
866
838
  "motion.reduced-hint": "\u5F00\u542F\u540E\u5FFD\u7565\u7CFB\u7EDF\u7684\u300C\u51CF\u5F31\u52A8\u6001\u6548\u679C\u300D\u504F\u597D\uFF0C\u4E00\u5F8B\u628A\u65F6\u957F\u538B\u5230 1ms\u3002\u5173\u95ED\u65F6\u8DDF\u968F\u7CFB\u7EDF\u3002",
867
839
  "motion.system-reduced": "\u7CFB\u7EDF\u5DF2\u8BF7\u6C42\u51CF\u5F31\u52A8\u6548\uFF0C\u672C\u9875\u52A8\u753B\u6B63\u5728\u88AB\u538B\u5236\u3002",
@@ -892,8 +864,8 @@
892
864
  "motion.spatial-split-hint": "The spatial curves overshoot (about 9% for the fast one, 1.4% for the default); the effects curve does not. Turning this off uses the effects curve everywhere.",
893
865
  "motion.quantize": "Snap durations to the Material ladder",
894
866
  "motion.quantize-hint": "Pull rewritten durations onto 50/100/150/200/250/300/350/400/450/500/550/600/700/800/900/1000 ms.",
895
- "motion.scroll": "Scroll on the same curve",
896
- "motion.scroll-hint": "Take over scripted smooth scrolling \u2014 chat follow, trajectory locate, card centring \u2014 and drive it on this page's curve. Any wheel, touch, or scroll key stops the scripted motion at once and hands control back to the browser.",
867
+ "motion.scroll": "Locating scroll on the same curve",
868
+ "motion.scroll-hint": "Take over the smooth locating jump of scrollIntoView \u2014 focus a trajectory row, centre a plugin card \u2014 and drive it on this page's curve. A wheel, touch, or scroll key over the container stops it at once. Chat follow and other scrollTo scrolling stay native: they read one end-of-scroll event per gesture to infer intent, and frame-by-frame scripted scrolling ends once per frame.",
897
869
  "motion.reduced": "Always reduce motion",
898
870
  "motion.reduced-hint": "Ignore the system reduce-motion preference and force every duration to 1 ms. Off follows the system.",
899
871
  "motion.system-reduced": "The system asked for reduced motion; this page is being held still.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-plugin/dsh-ui-experience",
3
- "version": "1.0.0-dev.43492422772",
3
+ "version": "1.0.0-dev.43493974985",
4
4
  "type": "module",
5
5
  "description": "UI experience enhancements for DeepSeek Harness: follow the system matugen Material You palette as the interface theme, and rewrite interface motion onto Material 3 / Material 3 Expressive curves.",
6
6
  "license": "MIT",