@kodaris/krubble-app-components 1.0.71 → 1.0.73

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.
@@ -5644,6 +5644,12 @@ let KRChatbot = class KRChatbot extends i$2 {
5644
5644
  * Input placeholder text.
5645
5645
  */
5646
5646
  this.placeholder = 'Type a message...';
5647
+ /**
5648
+ * Skills the user can invoke by typing `/` in the composer. Selecting one
5649
+ * inserts `/name ` into the message. The `/` menu shows the first 15 (filtered
5650
+ * as the user types); "Browse all skills" opens a dialog with the full list.
5651
+ */
5652
+ this.skills = [];
5647
5653
  /**
5648
5654
  * Whether the chat panel is open. Only relevant in floating mode.
5649
5655
  */
@@ -5673,6 +5679,20 @@ let KRChatbot = class KRChatbot extends i$2 {
5673
5679
  this._pendingInternalFiles = [];
5674
5680
  /** True while a file is being dragged over the panel (shows the drop overlay). */
5675
5681
  this._isDraggingFile = false;
5682
+ /** Whether the `/` skill menu is open above the composer. */
5683
+ this._skillMenuOpened = false;
5684
+ /** Current `/` query (text after the slash), used to filter the skill menu. */
5685
+ this._skillQuery = '';
5686
+ /** Highlighted item in the skill menu for keyboard navigation (-1 = none). */
5687
+ this._skillHighlighted = 0;
5688
+ /** Index of the message whose copy button just flashed "Copied" (-1 = none). */
5689
+ this._copiedIndex = -1;
5690
+ /** Whether the "Browse all skills" dialog is open. */
5691
+ this._skillBrowseOpened = false;
5692
+ /** Active tab in the browse dialog. */
5693
+ this._skillBrowseSource = 'org';
5694
+ /** Search text in the browse dialog. */
5695
+ this._skillBrowseQuery = '';
5676
5696
  this._handleMouseMove = (e) => {
5677
5697
  if (this._isDragging) {
5678
5698
  let newLeft = this._dragStartLeft + (e.clientX - this._dragStartX);
@@ -5786,6 +5806,40 @@ let KRChatbot = class KRChatbot extends i$2 {
5786
5806
  }
5787
5807
  }, 200);
5788
5808
  }
5809
+ // Position the fixed skill menu above the composer, following the krubble
5810
+ // select-field pattern (fixed element positioned from the trigger's rect).
5811
+ if (this._skillMenuOpened) {
5812
+ this._positionSkillMenu();
5813
+ }
5814
+ // Keep the keyboard-highlighted skill scrolled into view.
5815
+ if (changed.has('_skillHighlighted') && this._skillMenuOpened) {
5816
+ this._skillMenuEl
5817
+ ?.querySelector('.skill-menu__item--highlighted')
5818
+ ?.scrollIntoView({ block: 'nearest' });
5819
+ }
5820
+ }
5821
+ /** Anchor the fixed skill menu to the composer, opening upward above it. */
5822
+ _positionSkillMenu() {
5823
+ requestAnimationFrame(() => {
5824
+ if (!this._skillMenuEl || !this._inputWrapperEl) {
5825
+ return;
5826
+ }
5827
+ const inputRect = this._inputWrapperEl.getBoundingClientRect();
5828
+ this._skillMenuEl.style.left = inputRect.left + 'px';
5829
+ this._skillMenuEl.style.width = inputRect.width + 'px';
5830
+ // Bound the menu by the chat panel's own top edge so it stays within the
5831
+ // widget. In floating mode that keeps it inside the card; in inline mode the
5832
+ // panel top sits just below the app header/breadcrumb, so it stays clear of
5833
+ // those too. An 8px gap keeps it off both edges.
5834
+ const topLimit = this._panelEl.getBoundingClientRect().top + 8;
5835
+ const spaceAbove = inputRect.top - 8 - topLimit;
5836
+ this._skillMenuEl.style.maxHeight = spaceAbove + 'px';
5837
+ // Measure after the cap is applied so the upward anchor uses the real
5838
+ // height, then clamp the top edge to the panel bound.
5839
+ const menuRect = this._skillMenuEl.getBoundingClientRect();
5840
+ const top = Math.max(topLimit, inputRect.top - menuRect.height - 8);
5841
+ this._skillMenuEl.style.top = top + 'px';
5842
+ });
5789
5843
  }
5790
5844
  // --- Public methods ---
5791
5845
  /**
@@ -5979,13 +6033,129 @@ let KRChatbot = class KRChatbot extends i$2 {
5979
6033
  const textarea = e.target;
5980
6034
  textarea.style.height = 'auto';
5981
6035
  textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px';
6036
+ // Open the skill menu while the message is a `/` command being typed (a
6037
+ // leading slash with no space yet). The text after the slash filters it.
6038
+ const slashMatch = this._inputValue.match(/^\/(\S*)$/);
6039
+ if (slashMatch && this.skills.length) {
6040
+ this._skillMenuOpened = true;
6041
+ this._skillQuery = slashMatch[1];
6042
+ this._skillHighlighted = 0;
6043
+ }
6044
+ else {
6045
+ this._skillMenuOpened = false;
6046
+ }
5982
6047
  }
5983
6048
  _handleKeyDown(e) {
6049
+ // When the skill menu is open, arrow keys / Enter / Escape drive it. The two
6050
+ // top actions (Attach Files, Browse All Skills) are index 0 and 1, then the
6051
+ // filtered skills follow — all navigable as one list.
6052
+ if (this._skillMenuOpened) {
6053
+ const count = 2 + this._filteredSkills().length;
6054
+ if (e.key === 'ArrowDown') {
6055
+ e.preventDefault();
6056
+ this._skillHighlighted = (this._skillHighlighted + 1) % count;
6057
+ return;
6058
+ }
6059
+ if (e.key === 'ArrowUp') {
6060
+ e.preventDefault();
6061
+ this._skillHighlighted = (this._skillHighlighted - 1 + count) % count;
6062
+ return;
6063
+ }
6064
+ if (e.key === 'Enter' && !e.shiftKey) {
6065
+ e.preventDefault();
6066
+ this._activateSkillMenuItem(this._skillHighlighted);
6067
+ return;
6068
+ }
6069
+ if (e.key === 'Escape') {
6070
+ e.preventDefault();
6071
+ this._skillMenuOpened = false;
6072
+ return;
6073
+ }
6074
+ }
5984
6075
  if (e.key === 'Enter' && !e.shiftKey) {
5985
6076
  e.preventDefault();
5986
6077
  this._handleSubmit();
5987
6078
  }
5988
6079
  }
6080
+ /** Run the skill-menu item at the given flat index (0/1 = actions, then skills). */
6081
+ _activateSkillMenuItem(index) {
6082
+ if (index === 0) {
6083
+ this._openFilePicker();
6084
+ return;
6085
+ }
6086
+ if (index === 1) {
6087
+ this._handleSkillBrowseOpen();
6088
+ return;
6089
+ }
6090
+ const skill = this._filteredSkills()[index - 2];
6091
+ if (skill) {
6092
+ this._handleSkillSelect(skill);
6093
+ }
6094
+ }
6095
+ // --- Skills ---
6096
+ /** Skills matching the current `/` query, capped at the first 15. */
6097
+ _filteredSkills() {
6098
+ const query = this._skillQuery.toLowerCase();
6099
+ return this.skills
6100
+ .filter((skill) => skill.name.toLowerCase().includes(query) ||
6101
+ skill.label.toLowerCase().includes(query))
6102
+ .slice(0, 15);
6103
+ }
6104
+ /** The `+` button toggles the skill/attachment menu open (showing all skills). */
6105
+ _handlePlusClick() {
6106
+ if (this._skillMenuOpened) {
6107
+ this._skillMenuOpened = false;
6108
+ return;
6109
+ }
6110
+ this._skillQuery = '';
6111
+ this._skillHighlighted = 0;
6112
+ this._skillMenuOpened = true;
6113
+ }
6114
+ /** Insert `/name ` into the composer and focus it, ready for the message. */
6115
+ _handleSkillSelect(skill) {
6116
+ this._inputValue = `/${skill.name} `;
6117
+ this._skillMenuOpened = false;
6118
+ this._skillBrowseOpened = false;
6119
+ this._inputEl?.focus();
6120
+ }
6121
+ /** Copy a message's text to the clipboard and briefly flash "Copied". */
6122
+ _handleCopyMessage(content, index) {
6123
+ navigator.clipboard.writeText(content).then(() => {
6124
+ this._copiedIndex = index;
6125
+ setTimeout(() => {
6126
+ if (this._copiedIndex === index) {
6127
+ this._copiedIndex = -1;
6128
+ }
6129
+ }, 2000);
6130
+ }).catch(() => { });
6131
+ }
6132
+ _handleSkillBrowseOpen() {
6133
+ this._skillMenuOpened = false;
6134
+ this._skillBrowseQuery = '';
6135
+ this._skillBrowseOpened = true;
6136
+ }
6137
+ _handleSkillBrowseClose() {
6138
+ this._skillBrowseOpened = false;
6139
+ }
6140
+ _handleSkillBrowseSearch(e) {
6141
+ this._skillBrowseQuery = e.target.value;
6142
+ }
6143
+ _handleSkillBrowseTabChange(e) {
6144
+ this._skillBrowseSource = e.detail.activeTabId;
6145
+ }
6146
+ /** Skills for the browse dialog's active tab, filtered by its search box. */
6147
+ _browseSkills() {
6148
+ const query = this._skillBrowseQuery.toLowerCase();
6149
+ return this.skills.filter((skill) => {
6150
+ const source = skill.source || 'org';
6151
+ if (source !== this._skillBrowseSource) {
6152
+ return false;
6153
+ }
6154
+ return skill.name.toLowerCase().includes(query) ||
6155
+ skill.label.toLowerCase().includes(query) ||
6156
+ (skill.description || '').toLowerCase().includes(query);
6157
+ });
6158
+ }
5989
6159
  // --- Attachments ---
5990
6160
  _openFilePicker() {
5991
6161
  this._fileInputEl?.click();
@@ -6343,6 +6513,119 @@ let KRChatbot = class KRChatbot extends i$2 {
6343
6513
  }
6344
6514
  }
6345
6515
  // --- Render ---
6516
+ /** The `/` skill menu shown above the composer while typing a slash command. */
6517
+ _renderSkillMenu() {
6518
+ const skills = this._filteredSkills();
6519
+ return b `
6520
+ <div class="skill-menu">
6521
+ <div class="skill-menu__actions">
6522
+ <button
6523
+ class=${e({
6524
+ 'skill-menu__action': true,
6525
+ 'skill-menu__item--highlighted': this._skillHighlighted === 0,
6526
+ })}
6527
+ @click=${this._openFilePicker}
6528
+ >
6529
+ Attach Files
6530
+ </button>
6531
+ <button
6532
+ class=${e({
6533
+ 'skill-menu__action': true,
6534
+ 'skill-menu__item--highlighted': this._skillHighlighted === 1,
6535
+ })}
6536
+ @click=${this._handleSkillBrowseOpen}
6537
+ >
6538
+ Browse All Skills
6539
+ </button>
6540
+ </div>
6541
+ ${skills.length ? b `
6542
+ <ul class="skill-menu__list">
6543
+ ${skills.map((skill, index) => b `
6544
+ <li>
6545
+ <button
6546
+ class=${e({
6547
+ 'skill-menu__item': true,
6548
+ 'skill-menu__item--highlighted': index + 2 === this._skillHighlighted,
6549
+ })}
6550
+ @click=${() => this._handleSkillSelect(skill)}
6551
+ >
6552
+ <span class="skill-menu__name">${skill.label}</span>
6553
+ ${skill.description ? b `
6554
+ <span class="skill-menu__desc">${skill.description}</span>
6555
+ ` : A}
6556
+ </button>
6557
+ </li>
6558
+ `)}
6559
+ </ul>
6560
+ ` : b `
6561
+ <div class="skill-menu__empty">No matching skills</div>
6562
+ `}
6563
+ </div>
6564
+ `;
6565
+ }
6566
+ /** The "Browse all skills" dialog — a searchable grid of Org / Kodaris cards. */
6567
+ _renderSkillBrowse() {
6568
+ return b `
6569
+ <kr-dialog
6570
+ opened
6571
+ label="Browse Skills"
6572
+ width="720px"
6573
+ @close=${this._handleSkillBrowseClose}
6574
+ >
6575
+ <div class="skill-browse">
6576
+ <kr-text-field
6577
+ placeholder="Search skills..."
6578
+ .value=${this._skillBrowseQuery}
6579
+ @input=${this._handleSkillBrowseSearch}
6580
+ ></kr-text-field>
6581
+ <kr-tab-group
6582
+ active-tab-id=${this._skillBrowseSource}
6583
+ @tab-change=${this._handleSkillBrowseTabChange}
6584
+ >
6585
+ <kr-tab id="org" label="Your Org">
6586
+ ${this._renderSkillGrid()}
6587
+ </kr-tab>
6588
+ <kr-tab id="kodaris" label="Kodaris">
6589
+ ${this._renderSkillGrid()}
6590
+ </kr-tab>
6591
+ </kr-tab-group>
6592
+ </div>
6593
+ </kr-dialog>
6594
+ `;
6595
+ }
6596
+ /** The card grid for the browse dialog's active tab. */
6597
+ _renderSkillGrid() {
6598
+ const skills = this._browseSkills();
6599
+ if (!skills.length) {
6600
+ return b `<div class="skill-browse__empty">No matching skills</div>`;
6601
+ }
6602
+ return b `
6603
+ <div class="skill-browse__grid">
6604
+ ${skills.map((skill) => b `
6605
+ <button
6606
+ class="skill-card"
6607
+ @click=${() => this._handleSkillSelect(skill)}
6608
+ >
6609
+ <span class="skill-card__name">${skill.label}</span>
6610
+ ${skill.description ? b `
6611
+ <span class="skill-card__desc">${skill.description}</span>
6612
+ ` : A}
6613
+ </button>
6614
+ `)}
6615
+ </div>
6616
+ `;
6617
+ }
6618
+ /**
6619
+ * Render a user message, highlighting a leading `/skill-name` token when it
6620
+ * matches a known skill (like the way Claude highlights slash commands).
6621
+ */
6622
+ _renderUserContent(content) {
6623
+ const match = content.match(/^\/(\S+)(\s[\s\S]*)?$/);
6624
+ if (match && this.skills.some((skill) => skill.name === match[1])) {
6625
+ return b `<span class="message-skill">/${match[1]}</span>${match[2] || ''}`;
6626
+ }
6627
+ return content;
6628
+ }
6346
6629
  render() {
6347
6630
  return b `
6348
6631
  ${(this.hideToggle || this.variant === 'inline') ? A : b `
@@ -6499,7 +6782,7 @@ let KRChatbot = class KRChatbot extends i$2 {
6499
6782
  <span class="empty-text">What can I help you with?</span>
6500
6783
  </div>
6501
6784
  ` : A}
6502
- ${this.messages.map((msg) => b `
6785
+ ${this.messages.map((msg, index) => b `
6503
6786
  <div class=${e({
6504
6787
  'message': true,
6505
6788
  'message--user': msg.type === 'user',
@@ -6531,7 +6814,7 @@ let KRChatbot = class KRChatbot extends i$2 {
6531
6814
  ${msg.content ? b `
6532
6815
  <div class="message-content">
6533
6816
  ${msg.type === 'user'
6534
- ? msg.content
6817
+ ? this._renderUserContent(msg.content)
6535
6818
  : o(marked.parse(msg.content, { breaks: true }))}
6536
6819
  </div>
6537
6820
  ` : A}
@@ -6542,6 +6825,46 @@ let KRChatbot = class KRChatbot extends i$2 {
6542
6825
  <span></span>
6543
6826
  </div>
6544
6827
  ` : A}
6828
+ ${msg.content ? b `
6829
+ <button
6830
+ class="message-copy"
6831
+ title="Copy"
6832
+ @click=${() => this._handleCopyMessage(msg.content, index)}
6833
+ >
6834
+ ${this._copiedIndex === index ? b `
6835
+ <svg
6836
+ viewBox="0 0 24 24"
6837
+ fill="none"
6838
+ stroke="currentColor"
6839
+ stroke-width="1.5"
6840
+ stroke-linecap="round"
6841
+ stroke-linejoin="round"
6842
+ >
6843
+ <path d="M20 6 9 17l-5-5" />
6844
+ </svg>
6845
+ Copied
6846
+ ` : b `
6847
+ <svg
6848
+ viewBox="0 0 24 24"
6849
+ fill="none"
6850
+ stroke="currentColor"
6851
+ stroke-width="1.5"
6852
+ stroke-linecap="round"
6853
+ stroke-linejoin="round"
6854
+ >
6855
+ <rect
6856
+ x="9"
6857
+ y="9"
6858
+ width="13"
6859
+ height="13"
6860
+ rx="2"
6861
+ />
6862
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
6863
+ </svg>
6864
+ Copy
6865
+ `}
6866
+ </button>
6867
+ ` : A}
6545
6868
  </div>
6546
6869
  `)}
6547
6870
  </div>
@@ -6606,11 +6929,11 @@ let KRChatbot = class KRChatbot extends i$2 {
6606
6929
  hidden
6607
6930
  @change=${this._handleFileInputChange}
6608
6931
  />
6932
+ ${this._skillMenuOpened ? this._renderSkillMenu() : A}
6609
6933
  <div class="input-wrapper">
6610
6934
  <button
6611
6935
  class="input-plus"
6612
- title="Attach image"
6613
- @click=${this._openFilePicker}
6936
+ @click=${this._handlePlusClick}
6614
6937
  >+</button>
6615
6938
  <textarea
6616
6939
  class="input"
@@ -6667,6 +6990,7 @@ let KRChatbot = class KRChatbot extends i$2 {
6667
6990
  `}
6668
6991
  </div>
6669
6992
  </div>
6993
+ ${this._skillBrowseOpened ? this._renderSkillBrowse() : A}
6670
6994
  </div>
6671
6995
  `;
6672
6996
  }
@@ -6906,6 +7230,7 @@ KRChatbot.styles = i$5 `
6906
7230
  }
6907
7231
 
6908
7232
  .message {
7233
+ position: relative;
6909
7234
  display: flex;
6910
7235
  flex-direction: column;
6911
7236
  max-width: 100%;
@@ -6950,6 +7275,58 @@ KRChatbot.styles = i$5 `
6950
7275
  font-size: 14px;
6951
7276
  }
6952
7277
 
7278
+ /* Highlighted leading /skill token in a sent user message. Monospace + a
7279
+ stronger color makes it read as a distinct command without a pill. */
7280
+ .message-skill {
7281
+ font-family: 'SF Mono', SFMono-Regular, ui-monospace, Menlo, Consolas, monospace;
7282
+ font-weight: 600;
7283
+ color: #b45309;
7284
+ }
7285
+
7286
+ /* Per-message copy button. Kept in flow with a fixed height that is always
7287
+ reserved (via a negative bottom margin that cancels it) so message spacing
7288
+ is identical whether or not it's shown; only its opacity toggles on hover. */
7289
+ .message-copy {
7290
+ align-self: flex-start;
7291
+ margin-top: 4px;
7292
+ margin-bottom: -27px;
7293
+ display: inline-flex;
7294
+ align-items: center;
7295
+ gap: 4px;
7296
+ height: 23px;
7297
+ padding: 4px 8px;
7298
+ background: none;
7299
+ border: none;
7300
+ border-radius: 6px;
7301
+ font: inherit;
7302
+ font-size: 12px;
7303
+ color: #64668b;
7304
+ cursor: pointer;
7305
+ opacity: 0;
7306
+ pointer-events: none;
7307
+ transition: opacity 0.15s, background-color 0.15s, color 0.15s;
7308
+ }
7309
+
7310
+ .message--user .message-copy {
7311
+ align-self: flex-end;
7312
+ }
7313
+
7314
+ .message:hover .message-copy,
7315
+ .message-copy:focus-visible {
7316
+ opacity: 1;
7317
+ pointer-events: auto;
7318
+ }
7319
+
7320
+ .message-copy:hover {
7321
+ background: rgba(0, 0, 0, 0.06);
7322
+ color: #1a1a2e;
7323
+ }
7324
+
7325
+ .message-copy svg {
7326
+ width: 14px;
7327
+ height: 14px;
7328
+ }
7329
+
6953
7330
  .message--assistant .message-content {
6954
7331
  background: transparent;
6955
7332
  color: #000000;
@@ -7285,6 +7662,176 @@ KRChatbot.styles = i$5 `
7285
7662
  display: block;
7286
7663
  }
7287
7664
 
7665
+ /* Skill menu (opened by typing '/'). Fixed + JS-positioned above the
7666
+ composer, following the krubble select-field dropdown pattern. */
7667
+ .skill-menu {
7668
+ position: fixed;
7669
+ z-index: 10000;
7670
+ background: white;
7671
+ border: 1px solid #9ba7b6;
7672
+ border-radius: 8px;
7673
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
7674
+ overflow: hidden;
7675
+ display: flex;
7676
+ flex-direction: column;
7677
+ /* max-height is set dynamically to the space above the composer. */
7678
+ }
7679
+
7680
+ /* Floating mode is a compact card — use a lighter shadow and softer border. */
7681
+ :host(:not([variant='inline'])) .skill-menu {
7682
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
7683
+ border-color: #b0b0b0;
7684
+ }
7685
+
7686
+ .skill-menu__list {
7687
+ list-style: none;
7688
+ margin: 0;
7689
+ padding: 8px 0;
7690
+ overflow-y: auto;
7691
+ }
7692
+
7693
+ .skill-menu__item {
7694
+ display: flex;
7695
+ flex-direction: column;
7696
+ gap: 2px;
7697
+ width: 100%;
7698
+ padding: 8px 16px;
7699
+ background: none;
7700
+ border: none;
7701
+ text-align: left;
7702
+ cursor: pointer;
7703
+ font: inherit;
7704
+ color: #000000;
7705
+ }
7706
+
7707
+
7708
+ /* Floating mode is a compact card — tighten name/description spacing. */
7709
+ :host(:not([variant='inline'])) .skill-menu__item {
7710
+ gap: 1px;
7711
+ }
7712
+
7713
+ .skill-menu__name {
7714
+ font-weight: 500;
7715
+ }
7716
+
7717
+ /* Floating mode is a compact card — shrink the skill name a touch. */
7718
+ :host(:not([variant='inline'])) .skill-menu__name {
7719
+ font-size: 13px;
7720
+ }
7721
+
7722
+ .skill-menu__desc {
7723
+ font-size: 12px;
7724
+ color: #000000;
7725
+ overflow: hidden;
7726
+ text-overflow: ellipsis;
7727
+ white-space: nowrap;
7728
+ }
7729
+
7730
+ .skill-menu__empty {
7731
+ padding: 16px;
7732
+ text-align: center;
7733
+ color: #000000;
7734
+ }
7735
+
7736
+ .skill-menu__actions {
7737
+ display: flex;
7738
+ flex-direction: column;
7739
+ padding: 8px 0;
7740
+ border-bottom: 1px solid #eee;
7741
+ flex-shrink: 0;
7742
+ }
7743
+
7744
+ .skill-menu__action {
7745
+ width: 100%;
7746
+ padding: 6px 16px;
7747
+ background: transparent;
7748
+ border: none;
7749
+ text-align: left;
7750
+ cursor: pointer;
7751
+ font: inherit;
7752
+ font-size: 13px;
7753
+ color: var(--kr-chatbot-primary);
7754
+ font-weight: 500;
7755
+ }
7756
+
7757
+ /* Highlight (keyboard) and hover (mouse) are independent — either shades the
7758
+ row. Declared after the base rules so it wins the cascade for both the
7759
+ action buttons and the skill items. */
7760
+ .skill-menu__action.skill-menu__item--highlighted,
7761
+ .skill-menu__item.skill-menu__item--highlighted,
7762
+ .skill-menu__action:hover,
7763
+ .skill-menu__item:hover {
7764
+ background: #f3f4f6;
7765
+ }
7766
+
7767
+ /* Browse-all dialog body. Owns the height so the dialog is the same size on
7768
+ every tab; the search is fixed and the card grid flexes/scrolls to fill. */
7769
+ .skill-browse {
7770
+ display: flex;
7771
+ flex-direction: column;
7772
+ height: 70vh;
7773
+ }
7774
+
7775
+ .skill-browse kr-text-field {
7776
+ display: block;
7777
+ margin: 20px 20px 16px;
7778
+ flex-shrink: 0;
7779
+ }
7780
+
7781
+ .skill-browse kr-tab-group {
7782
+ flex: 1;
7783
+ min-height: 0;
7784
+ }
7785
+
7786
+ /* The grid is the scroller and spans the full width, so its scrollbar sits
7787
+ flush against the dialog edge; padding-right insets the cards from it. */
7788
+ .skill-browse__grid {
7789
+ height: 100%;
7790
+ overflow-y: auto;
7791
+ display: grid;
7792
+ grid-template-columns: repeat(3, 1fr);
7793
+ grid-auto-rows: min-content;
7794
+ align-content: start;
7795
+ gap: 12px;
7796
+ padding: 16px 20px 20px;
7797
+ }
7798
+
7799
+ .skill-card {
7800
+ display: flex;
7801
+ flex-direction: column;
7802
+ gap: 6px;
7803
+ padding: 14px;
7804
+ background: white;
7805
+ border: 1px solid #e5e7eb;
7806
+ border-radius: 10px;
7807
+ text-align: left;
7808
+ cursor: pointer;
7809
+ font: inherit;
7810
+ color: #000000;
7811
+ transition: border-color 0.15s ease, box-shadow 0.15s ease;
7812
+ }
7813
+
7814
+ .skill-card:hover {
7815
+ border-color: var(--kr-chatbot-primary);
7816
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
7817
+ }
7818
+
7819
+ .skill-card__name {
7820
+ font-weight: 600;
7821
+ }
7822
+
7823
+ .skill-card__desc {
7824
+ font-size: 12px;
7825
+ color: #000000;
7826
+ }
7827
+
7828
+ .skill-browse__empty {
7829
+ height: 100%;
7830
+ padding: 32px;
7831
+ text-align: center;
7832
+ color: #000000;
7833
+ }
7834
+
7288
7835
  /* Composer attachment chips (pre-send) */
7289
7836
  .composer-attachments {
7290
7837
  display: flex;
@@ -7384,6 +7931,9 @@ __decorate([
7384
7931
  __decorate([
7385
7932
  n({ type: String })
7386
7933
  ], KRChatbot.prototype, "placeholder", void 0);
7934
+ __decorate([
7935
+ n({ attribute: false })
7936
+ ], KRChatbot.prototype, "skills", void 0);
7387
7937
  __decorate([
7388
7938
  n({
7389
7939
  type: Boolean,
@@ -7418,6 +7968,27 @@ __decorate([
7418
7968
  __decorate([
7419
7969
  r()
7420
7970
  ], KRChatbot.prototype, "_isDraggingFile", void 0);
7971
+ __decorate([
7972
+ r()
7973
+ ], KRChatbot.prototype, "_skillMenuOpened", void 0);
7974
+ __decorate([
7975
+ r()
7976
+ ], KRChatbot.prototype, "_skillQuery", void 0);
7977
+ __decorate([
7978
+ r()
7979
+ ], KRChatbot.prototype, "_skillHighlighted", void 0);
7980
+ __decorate([
7981
+ r()
7982
+ ], KRChatbot.prototype, "_copiedIndex", void 0);
7983
+ __decorate([
7984
+ r()
7985
+ ], KRChatbot.prototype, "_skillBrowseOpened", void 0);
7986
+ __decorate([
7987
+ r()
7988
+ ], KRChatbot.prototype, "_skillBrowseSource", void 0);
7989
+ __decorate([
7990
+ r()
7991
+ ], KRChatbot.prototype, "_skillBrowseQuery", void 0);
7421
7992
  __decorate([
7422
7993
  e$3('.panel')
7423
7994
  ], KRChatbot.prototype, "_panelEl", void 0);
@@ -7430,6 +8001,12 @@ __decorate([
7430
8001
  __decorate([
7431
8002
  e$3('.file-input')
7432
8003
  ], KRChatbot.prototype, "_fileInputEl", void 0);
8004
+ __decorate([
8005
+ e$3('.input-wrapper')
8006
+ ], KRChatbot.prototype, "_inputWrapperEl", void 0);
8007
+ __decorate([
8008
+ e$3('.skill-menu')
8009
+ ], KRChatbot.prototype, "_skillMenuEl", void 0);
7433
8010
  KRChatbot = __decorate([
7434
8011
  t$2('kr-chatbot')
7435
8012
  ], KRChatbot);