@dsh-plugin/dsh-ui-experience 1.0.0-dev.43492422772 → 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
  } };
@@ -472,6 +477,8 @@
472
477
  var MAX_MS = 460;
473
478
  var FREE_PX = 240;
474
479
  var GROWTH_MS_PER_PX = 0.2;
480
+ var MOTION_EPSILON = 0.5;
481
+ var FOREIGN_TOLERANCE = 1;
475
482
  var SCROLL_KEYS = /* @__PURE__ */ new Set([
476
483
  "ArrowUp",
477
484
  "ArrowDown",
@@ -483,7 +490,20 @@
483
490
  "End",
484
491
  " "
485
492
  ]);
486
- var TAKEOVER_EVENTS = ["wheel", "touchstart", "keydown"];
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;
487
507
  function scrollDistance(legs) {
488
508
  let distance = 0;
489
509
  for (const leg of legs) {
@@ -502,15 +522,12 @@
502
522
  left: Math.max(0, element.scrollWidth - element.clientWidth)
503
523
  };
504
524
  }
505
- function scrollable(element) {
506
- const max = maxima(element);
507
- return max.top > 0 || max.left > 0;
508
- }
509
525
  function containers(element) {
510
526
  const out = [];
511
527
  let node = element.parentElement;
512
528
  while (node !== null) {
513
- if (scrollable(node)) out.push(node);
529
+ const max = maxima(node);
530
+ if (max.top > 0 || max.left > 0) out.push(node);
514
531
  node = node.parentElement;
515
532
  }
516
533
  return out;
@@ -521,149 +538,499 @@
521
538
  scrollBy: Element.prototype.scrollBy,
522
539
  scrollIntoView: Element.prototype.scrollIntoView
523
540
  };
541
+ const topAxis = Object.getOwnPropertyDescriptor(Element.prototype, "scrollTop");
542
+ const leftAxis = Object.getOwnPropertyDescriptor(Element.prototype, "scrollLeft");
524
543
  let settings;
525
544
  let run;
545
+ let wheel;
526
546
  let installed = false;
527
- function cancel() {
528
- if (run === void 0) return;
529
- window.cancelAnimationFrame(run.frame);
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();
594
+ const active = run;
595
+ if (active === void 0) return;
596
+ window.cancelAnimationFrame(active.frame);
530
597
  run = void 0;
598
+ for (const leg of active.legs) release(leg.element, notify);
599
+ for (const settle of active.done) settle();
531
600
  }
532
601
  function step(now) {
533
602
  const active = run;
534
603
  if (active === void 0) return;
535
604
  const elapsed = now - active.started;
536
605
  const progress = active.duration <= 0 ? 1 : Math.min(1, elapsed / active.duration);
537
- const eased = easeCurve(effectCurvePoints(settings?.mode ?? "m3e"), progress);
606
+ const eased = easeCurve(effectCurvePoints(settings.mode), progress);
538
607
  for (let index = 0; index < active.legs.length; index += 1) {
539
608
  const leg = active.legs[index];
540
609
  const written = active.written[index];
541
610
  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) {
611
+ if (Math.abs(leg.element.scrollTop - written.top) > FOREIGN_TOLERANCE || Math.abs(leg.element.scrollLeft - written.left) > FOREIGN_TOLERANCE) {
543
612
  cancel();
544
613
  return;
545
614
  }
615
+ }
616
+ for (const leg of active.legs) {
546
617
  const top = progress >= 1 ? leg.toTop : leg.fromTop + (leg.toTop - leg.fromTop) * eased;
547
618
  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;
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;
550
626
  written.top = leg.element.scrollTop;
551
627
  written.left = leg.element.scrollLeft;
552
628
  }
553
629
  if (progress >= 1) {
554
630
  run = void 0;
631
+ for (const leg of active.legs) release(leg.element, true);
632
+ for (const settle of active.done) settle();
555
633
  return;
556
634
  }
557
635
  active.frame = window.requestAnimationFrame(step);
558
636
  }
559
- function start(legs) {
637
+ function start(legs, done) {
560
638
  cancel();
561
639
  const active = settings;
562
- if (active === void 0) return;
640
+ for (const leg of legs) hold(leg.element);
563
641
  run = {
564
- legs,
642
+ legs: [...legs],
565
643
  started: performance.now(),
566
644
  duration: scrollDurationMs(scrollDistance(legs), active),
567
645
  written: legs.map((leg) => ({ top: leg.element.scrollTop, left: leg.element.scrollLeft })),
568
- frame: window.requestAnimationFrame(step)
646
+ frame: window.requestAnimationFrame(step),
647
+ done: [done]
569
648
  };
570
649
  }
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;
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;
575
663
  }
576
- function legsFor(element, top, left) {
664
+ function wantsInstant() {
665
+ const active = settings;
666
+ if (active.reduced === "always") return true;
667
+ return active.reduced === "system" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
668
+ }
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;
577
682
  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 }];
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;
582
693
  }
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
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);
591
728
  };
729
+ glide.frame = window.requestAnimationFrame(step2);
592
730
  }
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);
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);
598
760
  return;
599
761
  }
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);
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);
603
765
  }
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;
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();
610
795
  }
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);
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;
614
812
  }
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;
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;
621
819
  }
622
- const instant = { ...typeof options === "object" && options !== null ? options : {}, behavior: "instant" };
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) {
835
+ const options = args[0];
836
+ const requested = typeof options === "object" && options !== null ? options : void 0;
837
+ const active = settings;
838
+ if (active === void 0 || requested?.behavior !== "smooth") return shipped.scrollIntoView.apply(this, args);
839
+ const jump = { ...requested, behavior: "instant" };
840
+ if (wantsInstant()) return shipped.scrollIntoView.call(this, jump);
623
841
  const tracked = containers(this);
842
+ for (const element of tracked) hold(element);
624
843
  const before = tracked.map((element) => ({ top: element.scrollTop, left: element.scrollLeft }));
625
- shipped.scrollIntoView.call(this, instant);
844
+ shipped.scrollIntoView.call(this, jump);
626
845
  const legs = [];
627
846
  for (let index = 0; index < tracked.length; index += 1) {
628
847
  const element = tracked[index];
629
848
  const origin = before[index];
630
849
  if (element === void 0 || origin === void 0) continue;
631
850
  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;
851
+ writeAxis(element, "top", origin.top);
852
+ writeAxis(element, "left", origin.left);
853
+ const moved = Math.abs(landed.top - origin.top) > MOTION_EPSILON || Math.abs(landed.left - origin.left) > MOTION_EPSILON;
854
+ if (!moved) {
855
+ if (run?.legs.some((leg) => leg.element === element)) continue;
856
+ release(element, false);
857
+ continue;
858
+ }
635
859
  legs.push({ element, fromTop: origin.top, fromLeft: origin.left, toTop: landed.top, toLeft: landed.left });
636
860
  }
637
- if (legs.length > 0) start(legs);
861
+ if (legs.length === 0) return Promise.resolve();
862
+ return new Promise((resolve) => {
863
+ if (!merge(legs, resolve)) start(legs, resolve);
864
+ });
865
+ }
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
+ }
942
+ const active = run;
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;
638
947
  }
639
948
  function onTakeover(event) {
640
- if (run === void 0) return;
641
- if (event.type !== "keydown") {
642
- cancel();
949
+ const key = event.type === "keydown";
950
+ if (key) {
951
+ if (event.key === void 0 || !SCROLL_KEYS.has(event.key)) return;
952
+ const focus = event.target;
953
+ if (focus instanceof Element && focus.closest(EDITABLE) !== null) return;
954
+ }
955
+ if (run === void 0) {
956
+ clearGrace();
643
957
  return;
644
958
  }
645
- if (event.key !== void 0 && SCROLL_KEYS.has(event.key)) 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;
646
965
  }
647
966
  function install() {
648
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
+ }
649
973
  installed = true;
650
974
  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);
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"]) {
1002
+ window.addEventListener(kind, onTakeover, { capture: true, passive: true });
656
1003
  }
657
1004
  }
658
1005
  function uninstall() {
659
- cancel();
1006
+ finishGlides();
1007
+ cancel(false);
1008
+ clearGrace();
660
1009
  if (!installed) return;
661
1010
  installed = false;
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
+ }
662
1015
  const prototype = Element.prototype;
663
1016
  prototype.scrollTo = shipped.scrollTo;
664
1017
  prototype.scrollBy = shipped.scrollBy;
665
1018
  prototype.scrollIntoView = shipped.scrollIntoView;
666
- for (const kind of TAKEOVER_EVENTS) {
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"]) {
667
1034
  window.removeEventListener(kind, onTakeover, true);
668
1035
  }
669
1036
  }
@@ -672,7 +1039,552 @@
672
1039
  uninstall();
673
1040
  settings = next;
674
1041
  if (!next.scroll || next.mode === "off") return;
675
- if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
1042
+ install();
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`);
676
1588
  install();
677
1589
  },
678
1590
  restore: uninstall
@@ -766,16 +1678,14 @@
766
1678
  .dsh-uix-segment-tab:disabled{cursor:default;opacity:.4}
767
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}
768
1680
 
769
- /* Checkbox: Checkbox.module.css \u2014 a native input so the platform draws and keys
770
- it, tinted with the brand accent, with the explanation stacked underneath. */
771
- .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}
772
- .dsh-uix-check+.dsh-uix-check{border-top:0.5px solid var(--dsw-alias-border-l2)}
773
- .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}
774
- .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}
775
- .dsh-uix-check:has(input:disabled){cursor:default;opacity:.5}
776
- .dsh-uix-check-text{display:flex;flex-direction:column;gap:2px;min-width:0}
777
- .dsh-uix-check-label{font-size:13px;font-weight:500;line-height:1.5;color:var(--dsw-alias-label-primary)}
778
- .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)}
779
1689
  `;
780
1690
 
781
1691
  // src/client.ts
@@ -830,7 +1740,14 @@
830
1740
  durationScale: zod.number().optional(),
831
1741
  spatialSplit: zod.boolean().optional(),
832
1742
  quantize: zod.boolean().optional(),
833
- 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()
834
1751
  }), "SetMotionArgs")
835
1752
  ]
836
1753
  };
@@ -857,13 +1774,23 @@
857
1774
  "motion.duration": "\u65F6\u957F\u500D\u7387",
858
1775
  "motion.options": "\u9AD8\u7EA7\u9009\u9879",
859
1776
  "motion.spatial-split": "\u4F4D\u79FB\u4E0E\u6548\u679C\u4F7F\u7528\u4E0D\u540C\u66F2\u7EBF",
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",
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",
861
1778
  "motion.quantize": "\u65F6\u957F\u5BF9\u9F50\u5230 Material \u6863\u4F4D",
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",
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",
863
1790
  "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",
1791
+ "motion.scroll-hint": "\u5E73\u6ED1\u6EDA\u52A8\u3001\u6EDA\u8F6E\u4E0E\u5B9A\u4F4D\u8DF3\u8F6C\u6539\u7528\u672C\u9875\u66F2\u7EBF\u3002",
865
1792
  "motion.reduced": "\u59CB\u7EC8\u51CF\u5F31\u52A8\u6548",
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",
1793
+ "motion.reduced-hint": "\u5FFD\u7565\u7CFB\u7EDF\u504F\u597D\uFF0C\u4E00\u5F8B\u628A\u65F6\u957F\u538B\u5230 1ms\u3002",
867
1794
  "motion.system-reduced": "\u7CFB\u7EDF\u5DF2\u8BF7\u6C42\u51CF\u5F31\u52A8\u6548\uFF0C\u672C\u9875\u52A8\u753B\u6B63\u5728\u88AB\u538B\u5236\u3002",
868
1795
  "motion.save-failed": "\u52A8\u6548\u8BBE\u7F6E\u4FDD\u5B58\u5931\u8D25",
869
1796
  "motion.inert": "\u66F2\u7EBF\u65CF\u4E3A\u300C\u5173\u95ED\u300D\uFF0C\u4EE5\u4E0B\u9009\u9879\u4E0D\u4F1A\u751F\u6548\u3002"
@@ -889,13 +1816,23 @@
889
1816
  "motion.duration": "Duration scale",
890
1817
  "motion.options": "Advanced options",
891
1818
  "motion.spatial-split": "Spatial and effects take different curves",
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.",
1819
+ "motion.spatial-split-hint": "Movement overshoots, effects do not; off uses the effects curve for both.",
893
1820
  "motion.quantize": "Snap durations to the Material ladder",
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.",
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.",
895
1832
  "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.",
1833
+ "motion.scroll-hint": "Drive smooth scrolling, the wheel, and locating jumps on this page's curve.",
897
1834
  "motion.reduced": "Always reduce motion",
898
- "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.",
899
1836
  "motion.system-reduced": "The system asked for reduced motion; this page is being held still.",
900
1837
  "motion.save-failed": "Failed to save the motion settings",
901
1838
  "motion.inert": "The curve family is Off, so these options have no effect."
@@ -936,6 +1873,7 @@
936
1873
  borderWidth: "0.5px",
937
1874
  paths: { colors: DEFAULT_COLORS_PATH, tokens: DEFAULT_TOKENS_PATH },
938
1875
  motion: { ...MOTION_OFF },
1876
+ streamUnsupported: false,
939
1877
  problem: "",
940
1878
  notice: "",
941
1879
  saving: false
@@ -943,6 +1881,7 @@
943
1881
  const listeners = /* @__PURE__ */ new Set();
944
1882
  const motion = createMotion();
945
1883
  const scrolling = createScrollMotion();
1884
+ const streaming = createStreamMotion();
946
1885
  const publish = (patch) => {
947
1886
  view = { ...view, ...patch };
948
1887
  for (const listener of listeners) listener();
@@ -959,8 +1898,9 @@
959
1898
  if (disposed) return;
960
1899
  motion.apply(next);
961
1900
  scrolling.apply(next);
1901
+ streaming.apply(next);
962
1902
  };
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;
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;
964
1904
  const clearOverride = () => {
965
1905
  disposeOverride?.();
966
1906
  disposeOverride = void 0;
@@ -1011,7 +1951,12 @@
1011
1951
  return envelope.value;
1012
1952
  };
1013
1953
  const adopt = (answer) => {
1014
- 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 };
1015
1960
  if (answer.motion === void 0 && !motionUnsupported) {
1016
1961
  motionUnsupported = true;
1017
1962
  console.error("ui-experience: this host build predates the motion settings; the M3 motion row stays inert until DSH restarts");
@@ -1084,6 +2029,10 @@
1084
2029
  };
1085
2030
  const saveMotion = async (change) => {
1086
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
+ }
1087
2036
  writing = true;
1088
2037
  publish({ saving: true, problem: "", notice: "" });
1089
2038
  try {
@@ -1185,6 +2134,28 @@
1185
2134
  ),
1186
2135
  h("span", null, label)
1187
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
+ );
1188
2159
  const WALK_KEYS = ["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End"];
1189
2160
  const segmented = (id, value, segments, select, label, disabled) => {
1190
2161
  const selected = Math.max(0, segments.findIndex((segment) => segment.value === value));
@@ -1234,23 +2205,26 @@
1234
2205
  }, segment.label))
1235
2206
  );
1236
2207
  };
1237
- const checkbox = (checked, set, label, hint, disabled) => h(
1238
- "label",
1239
- { className: "dsh-uix-check" },
1240
- h("input", {
1241
- type: "checkbox",
1242
- 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,
1243
2222
  disabled,
1244
- onChange: (event) => {
1245
- set(event.target.checked);
2223
+ className: "dsh-uix-switch",
2224
+ onClick: () => {
2225
+ set(!checked);
1246
2226
  }
1247
- }),
1248
- h(
1249
- "span",
1250
- { className: "dsh-uix-check-text" },
1251
- h("span", { className: "dsh-uix-check-label" }, label),
1252
- hint === void 0 ? null : h("span", { className: "dsh-uix-check-hint" }, hint)
1253
- )
2227
+ }, h("span", { className: "dsh-uix-switch-thumb", "aria-hidden": true }))
1254
2228
  );
1255
2229
  function PaletteRow(props) {
1256
2230
  const tr = props.t ?? t;
@@ -1389,7 +2363,7 @@
1389
2363
  const policy = open ? h(
1390
2364
  "div",
1391
2365
  { className: "dsh-uix-form" },
1392
- checkbox(
2366
+ option(
1393
2367
  current.spatialSplit,
1394
2368
  (next) => {
1395
2369
  void saveMotion({ spatialSplit: next });
@@ -1398,7 +2372,7 @@
1398
2372
  tr("motion.spatial-split-hint"),
1399
2373
  locked
1400
2374
  ),
1401
- checkbox(
2375
+ option(
1402
2376
  current.quantize,
1403
2377
  (next) => {
1404
2378
  void saveMotion({ quantize: next });
@@ -1407,7 +2381,25 @@
1407
2381
  tr("motion.quantize-hint"),
1408
2382
  locked
1409
2383
  ),
1410
- 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(
1411
2403
  current.scroll,
1412
2404
  (next) => {
1413
2405
  void saveMotion({ scroll: next });
@@ -1416,7 +2408,7 @@
1416
2408
  tr("motion.scroll-hint"),
1417
2409
  locked
1418
2410
  ),
1419
- checkbox(
2411
+ option(
1420
2412
  current.reduced === "always",
1421
2413
  (next) => {
1422
2414
  void saveMotion({ reduced: next ? "always" : "system" });
@@ -1426,6 +2418,46 @@
1426
2418
  locked
1427
2419
  )
1428
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;
1429
2461
  return h(
1430
2462
  "div",
1431
2463
  { className: "dsh-uix-row" },
@@ -1475,7 +2507,8 @@
1475
2507
  disclosure(open, () => {
1476
2508
  setOpen(!open);
1477
2509
  }, tr("motion.options")),
1478
- policy
2510
+ policy,
2511
+ timing
1479
2512
  );
1480
2513
  }
1481
2514
  ctx.effect(() => ctx.slots.inject("settings.general.item", () => ctx.slots.register({
@@ -1499,6 +2532,7 @@
1499
2532
  teardown();
1500
2533
  motion.restore();
1501
2534
  scrolling.restore();
2535
+ streaming.restore();
1502
2536
  };
1503
2537
  }, "ui-experience: page-level layers");
1504
2538
  void poll();