@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.
@@ -5798,6 +5798,31 @@
5798
5798
  this._skillBrowseQuery = '';
5799
5799
  /** True while the mic is live and speech is being transcribed into the composer. */
5800
5800
  this._isDictating = false;
5801
+ /**
5802
+ * Close the `/` skill menu when the click lands outside it.
5803
+ *
5804
+ * Bound at the document because a click anywhere — including outside this
5805
+ * component entirely — should dismiss the menu. `composedPath()` rather than
5806
+ * `e.target` since the menu lives in this component's shadow root, where
5807
+ * `target` on a document listener is retargeted to the host element and every
5808
+ * inside click would look like an outside one.
5809
+ */
5810
+ this._handleDocumentClick = (e) => {
5811
+ if (!this._skillMenuOpened) {
5812
+ return;
5813
+ }
5814
+ // Excluded: the textarea, since clicking it keeps editing the same `/` command
5815
+ // and dismissing there would fight the user still typing it; and the plus
5816
+ // button, which owns the menu's open/close itself. The plus exclusion matters
5817
+ // in both directions — this listener runs after the button's own handler, so
5818
+ // without it a click that just opened the menu would immediately close it
5819
+ // again, and a click meant to toggle it shut would be double-handled.
5820
+ const path = e.composedPath();
5821
+ if (path.some((el) => el instanceof HTMLElement && (el.classList.contains('skill-menu') || el.classList.contains('input') || el.classList.contains('input-plus')))) {
5822
+ return;
5823
+ }
5824
+ this._skillMenuOpened = false;
5825
+ };
5801
5826
  this._handleMouseMove = (e) => {
5802
5827
  if (this._isDragging) {
5803
5828
  let newLeft = this._dragStartLeft + (e.clientX - this._dragStartX);
@@ -5941,6 +5966,7 @@
5941
5966
  this.opened = true;
5942
5967
  }
5943
5968
  this._restoreState();
5969
+ document.addEventListener('click', this._handleDocumentClick);
5944
5970
  }
5945
5971
  disconnectedCallback() {
5946
5972
  super.disconnectedCallback();
@@ -5950,6 +5976,7 @@
5950
5976
  }
5951
5977
  document.removeEventListener('mousemove', this._handleMouseMove);
5952
5978
  document.removeEventListener('mouseup', this._handleMouseUp);
5979
+ document.removeEventListener('click', this._handleDocumentClick);
5953
5980
  // Release the mic if the chatbot is torn down mid-dictation — otherwise the
5954
5981
  // recognizer keeps the browser's recording indicator lit with nothing to
5955
5982
  // transcribe into. abort() (not stop()) since the discarded text has nowhere to go.
@@ -6171,7 +6198,12 @@
6171
6198
  }
6172
6199
  const state = JSON.parse(raw);
6173
6200
  // Draft text and the debug drawer are persisted for all variants.
6174
- if (typeof state.message === 'string') {
6201
+ // A lone `/` is dropped — it's the in-progress half of a menu interaction,
6202
+ // not a draft, and restoring it would revive a stale menu the user never
6203
+ // used. Anything with an actual command after the slash (`/code`, `/code do
6204
+ // the thing`) is a real draft and restores normally. Trimmed before testing
6205
+ // so a stray space around the slash doesn't defeat the check.
6206
+ if (typeof state.message === 'string' && state.message.trim() !== '/') {
6175
6207
  this._inputValue = state.message;
6176
6208
  }
6177
6209
  this._debugPanelOpen = state.debugPanelOpen === 'true';
@@ -6719,6 +6751,15 @@
6719
6751
  // the menu interaction, and the picker is a separate window so the menu would
6720
6752
  // otherwise linger open behind it (and after a file is chosen).
6721
6753
  this._skillMenuOpened = false;
6754
+ // A lone `/` was only the means of reaching this action, so it shouldn't be
6755
+ // left behind in the composer once the picker opens. Nothing else is touched:
6756
+ // a real command (`/code`) stays because `/code` plus an attachment is a valid
6757
+ // invocation, and the plus button reaches this same method with an arbitrary
6758
+ // draft in the composer, which must survive.
6759
+ if (this._inputValue.trim() === '/') {
6760
+ this._inputValue = '';
6761
+ this._saveState();
6762
+ }
6722
6763
  this._pendingAttachAnalyze = aiAnalyze;
6723
6764
  this._fileInputEl?.click();
6724
6765
  }
@@ -7280,6 +7321,122 @@
7280
7321
  }
7281
7322
  }
7282
7323
  // --- Render ---
7324
+ /**
7325
+ * The composer's controls: attach, the textarea, the mic, and send/stop.
7326
+ *
7327
+ * While dictating, only the mic swaps — to a stop button — so the textarea
7328
+ * stays visible with the transcript streaming into it and send stays reachable.
7329
+ */
7330
+ _renderComposerControls() {
7331
+ return b `
7332
+ <button
7333
+ class="input-plus"
7334
+ @click=${this._handlePlusClick}
7335
+ >+</button>
7336
+ <textarea
7337
+ class="input"
7338
+ .value=${this._inputValue}
7339
+ placeholder=${this.placeholder}
7340
+ rows="1"
7341
+ @input=${this._handleInputChange}
7342
+ @keydown=${this._handleKeyDown}
7343
+ @paste=${this._handlePaste}
7344
+ ></textarea>
7345
+ ${window.SpeechRecognition || window.webkitSpeechRecognition ? b `
7346
+ ${this._isDictating ? b `
7347
+ <button
7348
+ class="mic-stop"
7349
+ title="Stop dictation"
7350
+ aria-label="Stop dictation"
7351
+ @click=${this._handleMicClick}
7352
+ >
7353
+ <svg
7354
+ xmlns="http://www.w3.org/2000/svg"
7355
+ fill="currentColor"
7356
+ viewBox="0 0 24 24"
7357
+ class="mic-stop-icon"
7358
+ aria-hidden="true"
7359
+ >
7360
+ <rect
7361
+ x="6"
7362
+ y="6"
7363
+ width="12"
7364
+ height="12"
7365
+ rx="2"
7366
+ />
7367
+ </svg>
7368
+ </button>
7369
+ ` : b `
7370
+ <button
7371
+ class="mic"
7372
+ title="Dictate a message"
7373
+ aria-label="Dictate a message"
7374
+ @click=${this._handleMicClick}
7375
+ >
7376
+ <svg
7377
+ xmlns="http://www.w3.org/2000/svg"
7378
+ viewBox="0 0 24 24"
7379
+ fill="none"
7380
+ stroke="currentColor"
7381
+ stroke-width="1.75"
7382
+ stroke-linecap="round"
7383
+ stroke-linejoin="round"
7384
+ class="mic-icon"
7385
+ aria-hidden="true"
7386
+ >
7387
+ <path d="M12 15a3 3 0 0 0 3-3V6a3 3 0 0 0-6 0v6a3 3 0 0 0 3 3Z"/>
7388
+ <path d="M19 11a7 7 0 0 1-14 0"/>
7389
+ <path d="M12 18v3"/>
7390
+ </svg>
7391
+ </button>
7392
+ `}
7393
+ ` : A}
7394
+ ${this._isStreaming ? b `
7395
+ <button
7396
+ class="stop"
7397
+ @click=${this._handleStop}
7398
+ title="Stop generating"
7399
+ >
7400
+ <svg
7401
+ xmlns="http://www.w3.org/2000/svg"
7402
+ fill="currentColor"
7403
+ viewBox="0 0 24 24"
7404
+ class="stop-icon"
7405
+ >
7406
+ <rect
7407
+ x="6"
7408
+ y="6"
7409
+ width="12"
7410
+ height="12"
7411
+ rx="2"
7412
+ />
7413
+ </svg>
7414
+ </button>
7415
+ ` : b `
7416
+ <button
7417
+ class=${e({
7418
+ 'send': true,
7419
+ 'send--disabled': !this._inputValue.trim() && !this._pendingConversationFiles.length,
7420
+ })}
7421
+ @click=${this._handleSubmit}
7422
+ title="Send message"
7423
+ >
7424
+ <svg
7425
+ width="16"
7426
+ height="16"
7427
+ viewBox="0 0 24 24"
7428
+ fill="none"
7429
+ stroke="currentColor"
7430
+ stroke-width="2.5"
7431
+ stroke-linecap="round"
7432
+ class="send-icon"
7433
+ >
7434
+ <path d="M12 19V5M5 12l7-7 7 7"/>
7435
+ </svg>
7436
+ </button>
7437
+ `}
7438
+ `;
7439
+ }
7283
7440
  /** The `/` skill menu shown above the composer while typing a slash command. */
7284
7441
  _renderSkillMenu() {
7285
7442
  const skills = this._invocableSkills;
@@ -7986,88 +8143,7 @@
7986
8143
  />
7987
8144
  ${this._skillMenuOpened ? this._renderSkillMenu() : A}
7988
8145
  <div class="input-wrapper">
7989
- <button
7990
- class="input-plus"
7991
- @click=${this._handlePlusClick}
7992
- >+</button>
7993
- <textarea
7994
- class="input"
7995
- .value=${this._inputValue}
7996
- placeholder=${this.placeholder}
7997
- rows="1"
7998
- @input=${this._handleInputChange}
7999
- @keydown=${this._handleKeyDown}
8000
- @paste=${this._handlePaste}
8001
- ></textarea>
8002
- ${window.SpeechRecognition || window.webkitSpeechRecognition ? b `
8003
- <button
8004
- class=${e({
8005
- mic: true,
8006
- 'mic--recording': this._isDictating,
8007
- })}
8008
- title=${this._isDictating ? 'Stop dictation' : 'Dictate a message'}
8009
- @click=${this._handleMicClick}
8010
- >
8011
- <svg
8012
- xmlns="http://www.w3.org/2000/svg"
8013
- viewBox="0 0 24 24"
8014
- fill="none"
8015
- stroke="currentColor"
8016
- stroke-width="1.75"
8017
- stroke-linecap="round"
8018
- stroke-linejoin="round"
8019
- class="mic-icon"
8020
- >
8021
- <path d="M12 15a3 3 0 0 0 3-3V6a3 3 0 0 0-6 0v6a3 3 0 0 0 3 3Z"/>
8022
- <path d="M19 11a7 7 0 0 1-14 0"/>
8023
- <path d="M12 18v3"/>
8024
- </svg>
8025
- </button>
8026
- ` : A}
8027
- ${this._isStreaming ? b `
8028
- <button
8029
- class="stop"
8030
- @click=${this._handleStop}
8031
- title="Stop generating"
8032
- >
8033
- <svg
8034
- xmlns="http://www.w3.org/2000/svg"
8035
- fill="currentColor"
8036
- viewBox="0 0 24 24"
8037
- class="stop-icon"
8038
- >
8039
- <rect
8040
- x="6"
8041
- y="6"
8042
- width="12"
8043
- height="12"
8044
- rx="2"
8045
- />
8046
- </svg>
8047
- </button>
8048
- ` : b `
8049
- <button
8050
- class=${e({
8051
- 'send': true,
8052
- 'send--disabled': !this._inputValue.trim() && !this._pendingConversationFiles.length,
8053
- })}
8054
- @click=${this._handleSubmit}
8055
- title="Send message"
8056
- >
8057
- <svg
8058
- width="16"
8059
- height="16"
8060
- viewBox="0 0 24 24"
8061
- fill="none"
8062
- stroke="currentColor"
8063
- stroke-width="2.5"
8064
- stroke-linecap="round"
8065
- class="send-icon"
8066
- >
8067
- <path d="M12 19V5M5 12l7-7 7 7"/>
8068
- </svg>
8069
- </button>
8070
- `}
8146
+ ${this._renderComposerControls()}
8071
8147
  </div>
8072
8148
  </div>
8073
8149
  ${this._skillBrowseOpened ? this._renderSkillBrowse() : A}
@@ -8867,6 +8943,14 @@
8867
8943
  color: #b0b0b0;
8868
8944
  }
8869
8945
 
8946
+ /* margin-right rather than a wider .input-wrapper gap, which would also push
8947
+ the plus button and textarea apart. Applied to both mic states so the
8948
+ spacing doesn't shift when dictation swaps one for the other. */
8949
+ .mic,
8950
+ .mic-stop {
8951
+ margin-right: 4px;
8952
+ }
8953
+
8870
8954
  .mic {
8871
8955
  width: 34px;
8872
8956
  height: 34px;
@@ -8886,30 +8970,42 @@
8886
8970
  background: #f8f9fc;
8887
8971
  }
8888
8972
 
8889
- /* Live dictation: red mic on a tinted circle, pulsing so it's clear the
8890
- microphone is still open even when the user has stopped speaking. */
8891
- .mic--recording {
8892
- color: var(--kr-chatbot-error-text);
8893
- background: var(--kr-chatbot-error-bg);
8894
- animation: chatbot-mic-pulse 1.6s ease-in-out infinite;
8895
- }
8896
-
8897
- .mic--recording:hover {
8898
- background: var(--kr-chatbot-error-bg);
8973
+ .mic-icon {
8974
+ width: 21px;
8975
+ height: 21px;
8976
+ }
8977
+
8978
+ /* Dictation swaps the mic for this stop button, sitting in the mic's slot so
8979
+ the composer's layout is unchanged and the textarea stays visible with the
8980
+ transcript streaming into it. Structurally identical to .stop; only the
8981
+ colors differ — dictation isn't an error state, so it takes a neutral grey
8982
+ instead of the error red, and that also keeps it distinct from the solid
8983
+ send button sitting right beside it. The icon is dark rather than white
8984
+ because white on this grey wouldn't meet contrast. */
8985
+ .mic-stop {
8986
+ width: 34px;
8987
+ height: 34px;
8988
+ border-radius: 50%;
8989
+ background: #eae7e7;
8990
+ color: #1f1f1f;
8991
+ border: none;
8992
+ cursor: pointer;
8993
+ display: flex;
8994
+ align-items: center;
8995
+ justify-content: center;
8996
+ flex-shrink: 0;
8997
+ transition: all 0.15s;
8899
8998
  }
8900
8999
 
8901
- .mic-icon {
8902
- width: 18px;
8903
- height: 18px;
9000
+ .mic-stop:hover {
9001
+ background: #ded9d9;
8904
9002
  }
8905
9003
 
8906
- @keyframes chatbot-mic-pulse {
8907
- 0%, 100% {
8908
- opacity: 1;
8909
- }
8910
- 50% {
8911
- opacity: 0.55;
8912
- }
9004
+ /* Larger than .stop-icon's 14px: the rect fills only half its 24-unit
9005
+ viewBox, so the drawn square is half this value. */
9006
+ .mic-stop-icon {
9007
+ width: 20px;
9008
+ height: 20px;
8913
9009
  }
8914
9010
 
8915
9011
  .send {