turbo_chat 0.1.8 → 0.1.11

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7539f6a1acc24a17b883f7545c2fd941fd87a7dec93f2806438e56a03a558724
4
- data.tar.gz: 64b476e34935a766f88d3e61a7632b3e15fa6de1582037bc77d9d7484a923a53
3
+ metadata.gz: 97ae091b4d44fd093c6fbff38d64027c8cb85f45c5663f05e2a66cbcb15c043d
4
+ data.tar.gz: 3983790dec17e23c5ce5d99dd44f154e0d3b154ca43b9d4e595bf6491ac91822
5
5
  SHA512:
6
- metadata.gz: 162fe8c035a2d541023a160cc4f70cd3f0f9678c8fab6cf7a1936fa675e748d3b76f669b8dcb04a83cc5e2729af12e2985a270946f6a7a3e4b1fa6312c7c373a
7
- data.tar.gz: '068556524d2dd13d2fcea185f369a4e9a5f15b324d66c6fa0e99cc8c8ed43a31dae47b85cf796c5c6b081cf5aa51caa47041ac81d9e88444b755ad32a42db81c'
6
+ metadata.gz: cc0717fd14c06e02daa493ed4887b962147e17b15d1d44880f9305be2cb51869066c3f29c08b8ad89b45cca4fa2e5ec13850d84fe56525a3ce5123eec8c8c936
7
+ data.tar.gz: 7f6d6ceecc1421e73b407d67da671e25fcb32adfdee714eaddacaa6187200e2c6a423f94e9ae955fc93546c2f2c78f3c659ebcf3a21313a8d5d25d1f49b0716b
data/CHANGELOG.md CHANGED
@@ -2,6 +2,29 @@
2
2
 
3
3
  All notable changes to `turbo_chat` will be documented in this file.
4
4
 
5
+ ## [0.1.11] - 2026-02-26
6
+
7
+ ### Changed
8
+ - Smoothed signal-state transitions (for example: thinking to planning to typing) to reduce flicker during rapid updates.
9
+ - Updated signal Turbo Stream updates to use morphing so signal entries transition in place more smoothly.
10
+
11
+ ## [0.1.10] - 2026-02-26
12
+
13
+ ### Added
14
+ - Unbounded chat now supports a viewport-edge scrollbar proxy on the far right side of the screen while preserving inner chat layout.
15
+
16
+ ### Changed
17
+ - Improved unbounded chat scrolling smoothness for wheel input and maintained synchronization between the edge scrollbar and message list position.
18
+ - Hardened page scroll locking for unbounded chat so the document does not scroll while chat remains scrollable.
19
+
20
+ ## [0.1.9] - 2026-02-25
21
+
22
+ ### Added
23
+ - Support improved mobile chat behavior to keep the composer pinned and visible on iOS-class viewports, with safer full-screen height handling.
24
+
25
+ ### Changed
26
+ - Updated RubyGems metadata links to the `main` branch.
27
+
5
28
  ## [0.1.8] - 2026-02-23
6
29
 
7
30
  ### Added
data/README.md CHANGED
@@ -126,6 +126,7 @@ TurboChat.configure do |config|
126
126
  config.show_timestamp = true
127
127
  config.show_role = false
128
128
  config.message_source_labels = TurboChat::Configuration::DEFAULT_MESSAGE_SOURCE_LABELS.dup
129
+ config.chat_style = "chat_style_bounded"
129
130
  config.signal_text_sheen = true
130
131
 
131
132
  config.emit_moderation_events = false
@@ -174,6 +175,14 @@ TurboChat.configure do |config|
174
175
  end
175
176
  ```
176
177
 
178
+ Chat UI layout style is configurable:
179
+
180
+ ```ruby
181
+ TurboChat.configure do |config|
182
+ config.chat_style = "chat_style_unbounded" # or "chat_style_bounded"
183
+ end
184
+ ```
185
+
177
186
  ## Roles
178
187
 
179
188
  Built-in roles:
@@ -1,6 +1,7 @@
1
1
  (function (namespace) {
2
2
  var datasetFlagEnabled = namespace.datasetFlagEnabled;
3
3
  var parseJsonObject = namespace.parseJsonObject;
4
+ var PAGE_SCROLL_LOCK_CLASS = "chat-page-scroll-locked";
4
5
 
5
6
  function setupInvitationEvents() {
6
7
  document.querySelectorAll("[data-chat-index]").forEach(function (element) {
@@ -64,7 +65,16 @@
64
65
  });
65
66
  }
66
67
 
68
+ function syncPageScrollLock() {
69
+ var hasUnboundedShell = document.querySelector(".chat-shell--style-unbounded");
70
+ document.documentElement.classList.toggle(PAGE_SCROLL_LOCK_CLASS, Boolean(hasUnboundedShell));
71
+ if (document.body) {
72
+ document.body.classList.toggle(PAGE_SCROLL_LOCK_CLASS, Boolean(hasUnboundedShell));
73
+ }
74
+ }
75
+
67
76
  function setupTurboChatUi() {
77
+ syncPageScrollLock();
68
78
  setupInvitationEvents();
69
79
  setupChatLifecycleEvents();
70
80
 
@@ -1,6 +1,8 @@
1
1
  (function (namespace) {
2
2
  var constants = namespace.constants || {};
3
3
  var MENTION_BLUR_HIDE_DELAY_MS = constants.MENTION_BLUR_HIDE_DELAY_MS || 120;
4
+ var GLOBAL_SCROLLBAR_CLASS = "chat-global-scrollbar";
5
+ var GLOBAL_SCROLLBAR_HIDDEN_CLASS = "chat-global-scrollbar--hidden";
4
6
 
5
7
  var datasetFlagEnabled = namespace.datasetFlagEnabled;
6
8
  var parseMentionTokens = namespace.parseMentionTokens;
@@ -10,6 +12,7 @@
10
12
  var setupMentionAutocomplete = namespace.setupMentionAutocomplete;
11
13
  var scrollMessageIntoView = namespace.scrollMessageIntoView;
12
14
  var scrollLastMessageIntoView = namespace.scrollLastMessageIntoView;
15
+ var prefersReducedMotion = namespace.prefersReducedMotion || function () { return false; };
13
16
 
14
17
  function syncOwnMessageClasses(container) {
15
18
  if (!container || !container.dataset) {
@@ -432,10 +435,381 @@
432
435
  });
433
436
 
434
437
  observer.observe(container, { childList: true });
438
+
439
+ setupUnboundedWheelScrollProxy(container);
440
+ }
441
+
442
+ function normalizeWheelDeltaY(event) {
443
+ if (!event) {
444
+ return 0;
445
+ }
446
+
447
+ var deltaY = event.deltaY;
448
+ if (!deltaY) {
449
+ return 0;
450
+ }
451
+
452
+ if (event.deltaMode === 1) {
453
+ return deltaY * 16;
454
+ }
455
+
456
+ if (event.deltaMode === 2) {
457
+ var viewportHeight = (typeof window !== "undefined" && window.innerHeight) || 800;
458
+ return deltaY * viewportHeight;
459
+ }
460
+
461
+ return deltaY;
462
+ }
463
+
464
+ function shouldIgnoreWheelProxy(event, container) {
465
+ if (!event) {
466
+ return true;
467
+ }
468
+
469
+ if (container.contains(event.target)) {
470
+ return true;
471
+ }
472
+
473
+ if (
474
+ event.target &&
475
+ typeof event.target.closest === "function" &&
476
+ event.target.closest(
477
+ ".chat-members-list-shell, .chat-invite-menu, .chat-mentions-menu, textarea, input, select, [contenteditable='true']"
478
+ )
479
+ ) {
480
+ return true;
481
+ }
482
+
483
+ return false;
484
+ }
485
+
486
+ function clampScrollTop(container, scrollTop) {
487
+ if (!container) {
488
+ return 0;
489
+ }
490
+
491
+ var maxScrollTop = Math.max(0, container.scrollHeight - container.clientHeight);
492
+ if (scrollTop < 0) {
493
+ return 0;
494
+ }
495
+
496
+ if (scrollTop > maxScrollTop) {
497
+ return maxScrollTop;
498
+ }
499
+
500
+ return scrollTop;
501
+ }
502
+
503
+ function startSmoothWheelScroll(state) {
504
+ if (!state || state.smoothScrollRafId) {
505
+ return;
506
+ }
507
+
508
+ function step() {
509
+ state.smoothScrollRafId = null;
510
+
511
+ var container = state.container;
512
+ if (!container || !container.isConnected) {
513
+ return;
514
+ }
515
+
516
+ var targetScrollTop = clampScrollTop(container, state.smoothTargetScrollTop);
517
+ state.smoothTargetScrollTop = targetScrollTop;
518
+
519
+ var currentScrollTop = container.scrollTop;
520
+ var remainingDelta = targetScrollTop - currentScrollTop;
521
+ if (Math.abs(remainingDelta) <= 0.5) {
522
+ if (currentScrollTop !== targetScrollTop) {
523
+ state.syncingFromWheelAnimation = true;
524
+ container.scrollTop = targetScrollTop;
525
+ state.syncingFromWheelAnimation = false;
526
+ queueGlobalScrollbarSync(state);
527
+ }
528
+ return;
529
+ }
530
+
531
+ state.syncingFromWheelAnimation = true;
532
+ container.scrollTop = currentScrollTop + remainingDelta * 0.22;
533
+ state.syncingFromWheelAnimation = false;
534
+ queueGlobalScrollbarSync(state);
535
+ state.smoothScrollRafId = window.requestAnimationFrame(step);
536
+ }
537
+
538
+ state.smoothScrollRafId = window.requestAnimationFrame(step);
539
+ }
540
+
541
+ function proxyWheelToMessages(state, event) {
542
+ var container = state && state.container;
543
+ if (!container) {
544
+ return false;
545
+ }
546
+
547
+ var deltaY = normalizeWheelDeltaY(event);
548
+ if (!deltaY) {
549
+ return false;
550
+ }
551
+
552
+ var currentTarget = typeof state.smoothTargetScrollTop === "number"
553
+ ? state.smoothTargetScrollTop
554
+ : container.scrollTop;
555
+ var nextScrollTop = clampScrollTop(container, currentTarget + deltaY);
556
+
557
+ if (nextScrollTop === currentTarget) {
558
+ return false;
559
+ }
560
+
561
+ state.smoothTargetScrollTop = nextScrollTop;
562
+ if (prefersReducedMotion()) {
563
+ container.scrollTop = nextScrollTop;
564
+ queueGlobalScrollbarSync(state);
565
+ return true;
566
+ }
567
+
568
+ if (typeof window === "undefined" || typeof window.requestAnimationFrame !== "function") {
569
+ container.scrollTop = nextScrollTop;
570
+ queueGlobalScrollbarSync(state);
571
+ return true;
572
+ }
573
+
574
+ startSmoothWheelScroll(state);
575
+ return true;
576
+ }
577
+
578
+ function createGlobalScrollbar() {
579
+ var scrollbar = document.createElement("div");
580
+ scrollbar.className = GLOBAL_SCROLLBAR_CLASS + " " + GLOBAL_SCROLLBAR_HIDDEN_CLASS;
581
+ scrollbar.setAttribute("aria-hidden", "true");
582
+
583
+ var spacer = document.createElement("div");
584
+ spacer.className = "chat-global-scrollbar-spacer";
585
+ scrollbar.appendChild(spacer);
586
+
587
+ if (document.body) {
588
+ document.body.appendChild(scrollbar);
589
+ }
590
+
591
+ return { scrollbar: scrollbar, spacer: spacer };
592
+ }
593
+
594
+ function ensureGlobalScrollbar(state) {
595
+ if (state.globalScrollbar && state.globalScrollbar.isConnected && state.globalScrollbarSpacer) {
596
+ return;
597
+ }
598
+
599
+ var created = createGlobalScrollbar();
600
+ state.globalScrollbar = created.scrollbar;
601
+ state.globalScrollbarSpacer = created.spacer;
602
+
603
+ state.globalScrollbar.addEventListener("scroll", function () {
604
+ var container = state.container;
605
+ if (!container || !container.isConnected || state.syncingFromMessages) {
606
+ return;
607
+ }
608
+
609
+ state.syncingFromScrollbar = true;
610
+ var targetTop = clampScrollTop(container, state.globalScrollbar.scrollTop);
611
+ state.smoothTargetScrollTop = targetTop;
612
+ container.scrollTop = targetTop;
613
+ state.syncingFromScrollbar = false;
614
+ });
615
+ }
616
+
617
+ function syncGlobalScrollbar(state) {
618
+ var container = state.container;
619
+ if (!container || !container.isConnected) {
620
+ return;
621
+ }
622
+
623
+ ensureGlobalScrollbar(state);
624
+ var globalScrollbar = state.globalScrollbar;
625
+ var spacer = state.globalScrollbarSpacer;
626
+ if (!globalScrollbar || !spacer) {
627
+ return;
628
+ }
629
+
630
+ var maxScrollTop = Math.max(0, container.scrollHeight - container.clientHeight);
631
+ var globalViewportHeight = globalScrollbar.clientHeight || ((typeof window !== "undefined" && window.innerHeight) || 0);
632
+ var spacerHeight = Math.max(globalViewportHeight, Math.ceil(maxScrollTop + globalViewportHeight));
633
+ spacer.style.height = spacerHeight + "px";
634
+
635
+ var hideScrollbar = maxScrollTop <= 1;
636
+ globalScrollbar.classList.toggle(GLOBAL_SCROLLBAR_HIDDEN_CLASS, hideScrollbar);
637
+ if (hideScrollbar) {
638
+ if (globalScrollbar.scrollTop !== 0) {
639
+ globalScrollbar.scrollTop = 0;
640
+ }
641
+ return;
642
+ }
643
+
644
+ var targetScrollTop = Math.min(maxScrollTop, container.scrollTop);
645
+ if (!state.smoothScrollRafId) {
646
+ state.smoothTargetScrollTop = targetScrollTop;
647
+ }
648
+ if (Math.abs(globalScrollbar.scrollTop - targetScrollTop) > 1) {
649
+ state.syncingFromMessages = true;
650
+ globalScrollbar.scrollTop = targetScrollTop;
651
+ state.syncingFromMessages = false;
652
+ }
653
+ }
654
+
655
+ function queueGlobalScrollbarSync(state) {
656
+ if (!state || state.syncRafId) {
657
+ return;
658
+ }
659
+
660
+ if (typeof window === "undefined" || typeof window.requestAnimationFrame !== "function") {
661
+ syncGlobalScrollbar(state);
662
+ return;
663
+ }
664
+
665
+ state.syncRafId = window.requestAnimationFrame(function () {
666
+ state.syncRafId = null;
667
+ syncGlobalScrollbar(state);
668
+ });
669
+ }
670
+
671
+ function bindContainerScrollProxy(state, container) {
672
+ if (state.boundContainer === container) {
673
+ return;
674
+ }
675
+
676
+ if (state.boundContainer && state.onContainerScroll) {
677
+ state.boundContainer.removeEventListener("scroll", state.onContainerScroll);
678
+ }
679
+
680
+ if (state.mutationObserver) {
681
+ state.mutationObserver.disconnect();
682
+ }
683
+
684
+ state.boundContainer = container;
685
+ state.container = container;
686
+
687
+ state.onContainerScroll = function () {
688
+ if (!state.globalScrollbar || state.syncingFromScrollbar) {
689
+ return;
690
+ }
691
+
692
+ if (!state.syncingFromWheelAnimation) {
693
+ state.smoothTargetScrollTop = container.scrollTop;
694
+ }
695
+ state.syncingFromMessages = true;
696
+ state.globalScrollbar.scrollTop = container.scrollTop;
697
+ state.syncingFromMessages = false;
698
+ };
699
+ container.addEventListener("scroll", state.onContainerScroll, { passive: true });
700
+
701
+ state.mutationObserver = new MutationObserver(function () {
702
+ queueGlobalScrollbarSync(state);
703
+ });
704
+ state.mutationObserver.observe(container, { childList: true, subtree: true, characterData: true });
705
+
706
+ queueGlobalScrollbarSync(state);
707
+ }
708
+
709
+ function cleanupDetachedGlobalScrollbars() {
710
+ if (document.querySelector(".chat-shell--style-unbounded")) {
711
+ return;
712
+ }
713
+
714
+ document.querySelectorAll("." + GLOBAL_SCROLLBAR_CLASS).forEach(function (node) {
715
+ node.remove();
716
+ });
717
+ }
718
+
719
+ function setupUnboundedWheelScrollProxy(container) {
720
+ if (!container) {
721
+ return;
722
+ }
723
+
724
+ var shell = container.closest(".chat-shell--style-unbounded");
725
+ if (!shell) {
726
+ return;
727
+ }
728
+
729
+ var state = shell.__chatWheelProxyState;
730
+ if (!state) {
731
+ state = {
732
+ shell: shell,
733
+ container: container,
734
+ boundContainer: null,
735
+ globalScrollbar: null,
736
+ globalScrollbarSpacer: null,
737
+ mutationObserver: null,
738
+ syncingFromMessages: false,
739
+ syncingFromScrollbar: false,
740
+ syncRafId: null,
741
+ onContainerScroll: null,
742
+ smoothTargetScrollTop: container.scrollTop,
743
+ smoothScrollRafId: null,
744
+ syncingFromWheelAnimation: false
745
+ };
746
+ shell.__chatWheelProxyState = state;
747
+
748
+ state.forwardWheel = function (event) {
749
+ if (state.globalScrollbar && state.globalScrollbar.contains(event.target)) {
750
+ return;
751
+ }
752
+
753
+ var activeContainer = state.container;
754
+ if (!activeContainer || !activeContainer.isConnected) {
755
+ return;
756
+ }
757
+
758
+ if (shouldIgnoreWheelProxy(event, activeContainer)) {
759
+ return;
760
+ }
761
+
762
+ if (proxyWheelToMessages(state, event)) {
763
+ event.preventDefault();
764
+ }
765
+ };
766
+
767
+ state.forwardWheelOutsideShell = function (event) {
768
+ if (!shell.isConnected || !state.container || !state.container.isConnected) {
769
+ return;
770
+ }
771
+
772
+ if (shell.contains(event.target)) {
773
+ return;
774
+ }
775
+
776
+ state.forwardWheel(event);
777
+ };
778
+
779
+ state.onViewportResize = function () {
780
+ queueGlobalScrollbarSync(state);
781
+ };
782
+
783
+ shell.addEventListener("wheel", state.forwardWheel, { passive: false });
784
+ document.addEventListener("wheel", state.forwardWheelOutsideShell, { passive: false });
785
+ if (typeof window !== "undefined") {
786
+ window.addEventListener("resize", state.onViewportResize);
787
+ if (window.visualViewport && typeof window.visualViewport.addEventListener === "function") {
788
+ window.visualViewport.addEventListener("resize", state.onViewportResize);
789
+ }
790
+ }
791
+ } else {
792
+ state.container = container;
793
+ }
794
+
795
+ bindContainerScrollProxy(state, container);
796
+ }
797
+
798
+ function syncAllGlobalScrollbars() {
799
+ document.querySelectorAll(".chat-shell--style-unbounded").forEach(function (shell) {
800
+ var state = shell.__chatWheelProxyState;
801
+ if (!state || !state.container || !state.container.isConnected) {
802
+ return;
803
+ }
804
+
805
+ queueGlobalScrollbarSync(state);
806
+ });
435
807
  }
436
808
 
437
809
  function setupAllMessageAutoScroll() {
438
810
  document.querySelectorAll(".chat-messages").forEach(setupMessageAutoScroll);
811
+ syncAllGlobalScrollbars();
812
+ cleanupDetachedGlobalScrollbars();
439
813
  }
440
814
 
441
815
  namespace.setupAllMessageAutoScroll = setupAllMessageAutoScroll;
@@ -5,6 +5,7 @@
5
5
  var SIGNAL_IDLE_GRACE_MS = constants.SIGNAL_IDLE_GRACE_MS || 2500;
6
6
  var SIGNAL_HEARTBEAT_MS = constants.SIGNAL_HEARTBEAT_MS || 4000;
7
7
  var SIGNAL_RETREAT_MS = constants.SIGNAL_RETREAT_MS || 180;
8
+ var SIGNAL_EMPTY_GRACE_MS = constants.SIGNAL_EMPTY_GRACE_MS || 180;
8
9
  var MENTION_BLUR_HIDE_DELAY_MS = constants.MENTION_BLUR_HIDE_DELAY_MS || 120;
9
10
  var COMPOSER_MAX_HEIGHT_PX = constants.COMPOSER_MAX_HEIGHT_PX || 210;
10
11
 
@@ -40,26 +41,89 @@
40
41
  });
41
42
  }
42
43
 
43
- function syncSignalContainerState(container) {
44
+ function containerBottomPadding(container) {
45
+ if (!container || typeof window === "undefined") {
46
+ return 0;
47
+ }
48
+
49
+ var cssPadding = window.getComputedStyle(container).paddingBottom;
50
+ var parsedPadding = parseFloat(cssPadding);
51
+ if (isNaN(parsedPadding) || parsedPadding <= 0) {
52
+ return 0;
53
+ }
54
+
55
+ return parsedPadding;
56
+ }
57
+
58
+ function visibleSignalNode(container) {
44
59
  if (!container) {
60
+ return null;
61
+ }
62
+
63
+ return container.querySelector(".chat-typing-indicator:not(.chat-typing-indicator--leaving)");
64
+ }
65
+
66
+ function clearSignalDeactivateTimer(container) {
67
+ if (!container || !container.__chatSignalDeactivateTimeoutId) {
45
68
  return;
46
69
  }
47
70
 
71
+ clearTimeout(container.__chatSignalDeactivateTimeoutId);
72
+ container.__chatSignalDeactivateTimeoutId = null;
73
+ }
74
+
75
+ function queueSignalDeactivate(container) {
76
+ if (!container || container.__chatSignalDeactivateTimeoutId) {
77
+ return;
78
+ }
79
+
80
+ container.__chatSignalDeactivateTimeoutId = setTimeout(function () {
81
+ container.__chatSignalDeactivateTimeoutId = null;
82
+ if (!container.isConnected) {
83
+ return;
84
+ }
85
+
86
+ syncSignalContainerState(container, { forceInactive: true });
87
+ }, SIGNAL_EMPTY_GRACE_MS);
88
+ }
89
+
90
+ function shouldStickMessagesToBottom(messagesContainer) {
91
+ if (!messagesContainer) {
92
+ return false;
93
+ }
94
+
95
+ var reservedBottomPadding = containerBottomPadding(messagesContainer);
96
+ var distanceFromBottom = messagesContainer.scrollHeight -
97
+ (messagesContainer.scrollTop + messagesContainer.clientHeight) -
98
+ reservedBottomPadding;
99
+ return distanceFromBottom <= 24;
100
+ }
101
+
102
+ function syncSignalContainerState(container, options) {
103
+ if (!container) {
104
+ return;
105
+ }
106
+
107
+ options = options || {};
48
108
  hideOwnSignals(container);
49
109
 
50
- var hasVisibleSignals = container.querySelector(
51
- ".chat-typing-indicator:not(.chat-typing-indicator--leaving)"
52
- );
53
- container.classList.toggle("chat-signals--active", Boolean(hasVisibleSignals));
110
+ var hasVisibleSignals = Boolean(visibleSignalNode(container));
111
+
112
+ if (!hasVisibleSignals && !options.forceInactive && container.classList.contains("chat-signals--active")) {
113
+ queueSignalDeactivate(container);
114
+ return;
115
+ }
116
+
117
+ if (hasVisibleSignals) {
118
+ clearSignalDeactivateTimer(container);
119
+ }
120
+
121
+ container.classList.toggle("chat-signals--active", hasVisibleSignals);
54
122
 
55
123
  var chatWindow = container.closest(".chat-window");
56
124
  if (chatWindow) {
57
125
  var messagesContainer = chatWindow.querySelector(".chat-messages");
58
- var shouldStickToBottom = false;
59
- if (messagesContainer) {
60
- var distanceFromBottom = messagesContainer.scrollHeight - (messagesContainer.scrollTop + messagesContainer.clientHeight);
61
- shouldStickToBottom = distanceFromBottom <= 24;
62
- }
126
+ var shouldStickToBottom = shouldStickMessagesToBottom(messagesContainer);
63
127
 
64
128
  var signalOffset = hasVisibleSignals ? Math.ceil(container.scrollHeight) + 8 : 0;
65
129
  chatWindow.style.setProperty("--chat-signal-offset", signalOffset + "px");
@@ -86,11 +150,31 @@
86
150
  container.dataset.chatSignalsBound = "true";
87
151
  syncSignalContainerState(container);
88
152
 
153
+ var syncRafId = null;
154
+ function queueSignalContainerSync() {
155
+ if (syncRafId) {
156
+ return;
157
+ }
158
+
159
+ if (typeof window === "undefined" || typeof window.requestAnimationFrame !== "function") {
160
+ syncSignalContainerState(container);
161
+ return;
162
+ }
163
+
164
+ syncRafId = window.requestAnimationFrame(function () {
165
+ syncRafId = null;
166
+ if (!container.isConnected) {
167
+ return;
168
+ }
169
+ syncSignalContainerState(container);
170
+ });
171
+ }
172
+
89
173
  var observer = new MutationObserver(function () {
90
- syncSignalContainerState(container);
174
+ queueSignalContainerSync();
91
175
  });
92
176
 
93
- observer.observe(container, { childList: true });
177
+ observer.observe(container, { childList: true, subtree: true, characterData: true });
94
178
  }
95
179
 
96
180
  function setupAllSignalContainers() {
@@ -154,6 +238,8 @@
154
238
  var pendingSignalClear = false;
155
239
  var emitTypingEvents = datasetFlagEnabled(element, "chatEmitTypingEvents");
156
240
  var emitMessageEvents = datasetFlagEnabled(element, "chatEmitMessageEvents");
241
+ var unboundedShell = element.closest(".chat-shell--style-unbounded");
242
+ var composerClearanceRafId = null;
157
243
  var mentionsEnabled = datasetFlagEnabled(element, "chatEnableMentions");
158
244
  var mentionOptions = mentionsEnabled ? parseMentionOptions(element.dataset.chatMentionOptions) : [];
159
245
  var mentionAutocomplete = setupMentionAutocomplete(messageInput, {
@@ -177,15 +263,67 @@
177
263
  setMentionOptions: updateMentionOptions
178
264
  };
179
265
 
266
+ function syncComposerClearance() {
267
+ if (!unboundedShell) {
268
+ return;
269
+ }
270
+
271
+ var viewportHeight = (typeof window !== "undefined" && window.innerHeight) || document.documentElement.clientHeight || 0;
272
+ var composerRect = element.getBoundingClientRect();
273
+ if (viewportHeight > 0 && composerRect && composerRect.height > 0) {
274
+ var coveredHeight = Math.max(0, viewportHeight - composerRect.top);
275
+ if (coveredHeight > 0) {
276
+ unboundedShell.style.setProperty("--chat-floating-composer-clearance", Math.ceil(coveredHeight + 8) + "px");
277
+ return;
278
+ }
279
+ }
280
+
281
+ var composerShell = element.querySelector(".chat-composer-shell");
282
+ if (!composerShell) {
283
+ return;
284
+ }
285
+
286
+ var composerHeight = Math.ceil(composerShell.getBoundingClientRect().height);
287
+ if (!composerHeight || composerHeight <= 0) {
288
+ return;
289
+ }
290
+
291
+ var clearance = composerHeight + 28;
292
+ unboundedShell.style.setProperty("--chat-floating-composer-clearance", clearance + "px");
293
+ }
294
+
295
+ function queueComposerClearanceSync() {
296
+ if (!unboundedShell || typeof window === "undefined" || typeof window.requestAnimationFrame !== "function") {
297
+ syncComposerClearance();
298
+ return;
299
+ }
300
+
301
+ if (composerClearanceRafId) {
302
+ return;
303
+ }
304
+
305
+ composerClearanceRafId = window.requestAnimationFrame(function () {
306
+ composerClearanceRafId = null;
307
+ syncComposerClearance();
308
+ });
309
+ }
310
+
180
311
  function autoResizeComposerInput() {
181
312
  messageInput.style.height = "auto";
182
313
  var contentHeight = messageInput.scrollHeight;
183
314
  var nextHeight = Math.min(contentHeight, COMPOSER_MAX_HEIGHT_PX);
184
315
  messageInput.style.height = nextHeight + "px";
185
316
  messageInput.style.overflowY = contentHeight > COMPOSER_MAX_HEIGHT_PX ? "auto" : "hidden";
317
+ queueComposerClearanceSync();
186
318
  }
187
319
 
188
320
  autoResizeComposerInput();
321
+ if (typeof window !== "undefined") {
322
+ window.addEventListener("resize", queueComposerClearanceSync);
323
+ if (window.visualViewport && typeof window.visualViewport.addEventListener === "function") {
324
+ window.visualViewport.addEventListener("resize", queueComposerClearanceSync);
325
+ }
326
+ }
189
327
 
190
328
  function emitTypingEvent(eventName) {
191
329
  if (!emitTypingEvents) {