@aparte/core 0.12.1 → 0.13.1

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.
Files changed (97) hide show
  1. package/dist/client/aparte-client.d.ts +25 -0
  2. package/dist/client/aparte-client.d.ts.map +1 -1
  3. package/dist/client/stream-adapter.d.ts.map +1 -1
  4. package/dist/components/bubble/aparte-chat-bubble.d.ts +117 -0
  5. package/dist/components/bubble/aparte-chat-bubble.d.ts.map +1 -1
  6. package/dist/components/chat/aparte-chat.d.ts +70 -15
  7. package/dist/components/chat/aparte-chat.d.ts.map +1 -1
  8. package/dist/components/composer/aparte-composer-action.d.ts +48 -7
  9. package/dist/components/composer/aparte-composer-action.d.ts.map +1 -1
  10. package/dist/components/composer/aparte-composer-add-attachment.d.ts +37 -4
  11. package/dist/components/composer/aparte-composer-add-attachment.d.ts.map +1 -1
  12. package/dist/components/composer/aparte-composer-attachments.d.ts +35 -3
  13. package/dist/components/composer/aparte-composer-attachments.d.ts.map +1 -1
  14. package/dist/components/composer/aparte-composer-cancel.d.ts +25 -2
  15. package/dist/components/composer/aparte-composer-cancel.d.ts.map +1 -1
  16. package/dist/components/composer/aparte-composer-input.d.ts +54 -5
  17. package/dist/components/composer/aparte-composer-input.d.ts.map +1 -1
  18. package/dist/components/composer/aparte-composer-send.d.ts +47 -4
  19. package/dist/components/composer/aparte-composer-send.d.ts.map +1 -1
  20. package/dist/components/composer/aparte-composer-toolbar.d.ts +15 -4
  21. package/dist/components/composer/aparte-composer-toolbar.d.ts.map +1 -1
  22. package/dist/components/composer/aparte-composer.d.ts +139 -7
  23. package/dist/components/composer/aparte-composer.d.ts.map +1 -1
  24. package/dist/components/composer/index.d.ts +1 -1
  25. package/dist/components/composer/index.d.ts.map +1 -1
  26. package/dist/components/conversation-list/aparte-conversation-list.d.ts +50 -8
  27. package/dist/components/conversation-list/aparte-conversation-list.d.ts.map +1 -1
  28. package/dist/components/elicitation/aparte-elicitation.d.ts +45 -0
  29. package/dist/components/elicitation/aparte-elicitation.d.ts.map +1 -1
  30. package/dist/components/index.d.ts +1 -1
  31. package/dist/components/index.d.ts.map +1 -1
  32. package/dist/components/status/aparte-chat-status.d.ts +47 -5
  33. package/dist/components/status/aparte-chat-status.d.ts.map +1 -1
  34. package/dist/components/viewport/aparte-chat-viewport.d.ts +80 -8
  35. package/dist/components/viewport/aparte-chat-viewport.d.ts.map +1 -1
  36. package/dist/config/aparte-config.d.ts +22 -0
  37. package/dist/config/aparte-config.d.ts.map +1 -1
  38. package/dist/config/icon-provider.d.ts +17 -15
  39. package/dist/config/icon-provider.d.ts.map +1 -1
  40. package/dist/custom-elements.json +7372 -5219
  41. package/dist/elicitation/approval-panel.d.ts.map +1 -1
  42. package/dist/elicitation/panel.d.ts +23 -2
  43. package/dist/elicitation/panel.d.ts.map +1 -1
  44. package/dist/generated/element-attributes.d.ts +21 -15
  45. package/dist/generated/element-attributes.d.ts.map +1 -1
  46. package/dist/glyphs-QXfbL3n_.js +81 -0
  47. package/dist/glyphs-QXfbL3n_.js.map +1 -0
  48. package/dist/icons/extended.d.ts +153 -0
  49. package/dist/icons/extended.d.ts.map +1 -0
  50. package/dist/icons/glyphs.d.ts +98 -0
  51. package/dist/icons/glyphs.d.ts.map +1 -0
  52. package/dist/icons/index.d.ts +24 -0
  53. package/dist/icons/index.d.ts.map +1 -0
  54. package/dist/icons.d.ts +14 -0
  55. package/dist/icons.d.ts.map +1 -0
  56. package/dist/icons.js +157 -0
  57. package/dist/icons.js.map +1 -0
  58. package/dist/index.css +5109 -2121
  59. package/dist/index.d.ts +37 -3
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +184 -68
  62. package/dist/index.js.map +1 -1
  63. package/dist/index.node.d.ts +2 -2
  64. package/dist/index.node.d.ts.map +1 -1
  65. package/dist/index.node.js +1 -1
  66. package/dist/index.node.js.map +1 -1
  67. package/dist/{is-awaiting-reply-BWTDvQ__.js → is-awaiting-reply-CWp1ffHh.js} +347 -171
  68. package/dist/is-awaiting-reply-CWp1ffHh.js.map +1 -0
  69. package/dist/primitives/icon/aparte-icon.d.ts +49 -0
  70. package/dist/primitives/icon/aparte-icon.d.ts.map +1 -0
  71. package/dist/primitives/icon/index.d.ts +2 -0
  72. package/dist/primitives/icon/index.d.ts.map +1 -0
  73. package/dist/primitives/index.d.ts +1 -0
  74. package/dist/primitives/index.d.ts.map +1 -1
  75. package/dist/primitives/progress-spinner/aparte-progress-spinner.d.ts +20 -5
  76. package/dist/primitives/progress-spinner/aparte-progress-spinner.d.ts.map +1 -1
  77. package/dist/primitives/select/aparte-optgroup.d.ts +30 -7
  78. package/dist/primitives/select/aparte-optgroup.d.ts.map +1 -1
  79. package/dist/primitives/select/aparte-option.d.ts +29 -3
  80. package/dist/primitives/select/aparte-option.d.ts.map +1 -1
  81. package/dist/primitives/select/aparte-select.d.ts +36 -11
  82. package/dist/primitives/select/aparte-select.d.ts.map +1 -1
  83. package/dist/renderers/segment-renderers.d.ts +18 -0
  84. package/dist/renderers/segment-renderers.d.ts.map +1 -1
  85. package/dist/renderers/segments/artifact/card.d.ts.map +1 -1
  86. package/dist/renderers/segments/thinking.d.ts.map +1 -1
  87. package/dist/renderers/segments/tool-call.d.ts.map +1 -1
  88. package/dist/types/element-map.d.ts +2 -0
  89. package/dist/types/element-map.d.ts.map +1 -1
  90. package/dist/types/index.d.ts +1 -1
  91. package/dist/types/index.d.ts.map +1 -1
  92. package/dist/types/segments.d.ts +92 -7
  93. package/dist/types/segments.d.ts.map +1 -1
  94. package/dist/types/tools.d.ts +2 -0
  95. package/dist/types/tools.d.ts.map +1 -1
  96. package/package.json +7 -1
  97. package/dist/is-awaiting-reply-BWTDvQ__.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { a4 as subscribeConfigChange, $ as resolveConfig, a1 as runWithConfig, p as AparteElicitationAbortError, x as buildApprovalPanel, y as buildElicitationPanel, K as escapeAttr, F as cssEscape, a7 as mergeSegmentUpdate, L as escapeHtml, O as getSegmentRenderer, P as installDefaultRenderersOnce, s as AparteMessageRepository, a8 as stampSegmentActivity, a9 as stampSegmentOnInsert, aa as stampSegmentOnUpdate, ab as renumberSegments, ac as openSegmentIds, ad as isTerminalStatus, ae as adoptMessageSegments, Q as isAwaitingReply, W as populateBubbleFromMessage, a6 as uuid, a0 as revokeAttachmentUrls } from "./is-awaiting-reply-BWTDvQ__.js";
2
- import { A, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, q, r, t, u, v, w, z, B, C, D, E, G, H, I, J, M, N, R, S, T, U, V, X, Y, Z, _, a2, a3, a5 } from "./is-awaiting-reply-BWTDvQ__.js";
1
+ import { $ as resolveConfig, a4 as subscribeConfigChange, a1 as runWithConfig, p as AparteElicitationAbortError, x as buildApprovalPanel, y as buildElicitationPanel, K as escapeAttr, F as cssEscape, a7 as mergeSegmentUpdate, L as escapeHtml, a8 as writeStreamedMarkdown, O as getSegmentRenderer, P as installDefaultRenderersOnce, s as AparteMessageRepository, a9 as stampSegmentActivity, aa as stampSegmentOnInsert, ab as stampSegmentOnUpdate, ac as renumberSegments, ad as openSegmentIds, ae as isTerminalStatus, af as adoptMessageSegments, Q as isAwaitingReply, W as populateBubbleFromMessage, a6 as uuid, a0 as revokeAttachmentUrls } from "./is-awaiting-reply-CWp1ffHh.js";
2
+ import { A, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, q, r, t, u, v, w, z, B, C, D, E, G, H, I, J, M, N, R, S, T, U, V, X, Y, Z, _, a2, a3, a5 } from "./is-awaiting-reply-CWp1ffHh.js";
3
3
  class AparteOption extends HTMLElement {
4
4
  static get observedAttributes() {
5
5
  return ["value", "disabled", "selected", "data-status"];
@@ -132,6 +132,7 @@ class AparteOptgroup extends HTMLElement {
132
132
  if (this.collapsible) {
133
133
  const chevron = document.createElement("span");
134
134
  chevron.className = "aparte-optgroup-chevron";
135
+ chevron.innerHTML = resolveConfig(this).getIcon("expand");
135
136
  header.appendChild(chevron);
136
137
  header.style.cursor = "pointer";
137
138
  header.addEventListener("click", (e2) => {
@@ -204,8 +205,7 @@ class AparteSelect extends HTMLElement {
204
205
  * document-level handlers and could not touch this one.
205
206
  *
206
207
  * This is verbatim the bug class `aparte-chat-viewport` documents having fixed
207
- * for its own listeners; a repo sweep found this as the last inline-arrow
208
- * listener on a persistent node.
208
+ * for its own listeners.
209
209
  */
210
210
  _boundHandleOptionClick = this._handleOptionClick.bind(this);
211
211
  _boundHandleDocumentClick = this._handleDocumentClick.bind(this);
@@ -289,7 +289,7 @@ class AparteSelect extends HTMLElement {
289
289
  labelSpan.textContent = placeholder;
290
290
  const chevronSpan = document.createElement("span");
291
291
  chevronSpan.className = "aparte-select-chevron";
292
- chevronSpan.innerHTML = `<svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor"><path d="M2.5 4.5L6 8L9.5 4.5" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round"/></svg>`;
292
+ chevronSpan.innerHTML = resolveConfig(this).getIcon("expand");
293
293
  trigger.append(labelSpan, chevronSpan);
294
294
  const dropdown = document.createElement("div");
295
295
  dropdown.className = "aparte-select-dropdown";
@@ -631,6 +631,32 @@ class AparteProgressSpinner extends HTMLElement {
631
631
  if (!customElements.get("aparte-progress-spinner")) {
632
632
  customElements.define("aparte-progress-spinner", AparteProgressSpinner);
633
633
  }
634
+ class AparteIcon extends HTMLElement {
635
+ static get observedAttributes() {
636
+ return ["name"];
637
+ }
638
+ _unsubscribe = null;
639
+ connectedCallback() {
640
+ this._render();
641
+ this._unsubscribe = subscribeConfigChange(this, () => this._render());
642
+ }
643
+ disconnectedCallback() {
644
+ this._unsubscribe?.();
645
+ this._unsubscribe = null;
646
+ }
647
+ attributeChangedCallback() {
648
+ if (this.isConnected) this._render();
649
+ }
650
+ _render() {
651
+ const name = this.getAttribute("name");
652
+ const glyph = name ? resolveConfig(this).getIconProvider()[name]?.() : null;
653
+ this.innerHTML = glyph ?? "";
654
+ this.firstElementChild?.setAttribute("aria-hidden", "true");
655
+ }
656
+ }
657
+ if (!customElements.get("aparte-icon")) {
658
+ customElements.define("aparte-icon", AparteIcon);
659
+ }
634
660
  class AparteElicitation extends HTMLElement {
635
661
  _pending = null;
636
662
  /**
@@ -705,11 +731,17 @@ class AparteElicitation extends HTMLElement {
705
731
  return new Promise((resolve, reject) => {
706
732
  let done = false;
707
733
  const slot = {};
734
+ const focusedBefore = document.activeElement;
708
735
  const close = () => {
709
736
  if (done) return false;
710
737
  done = true;
711
738
  this._pending = null;
739
+ const active = document.activeElement;
740
+ const focusWasInPanel = active instanceof Node && composer.contains(active);
712
741
  composer.hidePanel(slot.token);
742
+ if (focusWasInPanel && focusedBefore instanceof HTMLElement && focusedBefore.isConnected) {
743
+ focusedBefore.focus();
744
+ }
713
745
  return true;
714
746
  };
715
747
  const settle = (result) => {
@@ -727,17 +759,15 @@ class AparteElicitation extends HTMLElement {
727
759
  }
728
760
  const cfg = resolveConfig(this);
729
761
  if (request.kind === "approval") {
762
+ const approvalMode = () => panel2.isComplete() ? "submit" : "none";
730
763
  const panel2 = runWithConfig(cfg, () => buildApprovalPanel(request.message, request.options ?? [], () => {
731
- composer.setPanelSubmitEnabled(panel2.isComplete(), "submit");
764
+ composer.setPanelSubmitEnabled(panel2.isComplete(), approvalMode());
732
765
  }));
733
766
  panel2.onSettle((answer) => settle({ action: "accept", content: answer }));
734
767
  this._pending = { abort: () => fail(), composer, relabel: () => panel2.relabel() };
735
768
  slot.token = composer.showPanel(panel2.el, {
736
769
  submitEnabled: panel2.isComplete(),
737
- mode: "submit",
738
- // The composer's button carries the INSTRUCTION, which is written
739
- // text — exactly the act that button already means. The options
740
- // never route through it: a decision is its own click.
770
+ mode: approvalMode(),
741
771
  onSubmit: () => {
742
772
  if (panel2.isComplete()) settle({ action: "accept", content: panel2.getContent() });
743
773
  },
@@ -752,10 +782,11 @@ class AparteElicitation extends HTMLElement {
752
782
  }));
753
783
  const skip = document.createElement("button");
754
784
  skip.type = "button";
755
- skip.className = "aparte-elic-skip";
785
+ skip.className = "aparte-btn aparte-elic-skip";
756
786
  skip.textContent = cfg.t("elicitationSkip");
757
787
  skip.addEventListener("click", () => settle({ action: "decline" }));
758
788
  panel.dismiss.appendChild(skip);
789
+ panel.onSettle((content) => settle({ action: "accept", content }));
759
790
  this._pending = {
760
791
  abort: () => fail(),
761
792
  composer,
@@ -968,6 +999,14 @@ function warnMissingRenderer(type) {
968
999
  _warnedNoRenderer = true;
969
1000
  console.warn(`[aparte] No renderer for segment "${type}". Register one with registerSegmentRenderer({ type: '${type}', render }) from @aparte/core — see https://apartejs.dev/guides/customization/#custom-segment-types`);
970
1001
  }
1002
+ function unrenderedSegment(segment) {
1003
+ const fallback = typeof segment.fallback === "string" && segment.fallback.trim() ? segment.fallback : null;
1004
+ if (!fallback) warnMissingRenderer(segment.type);
1005
+ const el = document.createElement("div");
1006
+ el.className = fallback ? "aparte-segment aparte-segment-fallback" : "aparte-segment aparte-segment-unknown";
1007
+ el.textContent = fallback ?? `[Unknown segment type: ${segment.type}]`;
1008
+ return el;
1009
+ }
971
1010
  function resolveSegmentRenderer(type, config) {
972
1011
  const renderer = getSegmentRenderer(type, config);
973
1012
  if (renderer) return renderer;
@@ -980,7 +1019,6 @@ function segmentRenderResultToElement(result) {
980
1019
  wrapper.innerHTML = result;
981
1020
  return wrapper.firstElementChild;
982
1021
  }
983
- const INFO_ICON_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>';
984
1022
  class AparteChatBubble extends HTMLElement {
985
1023
  _contentEl = null;
986
1024
  _segmentsEl = null;
@@ -1106,6 +1144,7 @@ class AparteChatBubble extends HTMLElement {
1106
1144
  break;
1107
1145
  case "content":
1108
1146
  this._content = newValue || "";
1147
+ this._resetMarkdownStream();
1109
1148
  this._updateContent();
1110
1149
  break;
1111
1150
  case "timestamp":
@@ -1131,8 +1170,20 @@ class AparteChatBubble extends HTMLElement {
1131
1170
  setContent(content) {
1132
1171
  this._content = content;
1133
1172
  this.setAttribute("content", content);
1173
+ this._resetMarkdownStream();
1134
1174
  this._updateContent();
1135
1175
  }
1176
+ /**
1177
+ * Drop the incremental Markdown parser's state.
1178
+ *
1179
+ * Only needed where `_content` is REPLACED rather than grown. `appendToken` grows it, so
1180
+ * the parser's cursor stays valid there — which is the whole point of the seam.
1181
+ */
1182
+ _resetMarkdownStream() {
1183
+ const host = this;
1184
+ if (host._aparteSmd) host._aparteSmd.renderer.end();
1185
+ host._aparteSmd = void 0;
1186
+ }
1136
1187
  /** Get current content */
1137
1188
  getContent() {
1138
1189
  return this._content;
@@ -1267,11 +1318,7 @@ class AparteChatBubble extends HTMLElement {
1267
1318
  runWithConfig(this._cfg, () => renderer.setup?.(el, segment));
1268
1319
  }
1269
1320
  } else {
1270
- warnMissingRenderer(segment.type);
1271
- const fallback = document.createElement("div");
1272
- fallback.className = "aparte-segment aparte-segment-unknown";
1273
- fallback.textContent = `[Unknown segment type: ${segment.type}]`;
1274
- this._segmentsEl.appendChild(fallback);
1321
+ this._segmentsEl.appendChild(unrenderedSegment(segment));
1275
1322
  }
1276
1323
  if (this._contentEl) this._contentEl.style.display = "none";
1277
1324
  this._reflectError();
@@ -1350,9 +1397,16 @@ class AparteChatBubble extends HTMLElement {
1350
1397
  </div>
1351
1398
  <div class="aparte-footer">
1352
1399
  <div class="aparte-branch-picker" hidden>
1353
- <button class="aparte-branch-prev" aria-label="${escapeAttr(this._cfg.getLocale().previousResponse ?? "Previous response")}">&#8249;</button>
1400
+ <button class="aparte-btn aparte-btn--icon aparte-btn--sm aparte-branch-prev" aria-label="${escapeAttr(this._cfg.getLocale().previousResponse ?? "Previous response")}">&#8249;</button>
1354
1401
  <span class="aparte-branch-label">1 / 1</span>
1355
- <button class="aparte-branch-next" aria-label="${escapeAttr(this._cfg.getLocale().nextResponse ?? "Next response")}">&#8250;</button>
1402
+ <!-- The move has to be ANNOUNCED. Pressing the arrows deliberately does not
1403
+ take focus, so without a live region a screen-reader user gets the new
1404
+ branch and no indication anything changed. The visible label cannot be
1405
+ the region itself: a custom sibling-nav renderer may replace it with
1406
+ dots, which reads as nothing. No new locale key — the position is
1407
+ digits, and the buttons beside it already carry translated labels. -->
1408
+ <span class="aparte-sr-only aparte-branch-status" aria-live="polite"></span>
1409
+ <button class="aparte-btn aparte-btn--icon aparte-btn--sm aparte-branch-next" aria-label="${escapeAttr(this._cfg.getLocale().nextResponse ?? "Next response")}">&#8250;</button>
1356
1410
  </div>
1357
1411
  <div class="aparte-action-bar" role="toolbar" aria-label="${escapeAttr(this._cfg.getLocale().messageActions ?? "Message actions")}"></div>
1358
1412
  </div>
@@ -1453,7 +1507,10 @@ class AparteChatBubble extends HTMLElement {
1453
1507
  return;
1454
1508
  }
1455
1509
  this._contentEl.style.display = "";
1456
- this._contentEl.innerHTML = this._cfg.renderMarkdown(this._content);
1510
+ runWithConfig(
1511
+ this._cfg,
1512
+ () => writeStreamedMarkdown(this, this._contentEl, this._content, this._streaming)
1513
+ );
1457
1514
  this._updateWaiting();
1458
1515
  if (!this._streaming) this._highlightContentCode();
1459
1516
  }
@@ -1496,11 +1553,7 @@ class AparteChatBubble extends HTMLElement {
1496
1553
  runWithConfig(this._cfg, () => renderer.setup?.(el, segment));
1497
1554
  }
1498
1555
  } else {
1499
- warnMissingRenderer(segment.type);
1500
- const fallback = document.createElement("div");
1501
- fallback.className = "aparte-segment aparte-segment-unknown";
1502
- fallback.textContent = `[Unknown segment type: ${segment.type}]`;
1503
- this._segmentsEl.appendChild(fallback);
1556
+ this._segmentsEl.appendChild(unrenderedSegment(segment));
1504
1557
  }
1505
1558
  }
1506
1559
  if (this._contentEl) {
@@ -1649,6 +1702,11 @@ class AparteChatBubble extends HTMLElement {
1649
1702
  label.textContent = `${this._siblingIndex + 1} / ${this._siblingCount}`;
1650
1703
  }
1651
1704
  }
1705
+ const status = this._branchPickerEl.querySelector(".aparte-branch-status");
1706
+ if (status) {
1707
+ const position = `${this._siblingIndex + 1} / ${this._siblingCount}`;
1708
+ if (status.textContent !== position) status.textContent = position;
1709
+ }
1652
1710
  const prevBtn = this._branchPickerEl.querySelector(".aparte-branch-prev");
1653
1711
  const nextBtn = this._branchPickerEl.querySelector(".aparte-branch-next");
1654
1712
  if (prevBtn) prevBtn.disabled = this._siblingIndex === 0;
@@ -1715,7 +1773,7 @@ class AparteChatBubble extends HTMLElement {
1715
1773
  const roles = a4.bubble?.roles ?? ["user", "assistant"];
1716
1774
  if (!roles.includes(this._role)) continue;
1717
1775
  const btn = document.createElement("button");
1718
- btn.className = "aparte-action-btn aparte-action-custom";
1776
+ btn.className = "aparte-btn aparte-btn--icon aparte-action-btn aparte-action-custom";
1719
1777
  btn.dataset["action"] = `custom:${a4.id}`;
1720
1778
  btn.setAttribute("aria-label", a4.label);
1721
1779
  btn.setAttribute("title", a4.label);
@@ -1729,28 +1787,28 @@ class AparteChatBubble extends HTMLElement {
1729
1787
  switch (action) {
1730
1788
  case "copy": {
1731
1789
  const l2 = locale.copy ?? "Copy";
1732
- return `<button class="aparte-action-btn aparte-action-copy" data-action="copy" aria-label="${escapeAttr(l2)}" title="${escapeAttr(l2)}">${icons.copy()}</button>`;
1790
+ return `<button class="aparte-btn aparte-btn--icon aparte-action-btn aparte-action-copy" data-action="copy" aria-label="${escapeAttr(l2)}" title="${escapeAttr(l2)}">${icons.copy()}</button>`;
1733
1791
  }
1734
1792
  case "edit": {
1735
1793
  const l2 = locale.edit ?? "Edit message";
1736
- return `<button class="aparte-action-btn aparte-action-edit" data-action="edit" aria-label="${escapeAttr(l2)}" title="${escapeAttr(l2)}">${icons.edit()}</button>`;
1794
+ return `<button class="aparte-btn aparte-btn--icon aparte-action-btn aparte-action-edit" data-action="edit" aria-label="${escapeAttr(l2)}" title="${escapeAttr(l2)}">${icons.edit()}</button>`;
1737
1795
  }
1738
1796
  case "retry": {
1739
1797
  const l2 = locale.retry ?? "Retry";
1740
- return `<button class="aparte-action-btn aparte-action-retry" data-action="retry" aria-label="${escapeAttr(l2)}" title="${escapeAttr(l2)}">${icons.retry()}</button>`;
1798
+ return `<button class="aparte-btn aparte-btn--icon aparte-action-btn aparte-action-retry" data-action="retry" aria-label="${escapeAttr(l2)}" title="${escapeAttr(l2)}">${icons.retry()}</button>`;
1741
1799
  }
1742
1800
  case "thumbUp": {
1743
1801
  const l2 = locale.feedbackPositive ?? "Good response";
1744
- return `<button class="aparte-action-btn aparte-action-feedback-pos" data-action="feedback-positive" aria-label="${escapeAttr(l2)}" title="${escapeAttr(l2)}">${icons.thumbUp()}</button>`;
1802
+ return `<button class="aparte-btn aparte-btn--icon aparte-action-btn aparte-action-feedback-pos" data-action="feedback-positive" aria-label="${escapeAttr(l2)}" title="${escapeAttr(l2)}">${icons.thumbUp()}</button>`;
1745
1803
  }
1746
1804
  case "thumbDown": {
1747
1805
  const l2 = locale.feedbackNegative ?? "Bad response";
1748
- return `<button class="aparte-action-btn aparte-action-feedback-neg" data-action="feedback-negative" aria-label="${escapeAttr(l2)}" title="${escapeAttr(l2)}">${icons.thumbDown()}</button>`;
1806
+ return `<button class="aparte-btn aparte-btn--icon aparte-action-btn aparte-action-feedback-neg" data-action="feedback-negative" aria-label="${escapeAttr(l2)}" title="${escapeAttr(l2)}">${icons.thumbDown()}</button>`;
1749
1807
  }
1750
1808
  case "info": {
1751
1809
  if (!this._usage) return "";
1752
1810
  const l2 = locale.messageInfo ?? "Details";
1753
- return `<button class="aparte-action-btn aparte-action-info" data-action="info" aria-label="${escapeAttr(l2)}" title="${escapeAttr(l2)}">${INFO_ICON_SVG}</button>`;
1811
+ return `<button class="aparte-btn aparte-btn--icon aparte-action-btn aparte-action-info" data-action="info" aria-label="${escapeAttr(l2)}" title="${escapeAttr(l2)}">${this._cfg.getIcon("info")}</button>`;
1754
1812
  }
1755
1813
  default:
1756
1814
  return "";
@@ -1762,7 +1820,7 @@ class AparteChatBubble extends HTMLElement {
1762
1820
  const locale = this._cfg.getLocale();
1763
1821
  const saveLabel = locale.editConfirm ?? "Save";
1764
1822
  const cancelLabel = locale.editCancel ?? "Cancel";
1765
- this._actionBarEl.innerHTML = `<button class="aparte-action-btn aparte-action-edit-save" data-action="edit-save" aria-label="${escapeAttr(saveLabel)}" title="${escapeAttr(saveLabel)}">${this._cfg.getIcon("check")}</button><button class="aparte-action-btn aparte-action-edit-cancel" data-action="edit-cancel" aria-label="${escapeAttr(cancelLabel)}" title="${escapeAttr(cancelLabel)}">${this._cfg.getIcon("close")}</button>`;
1823
+ this._actionBarEl.innerHTML = `<button class="aparte-btn aparte-btn--icon aparte-btn--sm aparte-btn--success aparte-action-btn aparte-action-edit-save" data-action="edit-save" aria-label="${escapeAttr(saveLabel)}" title="${escapeAttr(saveLabel)}">${this._cfg.getIcon("check")}</button><button class="aparte-btn aparte-btn--icon aparte-btn--sm aparte-btn--danger aparte-action-btn aparte-action-edit-cancel" data-action="edit-cancel" aria-label="${escapeAttr(cancelLabel)}" title="${escapeAttr(cancelLabel)}">${this._cfg.getIcon("close")}</button>`;
1766
1824
  this._actionBarEl.querySelectorAll(".aparte-action-btn").forEach((btn) => {
1767
1825
  btn.addEventListener("click", (e2) => this._handleActionClick(e2));
1768
1826
  });
@@ -2100,6 +2158,8 @@ class AparteChatViewport extends HTMLElement {
2100
2158
  _layoutTransitionMs = 0;
2101
2159
  _repo = new AparteMessageRepository();
2102
2160
  _isAutoScrollEnabled = true;
2161
+ /** The last scroll position we saw, so growth can be told from a gesture. */
2162
+ _lastScrollTop = 0;
2103
2163
  _scrollThreshold = 50;
2104
2164
  /** When true, the next _autoScroll() call uses smooth instead of instant, then resets. */
2105
2165
  _smoothScrollOnce = false;
@@ -2572,6 +2632,11 @@ class AparteChatViewport extends HTMLElement {
2572
2632
  getMessage(messageId) {
2573
2633
  return this._repo.getMessageById(messageId);
2574
2634
  }
2635
+ /**
2636
+ * The messages on the currently ACTIVE path, root → head — not the whole tree.
2637
+ * A message that was retried contributes only the branch currently selected;
2638
+ * `exportTree()` is what returns every sibling.
2639
+ */
2575
2640
  getMessages() {
2576
2641
  return this._repo.getMessages();
2577
2642
  }
@@ -2836,11 +2901,11 @@ class AparteChatViewport extends HTMLElement {
2836
2901
  this.appendChild(container);
2837
2902
  this._container = container;
2838
2903
  this._scrollBtn = document.createElement("button");
2839
- this._scrollBtn.className = "aparte-scroll-btn aparte-scroll-btn--hidden";
2904
+ this._scrollBtn.className = "aparte-btn aparte-btn--surface aparte-btn--circle aparte-btn--lg aparte-scroll-btn aparte-scroll-btn--hidden";
2840
2905
  this._scrollBtn.setAttribute("type", "button");
2841
2906
  this._scrollBtn.setAttribute("aria-label", "Scroll to bottom");
2842
2907
  const scrollIcon = resolveConfig(this).getIcon("scrollDown");
2843
- this._scrollBtn.innerHTML = scrollIcon || `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/></svg>`;
2908
+ this._scrollBtn.innerHTML = scrollIcon;
2844
2909
  this.appendChild(this._scrollBtn);
2845
2910
  } else {
2846
2911
  this._container = this.querySelector(".aparte-viewport-container");
@@ -2869,11 +2934,11 @@ class AparteChatViewport extends HTMLElement {
2869
2934
  }
2870
2935
  this.classList.add("aparte-viewport--framework");
2871
2936
  const scrollBtn = document.createElement("button");
2872
- scrollBtn.className = "aparte-scroll-btn aparte-scroll-btn--hidden";
2937
+ scrollBtn.className = "aparte-btn aparte-btn--surface aparte-btn--circle aparte-btn--lg aparte-scroll-btn aparte-scroll-btn--hidden";
2873
2938
  scrollBtn.setAttribute("type", "button");
2874
2939
  scrollBtn.setAttribute("aria-label", "Scroll to bottom");
2875
2940
  const scrollIcon = resolveConfig(this).getIcon("scrollDown");
2876
- scrollBtn.innerHTML = scrollIcon || `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/></svg>`;
2941
+ scrollBtn.innerHTML = scrollIcon;
2877
2942
  this.appendChild(scrollBtn);
2878
2943
  this._scrollBtn = scrollBtn;
2879
2944
  }
@@ -2932,6 +2997,7 @@ class AparteChatViewport extends HTMLElement {
2932
2997
  this._scrollToBottom();
2933
2998
  }
2934
2999
  this._recalculateSpacer();
3000
+ this._updateScrollButton();
2935
3001
  });
2936
3002
  const wrapper = this.querySelector(".aparte-messages-wrapper");
2937
3003
  if (this._container) {
@@ -2964,7 +3030,14 @@ class AparteChatViewport extends HTMLElement {
2964
3030
  }
2965
3031
  _handleScroll() {
2966
3032
  if (!this._container) return;
2967
- this._isAutoScrollEnabled = this._isAtBottom();
3033
+ const top = this._container.scrollTop;
3034
+ const readerWentUp = top < this._lastScrollTop - 1;
3035
+ this._lastScrollTop = top;
3036
+ if (this._isAtBottom()) {
3037
+ this._isAutoScrollEnabled = true;
3038
+ } else if (readerWentUp) {
3039
+ this._isAutoScrollEnabled = false;
3040
+ }
2968
3041
  this._updateScrollButton();
2969
3042
  }
2970
3043
  _scrollToBottom() {
@@ -3293,18 +3366,31 @@ class AparteComposer extends HTMLElement {
3293
3366
  submitEnabled: this._panelSubmitEnabled
3294
3367
  };
3295
3368
  }
3369
+ /**
3370
+ * Set the composer's value — both what a send will submit and what the editor
3371
+ * shows. `<aparte-composer-input>` writes through any value it does not already
3372
+ * hold, so this prefills the visible field (a template button, a restored draft)
3373
+ * as readily as it stages text for an immediate `submit()`.
3374
+ */
3296
3375
  setValue(value) {
3297
3376
  this._value = value;
3298
3377
  this._emit("value-change", { value });
3299
3378
  }
3379
+ /** Append files to the pending attachments and notify. Does not de-duplicate. */
3300
3380
  addAttachments(files) {
3301
3381
  this._attachments = [...this._attachments, ...Array.from(files)];
3302
3382
  this._emit("attachments-change", { attachments: this._attachments });
3303
3383
  }
3384
+ /**
3385
+ * Drop one pending attachment and notify. Matched by IDENTITY — pass the same
3386
+ * `File` object the composer handed you, not an equal one; two picks of the same
3387
+ * file on disk are two distinct objects.
3388
+ */
3304
3389
  removeAttachment(file) {
3305
3390
  this._attachments = this._attachments.filter((f2) => f2 !== file);
3306
3391
  this._emit("attachments-change", { attachments: this._attachments });
3307
3392
  }
3393
+ /** Drop every pending attachment and notify. */
3308
3394
  clearAttachments() {
3309
3395
  this._attachments = [];
3310
3396
  this._emit("attachments-change", { attachments: [] });
@@ -3323,6 +3409,15 @@ class AparteComposer extends HTMLElement {
3323
3409
  * are the user's state and not an action to offer — hiding them would look like
3324
3410
  * losing them; and the toolbar, because switching model still does something.
3325
3411
  *
3412
+ * The send button is the part the PANEL decides, through `mode`. `'submit'` and
3413
+ * `'advance'` keep it; `'none'` says this panel has no act for it and it is not
3414
+ * drawn. That third value is what a panel whose options settle on the first click
3415
+ * needs — a single-choice question, an approval — and until it existed such a
3416
+ * panel left a permanently disabled button beside options that never routed
3417
+ * through it. Flip between them at any time with {@link setPanelSubmitEnabled},
3418
+ * which is how a panel that grows an act (an "Other…" field, a written
3419
+ * instruction) turns the button back on.
3420
+ *
3326
3421
  * Declared with an attribute + CSS rather than the inline `style.display` this
3327
3422
  * used to set on a child: an attribute is themeable, is visible to a consumer's
3328
3423
  * own rules, and does not clobber a `display` the consumer had set (the restore
@@ -3346,6 +3441,7 @@ class AparteComposer extends HTMLElement {
3346
3441
  this._panelActive = true;
3347
3442
  this._panelSubmitEnabled = options?.submitEnabled ?? false;
3348
3443
  this._panelMode = options?.mode ?? "submit";
3444
+ this._reflectPanelMode();
3349
3445
  this._panelOnSubmit = options?.onSubmit ?? null;
3350
3446
  this._panelOnEvict = options?.onEvict ?? null;
3351
3447
  const token = Symbol("aparte-composer-panel");
@@ -3405,6 +3501,7 @@ class AparteComposer extends HTMLElement {
3405
3501
  this._panelActive = false;
3406
3502
  this._panelSubmitEnabled = false;
3407
3503
  this._panelMode = "submit";
3504
+ this._reflectPanelMode();
3408
3505
  this._panelOnSubmit = null;
3409
3506
  this._panelOnEvict = null;
3410
3507
  this._panelToken = null;
@@ -3423,8 +3520,21 @@ class AparteComposer extends HTMLElement {
3423
3520
  if (!this._panelActive) return;
3424
3521
  this._panelSubmitEnabled = enabled;
3425
3522
  if (mode) this._panelMode = mode;
3523
+ this._reflectPanelMode();
3426
3524
  this._emit("panel-change", { active: true, submitEnabled: enabled, mode: this._panelMode });
3427
3525
  }
3526
+ /**
3527
+ * Publish the panel mode as an attribute, so CSS can act on it.
3528
+ *
3529
+ * The same reasoning `data-panel-active` is set for and the same one that took
3530
+ * the old `style.display` off a child: an attribute is themeable, is visible to
3531
+ * a consumer's own rules, and does not clobber what the consumer set. It is what
3532
+ * lets `'none'` remove the send button without this component reaching into it.
3533
+ */
3534
+ _reflectPanelMode() {
3535
+ if (this._panelActive) this.setAttribute("data-panel-mode", this._panelMode);
3536
+ else this.removeAttribute("data-panel-mode");
3537
+ }
3428
3538
  get panelActive() {
3429
3539
  return this._panelActive;
3430
3540
  }
@@ -3443,7 +3553,7 @@ class AparteComposer extends HTMLElement {
3443
3553
  /** Submit the current value. Called by aparte-composer-send or programmatically. */
3444
3554
  submit() {
3445
3555
  if (this._panelActive) {
3446
- if (this._panelSubmitEnabled) this._panelOnSubmit?.();
3556
+ if (this._panelMode !== "none" && this._panelSubmitEnabled) this._panelOnSubmit?.();
3447
3557
  return;
3448
3558
  }
3449
3559
  if (this._streaming) {
@@ -3596,6 +3706,10 @@ class AparteComposerInput extends HTMLElement {
3596
3706
  if (name === "disabled") this._updateDisabled(value !== null);
3597
3707
  }
3598
3708
  // ── Public API ──────────────────────────────────────────────────────────
3709
+ /**
3710
+ * The editor's text, with `<br>` serialized back to newlines — `textContent`
3711
+ * alone would collapse a multi-line draft onto a single line.
3712
+ */
3599
3713
  getValue() {
3600
3714
  if (!this._editor) return "";
3601
3715
  let out = "";
@@ -3609,6 +3723,7 @@ class AparteComposerInput extends HTMLElement {
3609
3723
  walk(this._editor);
3610
3724
  return out.trim();
3611
3725
  }
3726
+ /** Replace the editor's content and mirror the value onto the parent composer. */
3612
3727
  setValue(value) {
3613
3728
  if (!this._editor) return;
3614
3729
  this._editor.textContent = value;
@@ -3616,6 +3731,7 @@ class AparteComposerInput extends HTMLElement {
3616
3731
  this._adjustHeight();
3617
3732
  this._getRoot()?.setValue(value);
3618
3733
  }
3734
+ /** Empty the editor and mirror the empty value onto the parent composer. */
3619
3735
  clear() {
3620
3736
  if (!this._editor) return;
3621
3737
  this._editor.innerHTML = "";
@@ -3623,9 +3739,11 @@ class AparteComposerInput extends HTMLElement {
3623
3739
  this._adjustHeight();
3624
3740
  this._getRoot()?.setValue("");
3625
3741
  }
3742
+ /** Focus the inner contenteditable rather than the host element. */
3626
3743
  focus() {
3627
3744
  this._editor?.focus();
3628
3745
  }
3746
+ /** Blur the inner contenteditable rather than the host element. */
3629
3747
  blur() {
3630
3748
  this._editor?.blur();
3631
3749
  }
@@ -3678,7 +3796,9 @@ class AparteComposerInput extends HTMLElement {
3678
3796
  );
3679
3797
  this._unsubscribes.push(
3680
3798
  root._on("value-change", ({ value }) => {
3681
- if (value === "" && this.getValue() !== "") this.clear();
3799
+ if (this.getValue() === value.trim()) return;
3800
+ if (value === "") this.clear();
3801
+ else this.setValue(value);
3682
3802
  })
3683
3803
  );
3684
3804
  this._unsubscribes.push(
@@ -3814,7 +3934,7 @@ class AparteComposerSend extends HTMLElement {
3814
3934
  const root = this._getRoot();
3815
3935
  const disabled = !root || root.disabled || root.value.trim() === "";
3816
3936
  this.innerHTML = `<button
3817
- class="aparte-cs-button aparte-send-button"
3937
+ class="aparte-btn aparte-btn--primary aparte-btn--solid aparte-btn--icon aparte-cs-button aparte-send-button"
3818
3938
  aria-label="${escapeAttr(label)}"
3819
3939
  title="${escapeAttr(label)}"
3820
3940
  ${disabled ? "disabled" : ""}
@@ -3884,6 +4004,10 @@ class AparteComposerSend extends HTMLElement {
3884
4004
  const panel = this._panel;
3885
4005
  if (!this._button || !panel?.active) return;
3886
4006
  const cfg = resolveConfig(this);
4007
+ if (panel.mode === "none") {
4008
+ this._button.disabled = true;
4009
+ return;
4010
+ }
3887
4011
  const advancing = panel.mode === "advance";
3888
4012
  this._button.disabled = !panel.submitEnabled;
3889
4013
  this._button.innerHTML = advancing ? cfg.getIcon("nextBranch") : this._getSubmitIcon();
@@ -3962,7 +4086,7 @@ class AparteComposerCancel extends HTMLElement {
3962
4086
  const label = resolveConfig(this).t("stopButton") || "Stop";
3963
4087
  const icon = this._getStopIcon();
3964
4088
  this.innerHTML = `<button
3965
- class="aparte-cc-button"
4089
+ class="aparte-btn aparte-btn--icon aparte-cc-button"
3966
4090
  aria-label="${escapeAttr(label)}"
3967
4091
  title="${escapeAttr(label)}"
3968
4092
  hidden
@@ -4005,7 +4129,6 @@ class AparteComposerCancel extends HTMLElement {
4005
4129
  if (!customElements.get("aparte-composer-cancel")) {
4006
4130
  customElements.define("aparte-composer-cancel", AparteComposerCancel);
4007
4131
  }
4008
- const REMOVE_ICON = '<svg width="10" height="10" viewBox="0 0 12 12" fill="currentColor" aria-hidden="true"><path d="M9.5 3.205 8.795 2.5 6 5.295 3.205 2.5 2.5 3.205 5.295 6 2.5 8.795l.705.705L6 6.705l2.795 2.795.705-.705L6.705 6z"/></svg>';
4009
4132
  class AparteComposerAttachments extends HTMLElement {
4010
4133
  _unsubscribes = [];
4011
4134
  /** Object URLs minted for image previews — revoked on re-render/disconnect. */
@@ -4041,13 +4164,13 @@ class AparteComposerAttachments extends HTMLElement {
4041
4164
  this._revokeUrls();
4042
4165
  this.innerHTML = files.map((file) => {
4043
4166
  const name = this._escape(file.name);
4044
- const remove = `<button class="aparte-thumb__remove" type="button" aria-label="Remove ${name}">${REMOVE_ICON}</button>`;
4167
+ const remove = `<button class="aparte-btn aparte-btn--icon aparte-btn--sm aparte-thumb__remove" type="button" aria-label="Remove ${name}">${resolveConfig(this).getIcon("close")}</button>`;
4045
4168
  if (file.type.startsWith("image/")) {
4046
4169
  const url = URL.createObjectURL(file);
4047
4170
  this._objectUrls.push(url);
4048
- return `<div class="aparte-thumb aparte-thumb--image" title="${escapeAttr(name)}"><img class="aparte-thumb__img" src="${escapeAttr(url)}" alt="${escapeAttr(name)}" /><span class="aparte-thumb__name">${name}</span>${remove}</div>`;
4171
+ return `<div class="aparte-thumbnail aparte-thumb aparte-thumb--image" title="${escapeAttr(name)}"><img class="aparte-thumb__img" src="${escapeAttr(url)}" alt="${escapeAttr(name)}" /><span class="aparte-thumb__name">${name}</span>${remove}</div>`;
4049
4172
  }
4050
- return `<div class="aparte-thumb aparte-thumb--file" title="${escapeAttr(name)}"><span class="aparte-thumb__ext">${this._escape(this._ext(file.name))}</span><span class="aparte-thumb__name">${name}</span>${remove}</div>`;
4173
+ return `<div class="aparte-thumbnail aparte-thumb aparte-thumb--file" title="${escapeAttr(name)}"><span class="aparte-thumb__ext">${this._escape(this._ext(file.name))}</span><span class="aparte-thumb__name">${name}</span>${remove}</div>`;
4051
4174
  }).join("");
4052
4175
  this.querySelectorAll(".aparte-thumb__remove").forEach((btn, i2) => {
4053
4176
  btn.addEventListener("click", (e2) => {
@@ -4136,15 +4259,15 @@ class AparteComposerAddAttachment extends HTMLElement {
4136
4259
  const label = cfg.t("actionUpload") || "Attach file";
4137
4260
  this._button.setAttribute("aria-label", label);
4138
4261
  this._button.setAttribute("title", label);
4139
- this._button.innerHTML = cfg.getIcon("paperclip") || this._defaultIcon();
4262
+ this._button.innerHTML = cfg.getIcon("paperclip");
4140
4263
  }
4141
4264
  _render() {
4142
4265
  if (this.querySelector(".aparte-caa-button")) return;
4143
4266
  const label = resolveConfig(this).t("actionUpload") || "Attach file";
4144
- const icon = resolveConfig(this).getIcon("paperclip") || this._defaultIcon();
4267
+ const icon = resolveConfig(this).getIcon("paperclip");
4145
4268
  const disabled = this.hasAttribute("disabled") || this._getRoot()?.disabled || false;
4146
4269
  this.innerHTML = `<button
4147
- class="aparte-caa-button aparte-action-button"
4270
+ class="aparte-btn aparte-btn--icon aparte-caa-button aparte-action-button"
4148
4271
  aria-label="${escapeAttr(label)}"
4149
4272
  title="${escapeAttr(label)}"
4150
4273
  type="button"
@@ -4213,9 +4336,6 @@ class AparteComposerAddAttachment extends HTMLElement {
4213
4336
  root.removeEventListener("drop", onDrop);
4214
4337
  };
4215
4338
  }
4216
- _defaultIcon() {
4217
- return `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>`;
4218
- }
4219
4339
  }
4220
4340
  if (!customElements.get("aparte-composer-add-attachment")) {
4221
4341
  customElements.define("aparte-composer-add-attachment", AparteComposerAddAttachment);
@@ -4263,7 +4383,7 @@ class AparteComposerAction extends HTMLElement {
4263
4383
  const icon = this._resolveIcon(this.getAttribute("icon") ?? "");
4264
4384
  const disabled = this.hasAttribute("disabled") || this._getRoot()?.disabled || false;
4265
4385
  this.innerHTML = `<button
4266
- class="aparte-cact-button aparte-action-button"
4386
+ class="aparte-btn aparte-btn--icon aparte-cact-button aparte-action-button"
4267
4387
  aria-label="${label}"
4268
4388
  title="${label}"
4269
4389
  type="button"
@@ -4298,7 +4418,6 @@ class AparteComposerAction extends HTMLElement {
4298
4418
  if (icon.trimStart().startsWith("<")) return icon;
4299
4419
  return resolveConfig(this).getIcon(icon) ?? icon;
4300
4420
  }
4301
- /** Escape a value before it lands in a double-quoted HTML attribute. */
4302
4421
  }
4303
4422
  if (!customElements.get("aparte-composer-action")) {
4304
4423
  customElements.define("aparte-composer-action", AparteComposerAction);
@@ -4400,10 +4519,10 @@ class AparteConversationList extends HTMLElement {
4400
4519
  const unarchiveLabel = this._esc(locale["unarchiveConversation"] ?? "Unarchive conversation");
4401
4520
  const archiveAction = isArchived ? "unarchive" : "archive";
4402
4521
  const archiveAriaLabel = isArchived ? unarchiveLabel : archiveLabel;
4403
- const archiveIcon = isArchived ? `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="3 8 21 8"/><path d="M5 8v11a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8"/><polyline points="9 14 12 11 15 14"/><line x1="12" y1="11" x2="12" y2="19"/></svg>` : `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="3 8 21 8"/><path d="M5 8v11a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8"/><polyline points="9 14 12 17 15 14"/><line x1="12" y1="11" x2="12" y2="17"/></svg>`;
4522
+ const archiveGlyph = resolveConfig(this).getIcon(isArchived ? "unarchive" : "archive");
4404
4523
  return `
4405
4524
  <div
4406
- class="aparte-conv-item${activeClass}${archivedClass}"
4525
+ class="aparte-menu__item aparte-conv-item${activeClass}${archivedClass}"
4407
4526
  role="button"
4408
4527
  tabindex="0"
4409
4528
  data-conv-id="${escapedId}"
@@ -4411,24 +4530,21 @@ class AparteConversationList extends HTMLElement {
4411
4530
  >
4412
4531
  <span class="aparte-conv-item__title">${escapedTitle}</span>
4413
4532
  <button
4414
- class="aparte-conv-item__archive"
4533
+ class="aparte-btn aparte-btn--icon aparte-btn--sm aparte-conv-item__archive"
4415
4534
  type="button"
4416
4535
  data-archive-id="${escapedId}"
4417
4536
  data-archive-action="${escapeAttr(archiveAction)}"
4418
4537
  aria-label="${escapeAttr(archiveAriaLabel)}"
4419
4538
  tabindex="0"
4420
- >${archiveIcon}</button>
4539
+ >${archiveGlyph}</button>
4421
4540
  <button
4422
- class="aparte-conv-item__delete"
4541
+ class="aparte-btn aparte-btn--icon aparte-btn--sm aparte-conv-item__delete"
4423
4542
  type="button"
4424
4543
  data-delete-id="${escapedId}"
4425
4544
  aria-label="${deleteLabel}"
4426
4545
  tabindex="0"
4427
4546
  >
4428
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"
4429
- stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
4430
- <path d="M18 6L6 18M6 6l12 12"/>
4431
- </svg>
4547
+ ${resolveConfig(this).getIcon("close")}
4432
4548
  </button>
4433
4549
  </div>`;
4434
4550
  }
@@ -4471,11 +4587,10 @@ class AparteConversationList extends HTMLElement {
4471
4587
  };
4472
4588
  _onKeydown = (e2) => {
4473
4589
  if (e2.key !== "Enter" && e2.key !== " ") return;
4474
- const item = e2.target.closest("[data-conv-id]");
4475
- if (item) {
4476
- e2.preventDefault();
4477
- item.click();
4478
- }
4590
+ const target = e2.target;
4591
+ if (!target.matches("[data-conv-id]")) return;
4592
+ e2.preventDefault();
4593
+ target.click();
4479
4594
  };
4480
4595
  /** Update active class without full re-render (perf optimisation). */
4481
4596
  _updateActiveState() {
@@ -4535,6 +4650,7 @@ export {
4535
4650
  AparteElicitationAbortError,
4536
4651
  q as AparteError,
4537
4652
  r as AparteErrorCode,
4653
+ AparteIcon,
4538
4654
  AparteMessageRepository,
4539
4655
  AparteOptgroup,
4540
4656
  AparteOption,