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

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/lib/client.js CHANGED
@@ -12,7 +12,13 @@
12
12
  spatialSplit: true,
13
13
  quantize: false,
14
14
  reduced: "system",
15
- scroll: true
15
+ scroll: false,
16
+ stream: true,
17
+ followScroll: true,
18
+ streamRevealMinMs: 250,
19
+ streamRevealMaxMs: 1200,
20
+ streamEnterMs: 220,
21
+ streamRollMs: 260
16
22
  };
17
23
 
18
24
  // src/motion-plan.ts
@@ -403,8 +409,7 @@
403
409
  function bringsStylesheet(node) {
404
410
  if (node === void 0) return false;
405
411
  const name = node.nodeName?.toUpperCase();
406
- if (name === "STYLE" || name === "LINK") return true;
407
- return (node.querySelectorAll?.("style,link")?.length ?? 0) > 0;
412
+ return name === "STYLE" || name === "LINK";
408
413
  }
409
414
  function observeDocument() {
410
415
  const instance = new MutationObserver((records) => {
@@ -418,7 +423,7 @@
418
423
  }
419
424
  });
420
425
  instance.observe(document.head, { childList: true, subtree: true });
421
- instance.observe(document.body, { childList: true, subtree: true });
426
+ instance.observe(document.body, { childList: true });
422
427
  return { disconnect: () => {
423
428
  instance.disconnect();
424
429
  } };
@@ -485,7 +490,20 @@
485
490
  "End",
486
491
  " "
487
492
  ]);
488
- var EDITABLE = 'input, textarea, select, [contenteditable=""], [contenteditable="true"], [contenteditable="plaintext-only"]';
493
+ var EDITABLE = [
494
+ "input:not([type=checkbox]):not([type=radio]):not([type=button]):not([type=submit]):not([type=reset]):not([type=file]):not([type=range])",
495
+ "textarea",
496
+ "select",
497
+ '[contenteditable=""]',
498
+ '[contenteditable="true"]',
499
+ '[contenteditable="plaintext-only"]'
500
+ ].join(", ");
501
+ var JUMP_ANIMATE_MIN = 24;
502
+ var WHEEL_SETTLE_PX = 0.5;
503
+ var PIXELS_PER_LINE = 40;
504
+ var PAGE_FRACTION = 0.875;
505
+ var PAGE_OVERLAP = 40;
506
+ var WHEEL_MAX_MS = 900;
489
507
  function scrollDistance(legs) {
490
508
  let distance = 0;
491
509
  for (const leg of legs) {
@@ -515,23 +533,77 @@
515
533
  return out;
516
534
  }
517
535
  function createScrollMotion() {
518
- const shipped = Element.prototype.scrollIntoView;
536
+ const shipped = {
537
+ scrollTo: Element.prototype.scrollTo,
538
+ scrollBy: Element.prototype.scrollBy,
539
+ scrollIntoView: Element.prototype.scrollIntoView
540
+ };
541
+ const topAxis = Object.getOwnPropertyDescriptor(Element.prototype, "scrollTop");
542
+ const leftAxis = Object.getOwnPropertyDescriptor(Element.prototype, "scrollLeft");
519
543
  let settings;
520
544
  let run;
545
+ let wheel;
521
546
  let installed = false;
522
- function cancel() {
547
+ let writing = 0;
548
+ const shadowed = /* @__PURE__ */ new WeakMap();
549
+ let dispatching = false;
550
+ const END_GRACE_MS = 400;
551
+ const graceful = /* @__PURE__ */ new Set();
552
+ function writeAxis(element, axis, value) {
553
+ const accessor = axis === "top" ? topAxis : leftAxis;
554
+ if (accessor !== void 0) {
555
+ writing += 1;
556
+ try {
557
+ accessor.set.call(element, value);
558
+ } finally {
559
+ writing -= 1;
560
+ }
561
+ return;
562
+ }
563
+ if (axis === "top") element.scrollTop = value;
564
+ else element.scrollLeft = value;
565
+ }
566
+ function clearGrace() {
567
+ for (const element of graceful) shadowed.delete(element);
568
+ graceful.clear();
569
+ }
570
+ function hold(element) {
571
+ shadowed.set(element, { until: Number.POSITIVE_INFINITY, top: 0, left: 0 });
572
+ }
573
+ function release(element, notify) {
574
+ if (!shadowed.has(element)) return;
575
+ shadowed.set(element, {
576
+ until: performance.now() + END_GRACE_MS,
577
+ top: element.scrollTop,
578
+ left: element.scrollLeft
579
+ });
580
+ graceful.add(element);
581
+ if (!notify) return;
582
+ queueMicrotask(() => {
583
+ if (shadowed.get(element)?.until === Number.POSITIVE_INFINITY) return;
584
+ dispatching = true;
585
+ try {
586
+ element.dispatchEvent(new Event("scrollend"));
587
+ } finally {
588
+ dispatching = false;
589
+ }
590
+ });
591
+ }
592
+ function cancel(notify = true) {
593
+ stopWheel();
523
594
  const active = run;
524
595
  if (active === void 0) return;
525
596
  window.cancelAnimationFrame(active.frame);
526
597
  run = void 0;
527
- active.done();
598
+ for (const leg of active.legs) release(leg.element, notify);
599
+ for (const settle of active.done) settle();
528
600
  }
529
601
  function step(now) {
530
602
  const active = run;
531
603
  if (active === void 0) return;
532
604
  const elapsed = now - active.started;
533
605
  const progress = active.duration <= 0 ? 1 : Math.min(1, elapsed / active.duration);
534
- const eased = easeCurve(effectCurvePoints(settings?.mode ?? "m3e"), progress);
606
+ const eased = easeCurve(effectCurvePoints(settings.mode), progress);
535
607
  for (let index = 0; index < active.legs.length; index += 1) {
536
608
  const leg = active.legs[index];
537
609
  const written = active.written[index];
@@ -540,16 +612,24 @@
540
612
  cancel();
541
613
  return;
542
614
  }
615
+ }
616
+ for (const leg of active.legs) {
543
617
  const top = progress >= 1 ? leg.toTop : leg.fromTop + (leg.toTop - leg.fromTop) * eased;
544
618
  const left = progress >= 1 ? leg.toLeft : leg.fromLeft + (leg.toLeft - leg.fromLeft) * eased;
545
- if (leg.element.scrollTop !== top) leg.element.scrollTop = top;
546
- if (leg.element.scrollLeft !== left) leg.element.scrollLeft = left;
619
+ if (leg.element.scrollTop !== top) writeAxis(leg.element, "top", top);
620
+ if (leg.element.scrollLeft !== left) writeAxis(leg.element, "left", left);
621
+ }
622
+ for (let index = 0; index < active.legs.length; index += 1) {
623
+ const leg = active.legs[index];
624
+ const written = active.written[index];
625
+ if (leg === void 0 || written === void 0) continue;
547
626
  written.top = leg.element.scrollTop;
548
627
  written.left = leg.element.scrollLeft;
549
628
  }
550
629
  if (progress >= 1) {
551
630
  run = void 0;
552
- active.done();
631
+ for (const leg of active.legs) release(leg.element, true);
632
+ for (const settle of active.done) settle();
553
633
  return;
554
634
  }
555
635
  active.frame = window.requestAnimationFrame(step);
@@ -557,86 +637,400 @@
557
637
  function start(legs, done) {
558
638
  cancel();
559
639
  const active = settings;
560
- if (active === void 0) {
561
- done();
562
- return;
563
- }
640
+ for (const leg of legs) hold(leg.element);
564
641
  run = {
565
- legs,
642
+ legs: [...legs],
566
643
  started: performance.now(),
567
644
  duration: scrollDurationMs(scrollDistance(legs), active),
568
645
  written: legs.map((leg) => ({ top: leg.element.scrollTop, left: leg.element.scrollLeft })),
569
646
  frame: window.requestAnimationFrame(step),
570
- done
647
+ done: [done]
571
648
  };
572
649
  }
650
+ function merge(legs, done) {
651
+ const active = run;
652
+ if (active === void 0) return false;
653
+ if (active.legs.length !== legs.length) return false;
654
+ for (let index = 0; index < legs.length; index += 1) {
655
+ if (active.legs[index]?.element !== legs[index]?.element) return false;
656
+ }
657
+ active.legs = [...legs];
658
+ active.started = performance.now();
659
+ active.duration = scrollDurationMs(scrollDistance(legs), settings);
660
+ active.written = legs.map((leg) => ({ top: leg.element.scrollTop, left: leg.element.scrollLeft }));
661
+ active.done.push(done);
662
+ return true;
663
+ }
573
664
  function wantsInstant() {
574
665
  const active = settings;
575
- if (active === void 0) return true;
576
666
  if (active.reduced === "always") return true;
577
667
  return active.reduced === "system" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
578
668
  }
579
- function patchedScrollIntoView(...args) {
669
+ const glides = /* @__PURE__ */ new Map();
670
+ const lastWrite = /* @__PURE__ */ new WeakMap();
671
+ const GLIDE_MIN_PX = 4;
672
+ const GLIDE_MAX_PX = 600;
673
+ const GLIDE_MIN_MS = 90;
674
+ const GLIDE_MAX_MS = 300;
675
+ const GLIDE_PX_PER_MS = 1.8;
676
+ const GLIDE_FRAME_GAP_MS = 32;
677
+ const GLIDE_MAX_LAG_PX = 40;
678
+ function shouldGlide(element, value, now) {
679
+ const active = settings;
680
+ if (active === void 0 || !active.followScroll) return false;
681
+ if (wantsInstant()) return false;
682
+ const max = maxima(element);
683
+ if (max.top <= 0) return false;
684
+ const delta = value - topAxis.get.call(element);
685
+ const size = Math.abs(delta);
686
+ if (size < GLIDE_MIN_PX || size > GLIDE_MAX_PX) return false;
687
+ if (delta > 0 && Math.abs(value - max.top) > 4) return false;
688
+ if (max.top - topAxis.get.call(element) > GLIDE_MAX_LAG_PX) return false;
689
+ const previous = lastWrite.get(element);
690
+ lastWrite.set(element, now);
691
+ if (previous !== void 0 && now - previous < GLIDE_FRAME_GAP_MS) return false;
692
+ return true;
693
+ }
694
+ function startGlide(element, target) {
695
+ const active = settings;
696
+ const existing = glides.get(element);
697
+ if (existing !== void 0) window.cancelAnimationFrame(existing.frame);
698
+ else hold(element);
699
+ const from = topAxis.get.call(element);
700
+ const raw = Math.abs(target - from) / GLIDE_PX_PER_MS;
701
+ const duration = Math.min(GLIDE_MAX_MS, Math.max(GLIDE_MIN_MS, raw)) * active.durationScale;
702
+ const glide = {
703
+ target,
704
+ from,
705
+ started: performance.now(),
706
+ duration: active.quantize ? quantizeMs(duration) : duration,
707
+ frame: 0,
708
+ written: from
709
+ };
710
+ glides.set(element, glide);
711
+ const mode = active.mode;
712
+ const step2 = (now) => {
713
+ const live = glides.get(element);
714
+ if (live !== glide) return;
715
+ if (Math.abs(topAxis.get.call(element) - glide.written) > 1) {
716
+ finishGlide(element, false);
717
+ return;
718
+ }
719
+ const progress = Math.min(1, (now - glide.started) / Math.max(1, glide.duration));
720
+ const eased = easeCurve(effectCurvePoints(mode), progress);
721
+ writeAxis(element, "top", glide.from + (glide.target - glide.from) * eased);
722
+ glide.written = topAxis.get.call(element);
723
+ if (progress >= 1) {
724
+ finishGlide(element, true);
725
+ return;
726
+ }
727
+ glide.frame = window.requestAnimationFrame(step2);
728
+ };
729
+ glide.frame = window.requestAnimationFrame(step2);
730
+ }
731
+ function finishGlide(element, landed) {
732
+ const glide = glides.get(element);
733
+ if (glide === void 0) return;
734
+ window.cancelAnimationFrame(glide.frame);
735
+ glides.delete(element);
736
+ if (landed) writeAxis(element, "top", glide.target);
737
+ release(element, true);
738
+ }
739
+ function finishGlides() {
740
+ for (const [element] of glides) finishGlide(element, true);
741
+ }
742
+ function stopWheel() {
743
+ const active = wheel;
744
+ if (active === void 0) return;
745
+ window.cancelAnimationFrame(active.frame);
746
+ wheel = void 0;
747
+ release(active.element, true);
748
+ }
749
+ function wheelStep(now) {
750
+ const active = wheel;
751
+ if (active === void 0) return;
752
+ const element = active.element;
753
+ const progress = active.duration <= 0 ? 1 : Math.min(1, (now - active.started) / active.duration);
754
+ const eased = easeCurve(effectCurvePoints(settings.mode), progress);
755
+ if (progress >= 1) {
756
+ writeAxis(element, "top", active.top);
757
+ writeAxis(element, "left", active.left);
758
+ wheel = void 0;
759
+ release(element, true);
760
+ return;
761
+ }
762
+ writeAxis(element, "top", active.fromTop + (active.top - active.fromTop) * eased);
763
+ writeAxis(element, "left", active.fromLeft + (active.left - active.fromLeft) * eased);
764
+ active.frame = window.requestAnimationFrame(wheelStep);
765
+ }
766
+ function onWheel(event) {
767
+ const active = settings;
768
+ if (active === void 0) return false;
769
+ if (event.ctrlKey || event.metaKey) return false;
770
+ if (wantsInstant()) return false;
771
+ const target = event.target;
772
+ if (target === null) return false;
773
+ if (target instanceof Element && target.closest(EDITABLE) !== null) return false;
774
+ if (event.deltaX === 0 && event.deltaY === 0) return false;
775
+ const element = scrollportOf(target, {
776
+ top: event.deltaY !== 0,
777
+ left: event.deltaX !== 0
778
+ });
779
+ if (element === null) return false;
780
+ const max = maxima(element);
781
+ const delta = pixelDelta(event, element);
782
+ const base = wheel?.element === element ? wheel : void 0;
783
+ const fromTop = element.scrollTop;
784
+ const fromLeft = element.scrollLeft;
785
+ const top = Math.min(max.top, Math.max(0, (base?.top ?? fromTop) + delta.y));
786
+ const left = Math.min(max.left, Math.max(0, (base?.left ?? fromLeft) + delta.x));
787
+ if (Math.abs(top - fromTop) < WHEEL_SETTLE_PX && Math.abs(left - fromLeft) < WHEEL_SETTLE_PX) return false;
788
+ event.preventDefault();
789
+ const stopped = run;
790
+ if (stopped !== void 0) {
791
+ window.cancelAnimationFrame(stopped.frame);
792
+ run = void 0;
793
+ for (const leg of stopped.legs) release(leg.element, true);
794
+ for (const settle of stopped.done) settle();
795
+ }
796
+ if (base === void 0) hold(element);
797
+ else window.cancelAnimationFrame(base.frame);
798
+ const travelled = Math.max(Math.abs(top - fromTop), Math.abs(left - fromLeft));
799
+ const segment = {
800
+ element,
801
+ top,
802
+ left,
803
+ fromTop,
804
+ fromLeft,
805
+ started: performance.now(),
806
+ duration: Math.min(WHEEL_MAX_MS, scrollDurationMs(travelled, active)),
807
+ frame: 0
808
+ };
809
+ wheel = segment;
810
+ segment.frame = window.requestAnimationFrame(wheelStep);
811
+ return true;
812
+ }
813
+ function scrollportOf(node, axis) {
814
+ let current = node;
815
+ while (current !== null) {
816
+ const max = maxima(current);
817
+ if (axis.top && max.top > 0 || axis.left && max.left > 0) return current;
818
+ current = current.parentElement;
819
+ }
820
+ return null;
821
+ }
822
+ function pageStep(length) {
823
+ return Math.max(1, Math.max(length * PAGE_FRACTION, length - PAGE_OVERLAP));
824
+ }
825
+ function pixelDelta(event, element) {
826
+ if (event.deltaMode === 0) return { x: event.deltaX, y: event.deltaY };
827
+ const unitY = event.deltaMode === 1 ? PIXELS_PER_LINE : pageStep(element.clientHeight);
828
+ const unitX = event.deltaMode === 1 ? PIXELS_PER_LINE : pageStep(element.clientWidth);
829
+ return { x: event.deltaX * unitX, y: event.deltaY * unitY };
830
+ }
831
+ function animates(behavior, distance) {
832
+ return behavior === "instant" && Math.max(distance.top, distance.left) >= JUMP_ANIMATE_MIN;
833
+ }
834
+ function scrollIntoView(...args) {
580
835
  const options = args[0];
581
836
  const requested = typeof options === "object" && options !== null ? options : void 0;
582
837
  const active = settings;
583
- if (active === void 0 || requested?.behavior !== "smooth") {
584
- shipped.apply(this, args);
585
- return void 0;
586
- }
838
+ if (active === void 0 || requested?.behavior !== "smooth") return shipped.scrollIntoView.apply(this, args);
587
839
  const jump = { ...requested, behavior: "instant" };
588
- if (wantsInstant()) {
589
- shipped.call(this, jump);
590
- return void 0;
591
- }
840
+ if (wantsInstant()) return shipped.scrollIntoView.call(this, jump);
592
841
  const tracked = containers(this);
842
+ for (const element of tracked) hold(element);
593
843
  const before = tracked.map((element) => ({ top: element.scrollTop, left: element.scrollLeft }));
594
- shipped.call(this, jump);
844
+ shipped.scrollIntoView.call(this, jump);
595
845
  const legs = [];
596
846
  for (let index = 0; index < tracked.length; index += 1) {
597
847
  const element = tracked[index];
598
848
  const origin = before[index];
599
849
  if (element === void 0 || origin === void 0) continue;
600
850
  const landed = { top: element.scrollTop, left: element.scrollLeft };
601
- element.scrollTop = origin.top;
602
- element.scrollLeft = origin.left;
851
+ writeAxis(element, "top", origin.top);
852
+ writeAxis(element, "left", origin.left);
603
853
  const moved = Math.abs(landed.top - origin.top) > MOTION_EPSILON || Math.abs(landed.left - origin.left) > MOTION_EPSILON;
604
- if (!moved) continue;
854
+ if (!moved) {
855
+ if (run?.legs.some((leg) => leg.element === element)) continue;
856
+ release(element, false);
857
+ continue;
858
+ }
605
859
  legs.push({ element, fromTop: origin.top, fromLeft: origin.left, toTop: landed.top, toLeft: landed.left });
606
860
  }
607
- if (legs.length === 0) return void 0;
861
+ if (legs.length === 0) return Promise.resolve();
608
862
  return new Promise((resolve) => {
609
- start(legs, resolve);
863
+ if (!merge(legs, resolve)) start(legs, resolve);
610
864
  });
611
865
  }
612
- function onTakeover(event) {
866
+ function readOffsets(options, base) {
867
+ return {
868
+ left: options.left === void 0 ? void 0 : base.left + options.left,
869
+ top: options.top === void 0 ? void 0 : base.top + options.top
870
+ };
871
+ }
872
+ function legFor(element, top, left) {
873
+ const max = maxima(element);
874
+ const toTop = top === void 0 ? element.scrollTop : Math.min(max.top, Math.max(0, top));
875
+ const toLeft = left === void 0 ? element.scrollLeft : Math.min(max.left, Math.max(0, left));
876
+ if (Math.abs(toTop - element.scrollTop) < MOTION_EPSILON && Math.abs(toLeft - element.scrollLeft) < MOTION_EPSILON) return void 0;
877
+ return { element, fromTop: element.scrollTop, fromLeft: element.scrollLeft, toTop, toLeft };
878
+ }
879
+ function patchedOffset(element, args, shippedMethod, relative) {
880
+ if (writing > 0) return shippedMethod.apply(element, args);
881
+ const options = args[0];
882
+ const behavior = typeof options === "object" && options !== null ? options.behavior : void 0;
883
+ const active = settings;
884
+ if (active === void 0 || wantsInstant()) return shippedMethod.apply(element, args);
885
+ const base = relative ? { top: element.scrollTop, left: element.scrollLeft } : { top: 0, left: 0 };
886
+ const request = readOffsets(options, base);
887
+ const leg = legFor(element, request.top, request.left);
888
+ if (leg === void 0) return Promise.resolve();
889
+ if (behavior === "smooth") {
890
+ return new Promise((resolve) => {
891
+ if (!merge([leg], resolve)) start([leg], resolve);
892
+ });
893
+ }
894
+ if (!animates(behavior, {
895
+ top: Math.abs(leg.toTop - leg.fromTop),
896
+ left: Math.abs(leg.toLeft - leg.fromLeft)
897
+ })) {
898
+ shippedMethod.apply(element, args);
899
+ return Promise.resolve();
900
+ }
901
+ return new Promise((resolve) => {
902
+ start([leg], resolve);
903
+ });
904
+ }
905
+ function scrollTo(...args) {
906
+ return patchedOffset(this, args, shipped.scrollTo, false);
907
+ }
908
+ function scrollBy(...args) {
909
+ return patchedOffset(this, args, shipped.scrollBy, true);
910
+ }
911
+ function resolveTarget(target) {
912
+ if (target === null) return null;
913
+ return target;
914
+ }
915
+ function onScrollEnd(event) {
916
+ if (dispatching) return;
917
+ const target = resolveTarget(event.target);
918
+ if (target === null) return;
919
+ const entry = shadowed.get(target);
920
+ if (entry === void 0) return;
921
+ if (entry.until === Number.POSITIVE_INFINITY) {
922
+ event.stopImmediatePropagation();
923
+ return;
924
+ }
925
+ const unmoved = Math.abs(target.scrollTop - entry.top) <= FOREIGN_TOLERANCE && Math.abs(target.scrollLeft - entry.left) <= FOREIGN_TOLERANCE;
926
+ if (!unmoved || performance.now() > entry.until) {
927
+ shadowed.delete(target);
928
+ graceful.delete(target);
929
+ return;
930
+ }
931
+ event.stopImmediatePropagation();
932
+ }
933
+ function onWheelBubble(event) {
934
+ cancelIfOwned(event.target);
935
+ if (event.defaultPrevented) return;
936
+ onWheel(event);
937
+ }
938
+ function cancelIfOwned(target) {
939
+ for (const [element] of glides) {
940
+ if (target === null || element.contains(target)) finishGlide(element, false);
941
+ }
613
942
  const active = run;
614
- if (active === void 0) return;
615
- if (event.type === "keydown") {
943
+ if (active === void 0 || target === null) return false;
944
+ if (!active.legs.some((leg) => leg.element.contains(target))) return false;
945
+ cancel();
946
+ return true;
947
+ }
948
+ function onTakeover(event) {
949
+ const key = event.type === "keydown";
950
+ if (key) {
616
951
  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;
619
- cancel();
952
+ const focus = event.target;
953
+ if (focus instanceof Element && focus.closest(EDITABLE) !== null) return;
954
+ }
955
+ if (run === void 0) {
956
+ clearGrace();
620
957
  return;
621
958
  }
622
- const target = event.target;
623
- if (target === null || !active.legs.some((leg) => leg.element.contains(target))) return;
624
- cancel();
959
+ if (key) cancel();
960
+ else cancelIfOwned(event.target);
961
+ }
962
+ function ownsPatches() {
963
+ const prototype = Element.prototype;
964
+ return prototype.scrollTo === scrollTo && prototype.scrollBy === scrollBy && prototype.scrollIntoView === scrollIntoView;
625
965
  }
626
966
  function install() {
627
967
  if (installed) return;
968
+ const current = Element.prototype;
969
+ if (current.scrollTo !== shipped.scrollTo || current.scrollIntoView !== shipped.scrollIntoView) {
970
+ console.error("ui-experience: scroll methods were already replaced when the motion layer started; scripted scrolling is left off until the page reloads");
971
+ return;
972
+ }
628
973
  installed = true;
629
- Element.prototype.scrollIntoView = patchedScrollIntoView;
630
- for (const kind of ["wheel", "touchstart", "keydown"]) {
974
+ const prototype = Element.prototype;
975
+ prototype.scrollTo = scrollTo;
976
+ prototype.scrollBy = scrollBy;
977
+ prototype.scrollIntoView = scrollIntoView;
978
+ if (topAxis !== void 0) {
979
+ Object.defineProperty(Element.prototype, "scrollTop", {
980
+ configurable: true,
981
+ enumerable: topAxis.enumerable,
982
+ get() {
983
+ const glide = glides.get(this);
984
+ return glide === void 0 ? topAxis.get.call(this) : glide.target;
985
+ },
986
+ set(value) {
987
+ if (writing > 0 || !shouldGlide(this, value, performance.now())) {
988
+ topAxis.set.call(this, value);
989
+ return;
990
+ }
991
+ startGlide(this, value);
992
+ }
993
+ });
994
+ }
995
+ window.addEventListener("scrollend", onScrollEnd, true);
996
+ window.addEventListener(
997
+ "wheel",
998
+ onWheelBubble,
999
+ { capture: false, passive: false }
1000
+ );
1001
+ for (const kind of ["touchstart", "keydown"]) {
631
1002
  window.addEventListener(kind, onTakeover, { capture: true, passive: true });
632
1003
  }
633
1004
  }
634
1005
  function uninstall() {
635
- cancel();
1006
+ finishGlides();
1007
+ cancel(false);
1008
+ clearGrace();
636
1009
  if (!installed) return;
637
1010
  installed = false;
638
- Element.prototype.scrollIntoView = shipped;
639
- for (const kind of ["wheel", "touchstart", "keydown"]) {
1011
+ if (!ownsPatches()) {
1012
+ console.error("ui-experience: the scroll methods were replaced while scripted scrolling was active; leaving the current implementations in place instead of restoring stale ones");
1013
+ return;
1014
+ }
1015
+ const prototype = Element.prototype;
1016
+ prototype.scrollTo = shipped.scrollTo;
1017
+ prototype.scrollBy = shipped.scrollBy;
1018
+ prototype.scrollIntoView = shipped.scrollIntoView;
1019
+ if (topAxis !== void 0) {
1020
+ Object.defineProperty(Element.prototype, "scrollTop", {
1021
+ configurable: true,
1022
+ enumerable: topAxis.enumerable,
1023
+ get: topAxis.get,
1024
+ set: topAxis.set
1025
+ });
1026
+ }
1027
+ window.removeEventListener("scrollend", onScrollEnd, true);
1028
+ window.removeEventListener(
1029
+ "wheel",
1030
+ onWheelBubble,
1031
+ false
1032
+ );
1033
+ for (const kind of ["touchstart", "keydown"]) {
640
1034
  window.removeEventListener(kind, onTakeover, true);
641
1035
  }
642
1036
  }
@@ -647,6 +1041,552 @@
647
1041
  if (!next.scroll || next.mode === "off") return;
648
1042
  install();
649
1043
  },
1044
+ restore() {
1045
+ uninstall();
1046
+ settings = void 0;
1047
+ }
1048
+ };
1049
+ }
1050
+
1051
+ // src/stream.ts
1052
+ var STREAM_ATTRIBUTE = "data-dsh-uix-stream";
1053
+ var LAST_ATTRIBUTE = "data-dsh-uix-last";
1054
+ var WIDTH_ATTRIBUTE = "data-dsh-uix-width";
1055
+ var ENTER_ATTRIBUTE = "data-dsh-uix-enter";
1056
+ var ROLL_ATTRIBUTE = "data-dsh-uix-rolling";
1057
+ var ROW = "[data-chat-flow-kind], [data-chat-anchor-key], [data-submission-echo], [data-pending-steering]";
1058
+ var ENTER_FALLBACK_MS = 220;
1059
+ var LIVE = "[data-streaming], [data-process-activity] [data-shimmer]";
1060
+ var MIN_REVEAL_PX = 0.5;
1061
+ var REVEAL_EXPONENT = 0.62;
1062
+ var ONE_LINE_PX = 24;
1063
+ var FADE_BAND = 0.45;
1064
+ var SINGLE_LINE_RATIO = 1.5;
1065
+ var MIN_TILT_DEG = 15;
1066
+ var MAX_TILT_DEG = 45;
1067
+ var STREAM_CSS = `
1068
+ @property --dsh-uix-reveal {
1069
+ syntax: '<length>';
1070
+ inherits: false;
1071
+ initial-value: 0px;
1072
+ }
1073
+
1074
+ @property --dsh-uix-edge {
1075
+ syntax: '<length>';
1076
+ inherits: false;
1077
+ initial-value: 0px;
1078
+ }
1079
+
1080
+ @property --dsh-uix-tilt {
1081
+ syntax: '<angle>';
1082
+ inherits: false;
1083
+ initial-value: 180deg;
1084
+ }
1085
+
1086
+ /* The mask is a tilted reveal line with a soft trailing edge, so text arrives out
1087
+ of a gradient rather than at a hard boundary. The tilt is held inside 15deg-45deg
1088
+ off horizontal, which is what makes the edge read as a diagonal wipe; any
1089
+ shallower and it looks like a flat line across the block. */
1090
+ [${STREAM_ATTRIBUTE}="reveal"] {
1091
+ --dsh-uix-reveal: 0px;
1092
+ mask-image: linear-gradient(
1093
+ var(--dsh-uix-tilt),
1094
+ #000 calc(var(--dsh-uix-reveal) - ${FADE_BAND}lh),
1095
+ transparent var(--dsh-uix-reveal)
1096
+ );
1097
+ mask-size: 100% 100%;
1098
+ mask-repeat: no-repeat;
1099
+ }
1100
+
1101
+ /* One-line labels: a horizontal edge at the end of the text a reader has already
1102
+ seen sweeps out to the new width, so only the arriving characters are revealed.
1103
+
1104
+ The edge is a registered custom property driven by a transition. Keyframing the
1105
+ gradient itself does not work: Chrome snaps between two linear-gradients instead of
1106
+ interpolating them, which reads as no animation at all.
1107
+
1108
+ A TextShimmer root holds a decorative copy of its children for its own sweep, so
1109
+ its mask goes on the readable first child. The reasoning summary masks itself,
1110
+ which also replaces the static fade the application puts on it. */
1111
+ [${STREAM_ATTRIBUTE}="append"][data-shimmer] > :first-child,
1112
+ [${STREAM_ATTRIBUTE}="append"][data-streaming]:not([data-shimmer]) {
1113
+ --dsh-uix-edge: 100000px;
1114
+ mask-image: linear-gradient(
1115
+ to right,
1116
+ #000 calc(var(--dsh-uix-edge) - 1.2ch),
1117
+ transparent var(--dsh-uix-edge)
1118
+ );
1119
+ }
1120
+
1121
+ /* A TextShimmer root draws a second, inert copy of its children and sweeps a band of
1122
+ light through it \u2014 the application's own idle shimmer. This layer moves a mask over
1123
+ the readable copy instead, so the two would run at once: a band of light crossing a
1124
+ line whose text is also being uncovered, which reads as two animations fighting.
1125
+
1126
+ The decorative copy is always the second child, and only exists while the label is
1127
+ active, so this matches nothing on an idle one. */
1128
+ [${STREAM_ATTRIBUTE}="append"][data-shimmer] > :nth-child(2),
1129
+ [${STREAM_ATTRIBUTE}="append"] [data-shimmer] > :nth-child(2) {
1130
+ display: none;
1131
+ }
1132
+
1133
+ /* A label arrives as a whole line and rises into place; only later growth is swept,
1134
+ so arrival and extension read differently. */
1135
+ [${STREAM_ATTRIBUTE}="append"][data-shimmer][${ROLL_ATTRIBUTE}] > :first-child,
1136
+ [${STREAM_ATTRIBUTE}="append"][data-streaming]:not([data-shimmer])[${ROLL_ATTRIBUTE}] {
1137
+ animation: dsh-uix-arrive var(--dsh-uix-roll, 260ms) var(--dsh-uix-ease, ease) both;
1138
+ }
1139
+
1140
+ @keyframes dsh-uix-arrive {
1141
+ from { transform: translateY(0.85lh); opacity: 0; }
1142
+ to { transform: translateY(0); opacity: 1; }
1143
+ }
1144
+
1145
+ /* A row that arrives while the transcript is following its tail rises into place.
1146
+ Only opacity and transform are animated, so the height the follow loop reads back
1147
+ right after its write is the height it asked for. */
1148
+ [${ENTER_ATTRIBUTE}] {
1149
+ animation: dsh-uix-enter var(--dsh-uix-enter, 220ms) var(--dsh-uix-ease, ease) both;
1150
+ }
1151
+
1152
+ @keyframes dsh-uix-enter {
1153
+ from { opacity: 0; transform: translateY(-6px); }
1154
+ to { opacity: 1; transform: none; }
1155
+ }
1156
+
1157
+ @media (prefers-reduced-motion: reduce) {
1158
+ [${STREAM_ATTRIBUTE}="reveal"] {
1159
+ mask-image: none;
1160
+ }
1161
+
1162
+ [${ENTER_ATTRIBUTE}] {
1163
+ animation: none;
1164
+ }
1165
+
1166
+ [${STREAM_ATTRIBUTE}="append"][data-shimmer][${ROLL_ATTRIBUTE}] > :first-child,
1167
+ [${STREAM_ATTRIBUTE}="append"][data-streaming]:not([data-shimmer])[${ROLL_ATTRIBUTE}] {
1168
+ animation: none;
1169
+ }
1170
+
1171
+ [${STREAM_ATTRIBUTE}="append"][data-shimmer] > :first-child,
1172
+ [${STREAM_ATTRIBUTE}="append"][data-streaming]:not([data-shimmer]) {
1173
+ mask-image: none;
1174
+ }
1175
+ }
1176
+ `;
1177
+ function createStreamMotion() {
1178
+ const reveals = /* @__PURE__ */ new Map();
1179
+ let knownRows = /* @__PURE__ */ new WeakSet();
1180
+ let following = false;
1181
+ let sheet;
1182
+ let observer;
1183
+ let settings;
1184
+ let pending = false;
1185
+ let installed = false;
1186
+ let ease = "";
1187
+ let rollMs = 260;
1188
+ let enterMs = ENTER_FALLBACK_MS;
1189
+ let revealMinMs = 250;
1190
+ let revealMaxMs = 1200;
1191
+ const generation = /* @__PURE__ */ new WeakMap();
1192
+ const sweeps = /* @__PURE__ */ new WeakMap();
1193
+ const rests = /* @__PURE__ */ new WeakMap();
1194
+ const heights = /* @__PURE__ */ new WeakMap();
1195
+ const rolls = /* @__PURE__ */ new WeakMap();
1196
+ const slots = /* @__PURE__ */ new WeakMap();
1197
+ function nextGeneration(element) {
1198
+ const next = (generation.get(element) ?? 0) + 1;
1199
+ generation.set(element, next);
1200
+ return next;
1201
+ }
1202
+ function still() {
1203
+ const active = settings;
1204
+ if (active === void 0) return true;
1205
+ if (active.reduced === "always") return true;
1206
+ return active.reduced === "system" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
1207
+ }
1208
+ function lineHeightOf(element) {
1209
+ const lineHeight = Number.parseFloat(window.getComputedStyle(element).lineHeight);
1210
+ return Number.isFinite(lineHeight) && lineHeight > 0 ? lineHeight : 24;
1211
+ }
1212
+ function singleLine(element) {
1213
+ return element.scrollHeight <= lineHeightOf(element) * SINGLE_LINE_RATIO;
1214
+ }
1215
+ function modeOf(element) {
1216
+ if (element.hasAttribute("data-shimmer")) return "append";
1217
+ if (window.getComputedStyle(element).whiteSpace === "nowrap") return "append";
1218
+ return singleLine(element) ? "append" : "reveal";
1219
+ }
1220
+ function tiltFor(element, lineHeight) {
1221
+ const width = element.clientWidth;
1222
+ if (width <= 0 || lineHeight <= 0) return { angle: "180deg", sin: 0, cos: 1 };
1223
+ const natural = Math.atan(lineHeight / width) * 180 / Math.PI;
1224
+ const slant = Math.min(MAX_TILT_DEG, Math.max(MIN_TILT_DEG, natural)) * Math.PI / 180;
1225
+ return {
1226
+ angle: `${180 - slant * 180 / Math.PI}deg`,
1227
+ sin: Math.sin(slant),
1228
+ cos: Math.cos(slant)
1229
+ };
1230
+ }
1231
+ function front(element, tilt, height, lineHeight) {
1232
+ return tilt.sin * Math.max(0, element.clientWidth) + tilt.cos * height + lineHeight * FADE_BAND;
1233
+ }
1234
+ function textOf(element) {
1235
+ return (element.textContent ?? "").replace(/\s+/g, " ").trim();
1236
+ }
1237
+ function labelText(element) {
1238
+ if (!element.hasAttribute("data-shimmer")) return textOf(element);
1239
+ const content = firstChild(element);
1240
+ return content === null ? "" : textOf(content);
1241
+ }
1242
+ function contentWidth(element) {
1243
+ const root = maskTarget(element);
1244
+ if (root === null) return 0;
1245
+ const origin = root.getBoundingClientRect().left;
1246
+ const range = document.createRange();
1247
+ const walker = document.createTreeWalker(root, 4);
1248
+ let right = 0;
1249
+ for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) {
1250
+ const text = node.textContent ?? "";
1251
+ if (text.trim() === "") continue;
1252
+ range.selectNodeContents(node);
1253
+ right = Math.max(right, range.getBoundingClientRect().right - origin);
1254
+ }
1255
+ return right;
1256
+ }
1257
+ function firstChild(element) {
1258
+ return element.firstElementChild ?? null;
1259
+ }
1260
+ function maskTarget(element) {
1261
+ return element.hasAttribute("data-shimmer") ? firstChild(element) : element;
1262
+ }
1263
+ function slotOf(element) {
1264
+ const parent = element.parentElement;
1265
+ if (parent === null) return null;
1266
+ const children = parent.children;
1267
+ for (let index = 0; index < children.length; index += 1) {
1268
+ if (children[index] === element) return { parent, index };
1269
+ }
1270
+ return null;
1271
+ }
1272
+ function readSlot(slot) {
1273
+ if (slot === null) return void 0;
1274
+ return slots.get(slot.parent)?.get(slot.index);
1275
+ }
1276
+ function writeSlot(slot, text, width) {
1277
+ if (slot === null) return;
1278
+ let entries = slots.get(slot.parent);
1279
+ if (entries === void 0) {
1280
+ entries = /* @__PURE__ */ new Map();
1281
+ slots.set(slot.parent, entries);
1282
+ }
1283
+ entries.set(slot.index, { text, width });
1284
+ const count = slot.parent.children.length;
1285
+ for (const index of entries.keys()) {
1286
+ if (index >= count) entries.delete(index);
1287
+ }
1288
+ }
1289
+ function enterLabel(element) {
1290
+ const run2 = (rolls.get(element) ?? 0) + 1;
1291
+ rolls.set(element, run2);
1292
+ element.removeAttribute(ROLL_ATTRIBUTE);
1293
+ void element.scrollHeight;
1294
+ element.setAttribute(ROLL_ATTRIBUTE, "");
1295
+ window.setTimeout(() => {
1296
+ if (rolls.get(element) === run2) element.removeAttribute(ROLL_ATTRIBUTE);
1297
+ }, rollMs);
1298
+ }
1299
+ function append(element, previous, previousWidth) {
1300
+ const now = labelText(element);
1301
+ const target = maskTarget(element);
1302
+ if (target === null) return;
1303
+ const full = contentWidth(element);
1304
+ element.setAttribute(WIDTH_ATTRIBUTE, String(full));
1305
+ if (full <= 1) {
1306
+ rest(element, target);
1307
+ return;
1308
+ }
1309
+ const extending = previous !== "" && now.startsWith(previous) && previousWidth > 0;
1310
+ const start = extending ? previousWidth : 0;
1311
+ const gap = Math.max(MIN_REVEAL_PX, full - start);
1312
+ const duration = revealDuration(gap);
1313
+ const run2 = nextGeneration(element);
1314
+ const continuing = extending && (sweeps.get(element) ?? 0) > performance.now();
1315
+ target.style.removeProperty("mask-image");
1316
+ if (!continuing) {
1317
+ target.style.transition = "none";
1318
+ target.style.setProperty("--dsh-uix-edge", `${start}px`);
1319
+ void element.scrollHeight;
1320
+ }
1321
+ sweeps.set(element, performance.now() + duration);
1322
+ target.style.transition = `--dsh-uix-edge ${duration}ms ${ease}`;
1323
+ target.style.setProperty("--dsh-uix-edge", `${full}px`);
1324
+ window.setTimeout(() => {
1325
+ if (!owns(element, run2)) return;
1326
+ sweeps.delete(element);
1327
+ rest(element, target);
1328
+ }, duration);
1329
+ }
1330
+ function rest(element, target) {
1331
+ target.style.setProperty("mask-image", rests.get(element) ?? "none");
1332
+ target.style.removeProperty("--dsh-uix-edge");
1333
+ target.style.removeProperty("transition");
1334
+ }
1335
+ function owns(element, run2) {
1336
+ return generation.get(element) === run2;
1337
+ }
1338
+ function revealDuration(gap) {
1339
+ const active = settings;
1340
+ const perLine = revealMinMs / Math.pow(ONE_LINE_PX, REVEAL_EXPONENT);
1341
+ const shaped = perLine * Math.pow(Math.max(ONE_LINE_PX, gap), REVEAL_EXPONENT);
1342
+ const scaled = Math.min(revealMaxMs, Math.max(revealMinMs, shaped)) * active.durationScale;
1343
+ return active.quantize ? quantizeMs(scaled) : scaled;
1344
+ }
1345
+ function advance(element, state) {
1346
+ const active = settings;
1347
+ if (active === void 0) return;
1348
+ const height = lineHeightOf(element);
1349
+ const tilt = tiltFor(element, height);
1350
+ element.style.setProperty("--dsh-uix-tilt", tilt.angle);
1351
+ state.target = front(element, tilt, element.scrollHeight, height);
1352
+ if (state.frame !== 0) return;
1353
+ run(element, state);
1354
+ }
1355
+ function run(element, state) {
1356
+ const active = settings;
1357
+ if (active === void 0) return;
1358
+ const gap = state.target - state.current;
1359
+ if (gap < MIN_REVEAL_PX) {
1360
+ state.frame = 0;
1361
+ return;
1362
+ }
1363
+ state.from = state.current;
1364
+ state.started = performance.now();
1365
+ state.duration = revealDuration(gap);
1366
+ const mode = active.mode;
1367
+ const step = (now) => {
1368
+ const live = reveals.get(element);
1369
+ if (live === void 0 || live !== state) return;
1370
+ const progress = Math.min(1, (now - state.started) / Math.max(1, state.duration));
1371
+ const eased = easeCurve(effectCurvePoints(mode), progress);
1372
+ state.current = state.from + (state.target - state.from) * eased;
1373
+ element.style.setProperty("--dsh-uix-reveal", `${state.current}px`);
1374
+ if (progress >= 1) {
1375
+ state.current = state.target;
1376
+ element.style.setProperty("--dsh-uix-reveal", `${state.target}px`);
1377
+ state.frame = 0;
1378
+ if (state.target - state.current >= MIN_REVEAL_PX) run(element, state);
1379
+ return;
1380
+ }
1381
+ state.frame = window.requestAnimationFrame(step);
1382
+ };
1383
+ state.frame = window.requestAnimationFrame(step);
1384
+ }
1385
+ function forget(element, immediately = false) {
1386
+ const state = reveals.get(element);
1387
+ reveals.delete(element);
1388
+ nextGeneration(element);
1389
+ if (state !== void 0 && !immediately) {
1390
+ element.style.setProperty("--dsh-uix-reveal", `${state.target + element.scrollHeight}px`);
1391
+ }
1392
+ element.removeAttribute(STREAM_ATTRIBUTE);
1393
+ element.removeAttribute(LAST_ATTRIBUTE);
1394
+ element.removeAttribute(WIDTH_ATTRIBUTE);
1395
+ element.removeAttribute(ROLL_ATTRIBUTE);
1396
+ rolls.delete(element);
1397
+ const target = maskTarget(element);
1398
+ const clear = () => {
1399
+ element.style.removeProperty("--dsh-uix-reveal");
1400
+ element.style.removeProperty("--dsh-uix-tilt");
1401
+ element.style.removeProperty("transition");
1402
+ target?.style.removeProperty("--dsh-uix-edge");
1403
+ target?.style.removeProperty("transition");
1404
+ target?.style.removeProperty("mask-image");
1405
+ };
1406
+ rests.delete(element);
1407
+ if (immediately || state === void 0) clear();
1408
+ else window.setTimeout(clear, revealMaxMs);
1409
+ }
1410
+ function measure() {
1411
+ pending = false;
1412
+ const active = settings;
1413
+ if (active === void 0 || active.mode === "off" || still()) return;
1414
+ const marked = document.querySelectorAll(`[${STREAM_ATTRIBUTE}]`);
1415
+ for (let index = 0; index < marked.length; index += 1) {
1416
+ const element = marked[index];
1417
+ if (element === void 0) continue;
1418
+ if (element.getAttribute(STREAM_ATTRIBUTE) !== "reveal") continue;
1419
+ const state = reveals.get(element);
1420
+ if (state === void 0) continue;
1421
+ advance(element, state);
1422
+ }
1423
+ }
1424
+ function schedule() {
1425
+ if (pending) return;
1426
+ pending = true;
1427
+ window.requestAnimationFrame(measure);
1428
+ }
1429
+ function covered(element) {
1430
+ let node = element.parentElement;
1431
+ while (node !== null) {
1432
+ if (node.getAttribute(STREAM_ATTRIBUTE) === "reveal") return true;
1433
+ node = node.parentElement;
1434
+ }
1435
+ return false;
1436
+ }
1437
+ function mark() {
1438
+ const active = settings;
1439
+ if (active === void 0 || active.mode === "off" || still()) return;
1440
+ const live = document.querySelectorAll(LIVE);
1441
+ const seen = /* @__PURE__ */ new Set();
1442
+ const containers2 = /* @__PURE__ */ new Set();
1443
+ for (let index = 0; index < live.length; index += 1) {
1444
+ const element = live[index];
1445
+ if (element === void 0) continue;
1446
+ for (let node = element.parentElement; node !== null; node = node.parentElement) {
1447
+ containers2.add(node);
1448
+ }
1449
+ }
1450
+ for (let index = 0; index < live.length; index += 1) {
1451
+ const element = live[index];
1452
+ if (element === void 0) continue;
1453
+ const height = element.scrollHeight;
1454
+ const lineHeight = lineHeightOf(element);
1455
+ const before = heights.get(element);
1456
+ heights.set(element, height);
1457
+ if (covered(element)) continue;
1458
+ const mode = modeOf(element);
1459
+ if (mode === "append" && containers2.has(element)) {
1460
+ if (element.hasAttribute(STREAM_ATTRIBUTE)) forget(element, true);
1461
+ continue;
1462
+ }
1463
+ seen.add(element);
1464
+ const marked2 = element.getAttribute(STREAM_ATTRIBUTE);
1465
+ if (marked2 !== mode) {
1466
+ if (marked2 !== null) forget(element, true);
1467
+ if (mode === "reveal") {
1468
+ const tilt = tiltFor(element, lineHeight);
1469
+ const end = front(element, tilt, height, lineHeight);
1470
+ const open = before === void 0 ? end : Math.min(end, front(element, tilt, before, lineHeight));
1471
+ reveals.set(element, { current: open, from: open, target: open, frame: 0, started: 0, duration: 0 });
1472
+ element.style.setProperty("--dsh-uix-tilt", tilt.angle);
1473
+ element.style.setProperty("--dsh-uix-reveal", `${open}px`);
1474
+ } else {
1475
+ const target2 = maskTarget(element);
1476
+ if (target2 !== null) rests.set(element, window.getComputedStyle(target2).maskImage);
1477
+ }
1478
+ element.setAttribute(STREAM_ATTRIBUTE, mode);
1479
+ const target = maskTarget(element);
1480
+ if (mode === "append" && target !== null) rest(element, target);
1481
+ }
1482
+ if (mode === "reveal") continue;
1483
+ const text = labelText(element);
1484
+ const width = contentWidth(element);
1485
+ const slot = slotOf(element);
1486
+ const prior = readSlot(slot);
1487
+ const previous = prior?.text ?? element.getAttribute(LAST_ATTRIBUTE) ?? "";
1488
+ const previousWidth = prior?.width ?? Number.parseFloat(element.getAttribute(WIDTH_ATTRIBUTE) ?? "0");
1489
+ if (text === previous) continue;
1490
+ writeSlot(slot, text, width);
1491
+ element.setAttribute(LAST_ATTRIBUTE, text);
1492
+ element.setAttribute(WIDTH_ATTRIBUTE, String(width));
1493
+ const extending = previous !== "" && text.startsWith(previous) && previousWidth > 0;
1494
+ if (!extending) enterLabel(element);
1495
+ append(element, previous, Number.isFinite(previousWidth) ? previousWidth : 0);
1496
+ }
1497
+ unveil();
1498
+ const marked = document.querySelectorAll(`[${STREAM_ATTRIBUTE}]`);
1499
+ for (let index = 0; index < marked.length; index += 1) {
1500
+ const element = marked[index];
1501
+ if (element === void 0 || seen.has(element)) continue;
1502
+ forget(element);
1503
+ }
1504
+ schedule();
1505
+ }
1506
+ function atTail() {
1507
+ return document.querySelectorAll("[data-chat-following-tail]").length > 0;
1508
+ }
1509
+ function unveil() {
1510
+ const rows = document.querySelectorAll(ROW);
1511
+ if (rows.length === 0) return;
1512
+ const pinned = atTail();
1513
+ const next = /* @__PURE__ */ new WeakSet();
1514
+ for (let index = 0; index < rows.length; index += 1) {
1515
+ const row = rows[index];
1516
+ if (row === void 0) continue;
1517
+ next.add(row);
1518
+ if (knownRows.has(row)) continue;
1519
+ if (!pinned || !following) continue;
1520
+ row.setAttribute(ENTER_ATTRIBUTE, "");
1521
+ window.setTimeout(() => {
1522
+ row.removeAttribute(ENTER_ATTRIBUTE);
1523
+ }, enterMs);
1524
+ }
1525
+ knownRows = next;
1526
+ following = pinned;
1527
+ }
1528
+ function install() {
1529
+ if (installed) return;
1530
+ installed = true;
1531
+ sheet = document.createElement("style");
1532
+ sheet.textContent = STREAM_CSS;
1533
+ document.head.appendChild(sheet);
1534
+ observer = new MutationObserver(() => {
1535
+ mark();
1536
+ });
1537
+ observer.observe(document.body, {
1538
+ childList: true,
1539
+ subtree: true,
1540
+ characterData: true,
1541
+ attributes: true,
1542
+ attributeFilter: ["data-streaming", "data-shimmer"]
1543
+ });
1544
+ const existing = document.querySelectorAll(ROW);
1545
+ for (let index = 0; index < existing.length; index += 1) {
1546
+ const row = existing[index];
1547
+ if (row !== void 0) knownRows.add(row);
1548
+ }
1549
+ following = atTail();
1550
+ mark();
1551
+ }
1552
+ function uninstall() {
1553
+ observer?.disconnect();
1554
+ observer = void 0;
1555
+ sheet?.remove();
1556
+ sheet = void 0;
1557
+ installed = false;
1558
+ document.documentElement.style.removeProperty("--dsh-uix-ease");
1559
+ document.documentElement.style.removeProperty("--dsh-uix-roll");
1560
+ document.documentElement.style.removeProperty("--dsh-uix-enter");
1561
+ const entering = document.querySelectorAll(`[${ENTER_ATTRIBUTE}]`);
1562
+ for (let index = 0; index < entering.length; index += 1) {
1563
+ entering[index]?.removeAttribute(ENTER_ATTRIBUTE);
1564
+ }
1565
+ const marked = document.querySelectorAll(`[${STREAM_ATTRIBUTE}]`);
1566
+ for (let index = 0; index < marked.length; index += 1) {
1567
+ const element = marked[index];
1568
+ if (element !== void 0) forget(element, true);
1569
+ }
1570
+ reveals.clear();
1571
+ }
1572
+ return {
1573
+ apply(next) {
1574
+ uninstall();
1575
+ settings = next;
1576
+ if (next.mode === "off" || !next.stream) return;
1577
+ if (next.reduced === "system" && window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
1578
+ ease = `cubic-bezier(${effectCurvePoints(next.mode).join(", ")})`;
1579
+ const scaled = next.streamRollMs * next.durationScale;
1580
+ rollMs = next.quantize ? quantizeMs(scaled) : scaled;
1581
+ document.documentElement.style.setProperty("--dsh-uix-ease", ease);
1582
+ document.documentElement.style.setProperty("--dsh-uix-roll", `${rollMs}ms`);
1583
+ const scaledEnter = next.streamEnterMs * next.durationScale;
1584
+ enterMs = next.quantize ? quantizeMs(scaledEnter) : scaledEnter;
1585
+ revealMinMs = next.streamRevealMinMs;
1586
+ revealMaxMs = next.streamRevealMaxMs;
1587
+ document.documentElement.style.setProperty("--dsh-uix-enter", `${enterMs}ms`);
1588
+ install();
1589
+ },
650
1590
  restore: uninstall
651
1591
  };
652
1592
  }
@@ -738,16 +1678,14 @@
738
1678
  .dsh-uix-segment-tab:disabled{cursor:default;opacity:.4}
739
1679
  .dsh-uix-segment-tab:focus-visible{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:-2px}
740
1680
 
741
- /* Checkbox: Checkbox.module.css \u2014 a native input so the platform draws and keys
742
- it, tinted with the brand accent, with the explanation stacked underneath. */
743
- .dsh-uix-check{display:flex;align-items:flex-start;gap:6px;padding:10px 0;font-size:14px;line-height:20px;color:var(--dsw-alias-label-primary);cursor:pointer}
744
- .dsh-uix-check+.dsh-uix-check{border-top:0.5px solid var(--dsw-alias-border-l2)}
745
- .dsh-uix-check input{flex:0 0 auto;width:16px;height:16px;margin:2px 0 0;accent-color:var(--dsw-alias-brand-primary);cursor:inherit}
746
- .dsh-uix-check input:focus-visible{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:2px}
747
- .dsh-uix-check:has(input:disabled){cursor:default;opacity:.5}
748
- .dsh-uix-check-text{display:flex;flex-direction:column;gap:2px;min-width:0}
749
- .dsh-uix-check-label{font-size:13px;font-weight:500;line-height:1.5;color:var(--dsw-alias-label-primary)}
750
- .dsh-uix-check-hint{font-size:12px;line-height:1.5;font-weight:400;color:var(--dsw-alias-label-tertiary)}
1681
+ /* Boolean option row: DeveloperToolsRow.module.css .row / .title / .description \u2014
1682
+ the title column takes the width, the switch sits at the inline end. The copied
1683
+ Switch above is the same control, so an option row and the row above it match. */
1684
+ .dsh-uix-option{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:12px 0}
1685
+ .dsh-uix-option+.dsh-uix-option{border-top:0.5px solid var(--dsw-alias-border-l2)}
1686
+ .dsh-uix-option-text{display:flex;flex-direction:column;gap:4px;min-width:0}
1687
+ .dsh-uix-option-title{font-size:13px;font-weight:500;line-height:1.5;color:var(--dsw-alias-label-primary)}
1688
+ .dsh-uix-option-desc{font-size:12px;line-height:1.5;color:var(--dsw-alias-label-secondary)}
751
1689
  `;
752
1690
 
753
1691
  // src/client.ts
@@ -802,7 +1740,14 @@
802
1740
  durationScale: zod.number().optional(),
803
1741
  spatialSplit: zod.boolean().optional(),
804
1742
  quantize: zod.boolean().optional(),
805
- reduced: zod.string().optional()
1743
+ reduced: zod.string().optional(),
1744
+ scroll: zod.boolean().optional(),
1745
+ stream: zod.boolean().optional(),
1746
+ followScroll: zod.boolean().optional(),
1747
+ streamRevealMinMs: zod.number().optional(),
1748
+ streamRevealMaxMs: zod.number().optional(),
1749
+ streamEnterMs: zod.number().optional(),
1750
+ streamRollMs: zod.number().optional()
806
1751
  }), "SetMotionArgs")
807
1752
  ]
808
1753
  };
@@ -829,13 +1774,23 @@
829
1774
  "motion.duration": "\u65F6\u957F\u500D\u7387",
830
1775
  "motion.options": "\u9AD8\u7EA7\u9009\u9879",
831
1776
  "motion.spatial-split": "\u4F4D\u79FB\u4E0E\u6548\u679C\u4F7F\u7528\u4E0D\u540C\u66F2\u7EBF",
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",
1777
+ "motion.spatial-split-hint": "\u4F4D\u79FB\u5E26\u8FC7\u51B2\uFF0C\u6548\u679C\u4E0D\u5E26\uFF1B\u5173\u95ED\u540E\u7EDF\u4E00\u7528\u6548\u679C\u66F2\u7EBF\u3002",
833
1778
  "motion.quantize": "\u65F6\u957F\u5BF9\u9F50\u5230 Material \u6863\u4F4D",
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",
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",
1779
+ "motion.quantize-hint": "\u65F6\u957F\u5438\u9644\u5230 50 \u81F3 1000ms \u7684 Material \u6863\u4F4D\u3002",
1780
+ "motion.stream": "\u6D41\u5F0F\u6587\u672C\u6E10\u8FDB\u663E\u73B0",
1781
+ "motion.stream-hint": "\u6B63\u6587\u4E0E\u601D\u8003\u6458\u8981\u9010\u6BB5\u663E\u73B0\uFF1B\u53EA\u79FB\u52A8\u906E\u7F69\uFF0C\u4E0D\u622A\u65AD\u6587\u5B57\u3002",
1782
+ "motion.stream-timing": "\u6D41\u5F0F\u52A8\u753B\u65F6\u957F",
1783
+ "motion.follow-scroll": "\u8DDF\u968F\u5230\u5E95\u65F6\u5E73\u6ED1\u6ED1\u884C",
1784
+ "motion.follow-scroll-hint": "\u5173\u95ED\u540E\u5E95\u90E8\u8DDF\u968F\u77AC\u65F6\u8D34\u9F50\uFF0C\u53EA\u6709\u6587\u5B57\u5728\u52A8\uFF1B\u5F00\u542F\u65F6\u89C6\u53E3\u6ED1\u8FC7\u53BB\uFF0C\u89C2\u611F\u66F4\u8FDE\u7EED\u4F46\u59CB\u7EC8\u843D\u540E\u7EA6\u4E00\u884C\u3002",
1785
+ "motion.stream-reveal-min": "\u5355\u884C\u663E\u73B0\u4E0B\u9650\uFF08ms\uFF09",
1786
+ "motion.stream-reveal-max": "\u6574\u5757\u663E\u73B0\u4E0A\u9650\uFF08ms\uFF09",
1787
+ "motion.stream-enter": "\u65B0\u884C\u8FDB\u573A\uFF08ms\uFF09",
1788
+ "motion.stream-roll": "\u6458\u8981\u6362\u884C\u6EDA\u52A8\uFF08ms\uFF09",
1789
+ "motion.stream-timing-hint": "\u5355\u884C\u4E0B\u9650\u5C31\u662F\u7F13\u5B58\u7684\u53EF\u89C1\u7A97\u53E3\uFF1A\u8C03\u5927\u5B83\uFF0C\u8FB9\u754C\u540E\u65B9\u538B\u7740\u7684\u672A\u8BFB\u6587\u5B57\u66F4\u591A\u3001\u52A8\u753B\u66F4\u6162\u3002\u4E0A\u9650\u7528\u4E8E\u6574\u5757\u5185\u5BB9\u4E00\u6B21\u843D\u4E0B\u7684\u60C5\u5F62\u3002",
1790
+ "motion.scroll": "\u6EDA\u52A8\u4F7F\u7528\u540C\u4E00\u66F2\u7EBF",
1791
+ "motion.scroll-hint": "\u5E73\u6ED1\u6EDA\u52A8\u3001\u6EDA\u8F6E\u4E0E\u5B9A\u4F4D\u8DF3\u8F6C\u6539\u7528\u672C\u9875\u66F2\u7EBF\u3002",
837
1792
  "motion.reduced": "\u59CB\u7EC8\u51CF\u5F31\u52A8\u6548",
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",
1793
+ "motion.reduced-hint": "\u5FFD\u7565\u7CFB\u7EDF\u504F\u597D\uFF0C\u4E00\u5F8B\u628A\u65F6\u957F\u538B\u5230 1ms\u3002",
839
1794
  "motion.system-reduced": "\u7CFB\u7EDF\u5DF2\u8BF7\u6C42\u51CF\u5F31\u52A8\u6548\uFF0C\u672C\u9875\u52A8\u753B\u6B63\u5728\u88AB\u538B\u5236\u3002",
840
1795
  "motion.save-failed": "\u52A8\u6548\u8BBE\u7F6E\u4FDD\u5B58\u5931\u8D25",
841
1796
  "motion.inert": "\u66F2\u7EBF\u65CF\u4E3A\u300C\u5173\u95ED\u300D\uFF0C\u4EE5\u4E0B\u9009\u9879\u4E0D\u4F1A\u751F\u6548\u3002"
@@ -861,13 +1816,23 @@
861
1816
  "motion.duration": "Duration scale",
862
1817
  "motion.options": "Advanced options",
863
1818
  "motion.spatial-split": "Spatial and effects take different curves",
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.",
1819
+ "motion.spatial-split-hint": "Movement overshoots, effects do not; off uses the effects curve for both.",
865
1820
  "motion.quantize": "Snap durations to the Material ladder",
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.",
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.",
1821
+ "motion.quantize-hint": "Snap durations onto the 50-1000 ms Material ladder.",
1822
+ "motion.stream": "Progressive streamed text",
1823
+ "motion.stream-hint": "Reveal transcript and reasoning progressively; a mask moves, no text is cut.",
1824
+ "motion.stream-timing": "Stream timing",
1825
+ "motion.follow-scroll": "Glide when following the tail",
1826
+ "motion.follow-scroll-hint": "Off keeps the tail pinned exactly and only the text moves. On eases the viewport along \u2014 smoother, but about a line behind.",
1827
+ "motion.stream-reveal-min": "Reveal floor, one line (ms)",
1828
+ "motion.stream-reveal-max": "Reveal ceiling, whole block (ms)",
1829
+ "motion.stream-enter": "New row settle (ms)",
1830
+ "motion.stream-roll": "Summary roll (ms)",
1831
+ "motion.stream-timing-hint": "The floor is the visible buffer: raise it and more unread text stays behind the boundary, so the reveal reads slower. The ceiling covers a block that lands all at once.",
1832
+ "motion.scroll": "Scroll on the same curve",
1833
+ "motion.scroll-hint": "Drive smooth scrolling, the wheel, and locating jumps on this page's curve.",
869
1834
  "motion.reduced": "Always reduce motion",
870
- "motion.reduced-hint": "Ignore the system reduce-motion preference and force every duration to 1 ms. Off follows the system.",
1835
+ "motion.reduced-hint": "Ignore the system preference and force every duration to 1 ms.",
871
1836
  "motion.system-reduced": "The system asked for reduced motion; this page is being held still.",
872
1837
  "motion.save-failed": "Failed to save the motion settings",
873
1838
  "motion.inert": "The curve family is Off, so these options have no effect."
@@ -908,6 +1873,7 @@
908
1873
  borderWidth: "0.5px",
909
1874
  paths: { colors: DEFAULT_COLORS_PATH, tokens: DEFAULT_TOKENS_PATH },
910
1875
  motion: { ...MOTION_OFF },
1876
+ streamUnsupported: false,
911
1877
  problem: "",
912
1878
  notice: "",
913
1879
  saving: false
@@ -915,6 +1881,7 @@
915
1881
  const listeners = /* @__PURE__ */ new Set();
916
1882
  const motion = createMotion();
917
1883
  const scrolling = createScrollMotion();
1884
+ const streaming = createStreamMotion();
918
1885
  const publish = (patch) => {
919
1886
  view = { ...view, ...patch };
920
1887
  for (const listener of listeners) listener();
@@ -931,8 +1898,9 @@
931
1898
  if (disposed) return;
932
1899
  motion.apply(next);
933
1900
  scrolling.apply(next);
1901
+ streaming.apply(next);
934
1902
  };
935
- 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;
1903
+ 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 || left.stream !== right.stream || left.followScroll !== right.followScroll || left.streamRevealMinMs !== right.streamRevealMinMs || left.streamRevealMaxMs !== right.streamRevealMaxMs || left.streamEnterMs !== right.streamEnterMs || left.streamRollMs !== right.streamRollMs;
936
1904
  const clearOverride = () => {
937
1905
  disposeOverride?.();
938
1906
  disposeOverride = void 0;
@@ -983,7 +1951,12 @@
983
1951
  return envelope.value;
984
1952
  };
985
1953
  const adopt = (answer) => {
986
- const motionAnswer = answer.motion ?? MOTION_OFF;
1954
+ const reported = answer.motion;
1955
+ if (reported !== void 0 && reported.stream === void 0 && !view.streamUnsupported) {
1956
+ console.error("ui-experience: this host build predates the stream setting; progressive streamed text stays on and its switch stays hidden until DSH restarts");
1957
+ publish({ streamUnsupported: true });
1958
+ }
1959
+ const motionAnswer = reported === void 0 ? { ...MOTION_OFF } : { ...MOTION_OFF, ...reported, stream: reported.stream ?? true };
987
1960
  if (answer.motion === void 0 && !motionUnsupported) {
988
1961
  motionUnsupported = true;
989
1962
  console.error("ui-experience: this host build predates the motion settings; the M3 motion row stays inert until DSH restarts");
@@ -1056,6 +2029,10 @@
1056
2029
  };
1057
2030
  const saveMotion = async (change) => {
1058
2031
  if (disposed || writing) return;
2032
+ if (view.streamUnsupported && change.stream !== void 0) {
2033
+ console.error("ui-experience: the stream setting cannot be saved on this host build; restart DSH to pick it up");
2034
+ return;
2035
+ }
1059
2036
  writing = true;
1060
2037
  publish({ saving: true, problem: "", notice: "" });
1061
2038
  try {
@@ -1157,6 +2134,28 @@
1157
2134
  ),
1158
2135
  h("span", null, label)
1159
2136
  );
2137
+ const numberRow = (label, hint, value, commit, disabled) => h(
2138
+ "label",
2139
+ { className: "dsh-uix-field" },
2140
+ h("span", { className: "dsh-uix-label" }, label),
2141
+ h(
2142
+ "span",
2143
+ { className: "dsh-uix-input-wrap" },
2144
+ h("input", {
2145
+ className: "dsh-uix-input",
2146
+ type: "number",
2147
+ min: 0,
2148
+ step: 10,
2149
+ defaultValue: String(value),
2150
+ disabled,
2151
+ onChange: (event) => {
2152
+ const parsed = event.target.valueAsNumber;
2153
+ if (Number.isFinite(parsed) && parsed >= 0) commit(Math.round(parsed));
2154
+ }
2155
+ })
2156
+ ),
2157
+ h("p", { className: "dsh-uix-hint" }, hint)
2158
+ );
1160
2159
  const WALK_KEYS = ["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End"];
1161
2160
  const segmented = (id, value, segments, select, label, disabled) => {
1162
2161
  const selected = Math.max(0, segments.findIndex((segment) => segment.value === value));
@@ -1206,23 +2205,26 @@
1206
2205
  }, segment.label))
1207
2206
  );
1208
2207
  };
1209
- const checkbox = (checked, set, label, hint, disabled) => h(
1210
- "label",
1211
- { className: "dsh-uix-check" },
1212
- h("input", {
1213
- type: "checkbox",
1214
- checked,
2208
+ const option = (checked, set, label, hint, disabled) => h(
2209
+ "div",
2210
+ { className: "dsh-uix-option" },
2211
+ h(
2212
+ "div",
2213
+ { className: "dsh-uix-option-text" },
2214
+ h("div", { className: "dsh-uix-option-title" }, label),
2215
+ hint === void 0 ? null : h("div", { className: "dsh-uix-option-desc" }, hint)
2216
+ ),
2217
+ h("button", {
2218
+ type: "button",
2219
+ role: "switch",
2220
+ "aria-checked": checked,
2221
+ "aria-label": label,
1215
2222
  disabled,
1216
- onChange: (event) => {
1217
- set(event.target.checked);
2223
+ className: "dsh-uix-switch",
2224
+ onClick: () => {
2225
+ set(!checked);
1218
2226
  }
1219
- }),
1220
- h(
1221
- "span",
1222
- { className: "dsh-uix-check-text" },
1223
- h("span", { className: "dsh-uix-check-label" }, label),
1224
- hint === void 0 ? null : h("span", { className: "dsh-uix-check-hint" }, hint)
1225
- )
2227
+ }, h("span", { className: "dsh-uix-switch-thumb", "aria-hidden": true }))
1226
2228
  );
1227
2229
  function PaletteRow(props) {
1228
2230
  const tr = props.t ?? t;
@@ -1361,7 +2363,7 @@
1361
2363
  const policy = open ? h(
1362
2364
  "div",
1363
2365
  { className: "dsh-uix-form" },
1364
- checkbox(
2366
+ option(
1365
2367
  current.spatialSplit,
1366
2368
  (next) => {
1367
2369
  void saveMotion({ spatialSplit: next });
@@ -1370,7 +2372,7 @@
1370
2372
  tr("motion.spatial-split-hint"),
1371
2373
  locked
1372
2374
  ),
1373
- checkbox(
2375
+ option(
1374
2376
  current.quantize,
1375
2377
  (next) => {
1376
2378
  void saveMotion({ quantize: next });
@@ -1379,7 +2381,25 @@
1379
2381
  tr("motion.quantize-hint"),
1380
2382
  locked
1381
2383
  ),
1382
- checkbox(
2384
+ state.streamUnsupported ? null : option(
2385
+ current.stream,
2386
+ (next) => {
2387
+ void saveMotion({ stream: next });
2388
+ },
2389
+ tr("motion.stream"),
2390
+ tr("motion.stream-hint"),
2391
+ locked
2392
+ ),
2393
+ option(
2394
+ current.followScroll,
2395
+ (next) => {
2396
+ void saveMotion({ followScroll: next });
2397
+ },
2398
+ tr("motion.follow-scroll"),
2399
+ tr("motion.follow-scroll-hint"),
2400
+ locked
2401
+ ),
2402
+ option(
1383
2403
  current.scroll,
1384
2404
  (next) => {
1385
2405
  void saveMotion({ scroll: next });
@@ -1388,7 +2408,7 @@
1388
2408
  tr("motion.scroll-hint"),
1389
2409
  locked
1390
2410
  ),
1391
- checkbox(
2411
+ option(
1392
2412
  current.reduced === "always",
1393
2413
  (next) => {
1394
2414
  void saveMotion({ reduced: next ? "always" : "system" });
@@ -1398,6 +2418,46 @@
1398
2418
  locked
1399
2419
  )
1400
2420
  ) : null;
2421
+ const timing = current.stream && !state.streamUnsupported && open ? h(
2422
+ "div",
2423
+ { className: "dsh-uix-form" },
2424
+ numberRow(
2425
+ tr("motion.stream-reveal-min"),
2426
+ tr("motion.stream-timing-hint"),
2427
+ current.streamRevealMinMs,
2428
+ (next) => {
2429
+ void saveMotion({ streamRevealMinMs: next });
2430
+ },
2431
+ locked
2432
+ ),
2433
+ numberRow(
2434
+ tr("motion.stream-reveal-max"),
2435
+ "",
2436
+ current.streamRevealMaxMs,
2437
+ (next) => {
2438
+ void saveMotion({ streamRevealMaxMs: next });
2439
+ },
2440
+ locked
2441
+ ),
2442
+ numberRow(
2443
+ tr("motion.stream-enter"),
2444
+ "",
2445
+ current.streamEnterMs,
2446
+ (next) => {
2447
+ void saveMotion({ streamEnterMs: next });
2448
+ },
2449
+ locked
2450
+ ),
2451
+ numberRow(
2452
+ tr("motion.stream-roll"),
2453
+ "",
2454
+ current.streamRollMs,
2455
+ (next) => {
2456
+ void saveMotion({ streamRollMs: next });
2457
+ },
2458
+ locked
2459
+ )
2460
+ ) : null;
1401
2461
  return h(
1402
2462
  "div",
1403
2463
  { className: "dsh-uix-row" },
@@ -1447,7 +2507,8 @@
1447
2507
  disclosure(open, () => {
1448
2508
  setOpen(!open);
1449
2509
  }, tr("motion.options")),
1450
- policy
2510
+ policy,
2511
+ timing
1451
2512
  );
1452
2513
  }
1453
2514
  ctx.effect(() => ctx.slots.inject("settings.general.item", () => ctx.slots.register({
@@ -1471,6 +2532,7 @@
1471
2532
  teardown();
1472
2533
  motion.restore();
1473
2534
  scrolling.restore();
2535
+ streaming.restore();
1474
2536
  };
1475
2537
  }, "ui-experience: page-level layers");
1476
2538
  void poll();