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

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 CHANGED
@@ -141,6 +141,20 @@ M3 那侧的分离要说明白:Material 3 的 `emphasized` 整体是两段首
141
141
  - 无需随 DSH 更新维护:新增或改名的组件自动被覆盖。
142
142
  - 精确还原:写入前整块记录 `cssText`,关闭时原样写回;若期间被第三方改过则在日志中说明后仍然还原。
143
143
 
144
+ ### 滚动
145
+
146
+ `scroll-behavior: smooth` 与脚本调用的 `scrollTo({behavior:'smooth'})` 都不受 CSS 曲线控制——时长和曲线由浏览器定,CSS 读不到,JS 也拿不到。勾选「滚动使用同一曲线」后,本层接管 `Element.prototype` 的 `scrollTo` / `scrollBy` / `scrollIntoView`,用脚本滚动实现同一套曲线。
147
+
148
+ 三条固定的规则,且不可配置:
149
+
150
+ 1. **读者一动就交还原生。** 滚轮、触摸、滚动按键直接取消脚本动画,不混合、不恢复。任何本层没写过的位置变化(拖滚动条、辅助技术滚动、容器在动画中重排)也会在下一帧取消。`pointerdown` 不在此列:页面上任何一次点击都取消,会让「输出还在滚动时点一下按钮」把跟随掐断,而原生平滑滚动不会。
151
+ 2. **用效果曲线,不用位移曲线。** 位移曲线带过冲,滚动时过冲意味着内容冲过目标位置再退回来,终点错两次。曲线族照样生效,带过冲的那半永远不用于滚动。
152
+ 3. **时长按距离增长。** 240px 以内固定 180ms,之后每像素加 0.2ms,封顶 460ms;再乘时长倍率,开了「时长对齐」时同样吸附到 Material 档位。滚动的手感预期和元素过渡不同:读者对「滚了多远、停在哪」有精确期待,长距离用固定时长会显得急促。
153
+
154
+ `scrollIntoView` 的终点由对齐方式、`scroll-margin`、嵌套容器和元素自身盒模型共同决定,自己算就是「滚到错的那一行」的来源。这里的做法是:先向浏览器要一次瞬时跳转,读出每个被移动容器的落点,把它们全部放回原位,再动画到读到的落点。跳转与还原在同一个任务内完成,中间位置不会绘制。
155
+
156
+ `scrollend` 交给浏览器:逐帧赋值 `scrollTop` 是一串瞬时滚动,浏览器自己的滚动结束检测会在最后一帧落下后触发一次,依赖它结算状态的消费方(聊天记录的跟随控制器)看到的序列与原生平滑滚动一致。
157
+
144
158
  ### 滚动驱动动画不动
145
159
 
146
160
  `scroll-timeline` / `animation-timeline` 驱动的动画,进度来自滚动容器而不是时钟。这类动画的 `animation-timing-function` 不是「一条随时间推进的曲线」,而是把滚动位置映射到迭代进度——给它套缓动,等于让遮罩在滚动前段就淡完然后定住,改的是效果本身而非手感。因此只要某条规则的 `animation-timeline` 不是 `auto` / `none`,本层就整条跳过。
@@ -190,7 +204,9 @@ DSH 目前只有一处用到(提问卡片标题的溢出遮罩 `--question-cop
190
204
  - M3 动效的「位移 / 效果」判定依据属性名与关键帧内容,不分析属性的实际取值变化;判定不出来的情况(例如只由 `all` 声明的过渡)按效果处理,宁可不给过冲。
191
205
  - 跨源样式表里的规则无法改写,只会在日志中记录。
192
206
  - 本层只改写曲线与时长,不新增、不改写关键帧本身;DSH 没定义的动效不会因此出现。
193
- - **滚动本身不归本层管**。三种机制各走各的:`scroll-behavior: smooth`(全仓一处,dock 标签条)不是 transition/animation,本层不碰,只有减弱动效时随压制表置为 `auto`;脚本调用的 `scrollTo({behavior:'smooth'})`(聊天跟随、轨迹表定位、插件页高亮)由浏览器决定时长与曲线,CSS 与 JS 都读不到也改不了;第三条即上面说的滚动驱动动画,本层显式跳过。
207
+ - CSS 的 `scroll-behavior: smooth`(全仓一处,dock 标签条)不是 transition/animation,本层不碰;只有减弱动效时随压制表置为 `auto`。
208
+ - 脚本滚动由「滚动使用同一曲线」单独控制,与曲线族无关(关掉曲线族就一并停止接管)。
209
+ - 滚动驱动动画(`animation-timeline`)不属于时间轴动画,本层显式跳过,见上一节。
194
210
  - 浏览器半在刷新页面时重载,宿主半只在进程重启时重载。两者版本不一致时(刚升级插件但没重启 DSH),M3 动效行会保持无操作并在控制台记录一行原因,配色功能不受影响。
195
211
 
196
212
  ## 样式基线
package/lib/client.js CHANGED
@@ -11,7 +11,8 @@
11
11
  durationScale: 1,
12
12
  spatialSplit: true,
13
13
  quantize: false,
14
- reduced: "system"
14
+ reduced: "system",
15
+ scroll: true
15
16
  };
16
17
 
17
18
  // src/motion-plan.ts
@@ -77,11 +78,36 @@
77
78
  "border-bottom-left-radius",
78
79
  "border-bottom-right-radius"
79
80
  ]);
80
- var CURVE_M3_SPATIAL = "cubic-bezier(0.05, 0.7, 0.1, 1)";
81
- var CURVE_M3_EFFECTS = "cubic-bezier(0.2, 0, 0, 1)";
82
- var CURVE_M3E_SPATIAL_FAST = "cubic-bezier(0.42, 1.67, 0.21, 0.9)";
83
- var CURVE_M3E_SPATIAL = "cubic-bezier(0.38, 1.21, 0.22, 1)";
84
- var CURVE_M3E_EFFECTS = "cubic-bezier(0.34, 0.8, 0.34, 1)";
81
+ var POINTS_M3_SPATIAL = [0.05, 0.7, 0.1, 1];
82
+ var POINTS_M3_EFFECTS = [0.2, 0, 0, 1];
83
+ var POINTS_M3E_SPATIAL_FAST = [0.42, 1.67, 0.21, 0.9];
84
+ var POINTS_M3E_SPATIAL = [0.38, 1.21, 0.22, 1];
85
+ var POINTS_M3E_EFFECTS = [0.34, 0.8, 0.34, 1];
86
+ var CURVE_M3_SPATIAL = `cubic-bezier(${POINTS_M3_SPATIAL.join(", ")})`;
87
+ var CURVE_M3_EFFECTS = `cubic-bezier(${POINTS_M3_EFFECTS.join(", ")})`;
88
+ var CURVE_M3E_SPATIAL_FAST = `cubic-bezier(${POINTS_M3E_SPATIAL_FAST.join(", ")})`;
89
+ var CURVE_M3E_SPATIAL = `cubic-bezier(${POINTS_M3E_SPATIAL.join(", ")})`;
90
+ var CURVE_M3E_EFFECTS = `cubic-bezier(${POINTS_M3E_EFFECTS.join(", ")})`;
91
+ function effectCurvePoints(mode) {
92
+ return mode === "m3" ? POINTS_M3_EFFECTS : POINTS_M3E_EFFECTS;
93
+ }
94
+ function easeCurve(points, x) {
95
+ const [x1, y1, x2, y2] = points;
96
+ const clamped = Math.min(1, Math.max(0, x));
97
+ let low = 0;
98
+ let high = 1;
99
+ let t = clamped;
100
+ for (let step = 0; step < 24; step += 1) {
101
+ const inverse2 = 1 - t;
102
+ const value = 3 * inverse2 * inverse2 * t * x1 + 3 * inverse2 * t * t * x2 + t * t * t;
103
+ if (Math.abs(value - clamped) < 1e-5) break;
104
+ if (value < clamped) low = t;
105
+ else high = t;
106
+ t = (low + high) / 2;
107
+ }
108
+ const inverse = 1 - t;
109
+ return 3 * inverse * inverse * t * y1 + 3 * inverse * t * t * y2 + t * t * t;
110
+ }
85
111
  var FAST_SPATIAL_MAX_MS = 200;
86
112
  var MATERIAL_DURATIONS_MS = [
87
113
  50,
@@ -441,6 +467,218 @@
441
467
  };
442
468
  }
443
469
 
470
+ // src/scroll.ts
471
+ var MIN_MS = 180;
472
+ var MAX_MS = 460;
473
+ var FREE_PX = 240;
474
+ var GROWTH_MS_PER_PX = 0.2;
475
+ var SCROLL_KEYS = /* @__PURE__ */ new Set([
476
+ "ArrowUp",
477
+ "ArrowDown",
478
+ "ArrowLeft",
479
+ "ArrowRight",
480
+ "PageUp",
481
+ "PageDown",
482
+ "Home",
483
+ "End",
484
+ " "
485
+ ]);
486
+ var TAKEOVER_EVENTS = ["wheel", "touchstart", "keydown"];
487
+ function scrollDistance(legs) {
488
+ let distance = 0;
489
+ for (const leg of legs) {
490
+ distance = Math.max(distance, Math.abs(leg.toTop - leg.fromTop), Math.abs(leg.toLeft - leg.fromLeft));
491
+ }
492
+ return distance;
493
+ }
494
+ function scrollDurationMs(distance, settings) {
495
+ const grown = MIN_MS + Math.max(0, distance - FREE_PX) * GROWTH_MS_PER_PX;
496
+ const scaled = Math.min(MAX_MS, grown) * settings.durationScale;
497
+ return settings.quantize ? quantizeMs(scaled) : scaled;
498
+ }
499
+ function maxima(element) {
500
+ return {
501
+ top: Math.max(0, element.scrollHeight - element.clientHeight),
502
+ left: Math.max(0, element.scrollWidth - element.clientWidth)
503
+ };
504
+ }
505
+ function scrollable(element) {
506
+ const max = maxima(element);
507
+ return max.top > 0 || max.left > 0;
508
+ }
509
+ function containers(element) {
510
+ const out = [];
511
+ let node = element.parentElement;
512
+ while (node !== null) {
513
+ if (scrollable(node)) out.push(node);
514
+ node = node.parentElement;
515
+ }
516
+ return out;
517
+ }
518
+ function createScrollMotion() {
519
+ const shipped = {
520
+ scrollTo: Element.prototype.scrollTo,
521
+ scrollBy: Element.prototype.scrollBy,
522
+ scrollIntoView: Element.prototype.scrollIntoView
523
+ };
524
+ let settings;
525
+ let run;
526
+ let installed = false;
527
+ function cancel() {
528
+ if (run === void 0) return;
529
+ window.cancelAnimationFrame(run.frame);
530
+ run = void 0;
531
+ }
532
+ function step(now) {
533
+ const active = run;
534
+ if (active === void 0) return;
535
+ const elapsed = now - active.started;
536
+ const progress = active.duration <= 0 ? 1 : Math.min(1, elapsed / active.duration);
537
+ const eased = easeCurve(effectCurvePoints(settings?.mode ?? "m3e"), progress);
538
+ for (let index = 0; index < active.legs.length; index += 1) {
539
+ const leg = active.legs[index];
540
+ const written = active.written[index];
541
+ 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) {
543
+ cancel();
544
+ return;
545
+ }
546
+ const top = progress >= 1 ? leg.toTop : leg.fromTop + (leg.toTop - leg.fromTop) * eased;
547
+ const left = progress >= 1 ? leg.toLeft : leg.fromLeft + (leg.toLeft - leg.fromLeft) * eased;
548
+ if (leg.element.scrollTop !== top) leg.element.scrollTop = top;
549
+ if (leg.element.scrollLeft !== left) leg.element.scrollLeft = left;
550
+ written.top = leg.element.scrollTop;
551
+ written.left = leg.element.scrollLeft;
552
+ }
553
+ if (progress >= 1) {
554
+ run = void 0;
555
+ return;
556
+ }
557
+ active.frame = window.requestAnimationFrame(step);
558
+ }
559
+ function start(legs) {
560
+ cancel();
561
+ const active = settings;
562
+ if (active === void 0) return;
563
+ run = {
564
+ legs,
565
+ started: performance.now(),
566
+ duration: scrollDurationMs(scrollDistance(legs), active),
567
+ 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
591
+ };
592
+ }
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);
603
+ }
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;
610
+ }
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;
621
+ }
622
+ const instant = { ...typeof options === "object" && options !== null ? options : {}, behavior: "instant" };
623
+ const tracked = containers(this);
624
+ const before = tracked.map((element) => ({ top: element.scrollTop, left: element.scrollLeft }));
625
+ shipped.scrollIntoView.call(this, instant);
626
+ const legs = [];
627
+ for (let index = 0; index < tracked.length; index += 1) {
628
+ const element = tracked[index];
629
+ const origin = before[index];
630
+ if (element === void 0 || origin === void 0) continue;
631
+ const landed = { top: element.scrollTop, left: element.scrollLeft };
632
+ element.scrollTop = origin.top;
633
+ element.scrollLeft = origin.left;
634
+ if (Math.abs(landed.top - origin.top) < 1 && Math.abs(landed.left - origin.left) < 1) continue;
635
+ legs.push({ element, fromTop: origin.top, fromLeft: origin.left, toTop: landed.top, toLeft: landed.left });
636
+ }
637
+ if (legs.length > 0) start(legs);
638
+ }
639
+ function onTakeover(event) {
640
+ if (run === void 0) return;
641
+ if (event.type !== "keydown") {
642
+ cancel();
643
+ return;
644
+ }
645
+ if (event.key !== void 0 && SCROLL_KEYS.has(event.key)) cancel();
646
+ }
647
+ function install() {
648
+ if (installed) return;
649
+ 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);
656
+ }
657
+ }
658
+ function uninstall() {
659
+ cancel();
660
+ if (!installed) return;
661
+ 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) {
667
+ window.removeEventListener(kind, onTakeover, true);
668
+ }
669
+ }
670
+ return {
671
+ apply(next) {
672
+ uninstall();
673
+ settings = next;
674
+ if (!next.scroll || next.mode === "off") return;
675
+ if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
676
+ install();
677
+ },
678
+ restore: uninstall
679
+ };
680
+ }
681
+
444
682
  // src/styles.ts
445
683
  var ROW_CSS = `
446
684
  /* Row chrome: AppearanceRow.module.css .group \u2014 16/0 padding, hairline
@@ -622,6 +860,8 @@
622
860
  "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",
623
861
  "motion.quantize": "\u65F6\u957F\u5BF9\u9F50\u5230 Material \u6863\u4F4D",
624
862
  "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",
625
865
  "motion.reduced": "\u59CB\u7EC8\u51CF\u5F31\u52A8\u6548",
626
866
  "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",
627
867
  "motion.system-reduced": "\u7CFB\u7EDF\u5DF2\u8BF7\u6C42\u51CF\u5F31\u52A8\u6548\uFF0C\u672C\u9875\u52A8\u753B\u6B63\u5728\u88AB\u538B\u5236\u3002",
@@ -652,6 +892,8 @@
652
892
  "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.",
653
893
  "motion.quantize": "Snap durations to the Material ladder",
654
894
  "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.",
655
897
  "motion.reduced": "Always reduce motion",
656
898
  "motion.reduced-hint": "Ignore the system reduce-motion preference and force every duration to 1 ms. Off follows the system.",
657
899
  "motion.system-reduced": "The system asked for reduced motion; this page is being held still.",
@@ -700,6 +942,7 @@
700
942
  };
701
943
  const listeners = /* @__PURE__ */ new Set();
702
944
  const motion = createMotion();
945
+ const scrolling = createScrollMotion();
703
946
  const publish = (patch) => {
704
947
  view = { ...view, ...patch };
705
948
  for (const listener of listeners) listener();
@@ -715,8 +958,9 @@
715
958
  const applyMotion = (next) => {
716
959
  if (disposed) return;
717
960
  motion.apply(next);
961
+ scrolling.apply(next);
718
962
  };
719
- const motionDiffers = (left, right) => left.mode !== right.mode || left.durationScale !== right.durationScale || left.spatialSplit !== right.spatialSplit || left.quantize !== right.quantize || left.reduced !== right.reduced;
963
+ const motionDiffers = (left, right) => left.mode !== right.mode || left.durationScale !== right.durationScale || left.spatialSplit !== right.spatialSplit || left.quantize !== right.quantize || left.reduced !== right.reduced || left.scroll !== right.scroll;
720
964
  const clearOverride = () => {
721
965
  disposeOverride?.();
722
966
  disposeOverride = void 0;
@@ -1163,6 +1407,15 @@
1163
1407
  tr("motion.quantize-hint"),
1164
1408
  locked
1165
1409
  ),
1410
+ checkbox(
1411
+ current.scroll,
1412
+ (next) => {
1413
+ void saveMotion({ scroll: next });
1414
+ },
1415
+ tr("motion.scroll"),
1416
+ tr("motion.scroll-hint"),
1417
+ locked
1418
+ ),
1166
1419
  checkbox(
1167
1420
  current.reduced === "always",
1168
1421
  (next) => {
@@ -1245,6 +1498,7 @@
1245
1498
  disposed = true;
1246
1499
  teardown();
1247
1500
  motion.restore();
1501
+ scrolling.restore();
1248
1502
  };
1249
1503
  }, "ui-experience: page-level layers");
1250
1504
  void poll();
package/lib/index.js CHANGED
@@ -39,7 +39,8 @@ var Config = z.object({
39
39
  motionDurationScale: z.number().min(0.5).max(2).default(1).volatile(),
40
40
  motionSpatialSplit: z.boolean().default(true).volatile(),
41
41
  motionQuantize: z.boolean().default(false).volatile(),
42
- motionReduced: z.string().pattern(MOTION_REDUCED).default("system").volatile()
42
+ motionReduced: z.string().pattern(MOTION_REDUCED).default("system").volatile(),
43
+ motionScroll: z.boolean().default(true).volatile()
43
44
  });
44
45
  var REQUIRED_COLOR_KEYS = [
45
46
  "surface_container_lowest",
@@ -319,7 +320,8 @@ function effectiveMotion(config) {
319
320
  durationScale: config.motionDurationScale.get(),
320
321
  spatialSplit: config.motionSpatialSplit.get(),
321
322
  quantize: config.motionQuantize.get(),
322
- reduced: config.motionReduced.get()
323
+ reduced: config.motionReduced.get(),
324
+ scroll: config.motionScroll.get()
323
325
  };
324
326
  }
325
327
  var paletteCache;
@@ -570,7 +572,8 @@ var UiExperienceService = class extends TypertRemoteService {
570
572
  const scale = args?.durationScale;
571
573
  const spatialSplit = args?.spatialSplit;
572
574
  const quantize = args?.quantize;
573
- if (mode === void 0 && reduced === void 0 && scale === void 0 && spatialSplit === void 0 && quantize === void 0) {
575
+ const scroll = args?.scroll;
576
+ if (mode === void 0 && reduced === void 0 && scale === void 0 && spatialSplit === void 0 && quantize === void 0 && scroll === void 0) {
574
577
  throw new Error("ui-experience: setMotion requires at least one field");
575
578
  }
576
579
  if (mode !== void 0 && !MOTION_MODE.test(mode)) {
@@ -588,16 +591,20 @@ var UiExperienceService = class extends TypertRemoteService {
588
591
  if (quantize !== void 0 && typeof quantize !== "boolean") {
589
592
  throw new Error(`ui-experience: quantize must be a boolean, got ${typeof quantize}`);
590
593
  }
594
+ if (scroll !== void 0 && typeof scroll !== "boolean") {
595
+ throw new Error(`ui-experience: scroll must be a boolean, got ${typeof scroll}`);
596
+ }
591
597
  await this.persist((current) => ({
592
598
  ...current,
593
599
  ...mode === void 0 ? {} : { motion: mode },
594
600
  ...reduced === void 0 ? {} : { motionReduced: reduced },
595
601
  ...scale === void 0 ? {} : { motionDurationScale: scale },
596
602
  ...spatialSplit === void 0 ? {} : { motionSpatialSplit: spatialSplit },
597
- ...quantize === void 0 ? {} : { motionQuantize: quantize }
603
+ ...quantize === void 0 ? {} : { motionQuantize: quantize },
604
+ ...scroll === void 0 ? {} : { motionScroll: scroll }
598
605
  }));
599
606
  const applied = effectiveMotion(this.config);
600
- logLine(`setMotion: mode=${applied.mode} durationScale=${String(applied.durationScale)} spatialSplit=${String(applied.spatialSplit)} quantize=${String(applied.quantize)} reduced=${applied.reduced}`);
607
+ logLine(`setMotion: mode=${applied.mode} durationScale=${String(applied.durationScale)} spatialSplit=${String(applied.spatialSplit)} quantize=${String(applied.quantize)} reduced=${applied.reduced} scroll=${String(applied.scroll)}`);
601
608
  return applied;
602
609
  }
603
610
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-plugin/dsh-ui-experience",
3
- "version": "1.0.0-dev.43491412024",
3
+ "version": "1.0.0-dev.43492422772",
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",