@kodaris/krubble-app-components 1.0.101 → 1.0.102

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
  }
@@ -7277,6 +7318,122 @@ let KRChatbot = class KRChatbot extends i$3 {
7277
7318
  }
7278
7319
  }
7279
7320
  // --- Render ---
7321
+ /**
7322
+ * The composer's controls: attach, the textarea, the mic, and send/stop.
7323
+ *
7324
+ * While dictating, only the mic swaps — to a stop button — so the textarea
7325
+ * stays visible with the transcript streaming into it and send stays reachable.
7326
+ */
7327
+ _renderComposerControls() {
7328
+ return b `
7329
+ <button
7330
+ class="input-plus"
7331
+ @click=${this._handlePlusClick}
7332
+ >+</button>
7333
+ <textarea
7334
+ class="input"
7335
+ .value=${this._inputValue}
7336
+ placeholder=${this.placeholder}
7337
+ rows="1"
7338
+ @input=${this._handleInputChange}
7339
+ @keydown=${this._handleKeyDown}
7340
+ @paste=${this._handlePaste}
7341
+ ></textarea>
7342
+ ${window.SpeechRecognition || window.webkitSpeechRecognition ? b `
7343
+ ${this._isDictating ? b `
7344
+ <button
7345
+ class="mic-stop"
7346
+ title="Stop dictation"
7347
+ aria-label="Stop dictation"
7348
+ @click=${this._handleMicClick}
7349
+ >
7350
+ <svg
7351
+ xmlns="http://www.w3.org/2000/svg"
7352
+ fill="currentColor"
7353
+ viewBox="0 0 24 24"
7354
+ class="mic-stop-icon"
7355
+ aria-hidden="true"
7356
+ >
7357
+ <rect
7358
+ x="6"
7359
+ y="6"
7360
+ width="12"
7361
+ height="12"
7362
+ rx="2"
7363
+ />
7364
+ </svg>
7365
+ </button>
7366
+ ` : b `
7367
+ <button
7368
+ class="mic"
7369
+ title="Dictate a message"
7370
+ aria-label="Dictate a message"
7371
+ @click=${this._handleMicClick}
7372
+ >
7373
+ <svg
7374
+ xmlns="http://www.w3.org/2000/svg"
7375
+ viewBox="0 0 24 24"
7376
+ fill="none"
7377
+ stroke="currentColor"
7378
+ stroke-width="1.75"
7379
+ stroke-linecap="round"
7380
+ stroke-linejoin="round"
7381
+ class="mic-icon"
7382
+ aria-hidden="true"
7383
+ >
7384
+ <path d="M12 15a3 3 0 0 0 3-3V6a3 3 0 0 0-6 0v6a3 3 0 0 0 3 3Z"/>
7385
+ <path d="M19 11a7 7 0 0 1-14 0"/>
7386
+ <path d="M12 18v3"/>
7387
+ </svg>
7388
+ </button>
7389
+ `}
7390
+ ` : A}
7391
+ ${this._isStreaming ? b `
7392
+ <button
7393
+ class="stop"
7394
+ @click=${this._handleStop}
7395
+ title="Stop generating"
7396
+ >
7397
+ <svg
7398
+ xmlns="http://www.w3.org/2000/svg"
7399
+ fill="currentColor"
7400
+ viewBox="0 0 24 24"
7401
+ class="stop-icon"
7402
+ >
7403
+ <rect
7404
+ x="6"
7405
+ y="6"
7406
+ width="12"
7407
+ height="12"
7408
+ rx="2"
7409
+ />
7410
+ </svg>
7411
+ </button>
7412
+ ` : b `
7413
+ <button
7414
+ class=${e({
7415
+ 'send': true,
7416
+ 'send--disabled': !this._inputValue.trim() && !this._pendingConversationFiles.length,
7417
+ })}
7418
+ @click=${this._handleSubmit}
7419
+ title="Send message"
7420
+ >
7421
+ <svg
7422
+ width="16"
7423
+ height="16"
7424
+ viewBox="0 0 24 24"
7425
+ fill="none"
7426
+ stroke="currentColor"
7427
+ stroke-width="2.5"
7428
+ stroke-linecap="round"
7429
+ class="send-icon"
7430
+ >
7431
+ <path d="M12 19V5M5 12l7-7 7 7"/>
7432
+ </svg>
7433
+ </button>
7434
+ `}
7435
+ `;
7436
+ }
7280
7437
  /** The `/` skill menu shown above the composer while typing a slash command. */
7281
7438
  _renderSkillMenu() {
7282
7439
  const skills = this._invocableSkills;
@@ -7983,88 +8140,7 @@ let KRChatbot = class KRChatbot extends i$3 {
7983
8140
  />
7984
8141
  ${this._skillMenuOpened ? this._renderSkillMenu() : A}
7985
8142
  <div class="input-wrapper">
7986
- <button
7987
- class="input-plus"
7988
- @click=${this._handlePlusClick}
7989
- >+</button>
7990
- <textarea
7991
- class="input"
7992
- .value=${this._inputValue}
7993
- placeholder=${this.placeholder}
7994
- rows="1"
7995
- @input=${this._handleInputChange}
7996
- @keydown=${this._handleKeyDown}
7997
- @paste=${this._handlePaste}
7998
- ></textarea>
7999
- ${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"
8017
- >
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>
8023
- ` : A}
8024
- ${this._isStreaming ? b `
8025
- <button
8026
- class="stop"
8027
- @click=${this._handleStop}
8028
- title="Stop generating"
8029
- >
8030
- <svg
8031
- xmlns="http://www.w3.org/2000/svg"
8032
- fill="currentColor"
8033
- viewBox="0 0 24 24"
8034
- class="stop-icon"
8035
- >
8036
- <rect
8037
- x="6"
8038
- y="6"
8039
- width="12"
8040
- height="12"
8041
- rx="2"
8042
- />
8043
- </svg>
8044
- </button>
8045
- ` : b `
8046
- <button
8047
- class=${e({
8048
- 'send': true,
8049
- 'send--disabled': !this._inputValue.trim() && !this._pendingConversationFiles.length,
8050
- })}
8051
- @click=${this._handleSubmit}
8052
- title="Send message"
8053
- >
8054
- <svg
8055
- width="16"
8056
- height="16"
8057
- viewBox="0 0 24 24"
8058
- fill="none"
8059
- stroke="currentColor"
8060
- stroke-width="2.5"
8061
- stroke-linecap="round"
8062
- class="send-icon"
8063
- >
8064
- <path d="M12 19V5M5 12l7-7 7 7"/>
8065
- </svg>
8066
- </button>
8067
- `}
8143
+ ${this._renderComposerControls()}
8068
8144
  </div>
8069
8145
  </div>
8070
8146
  ${this._skillBrowseOpened ? this._renderSkillBrowse() : A}
@@ -8864,6 +8940,14 @@ KRChatbot.styles = i$6 `
8864
8940
  color: #b0b0b0;
8865
8941
  }
8866
8942
 
8943
+ /* margin-right rather than a wider .input-wrapper gap, which would also push
8944
+ the plus button and textarea apart. Applied to both mic states so the
8945
+ spacing doesn't shift when dictation swaps one for the other. */
8946
+ .mic,
8947
+ .mic-stop {
8948
+ margin-right: 4px;
8949
+ }
8950
+
8867
8951
  .mic {
8868
8952
  width: 34px;
8869
8953
  height: 34px;
@@ -8883,30 +8967,42 @@ KRChatbot.styles = i$6 `
8883
8967
  background: #f8f9fc;
8884
8968
  }
8885
8969
 
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);
8970
+ .mic-icon {
8971
+ width: 21px;
8972
+ height: 21px;
8973
+ }
8974
+
8975
+ /* Dictation swaps the mic for this stop button, sitting in the mic's slot so
8976
+ the composer's layout is unchanged and the textarea stays visible with the
8977
+ transcript streaming into it. Structurally identical to .stop; only the
8978
+ colors differ — dictation isn't an error state, so it takes a neutral grey
8979
+ instead of the error red, and that also keeps it distinct from the solid
8980
+ send button sitting right beside it. The icon is dark rather than white
8981
+ because white on this grey wouldn't meet contrast. */
8982
+ .mic-stop {
8983
+ width: 34px;
8984
+ height: 34px;
8985
+ border-radius: 50%;
8986
+ background: #eae7e7;
8987
+ color: #1f1f1f;
8988
+ border: none;
8989
+ cursor: pointer;
8990
+ display: flex;
8991
+ align-items: center;
8992
+ justify-content: center;
8993
+ flex-shrink: 0;
8994
+ transition: all 0.15s;
8896
8995
  }
8897
8996
 
8898
- .mic-icon {
8899
- width: 18px;
8900
- height: 18px;
8997
+ .mic-stop:hover {
8998
+ background: #ded9d9;
8901
8999
  }
8902
9000
 
8903
- @keyframes chatbot-mic-pulse {
8904
- 0%, 100% {
8905
- opacity: 1;
8906
- }
8907
- 50% {
8908
- opacity: 0.55;
8909
- }
9001
+ /* Larger than .stop-icon's 14px: the rect fills only half its 24-unit
9002
+ viewBox, so the drawn square is half this value. */
9003
+ .mic-stop-icon {
9004
+ width: 20px;
9005
+ height: 20px;
8910
9006
  }
8911
9007
 
8912
9008
  .send {