@dsh-plugin/dsh-ui-experience 1.0.0-dev.43491051069 → 1.0.0-dev.43492311898

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,26 @@ 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,再乘时长倍率。滚动的手感预期和元素过渡不同:读者对「滚了多远、停在哪」有精确期待,长距离用固定时长会显得急促。
153
+
154
+ `scrollIntoView` 的终点由对齐方式、`scroll-margin`、嵌套容器和元素自身盒模型共同决定,自己算就是「滚到错的那一行」的来源。这里的做法是:先向浏览器要一次瞬时跳转,读出每个被移动容器的落点,把它们全部放回原位,再动画到读到的落点。跳转与还原在同一个任务内完成,中间位置不会绘制。
155
+
156
+ `scrollend` 交给浏览器:逐帧赋值 `scrollTop` 是一串瞬时滚动,浏览器自己的滚动结束检测会在最后一帧落下后触发一次,依赖它结算状态的消费方(聊天记录的跟随控制器)看到的序列与原生平滑滚动一致。
157
+
158
+ ### 滚动驱动动画不动
159
+
160
+ `scroll-timeline` / `animation-timeline` 驱动的动画,进度来自滚动容器而不是时钟。这类动画的 `animation-timing-function` 不是「一条随时间推进的曲线」,而是把滚动位置映射到迭代进度——给它套缓动,等于让遮罩在滚动前段就淡完然后定住,改的是效果本身而非手感。因此只要某条规则的 `animation-timeline` 不是 `auto` / `none`,本层就整条跳过。
161
+
162
+ DSH 目前只有一处用到(提问卡片标题的溢出遮罩 `--question-copy`)。
163
+
144
164
  ### 关键帧感知
145
165
 
146
166
  动画该用哪条曲线,取决于它的 `@keyframes` 到底在动什么。扫描时先建立「关键帧名 → 它改动的属性集合」,再据此判定:同时含 `transform` 的是位移,纯 `opacity` 的是效果。查不到名字(定义在跨源样式表里)时按效果处理。
@@ -184,6 +204,9 @@ M3 那侧的分离要说明白:Material 3 的 `emphasized` 整体是两段首
184
204
  - M3 动效的「位移 / 效果」判定依据属性名与关键帧内容,不分析属性的实际取值变化;判定不出来的情况(例如只由 `all` 声明的过渡)按效果处理,宁可不给过冲。
185
205
  - 跨源样式表里的规则无法改写,只会在日志中记录。
186
206
  - 本层只改写曲线与时长,不新增、不改写关键帧本身;DSH 没定义的动效不会因此出现。
207
+ - CSS 的 `scroll-behavior: smooth`(全仓一处,dock 标签条)不是 transition/animation,本层不碰;只有减弱动效时随压制表置为 `auto`。
208
+ - 脚本滚动由「滚动使用同一曲线」单独控制,与曲线族无关(关掉曲线族就一并停止接管)。
209
+ - 滚动驱动动画(`animation-timeline`)不属于时间轴动画,本层显式跳过,见上一节。
187
210
  - 浏览器半在刷新页面时重载,宿主半只在进程重启时重载。两者版本不一致时(刚升级插件但没重启 DSH),M3 动效行会保持无操作并在控制台记录一行原因,配色功能不受影响。
188
211
 
189
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,
@@ -186,7 +212,8 @@
186
212
  selector: "animation-name",
187
213
  duration: "animation-duration",
188
214
  timing: "animation-timing-function",
189
- iterations: "animation-iteration-count"
215
+ iterations: "animation-iteration-count",
216
+ timeline: "animation-timeline"
190
217
  };
191
218
  var DURATION_TOKENS = [
192
219
  "--ds-transition-duration",
@@ -256,8 +283,9 @@
256
283
  const durations = splitList(declaration.getPropertyValue(family.duration));
257
284
  const timings = splitList(declaration.getPropertyValue(family.timing));
258
285
  const iterations = family.iterations === void 0 ? [] : splitList(declaration.getPropertyValue(family.iterations));
286
+ const timelines = family.timeline === void 0 ? [] : splitList(declaration.getPropertyValue(family.timeline));
259
287
  if (selectors.length === 0 && durations.length === 0 && timings.length === 0) return false;
260
- const count = Math.max(selectors.length, durations.length, timings.length, 1);
288
+ const count = Math.max(selectors.length, durations.length, timings.length, timelines.length, 1);
261
289
  const nextTimings = [];
262
290
  const nextDurations = [];
263
291
  let timingChanged = false;
@@ -268,7 +296,9 @@
268
296
  const originalDuration = entryAt(durations, index);
269
297
  const originalMs = originalDuration === void 0 ? void 0 : parseDurationMs(originalDuration);
270
298
  const infinite = entryAt(iterations, index) === "infinite";
271
- if (selector === "none") {
299
+ const timeline = entryAt(timelines, index);
300
+ const scrollDriven = timeline !== void 0 && timeline !== "auto" && timeline !== "none";
301
+ if (selector === "none" || scrollDriven) {
272
302
  if (originalTiming !== void 0) nextTimings.push(originalTiming);
273
303
  if (originalDuration !== void 0) nextDurations.push(originalDuration);
274
304
  continue;
@@ -437,6 +467,217 @@
437
467
  };
438
468
  }
439
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
+ return Math.min(MAX_MS, grown) * settings.durationScale;
497
+ }
498
+ function maxima(element) {
499
+ return {
500
+ top: Math.max(0, element.scrollHeight - element.clientHeight),
501
+ left: Math.max(0, element.scrollWidth - element.clientWidth)
502
+ };
503
+ }
504
+ function scrollable(element) {
505
+ const max = maxima(element);
506
+ return max.top > 0 || max.left > 0;
507
+ }
508
+ function containers(element) {
509
+ const out = [];
510
+ let node = element.parentElement;
511
+ while (node !== null) {
512
+ if (scrollable(node)) out.push(node);
513
+ node = node.parentElement;
514
+ }
515
+ return out;
516
+ }
517
+ function createScrollMotion() {
518
+ const shipped = {
519
+ scrollTo: Element.prototype.scrollTo,
520
+ scrollBy: Element.prototype.scrollBy,
521
+ scrollIntoView: Element.prototype.scrollIntoView
522
+ };
523
+ let settings;
524
+ let run;
525
+ let installed = false;
526
+ function cancel() {
527
+ if (run === void 0) return;
528
+ window.cancelAnimationFrame(run.frame);
529
+ run = void 0;
530
+ }
531
+ function step(now) {
532
+ const active = run;
533
+ if (active === void 0) return;
534
+ const elapsed = now - active.started;
535
+ const progress = active.duration <= 0 ? 1 : Math.min(1, elapsed / active.duration);
536
+ const eased = easeCurve(effectCurvePoints(settings?.mode ?? "m3e"), progress);
537
+ for (let index = 0; index < active.legs.length; index += 1) {
538
+ const leg = active.legs[index];
539
+ const written = active.written[index];
540
+ if (leg === void 0 || written === void 0) continue;
541
+ if (Math.abs(leg.element.scrollTop - written.top) > 1 || Math.abs(leg.element.scrollLeft - written.left) > 1) {
542
+ cancel();
543
+ return;
544
+ }
545
+ const top = progress >= 1 ? leg.toTop : leg.fromTop + (leg.toTop - leg.fromTop) * eased;
546
+ const left = progress >= 1 ? leg.toLeft : leg.fromLeft + (leg.toLeft - leg.fromLeft) * eased;
547
+ if (leg.element.scrollTop !== top) leg.element.scrollTop = top;
548
+ if (leg.element.scrollLeft !== left) leg.element.scrollLeft = left;
549
+ written.top = leg.element.scrollTop;
550
+ written.left = leg.element.scrollLeft;
551
+ }
552
+ if (progress >= 1) {
553
+ run = void 0;
554
+ return;
555
+ }
556
+ active.frame = window.requestAnimationFrame(step);
557
+ }
558
+ function start(legs) {
559
+ cancel();
560
+ const active = settings;
561
+ if (active === void 0) return;
562
+ run = {
563
+ legs,
564
+ started: performance.now(),
565
+ duration: scrollDurationMs(scrollDistance(legs), active),
566
+ written: legs.map((leg) => ({ top: leg.element.scrollTop, left: leg.element.scrollLeft })),
567
+ frame: window.requestAnimationFrame(step)
568
+ };
569
+ }
570
+ function defer(behavior) {
571
+ if (settings === void 0) return true;
572
+ if (behavior !== "smooth") return true;
573
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
574
+ }
575
+ function legsFor(element, top, left) {
576
+ const max = maxima(element);
577
+ const toTop = top === void 0 ? element.scrollTop : Math.min(max.top, Math.max(0, top));
578
+ const toLeft = left === void 0 ? element.scrollLeft : Math.min(max.left, Math.max(0, left));
579
+ if (Math.abs(toTop - element.scrollTop) < 1 && Math.abs(toLeft - element.scrollLeft) < 1) return void 0;
580
+ return [{ element, fromTop: element.scrollTop, fromLeft: element.scrollLeft, toTop, toLeft }];
581
+ }
582
+ function readOffsets(options, y, base) {
583
+ if (typeof options === "number") {
584
+ return { left: base.left + options, top: y === void 0 ? void 0 : base.top + y };
585
+ }
586
+ if (options === void 0 || options === null) return { top: void 0, left: void 0 };
587
+ return {
588
+ left: options.left === void 0 ? void 0 : base.left + options.left,
589
+ top: options.top === void 0 ? void 0 : base.top + options.top
590
+ };
591
+ }
592
+ function patchedScrollTo(options, y) {
593
+ const behavior = typeof options === "object" && options !== null ? options.behavior : void 0;
594
+ if (defer(behavior)) {
595
+ ;
596
+ shipped.scrollTo.call(this, options, y);
597
+ return;
598
+ }
599
+ const request = readOffsets(options, y, { top: 0, left: 0 });
600
+ const legs = legsFor(this, request.top, request.left);
601
+ if (legs !== void 0) start(legs);
602
+ }
603
+ function patchedScrollBy(options, y) {
604
+ const behavior = typeof options === "object" && options !== null ? options.behavior : void 0;
605
+ if (defer(behavior)) {
606
+ ;
607
+ shipped.scrollBy.call(this, options, y);
608
+ return;
609
+ }
610
+ const request = readOffsets(options, y, { top: this.scrollTop, left: this.scrollLeft });
611
+ const legs = legsFor(this, request.top, request.left);
612
+ if (legs !== void 0) start(legs);
613
+ }
614
+ function patchedScrollIntoView(options) {
615
+ const behavior = typeof options === "object" && options !== null ? options.behavior : void 0;
616
+ if (defer(behavior)) {
617
+ ;
618
+ shipped.scrollIntoView.call(this, options);
619
+ return;
620
+ }
621
+ const instant = { ...typeof options === "object" && options !== null ? options : {}, behavior: "instant" };
622
+ const tracked = containers(this);
623
+ const before = tracked.map((element) => ({ top: element.scrollTop, left: element.scrollLeft }));
624
+ shipped.scrollIntoView.call(this, instant);
625
+ const legs = [];
626
+ for (let index = 0; index < tracked.length; index += 1) {
627
+ const element = tracked[index];
628
+ const origin = before[index];
629
+ if (element === void 0 || origin === void 0) continue;
630
+ const landed = { top: element.scrollTop, left: element.scrollLeft };
631
+ element.scrollTop = origin.top;
632
+ element.scrollLeft = origin.left;
633
+ if (Math.abs(landed.top - origin.top) < 1 && Math.abs(landed.left - origin.left) < 1) continue;
634
+ legs.push({ element, fromTop: origin.top, fromLeft: origin.left, toTop: landed.top, toLeft: landed.left });
635
+ }
636
+ if (legs.length > 0) start(legs);
637
+ }
638
+ function onTakeover(event) {
639
+ if (run === void 0) return;
640
+ if (event.type !== "keydown") {
641
+ cancel();
642
+ return;
643
+ }
644
+ if (event.key !== void 0 && SCROLL_KEYS.has(event.key)) cancel();
645
+ }
646
+ function install() {
647
+ if (installed) return;
648
+ installed = true;
649
+ const prototype = Element.prototype;
650
+ prototype.scrollTo = patchedScrollTo;
651
+ prototype.scrollBy = patchedScrollBy;
652
+ prototype.scrollIntoView = patchedScrollIntoView;
653
+ for (const kind of TAKEOVER_EVENTS) {
654
+ window.addEventListener(kind, onTakeover, true);
655
+ }
656
+ }
657
+ function uninstall() {
658
+ cancel();
659
+ if (!installed) return;
660
+ installed = false;
661
+ const prototype = Element.prototype;
662
+ prototype.scrollTo = shipped.scrollTo;
663
+ prototype.scrollBy = shipped.scrollBy;
664
+ prototype.scrollIntoView = shipped.scrollIntoView;
665
+ for (const kind of TAKEOVER_EVENTS) {
666
+ window.removeEventListener(kind, onTakeover, true);
667
+ }
668
+ }
669
+ return {
670
+ apply(next) {
671
+ uninstall();
672
+ settings = next;
673
+ if (!next.scroll || next.mode === "off") return;
674
+ if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
675
+ install();
676
+ },
677
+ restore: uninstall
678
+ };
679
+ }
680
+
440
681
  // src/styles.ts
441
682
  var ROW_CSS = `
442
683
  /* Row chrome: AppearanceRow.module.css .group \u2014 16/0 padding, hairline
@@ -618,6 +859,8 @@
618
859
  "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",
619
860
  "motion.quantize": "\u65F6\u957F\u5BF9\u9F50\u5230 Material \u6863\u4F4D",
620
861
  "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",
862
+ "motion.scroll": "\u6EDA\u52A8\u4F7F\u7528\u540C\u4E00\u66F2\u7EBF",
863
+ "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",
621
864
  "motion.reduced": "\u59CB\u7EC8\u51CF\u5F31\u52A8\u6548",
622
865
  "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",
623
866
  "motion.system-reduced": "\u7CFB\u7EDF\u5DF2\u8BF7\u6C42\u51CF\u5F31\u52A8\u6548\uFF0C\u672C\u9875\u52A8\u753B\u6B63\u5728\u88AB\u538B\u5236\u3002",
@@ -648,6 +891,8 @@
648
891
  "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.",
649
892
  "motion.quantize": "Snap durations to the Material ladder",
650
893
  "motion.quantize-hint": "Pull rewritten durations onto 50/100/150/200/250/300/350/400/450/500/550/600/700/800/900/1000 ms.",
894
+ "motion.scroll": "Scroll on the same curve",
895
+ "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.",
651
896
  "motion.reduced": "Always reduce motion",
652
897
  "motion.reduced-hint": "Ignore the system reduce-motion preference and force every duration to 1 ms. Off follows the system.",
653
898
  "motion.system-reduced": "The system asked for reduced motion; this page is being held still.",
@@ -696,6 +941,7 @@
696
941
  };
697
942
  const listeners = /* @__PURE__ */ new Set();
698
943
  const motion = createMotion();
944
+ const scrolling = createScrollMotion();
699
945
  const publish = (patch) => {
700
946
  view = { ...view, ...patch };
701
947
  for (const listener of listeners) listener();
@@ -711,8 +957,9 @@
711
957
  const applyMotion = (next) => {
712
958
  if (disposed) return;
713
959
  motion.apply(next);
960
+ scrolling.apply(next);
714
961
  };
715
- const motionDiffers = (left, right) => left.mode !== right.mode || left.durationScale !== right.durationScale || left.spatialSplit !== right.spatialSplit || left.quantize !== right.quantize || left.reduced !== right.reduced;
962
+ 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;
716
963
  const clearOverride = () => {
717
964
  disposeOverride?.();
718
965
  disposeOverride = void 0;
@@ -1159,6 +1406,15 @@
1159
1406
  tr("motion.quantize-hint"),
1160
1407
  locked
1161
1408
  ),
1409
+ checkbox(
1410
+ current.scroll,
1411
+ (next) => {
1412
+ void saveMotion({ scroll: next });
1413
+ },
1414
+ tr("motion.scroll"),
1415
+ tr("motion.scroll-hint"),
1416
+ locked
1417
+ ),
1162
1418
  checkbox(
1163
1419
  current.reduced === "always",
1164
1420
  (next) => {
@@ -1241,6 +1497,7 @@
1241
1497
  disposed = true;
1242
1498
  teardown();
1243
1499
  motion.restore();
1500
+ scrolling.restore();
1244
1501
  };
1245
1502
  }, "ui-experience: page-level layers");
1246
1503
  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.43491051069",
3
+ "version": "1.0.0-dev.43492311898",
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",