@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.
- package/README.md +24 -8
- package/lib/client.js +60 -88
- 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
|
|
146
|
+
`scroll-behavior: smooth` 与脚本调用的 `scrollTo({behavior:'smooth'})` 都不受 CSS 曲线控制——时长和曲线由浏览器定,CSS 读不到,JS 也拿不到。勾选「定位滚动使用同一曲线」后,本层接管 `Element.prototype.scrollIntoView`,用脚本滚动把**一次性定位**(轨迹行聚焦、插件卡片居中、提问卡定位)换成本页曲线。
|
|
147
147
|
|
|
148
|
-
|
|
148
|
+
#### 为什么只接管 scrollIntoView
|
|
149
149
|
|
|
150
|
-
|
|
151
|
-
2. **用效果曲线,不用位移曲线。** 位移曲线带过冲,滚动时过冲意味着内容冲过目标位置再退回来,终点错两次。曲线族照样生效,带过冲的那半永远不用于滚动。
|
|
152
|
-
3. **时长按距离增长。** 240px 以内固定 180ms,之后每像素加 0.2ms,封顶 460ms;再乘时长倍率,开了「时长对齐」时同样吸附到 Material 档位。滚动的手感预期和元素过渡不同:读者对「滚了多远、停在哪」有精确期待,长距离用固定时长会显得急促。
|
|
150
|
+
`scrollTo` / `scrollBy` 若一并接管,会破坏聊天跟随。跟随控制器先设一个目标,再等 `scrollend` 结算;在 Chrome 156 实测:
|
|
153
151
|
|
|
154
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
529
|
-
|
|
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) >
|
|
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)
|
|
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
|
|
594
|
-
const
|
|
595
|
-
if (
|
|
596
|
-
|
|
597
|
-
|
|
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
|
|
605
|
-
const
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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
|
|
612
|
-
|
|
613
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
641
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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
|
-
|
|
663
|
-
|
|
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\
|
|
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": "
|
|
896
|
-
"motion.scroll-hint": "Take over
|
|
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.
|
|
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",
|