@kodaris/krubble-app-components 1.0.71 → 1.0.72

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.
package/dist/chatbot.js CHANGED
@@ -10,6 +10,8 @@ import { classMap } from 'lit/directives/class-map.js';
10
10
  import { unsafeHTML } from 'lit/directives/unsafe-html.js';
11
11
  import { repeat } from 'lit/directives/repeat.js';
12
12
  import { marked } from 'marked';
13
+ // Importing these named exports also registers their custom elements
14
+ // (kr-dialog, kr-tab-group, kr-tab, kr-text-field) used by the browse dialog.
13
15
  import { KRFilePreview } from '@kodaris/krubble-components';
14
16
  import { KRHttp } from '@kodaris/krubble-http';
15
17
  /** Image MIME types the chatbot accepts (matches what Bedrock can ingest). */
@@ -111,6 +113,12 @@ let KRChatbot = class KRChatbot extends LitElement {
111
113
  * Input placeholder text.
112
114
  */
113
115
  this.placeholder = 'Type a message...';
116
+ /**
117
+ * Skills the user can invoke by typing `/` in the composer. Selecting one
118
+ * inserts `/name ` into the message. The `/` menu shows the first 15 (filtered
119
+ * as the user types); "Browse all skills" opens a dialog with the full list.
120
+ */
121
+ this.skills = [];
114
122
  /**
115
123
  * Whether the chat panel is open. Only relevant in floating mode.
116
124
  */
@@ -140,6 +148,20 @@ let KRChatbot = class KRChatbot extends LitElement {
140
148
  this._pendingInternalFiles = [];
141
149
  /** True while a file is being dragged over the panel (shows the drop overlay). */
142
150
  this._isDraggingFile = false;
151
+ /** Whether the `/` skill menu is open above the composer. */
152
+ this._skillMenuOpened = false;
153
+ /** Current `/` query (text after the slash), used to filter the skill menu. */
154
+ this._skillQuery = '';
155
+ /** Highlighted item in the skill menu for keyboard navigation (-1 = none). */
156
+ this._skillHighlighted = 0;
157
+ /** Index of the message whose copy button just flashed "Copied" (-1 = none). */
158
+ this._copiedIndex = -1;
159
+ /** Whether the "Browse all skills" dialog is open. */
160
+ this._skillBrowseOpened = false;
161
+ /** Active tab in the browse dialog. */
162
+ this._skillBrowseSource = 'org';
163
+ /** Search text in the browse dialog. */
164
+ this._skillBrowseQuery = '';
143
165
  this._handleMouseMove = (e) => {
144
166
  if (this._isDragging) {
145
167
  let newLeft = this._dragStartLeft + (e.clientX - this._dragStartX);
@@ -253,6 +275,40 @@ let KRChatbot = class KRChatbot extends LitElement {
253
275
  }
254
276
  }, 200);
255
277
  }
278
+ // Position the fixed skill menu above the composer, following the krubble
279
+ // select-field pattern (fixed element positioned from the trigger's rect).
280
+ if (this._skillMenuOpened) {
281
+ this._positionSkillMenu();
282
+ }
283
+ // Keep the keyboard-highlighted skill scrolled into view.
284
+ if (changed.has('_skillHighlighted') && this._skillMenuOpened) {
285
+ this._skillMenuEl
286
+ ?.querySelector('.skill-menu__item--highlighted')
287
+ ?.scrollIntoView({ block: 'nearest' });
288
+ }
289
+ }
290
+ /** Anchor the fixed skill menu to the composer, opening upward above it. */
291
+ _positionSkillMenu() {
292
+ requestAnimationFrame(() => {
293
+ if (!this._skillMenuEl || !this._inputWrapperEl) {
294
+ return;
295
+ }
296
+ const inputRect = this._inputWrapperEl.getBoundingClientRect();
297
+ this._skillMenuEl.style.left = inputRect.left + 'px';
298
+ this._skillMenuEl.style.width = inputRect.width + 'px';
299
+ // Bound the menu by the chat panel's own top edge so it stays within the
300
+ // widget. In floating mode that keeps it inside the card; in inline mode the
301
+ // panel top sits just below the app header/breadcrumb, so it stays clear of
302
+ // those too. An 8px gap keeps it off both edges.
303
+ const topLimit = this._panelEl.getBoundingClientRect().top + 8;
304
+ const spaceAbove = inputRect.top - 8 - topLimit;
305
+ this._skillMenuEl.style.maxHeight = spaceAbove + 'px';
306
+ // Measure after the cap is applied so the upward anchor uses the real
307
+ // height, then clamp the top edge to the panel bound.
308
+ const menuRect = this._skillMenuEl.getBoundingClientRect();
309
+ const top = Math.max(topLimit, inputRect.top - menuRect.height - 8);
310
+ this._skillMenuEl.style.top = top + 'px';
311
+ });
256
312
  }
257
313
  // --- Public methods ---
258
314
  /**
@@ -446,13 +502,129 @@ let KRChatbot = class KRChatbot extends LitElement {
446
502
  const textarea = e.target;
447
503
  textarea.style.height = 'auto';
448
504
  textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px';
505
+ // Open the skill menu while the message is a `/` command being typed (a
506
+ // leading slash with no space yet). The text after the slash filters it.
507
+ const slashMatch = this._inputValue.match(/^\/(\S*)$/);
508
+ if (slashMatch && this.skills.length) {
509
+ this._skillMenuOpened = true;
510
+ this._skillQuery = slashMatch[1];
511
+ this._skillHighlighted = 0;
512
+ }
513
+ else {
514
+ this._skillMenuOpened = false;
515
+ }
449
516
  }
450
517
  _handleKeyDown(e) {
518
+ // When the skill menu is open, arrow keys / Enter / Escape drive it. The two
519
+ // top actions (Attach Files, Browse All Skills) are index 0 and 1, then the
520
+ // filtered skills follow — all navigable as one list.
521
+ if (this._skillMenuOpened) {
522
+ const count = 2 + this._filteredSkills().length;
523
+ if (e.key === 'ArrowDown') {
524
+ e.preventDefault();
525
+ this._skillHighlighted = (this._skillHighlighted + 1) % count;
526
+ return;
527
+ }
528
+ if (e.key === 'ArrowUp') {
529
+ e.preventDefault();
530
+ this._skillHighlighted = (this._skillHighlighted - 1 + count) % count;
531
+ return;
532
+ }
533
+ if (e.key === 'Enter' && !e.shiftKey) {
534
+ e.preventDefault();
535
+ this._activateSkillMenuItem(this._skillHighlighted);
536
+ return;
537
+ }
538
+ if (e.key === 'Escape') {
539
+ e.preventDefault();
540
+ this._skillMenuOpened = false;
541
+ return;
542
+ }
543
+ }
451
544
  if (e.key === 'Enter' && !e.shiftKey) {
452
545
  e.preventDefault();
453
546
  this._handleSubmit();
454
547
  }
455
548
  }
549
+ /** Run the skill-menu item at the given flat index (0/1 = actions, then skills). */
550
+ _activateSkillMenuItem(index) {
551
+ if (index === 0) {
552
+ this._openFilePicker();
553
+ return;
554
+ }
555
+ if (index === 1) {
556
+ this._handleSkillBrowseOpen();
557
+ return;
558
+ }
559
+ const skill = this._filteredSkills()[index - 2];
560
+ if (skill) {
561
+ this._handleSkillSelect(skill);
562
+ }
563
+ }
564
+ // --- Skills ---
565
+ /** Skills matching the current `/` query, capped at the first 15. */
566
+ _filteredSkills() {
567
+ const query = this._skillQuery.toLowerCase();
568
+ return this.skills
569
+ .filter((skill) => skill.name.toLowerCase().includes(query) ||
570
+ skill.label.toLowerCase().includes(query))
571
+ .slice(0, 15);
572
+ }
573
+ /** The `+` button toggles the skill/attachment menu open (showing all skills). */
574
+ _handlePlusClick() {
575
+ if (this._skillMenuOpened) {
576
+ this._skillMenuOpened = false;
577
+ return;
578
+ }
579
+ this._skillQuery = '';
580
+ this._skillHighlighted = 0;
581
+ this._skillMenuOpened = true;
582
+ }
583
+ /** Insert `/name ` into the composer and focus it, ready for the message. */
584
+ _handleSkillSelect(skill) {
585
+ this._inputValue = `/${skill.name} `;
586
+ this._skillMenuOpened = false;
587
+ this._skillBrowseOpened = false;
588
+ this._inputEl?.focus();
589
+ }
590
+ /** Copy a message's text to the clipboard and briefly flash "Copied". */
591
+ _handleCopyMessage(content, index) {
592
+ navigator.clipboard.writeText(content).then(() => {
593
+ this._copiedIndex = index;
594
+ setTimeout(() => {
595
+ if (this._copiedIndex === index) {
596
+ this._copiedIndex = -1;
597
+ }
598
+ }, 2000);
599
+ }).catch(() => { });
600
+ }
601
+ _handleSkillBrowseOpen() {
602
+ this._skillMenuOpened = false;
603
+ this._skillBrowseQuery = '';
604
+ this._skillBrowseOpened = true;
605
+ }
606
+ _handleSkillBrowseClose() {
607
+ this._skillBrowseOpened = false;
608
+ }
609
+ _handleSkillBrowseSearch(e) {
610
+ this._skillBrowseQuery = e.target.value;
611
+ }
612
+ _handleSkillBrowseTabChange(e) {
613
+ this._skillBrowseSource = e.detail.activeTabId;
614
+ }
615
+ /** Skills for the browse dialog's active tab, filtered by its search box. */
616
+ _browseSkills() {
617
+ const query = this._skillBrowseQuery.toLowerCase();
618
+ return this.skills.filter((skill) => {
619
+ const source = skill.source || 'org';
620
+ if (source !== this._skillBrowseSource) {
621
+ return false;
622
+ }
623
+ return skill.name.toLowerCase().includes(query) ||
624
+ skill.label.toLowerCase().includes(query) ||
625
+ (skill.description || '').toLowerCase().includes(query);
626
+ });
627
+ }
456
628
  // --- Attachments ---
457
629
  _openFilePicker() {
458
630
  this._fileInputEl?.click();
@@ -810,6 +982,119 @@ let KRChatbot = class KRChatbot extends LitElement {
810
982
  }
811
983
  }
812
984
  // --- Render ---
985
+ /** The `/` skill menu shown above the composer while typing a slash command. */
986
+ _renderSkillMenu() {
987
+ const skills = this._filteredSkills();
988
+ return html `
989
+ <div class="skill-menu">
990
+ <div class="skill-menu__actions">
991
+ <button
992
+ class=${classMap({
993
+ 'skill-menu__action': true,
994
+ 'skill-menu__item--highlighted': this._skillHighlighted === 0,
995
+ })}
996
+ @click=${this._openFilePicker}
997
+ >
998
+ Attach Files
999
+ </button>
1000
+ <button
1001
+ class=${classMap({
1002
+ 'skill-menu__action': true,
1003
+ 'skill-menu__item--highlighted': this._skillHighlighted === 1,
1004
+ })}
1005
+ @click=${this._handleSkillBrowseOpen}
1006
+ >
1007
+ Browse All Skills
1008
+ </button>
1009
+ </div>
1010
+ ${skills.length ? html `
1011
+ <ul class="skill-menu__list">
1012
+ ${skills.map((skill, index) => html `
1013
+ <li>
1014
+ <button
1015
+ class=${classMap({
1016
+ 'skill-menu__item': true,
1017
+ 'skill-menu__item--highlighted': index + 2 === this._skillHighlighted,
1018
+ })}
1019
+ @click=${() => this._handleSkillSelect(skill)}
1020
+ >
1021
+ <span class="skill-menu__name">${skill.label}</span>
1022
+ ${skill.description ? html `
1023
+ <span class="skill-menu__desc">${skill.description}</span>
1024
+ ` : nothing}
1025
+ </button>
1026
+ </li>
1027
+ `)}
1028
+ </ul>
1029
+ ` : html `
1030
+ <div class="skill-menu__empty">No matching skills</div>
1031
+ `}
1032
+ </div>
1033
+ `;
1034
+ }
1035
+ /** The "Browse all skills" dialog — a searchable grid of Org / Kodaris cards. */
1036
+ _renderSkillBrowse() {
1037
+ return html `
1038
+ <kr-dialog
1039
+ opened
1040
+ label="Browse Skills"
1041
+ width="720px"
1042
+ @close=${this._handleSkillBrowseClose}
1043
+ >
1044
+ <div class="skill-browse">
1045
+ <kr-text-field
1046
+ placeholder="Search skills..."
1047
+ .value=${this._skillBrowseQuery}
1048
+ @input=${this._handleSkillBrowseSearch}
1049
+ ></kr-text-field>
1050
+ <kr-tab-group
1051
+ active-tab-id=${this._skillBrowseSource}
1052
+ @tab-change=${this._handleSkillBrowseTabChange}
1053
+ >
1054
+ <kr-tab id="org" label="Your Org">
1055
+ ${this._renderSkillGrid()}
1056
+ </kr-tab>
1057
+ <kr-tab id="kodaris" label="Kodaris">
1058
+ ${this._renderSkillGrid()}
1059
+ </kr-tab>
1060
+ </kr-tab-group>
1061
+ </div>
1062
+ </kr-dialog>
1063
+ `;
1064
+ }
1065
+ /** The card grid for the browse dialog's active tab. */
1066
+ _renderSkillGrid() {
1067
+ const skills = this._browseSkills();
1068
+ if (!skills.length) {
1069
+ return html `<div class="skill-browse__empty">No matching skills</div>`;
1070
+ }
1071
+ return html `
1072
+ <div class="skill-browse__grid">
1073
+ ${skills.map((skill) => html `
1074
+ <button
1075
+ class="skill-card"
1076
+ @click=${() => this._handleSkillSelect(skill)}
1077
+ >
1078
+ <span class="skill-card__name">${skill.label}</span>
1079
+ ${skill.description ? html `
1080
+ <span class="skill-card__desc">${skill.description}</span>
1081
+ ` : nothing}
1082
+ </button>
1083
+ `)}
1084
+ </div>
1085
+ `;
1086
+ }
1087
+ /**
1088
+ * Render a user message, highlighting a leading `/skill-name` token when it
1089
+ * matches a known skill (like the way Claude highlights slash commands).
1090
+ */
1091
+ _renderUserContent(content) {
1092
+ const match = content.match(/^\/(\S+)(\s[\s\S]*)?$/);
1093
+ if (match && this.skills.some((skill) => skill.name === match[1])) {
1094
+ return html `<span class="message-skill">/${match[1]}</span>${match[2] || ''}`;
1095
+ }
1096
+ return content;
1097
+ }
813
1098
  render() {
814
1099
  return html `
815
1100
  ${(this.hideToggle || this.variant === 'inline') ? nothing : html `
@@ -966,7 +1251,7 @@ let KRChatbot = class KRChatbot extends LitElement {
966
1251
  <span class="empty-text">What can I help you with?</span>
967
1252
  </div>
968
1253
  ` : nothing}
969
- ${this.messages.map((msg) => html `
1254
+ ${this.messages.map((msg, index) => html `
970
1255
  <div class=${classMap({
971
1256
  'message': true,
972
1257
  'message--user': msg.type === 'user',
@@ -998,7 +1283,7 @@ let KRChatbot = class KRChatbot extends LitElement {
998
1283
  ${msg.content ? html `
999
1284
  <div class="message-content">
1000
1285
  ${msg.type === 'user'
1001
- ? msg.content
1286
+ ? this._renderUserContent(msg.content)
1002
1287
  : unsafeHTML(marked.parse(msg.content, { breaks: true }))}
1003
1288
  </div>
1004
1289
  ` : nothing}
@@ -1009,6 +1294,46 @@ let KRChatbot = class KRChatbot extends LitElement {
1009
1294
  <span></span>
1010
1295
  </div>
1011
1296
  ` : nothing}
1297
+ ${msg.content ? html `
1298
+ <button
1299
+ class="message-copy"
1300
+ title="Copy"
1301
+ @click=${() => this._handleCopyMessage(msg.content, index)}
1302
+ >
1303
+ ${this._copiedIndex === index ? html `
1304
+ <svg
1305
+ viewBox="0 0 24 24"
1306
+ fill="none"
1307
+ stroke="currentColor"
1308
+ stroke-width="1.5"
1309
+ stroke-linecap="round"
1310
+ stroke-linejoin="round"
1311
+ >
1312
+ <path d="M20 6 9 17l-5-5" />
1313
+ </svg>
1314
+ Copied
1315
+ ` : html `
1316
+ <svg
1317
+ viewBox="0 0 24 24"
1318
+ fill="none"
1319
+ stroke="currentColor"
1320
+ stroke-width="1.5"
1321
+ stroke-linecap="round"
1322
+ stroke-linejoin="round"
1323
+ >
1324
+ <rect
1325
+ x="9"
1326
+ y="9"
1327
+ width="13"
1328
+ height="13"
1329
+ rx="2"
1330
+ />
1331
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
1332
+ </svg>
1333
+ Copy
1334
+ `}
1335
+ </button>
1336
+ ` : nothing}
1012
1337
  </div>
1013
1338
  `)}
1014
1339
  </div>
@@ -1073,11 +1398,11 @@ let KRChatbot = class KRChatbot extends LitElement {
1073
1398
  hidden
1074
1399
  @change=${this._handleFileInputChange}
1075
1400
  />
1401
+ ${this._skillMenuOpened ? this._renderSkillMenu() : nothing}
1076
1402
  <div class="input-wrapper">
1077
1403
  <button
1078
1404
  class="input-plus"
1079
- title="Attach image"
1080
- @click=${this._openFilePicker}
1405
+ @click=${this._handlePlusClick}
1081
1406
  >+</button>
1082
1407
  <textarea
1083
1408
  class="input"
@@ -1134,6 +1459,7 @@ let KRChatbot = class KRChatbot extends LitElement {
1134
1459
  `}
1135
1460
  </div>
1136
1461
  </div>
1462
+ ${this._skillBrowseOpened ? this._renderSkillBrowse() : nothing}
1137
1463
  </div>
1138
1464
  `;
1139
1465
  }
@@ -1373,6 +1699,7 @@ KRChatbot.styles = css `
1373
1699
  }
1374
1700
 
1375
1701
  .message {
1702
+ position: relative;
1376
1703
  display: flex;
1377
1704
  flex-direction: column;
1378
1705
  max-width: 100%;
@@ -1417,6 +1744,58 @@ KRChatbot.styles = css `
1417
1744
  font-size: 14px;
1418
1745
  }
1419
1746
 
1747
+ /* Highlighted leading /skill token in a sent user message. Monospace + a
1748
+ stronger color makes it read as a distinct command without a pill. */
1749
+ .message-skill {
1750
+ font-family: 'SF Mono', SFMono-Regular, ui-monospace, Menlo, Consolas, monospace;
1751
+ font-weight: 600;
1752
+ color: #b45309;
1753
+ }
1754
+
1755
+ /* Per-message copy button. Kept in flow with a fixed height that is always
1756
+ reserved (via a negative bottom margin that cancels it) so message spacing
1757
+ is identical whether or not it's shown; only its opacity toggles on hover. */
1758
+ .message-copy {
1759
+ align-self: flex-start;
1760
+ margin-top: 4px;
1761
+ margin-bottom: -27px;
1762
+ display: inline-flex;
1763
+ align-items: center;
1764
+ gap: 4px;
1765
+ height: 23px;
1766
+ padding: 4px 8px;
1767
+ background: none;
1768
+ border: none;
1769
+ border-radius: 6px;
1770
+ font: inherit;
1771
+ font-size: 12px;
1772
+ color: #64668b;
1773
+ cursor: pointer;
1774
+ opacity: 0;
1775
+ pointer-events: none;
1776
+ transition: opacity 0.15s, background-color 0.15s, color 0.15s;
1777
+ }
1778
+
1779
+ .message--user .message-copy {
1780
+ align-self: flex-end;
1781
+ }
1782
+
1783
+ .message:hover .message-copy,
1784
+ .message-copy:focus-visible {
1785
+ opacity: 1;
1786
+ pointer-events: auto;
1787
+ }
1788
+
1789
+ .message-copy:hover {
1790
+ background: rgba(0, 0, 0, 0.06);
1791
+ color: #1a1a2e;
1792
+ }
1793
+
1794
+ .message-copy svg {
1795
+ width: 14px;
1796
+ height: 14px;
1797
+ }
1798
+
1420
1799
  .message--assistant .message-content {
1421
1800
  background: transparent;
1422
1801
  color: #000000;
@@ -1752,6 +2131,176 @@ KRChatbot.styles = css `
1752
2131
  display: block;
1753
2132
  }
1754
2133
 
2134
+ /* Skill menu (opened by typing '/'). Fixed + JS-positioned above the
2135
+ composer, following the krubble select-field dropdown pattern. */
2136
+ .skill-menu {
2137
+ position: fixed;
2138
+ z-index: 10000;
2139
+ background: white;
2140
+ border: 1px solid #9ba7b6;
2141
+ border-radius: 8px;
2142
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
2143
+ overflow: hidden;
2144
+ display: flex;
2145
+ flex-direction: column;
2146
+ /* max-height is set dynamically to the space above the composer. */
2147
+ }
2148
+
2149
+ /* Floating mode is a compact card — use a lighter shadow and softer border. */
2150
+ :host(:not([variant='inline'])) .skill-menu {
2151
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
2152
+ border-color: #b0b0b0;
2153
+ }
2154
+
2155
+ .skill-menu__list {
2156
+ list-style: none;
2157
+ margin: 0;
2158
+ padding: 8px 0;
2159
+ overflow-y: auto;
2160
+ }
2161
+
2162
+ .skill-menu__item {
2163
+ display: flex;
2164
+ flex-direction: column;
2165
+ gap: 2px;
2166
+ width: 100%;
2167
+ padding: 8px 16px;
2168
+ background: none;
2169
+ border: none;
2170
+ text-align: left;
2171
+ cursor: pointer;
2172
+ font: inherit;
2173
+ color: #000000;
2174
+ }
2175
+
2176
+
2177
+ /* Floating mode is a compact card — tighten name/description spacing. */
2178
+ :host(:not([variant='inline'])) .skill-menu__item {
2179
+ gap: 1px;
2180
+ }
2181
+
2182
+ .skill-menu__name {
2183
+ font-weight: 500;
2184
+ }
2185
+
2186
+ /* Floating mode is a compact card — shrink the skill name a touch. */
2187
+ :host(:not([variant='inline'])) .skill-menu__name {
2188
+ font-size: 13px;
2189
+ }
2190
+
2191
+ .skill-menu__desc {
2192
+ font-size: 12px;
2193
+ color: #000000;
2194
+ overflow: hidden;
2195
+ text-overflow: ellipsis;
2196
+ white-space: nowrap;
2197
+ }
2198
+
2199
+ .skill-menu__empty {
2200
+ padding: 16px;
2201
+ text-align: center;
2202
+ color: #000000;
2203
+ }
2204
+
2205
+ .skill-menu__actions {
2206
+ display: flex;
2207
+ flex-direction: column;
2208
+ padding: 8px 0;
2209
+ border-bottom: 1px solid #eee;
2210
+ flex-shrink: 0;
2211
+ }
2212
+
2213
+ .skill-menu__action {
2214
+ width: 100%;
2215
+ padding: 6px 16px;
2216
+ background: transparent;
2217
+ border: none;
2218
+ text-align: left;
2219
+ cursor: pointer;
2220
+ font: inherit;
2221
+ font-size: 13px;
2222
+ color: var(--kr-chatbot-primary);
2223
+ font-weight: 500;
2224
+ }
2225
+
2226
+ /* Highlight (keyboard) and hover (mouse) are independent — either shades the
2227
+ row. Declared after the base rules so it wins the cascade for both the
2228
+ action buttons and the skill items. */
2229
+ .skill-menu__action.skill-menu__item--highlighted,
2230
+ .skill-menu__item.skill-menu__item--highlighted,
2231
+ .skill-menu__action:hover,
2232
+ .skill-menu__item:hover {
2233
+ background: #f3f4f6;
2234
+ }
2235
+
2236
+ /* Browse-all dialog body. Owns the height so the dialog is the same size on
2237
+ every tab; the search is fixed and the card grid flexes/scrolls to fill. */
2238
+ .skill-browse {
2239
+ display: flex;
2240
+ flex-direction: column;
2241
+ height: 70vh;
2242
+ }
2243
+
2244
+ .skill-browse kr-text-field {
2245
+ display: block;
2246
+ margin: 20px 20px 16px;
2247
+ flex-shrink: 0;
2248
+ }
2249
+
2250
+ .skill-browse kr-tab-group {
2251
+ flex: 1;
2252
+ min-height: 0;
2253
+ }
2254
+
2255
+ /* The grid is the scroller and spans the full width, so its scrollbar sits
2256
+ flush against the dialog edge; padding-right insets the cards from it. */
2257
+ .skill-browse__grid {
2258
+ height: 100%;
2259
+ overflow-y: auto;
2260
+ display: grid;
2261
+ grid-template-columns: repeat(3, 1fr);
2262
+ grid-auto-rows: min-content;
2263
+ align-content: start;
2264
+ gap: 12px;
2265
+ padding: 16px 20px 20px;
2266
+ }
2267
+
2268
+ .skill-card {
2269
+ display: flex;
2270
+ flex-direction: column;
2271
+ gap: 6px;
2272
+ padding: 14px;
2273
+ background: white;
2274
+ border: 1px solid #e5e7eb;
2275
+ border-radius: 10px;
2276
+ text-align: left;
2277
+ cursor: pointer;
2278
+ font: inherit;
2279
+ color: #000000;
2280
+ transition: border-color 0.15s ease, box-shadow 0.15s ease;
2281
+ }
2282
+
2283
+ .skill-card:hover {
2284
+ border-color: var(--kr-chatbot-primary);
2285
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
2286
+ }
2287
+
2288
+ .skill-card__name {
2289
+ font-weight: 600;
2290
+ }
2291
+
2292
+ .skill-card__desc {
2293
+ font-size: 12px;
2294
+ color: #000000;
2295
+ }
2296
+
2297
+ .skill-browse__empty {
2298
+ height: 100%;
2299
+ padding: 32px;
2300
+ text-align: center;
2301
+ color: #000000;
2302
+ }
2303
+
1755
2304
  /* Composer attachment chips (pre-send) */
1756
2305
  .composer-attachments {
1757
2306
  display: flex;
@@ -1851,6 +2400,9 @@ __decorate([
1851
2400
  __decorate([
1852
2401
  property({ type: String })
1853
2402
  ], KRChatbot.prototype, "placeholder", void 0);
2403
+ __decorate([
2404
+ property({ attribute: false })
2405
+ ], KRChatbot.prototype, "skills", void 0);
1854
2406
  __decorate([
1855
2407
  property({
1856
2408
  type: Boolean,
@@ -1885,6 +2437,27 @@ __decorate([
1885
2437
  __decorate([
1886
2438
  state()
1887
2439
  ], KRChatbot.prototype, "_isDraggingFile", void 0);
2440
+ __decorate([
2441
+ state()
2442
+ ], KRChatbot.prototype, "_skillMenuOpened", void 0);
2443
+ __decorate([
2444
+ state()
2445
+ ], KRChatbot.prototype, "_skillQuery", void 0);
2446
+ __decorate([
2447
+ state()
2448
+ ], KRChatbot.prototype, "_skillHighlighted", void 0);
2449
+ __decorate([
2450
+ state()
2451
+ ], KRChatbot.prototype, "_copiedIndex", void 0);
2452
+ __decorate([
2453
+ state()
2454
+ ], KRChatbot.prototype, "_skillBrowseOpened", void 0);
2455
+ __decorate([
2456
+ state()
2457
+ ], KRChatbot.prototype, "_skillBrowseSource", void 0);
2458
+ __decorate([
2459
+ state()
2460
+ ], KRChatbot.prototype, "_skillBrowseQuery", void 0);
1888
2461
  __decorate([
1889
2462
  query('.panel')
1890
2463
  ], KRChatbot.prototype, "_panelEl", void 0);
@@ -1897,6 +2470,12 @@ __decorate([
1897
2470
  __decorate([
1898
2471
  query('.file-input')
1899
2472
  ], KRChatbot.prototype, "_fileInputEl", void 0);
2473
+ __decorate([
2474
+ query('.input-wrapper')
2475
+ ], KRChatbot.prototype, "_inputWrapperEl", void 0);
2476
+ __decorate([
2477
+ query('.skill-menu')
2478
+ ], KRChatbot.prototype, "_skillMenuEl", void 0);
1900
2479
  KRChatbot = __decorate([
1901
2480
  customElement('kr-chatbot')
1902
2481
  ], KRChatbot);