@kodaris/krubble-app-components 1.0.147 → 1.0.148

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.
@@ -2900,6 +2900,13 @@
2900
2900
  */
2901
2901
  this._reconnectAttempts = 0;
2902
2902
  this._userHasScrolledUp = false;
2903
+ /**
2904
+ * Keep the end of the streaming response in view. Set at the start of every
2905
+ * turn (send, confirmation decision, reconnect); cleared when the user scrolls
2906
+ * up on their own (wheel, touch drag, scrollbar drag) and restored when they
2907
+ * scroll back to the bottom. Checked in updated() after every streamed chunk.
2908
+ */
2909
+ this._followStream = false;
2903
2910
  this._isDragging = false;
2904
2911
  this._isResizing = false;
2905
2912
  this._overlay = null;
@@ -3555,6 +3562,35 @@
3555
3562
  });
3556
3563
  }
3557
3564
  }
3565
+ // Streamed chunks mutate the message in place and call requestUpdate(), so
3566
+ // `changed` never has 'messages' mid-turn — run the follow on every update
3567
+ // instead. The user message stays pinned at the top (Gemini-style) until the
3568
+ // response grows past the bottom of the view; from then on the end stays in
3569
+ // view. Never scrolls to the empty runway below the content.
3570
+ if (this._isStreaming && this._followStream && this._messagesEl) {
3571
+ requestAnimationFrame(() => {
3572
+ const container = this._messagesEl;
3573
+ if (!container || !this._isStreaming || !this._followStream) {
3574
+ return;
3575
+ }
3576
+ const turnEls = container.querySelectorAll('.agent-turn');
3577
+ const turnEl = turnEls[turnEls.length - 1];
3578
+ const lastEl = turnEl?.lastElementChild;
3579
+ if (!lastEl) {
3580
+ return;
3581
+ }
3582
+ // Bottom edge of the turn's last rendered child, in the container's
3583
+ // scroll coordinates — the runway (min-height on the turn) sits below it.
3584
+ const contentBottom = lastEl.getBoundingClientRect().bottom
3585
+ - container.getBoundingClientRect().top
3586
+ + container.scrollTop
3587
+ + 24;
3588
+ const visibleBottom = container.scrollTop + container.clientHeight;
3589
+ if (contentBottom > visibleBottom) {
3590
+ container.scrollTop = Math.min(contentBottom - container.clientHeight, container.scrollHeight - container.clientHeight);
3591
+ }
3592
+ });
3593
+ }
3558
3594
  if (changed.has('opened') && this.opened && this._inputEl) {
3559
3595
  setTimeout(() => {
3560
3596
  if (this._inputEl) {
@@ -4623,6 +4659,7 @@
4623
4659
  this._saveState();
4624
4660
  this._error = '';
4625
4661
  this._userHasScrolledUp = false;
4662
+ this._followStream = true;
4626
4663
  // Clear the composer, but DON'T revoke the preview object URLs — the sent
4627
4664
  // message now renders them until the upload's real files replace them.
4628
4665
  this._pendingConversationFiles = [];
@@ -4857,6 +4894,7 @@
4857
4894
  this._turnFinishedCleanly = false;
4858
4895
  this._reconnectAttempts = 0;
4859
4896
  this._userHasScrolledUp = false;
4897
+ this._followStream = true;
4860
4898
  this.send({
4861
4899
  message: '',
4862
4900
  messages: this.messages,
@@ -4957,6 +4995,33 @@
4957
4995
  }
4958
4996
  else {
4959
4997
  this._userHasScrolledUp = false;
4998
+ // Back at the bottom mid-turn: resume following the stream end.
4999
+ if (this._isStreaming) {
5000
+ this._followStream = true;
5001
+ }
5002
+ }
5003
+ }
5004
+ /**
5005
+ * User-driven scroll intent while a response streams. Scroll events alone can't
5006
+ * tell the user apart from our own follow-scroll (and from scrollTop clamping
5007
+ * when the runway clears), so listen for the gestures instead: a wheel/trackpad
5008
+ * move up, any touch drag, or a pointer down on the scrollbar track. Any of
5009
+ * these stops the follow; scrolling back to the bottom restores it.
5010
+ */
5011
+ _handleMessagesWheel(e) {
5012
+ if (this._isStreaming && e.deltaY < 0) {
5013
+ this._followStream = false;
5014
+ }
5015
+ }
5016
+ _handleMessagesTouchMove() {
5017
+ if (this._isStreaming) {
5018
+ this._followStream = false;
5019
+ }
5020
+ }
5021
+ _handleMessagesPointerDown(e) {
5022
+ const container = e.currentTarget;
5023
+ if (this._isStreaming && e.offsetX >= container.clientWidth) {
5024
+ this._followStream = false;
4960
5025
  }
4961
5026
  }
4962
5027
  _handleErrorDismiss() {
@@ -5529,6 +5594,7 @@
5529
5594
  // (a no-op).
5530
5595
  this._reconnectAttempts = 0;
5531
5596
  this._isStreaming = true;
5597
+ this._followStream = true;
5532
5598
  this._reader = response.body.getReader();
5533
5599
  this._decoder = new TextDecoder();
5534
5600
  this._buffer = '';
@@ -6647,7 +6713,13 @@
6647
6713
  </div>
6648
6714
  </div>
6649
6715
 
6650
- <div class="messages" @scroll=${this._handleMessagesScroll}>
6716
+ <div
6717
+ class="messages"
6718
+ @scroll=${this._handleMessagesScroll}
6719
+ @wheel=${this._handleMessagesWheel}
6720
+ @touchmove=${this._handleMessagesTouchMove}
6721
+ @pointerdown=${this._handleMessagesPointerDown}
6722
+ >
6651
6723
  <div class="messages-inner">
6652
6724
  ${this.messages.length === 0 ? b `
6653
6725
  <div class="empty">
@@ -11547,6 +11619,7 @@
11547
11619
  * activeId="overview"
11548
11620
  * .actions=${[
11549
11621
  * { id: 'edit', label: 'Edit', variant: 'flat' },
11622
+ * { id: 'preview', label: 'Preview', href: '/s/companies/123', target: '_blank' },
11550
11623
  * { id: 'archive', label: 'Archive' },
11551
11624
  * { id: 'delete', label: 'Delete' },
11552
11625
  * { id: 'export', label: 'Export' },
@@ -11558,7 +11631,8 @@
11558
11631
  *
11559
11632
  * Actions render in a footer at the bottom of the nav column. With more than
11560
11633
  * `maxVisibleActions` (default 5), the overflow collapses into a "More" dropdown.
11561
- * Listen for `action-click` to handle a click: `e.detail.action` is the action id.
11634
+ * Listen for `action-click` to handle a click: `e.detail.action` is the action id. An action with
11635
+ * an `href` renders as a link and navigates on its own; it does not fire `action-click`.
11562
11636
  *
11563
11637
  * @slot - The main screen content
11564
11638
  *
@@ -11569,7 +11643,7 @@
11569
11643
  * @property {KRScreenNavAction[]} actions - Action buttons rendered in the nav footer
11570
11644
  * @property {number} maxVisibleActions - Max inline actions before collapsing into a "More" dropdown (default 5)
11571
11645
  *
11572
- * @fires action-click - When an action button (inline or overflow) is clicked; detail is `{ action: id }`
11646
+ * @fires action-click - When an action button (inline or overflow) without an `href` is clicked; detail is `{ action: id }`
11573
11647
  */
11574
11648
  class KRScreenNav extends i$3 {
11575
11649
  constructor() {
@@ -11663,6 +11737,10 @@
11663
11737
  }
11664
11738
  }
11665
11739
  handleActionClick(e, action) {
11740
+ // A link action navigates on its own through the kr-button's anchor; it doesn't fire action-click.
11741
+ if (action.href) {
11742
+ return;
11743
+ }
11666
11744
  this.dispatchEvent(new CustomEvent('action-click', {
11667
11745
  detail: { action: action.id },
11668
11746
  bubbles: true,
@@ -11689,6 +11767,8 @@
11689
11767
  overflowOptions = this.actions.slice(this.maxVisibleActions - 1).map((action) => ({
11690
11768
  id: action.id,
11691
11769
  label: action.label,
11770
+ href: action.href,
11771
+ hrefTarget: action.target,
11692
11772
  }));
11693
11773
  }
11694
11774
  return b `
@@ -11724,6 +11804,8 @@
11724
11804
  class="nav-actions__button"
11725
11805
  variant=${action.variant || 'outline'}
11726
11806
  size="small"
11807
+ href=${action.href || A}
11808
+ target=${action.target || A}
11727
11809
  @click=${(e) => this.handleActionClick(e, action)}
11728
11810
  >
11729
11811
  ${action.label}