@kodaris/krubble-app-components 1.0.101 → 1.0.103

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.
@@ -5795,6 +5795,31 @@ let KRChatbot = class KRChatbot extends i$3 {
5795
5795
  this._skillBrowseQuery = '';
5796
5796
  /** True while the mic is live and speech is being transcribed into the composer. */
5797
5797
  this._isDictating = false;
5798
+ /**
5799
+ * Close the `/` skill menu when the click lands outside it.
5800
+ *
5801
+ * Bound at the document because a click anywhere — including outside this
5802
+ * component entirely — should dismiss the menu. `composedPath()` rather than
5803
+ * `e.target` since the menu lives in this component's shadow root, where
5804
+ * `target` on a document listener is retargeted to the host element and every
5805
+ * inside click would look like an outside one.
5806
+ */
5807
+ this._handleDocumentClick = (e) => {
5808
+ if (!this._skillMenuOpened) {
5809
+ return;
5810
+ }
5811
+ // Excluded: the textarea, since clicking it keeps editing the same `/` command
5812
+ // and dismissing there would fight the user still typing it; and the plus
5813
+ // button, which owns the menu's open/close itself. The plus exclusion matters
5814
+ // in both directions — this listener runs after the button's own handler, so
5815
+ // without it a click that just opened the menu would immediately close it
5816
+ // again, and a click meant to toggle it shut would be double-handled.
5817
+ const path = e.composedPath();
5818
+ if (path.some((el) => el instanceof HTMLElement && (el.classList.contains('skill-menu') || el.classList.contains('input') || el.classList.contains('input-plus')))) {
5819
+ return;
5820
+ }
5821
+ this._skillMenuOpened = false;
5822
+ };
5798
5823
  this._handleMouseMove = (e) => {
5799
5824
  if (this._isDragging) {
5800
5825
  let newLeft = this._dragStartLeft + (e.clientX - this._dragStartX);
@@ -5938,6 +5963,7 @@ let KRChatbot = class KRChatbot extends i$3 {
5938
5963
  this.opened = true;
5939
5964
  }
5940
5965
  this._restoreState();
5966
+ document.addEventListener('click', this._handleDocumentClick);
5941
5967
  }
5942
5968
  disconnectedCallback() {
5943
5969
  super.disconnectedCallback();
@@ -5947,6 +5973,7 @@ let KRChatbot = class KRChatbot extends i$3 {
5947
5973
  }
5948
5974
  document.removeEventListener('mousemove', this._handleMouseMove);
5949
5975
  document.removeEventListener('mouseup', this._handleMouseUp);
5976
+ document.removeEventListener('click', this._handleDocumentClick);
5950
5977
  // Release the mic if the chatbot is torn down mid-dictation — otherwise the
5951
5978
  // recognizer keeps the browser's recording indicator lit with nothing to
5952
5979
  // transcribe into. abort() (not stop()) since the discarded text has nowhere to go.
@@ -6168,7 +6195,12 @@ let KRChatbot = class KRChatbot extends i$3 {
6168
6195
  }
6169
6196
  const state = JSON.parse(raw);
6170
6197
  // Draft text and the debug drawer are persisted for all variants.
6171
- if (typeof state.message === 'string') {
6198
+ // A lone `/` is dropped — it's the in-progress half of a menu interaction,
6199
+ // not a draft, and restoring it would revive a stale menu the user never
6200
+ // used. Anything with an actual command after the slash (`/code`, `/code do
6201
+ // the thing`) is a real draft and restores normally. Trimmed before testing
6202
+ // so a stray space around the slash doesn't defeat the check.
6203
+ if (typeof state.message === 'string' && state.message.trim() !== '/') {
6172
6204
  this._inputValue = state.message;
6173
6205
  }
6174
6206
  this._debugPanelOpen = state.debugPanelOpen === 'true';
@@ -6716,6 +6748,15 @@ let KRChatbot = class KRChatbot extends i$3 {
6716
6748
  // the menu interaction, and the picker is a separate window so the menu would
6717
6749
  // otherwise linger open behind it (and after a file is chosen).
6718
6750
  this._skillMenuOpened = false;
6751
+ // A lone `/` was only the means of reaching this action, so it shouldn't be
6752
+ // left behind in the composer once the picker opens. Nothing else is touched:
6753
+ // a real command (`/code`) stays because `/code` plus an attachment is a valid
6754
+ // invocation, and the plus button reaches this same method with an arbitrary
6755
+ // draft in the composer, which must survive.
6756
+ if (this._inputValue.trim() === '/') {
6757
+ this._inputValue = '';
6758
+ this._saveState();
6759
+ }
6719
6760
  this._pendingAttachAnalyze = aiAnalyze;
6720
6761
  this._fileInputEl?.click();
6721
6762
  }
@@ -7997,29 +8038,53 @@ let KRChatbot = class KRChatbot extends i$3 {
7997
8038
  @paste=${this._handlePaste}
7998
8039
  ></textarea>
7999
8040
  ${window.SpeechRecognition || window.webkitSpeechRecognition ? b `
8000
- <button
8001
- class=${e({
8002
- mic: true,
8003
- 'mic--recording': this._isDictating,
8004
- })}
8005
- title=${this._isDictating ? 'Stop dictation' : 'Dictate a message'}
8006
- @click=${this._handleMicClick}
8007
- >
8008
- <svg
8009
- xmlns="http://www.w3.org/2000/svg"
8010
- viewBox="0 0 24 24"
8011
- fill="none"
8012
- stroke="currentColor"
8013
- stroke-width="1.75"
8014
- stroke-linecap="round"
8015
- stroke-linejoin="round"
8016
- class="mic-icon"
8041
+ ${this._isDictating ? b `
8042
+ <button
8043
+ class="mic-stop"
8044
+ title="Stop dictation"
8045
+ aria-label="Stop dictation"
8046
+ @click=${this._handleMicClick}
8017
8047
  >
8018
- <path d="M12 15a3 3 0 0 0 3-3V6a3 3 0 0 0-6 0v6a3 3 0 0 0 3 3Z"/>
8019
- <path d="M19 11a7 7 0 0 1-14 0"/>
8020
- <path d="M12 18v3"/>
8021
- </svg>
8022
- </button>
8048
+ <svg
8049
+ xmlns="http://www.w3.org/2000/svg"
8050
+ fill="currentColor"
8051
+ viewBox="0 0 24 24"
8052
+ class="mic-stop-icon"
8053
+ aria-hidden="true"
8054
+ >
8055
+ <rect
8056
+ x="6"
8057
+ y="6"
8058
+ width="12"
8059
+ height="12"
8060
+ rx="2"
8061
+ />
8062
+ </svg>
8063
+ </button>
8064
+ ` : b `
8065
+ <button
8066
+ class="mic"
8067
+ title="Dictate a message"
8068
+ aria-label="Dictate a message"
8069
+ @click=${this._handleMicClick}
8070
+ >
8071
+ <svg
8072
+ xmlns="http://www.w3.org/2000/svg"
8073
+ viewBox="0 0 24 24"
8074
+ fill="none"
8075
+ stroke="currentColor"
8076
+ stroke-width="1.75"
8077
+ stroke-linecap="round"
8078
+ stroke-linejoin="round"
8079
+ class="mic-icon"
8080
+ aria-hidden="true"
8081
+ >
8082
+ <path d="M12 15a3 3 0 0 0 3-3V6a3 3 0 0 0-6 0v6a3 3 0 0 0 3 3Z"/>
8083
+ <path d="M19 11a7 7 0 0 1-14 0"/>
8084
+ <path d="M12 18v3"/>
8085
+ </svg>
8086
+ </button>
8087
+ `}
8023
8088
  ` : A}
8024
8089
  ${this._isStreaming ? b `
8025
8090
  <button
@@ -8864,6 +8929,14 @@ KRChatbot.styles = i$6 `
8864
8929
  color: #b0b0b0;
8865
8930
  }
8866
8931
 
8932
+ /* margin-right rather than a wider .input-wrapper gap, which would also push
8933
+ the plus button and textarea apart. Applied to both mic states so the
8934
+ spacing doesn't shift when dictation swaps one for the other. */
8935
+ .mic,
8936
+ .mic-stop {
8937
+ margin-right: 4px;
8938
+ }
8939
+
8867
8940
  .mic {
8868
8941
  width: 34px;
8869
8942
  height: 34px;
@@ -8883,30 +8956,42 @@ KRChatbot.styles = i$6 `
8883
8956
  background: #f8f9fc;
8884
8957
  }
8885
8958
 
8886
- /* Live dictation: red mic on a tinted circle, pulsing so it's clear the
8887
- microphone is still open even when the user has stopped speaking. */
8888
- .mic--recording {
8889
- color: var(--kr-chatbot-error-text);
8890
- background: var(--kr-chatbot-error-bg);
8891
- animation: chatbot-mic-pulse 1.6s ease-in-out infinite;
8892
- }
8893
-
8894
- .mic--recording:hover {
8895
- background: var(--kr-chatbot-error-bg);
8959
+ .mic-icon {
8960
+ width: 21px;
8961
+ height: 21px;
8962
+ }
8963
+
8964
+ /* Dictation swaps the mic for this stop button, sitting in the mic's slot so
8965
+ the composer's layout is unchanged and the textarea stays visible with the
8966
+ transcript streaming into it. Structurally identical to .stop; only the
8967
+ colors differ — dictation isn't an error state, so it takes a neutral grey
8968
+ instead of the error red, and that also keeps it distinct from the solid
8969
+ send button sitting right beside it. The icon is dark rather than white
8970
+ because white on this grey wouldn't meet contrast. */
8971
+ .mic-stop {
8972
+ width: 34px;
8973
+ height: 34px;
8974
+ border-radius: 50%;
8975
+ background: #eae7e7;
8976
+ color: #1f1f1f;
8977
+ border: none;
8978
+ cursor: pointer;
8979
+ display: flex;
8980
+ align-items: center;
8981
+ justify-content: center;
8982
+ flex-shrink: 0;
8983
+ transition: all 0.15s;
8896
8984
  }
8897
8985
 
8898
- .mic-icon {
8899
- width: 18px;
8900
- height: 18px;
8986
+ .mic-stop:hover {
8987
+ background: #ded9d9;
8901
8988
  }
8902
8989
 
8903
- @keyframes chatbot-mic-pulse {
8904
- 0%, 100% {
8905
- opacity: 1;
8906
- }
8907
- 50% {
8908
- opacity: 0.55;
8909
- }
8990
+ /* Larger than .stop-icon's 14px: the rect fills only half its 24-unit
8991
+ viewBox, so the drawn square is half this value. */
8992
+ .mic-stop-icon {
8993
+ width: 20px;
8994
+ height: 20px;
8910
8995
  }
8911
8996
 
8912
8997
  .send {