@aparte/core 0.16.6 → 0.16.8

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 (69) hide show
  1. package/README.md +5 -4
  2. package/dist/client/aparte-client.d.ts +14 -0
  3. package/dist/client/aparte-client.d.ts.map +1 -1
  4. package/dist/components/bubble/aparte-chat-bubble.d.ts +33 -18
  5. package/dist/components/bubble/aparte-chat-bubble.d.ts.map +1 -1
  6. package/dist/components/chat/aparte-chat.d.ts +32 -25
  7. package/dist/components/chat/aparte-chat.d.ts.map +1 -1
  8. package/dist/components/composer/aparte-composer-action.d.ts +1 -1
  9. package/dist/components/composer/aparte-composer-add-attachment.d.ts +1 -1
  10. package/dist/components/composer/aparte-composer-attachments.d.ts +1 -1
  11. package/dist/components/composer/aparte-composer-cancel.d.ts +2 -2
  12. package/dist/components/composer/aparte-composer-cancel.d.ts.map +1 -1
  13. package/dist/components/composer/aparte-composer-input.d.ts +6 -6
  14. package/dist/components/composer/aparte-composer-send.d.ts +3 -3
  15. package/dist/components/composer/aparte-composer-send.d.ts.map +1 -1
  16. package/dist/components/composer/aparte-composer.d.ts +9 -2
  17. package/dist/components/composer/aparte-composer.d.ts.map +1 -1
  18. package/dist/components/context/aparte-context.d.ts +0 -1
  19. package/dist/components/context/aparte-context.d.ts.map +1 -1
  20. package/dist/components/conversation-list/aparte-conversation-list.d.ts +6 -6
  21. package/dist/components/elicitation/aparte-elicitation.d.ts +20 -18
  22. package/dist/components/elicitation/aparte-elicitation.d.ts.map +1 -1
  23. package/dist/components/scroll-rail/aparte-scroll-rail.d.ts +9 -2
  24. package/dist/components/scroll-rail/aparte-scroll-rail.d.ts.map +1 -1
  25. package/dist/components/sidebar/aparte-sidebar.d.ts +16 -2
  26. package/dist/components/sidebar/aparte-sidebar.d.ts.map +1 -1
  27. package/dist/components/split/aparte-split.d.ts +9 -4
  28. package/dist/components/split/aparte-split.d.ts.map +1 -1
  29. package/dist/components/status/aparte-chat-status.d.ts +3 -2
  30. package/dist/components/status/aparte-chat-status.d.ts.map +1 -1
  31. package/dist/components/viewport/aparte-chat-viewport.d.ts +27 -4
  32. package/dist/components/viewport/aparte-chat-viewport.d.ts.map +1 -1
  33. package/dist/config/icon-provider.d.ts +6 -0
  34. package/dist/config/icon-provider.d.ts.map +1 -1
  35. package/dist/config/locale.d.ts +3 -1
  36. package/dist/config/locale.d.ts.map +1 -1
  37. package/dist/conversations/conversation-controller.d.ts.map +1 -1
  38. package/dist/custom-elements.json +10458 -10325
  39. package/dist/{dialog-triggers-CVCxqKg3.js → dialog-triggers-D8wNICrT.js} +36 -19
  40. package/dist/dialog-triggers-D8wNICrT.js.map +1 -0
  41. package/dist/generated/element-attributes.d.ts +13 -3
  42. package/dist/generated/element-attributes.d.ts.map +1 -1
  43. package/dist/{glyphs-DQTeOj4w.js → glyphs-C8z2oJUg.js} +36 -27
  44. package/dist/{glyphs-DQTeOj4w.js.map → glyphs-C8z2oJUg.js.map} +1 -1
  45. package/dist/icons/extended.d.ts +0 -6
  46. package/dist/icons/extended.d.ts.map +1 -1
  47. package/dist/icons/glyphs.d.ts +13 -4
  48. package/dist/icons/glyphs.d.ts.map +1 -1
  49. package/dist/icons.js +28 -31
  50. package/dist/icons.js.map +1 -1
  51. package/dist/index.css +909 -357
  52. package/dist/index.js +176 -54
  53. package/dist/index.js.map +1 -1
  54. package/dist/index.node.js +1 -1
  55. package/dist/primitives/icon/aparte-icon.d.ts +1 -1
  56. package/dist/primitives/progress-spinner/aparte-progress-spinner.d.ts +2 -3
  57. package/dist/primitives/progress-spinner/aparte-progress-spinner.d.ts.map +1 -1
  58. package/dist/primitives/select/aparte-optgroup.d.ts +1 -1
  59. package/dist/primitives/select/aparte-optgroup.d.ts.map +1 -1
  60. package/dist/primitives/select/aparte-option.d.ts +4 -4
  61. package/dist/primitives/select/aparte-option.d.ts.map +1 -1
  62. package/dist/primitives/select/aparte-select.d.ts +21 -12
  63. package/dist/primitives/select/aparte-select.d.ts.map +1 -1
  64. package/dist/renderers/segments/code.d.ts.map +1 -1
  65. package/dist/renderers/segments/error.d.ts.map +1 -1
  66. package/dist/renderers/segments/thinking.d.ts.map +1 -1
  67. package/dist/renderers/segments/tool-call.d.ts.map +1 -1
  68. package/package.json +3 -2
  69. package/dist/dialog-triggers-CVCxqKg3.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,5 +1,8 @@
1
- import { $ as resolveConfig, a4 as subscribeConfigChange, a1 as runWithConfig, o as AparteElicitationAbortError, w as buildApprovalPanel, x as buildElicitationPanel, J as escapeAttr, F as cssEscape, a7 as mergeSegmentUpdate, K as escapeHtml, a8 as writeStreamedMarkdown, C as copyText, N as getSegmentRenderer, O as installDefaultRenderersOnce, r 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, e as APARTE_DEFAULT_LOCALE, P as installDialogTriggersOnce } from "./dialog-triggers-CVCxqKg3.js";
2
- import { A, a, b, c, d, f, g, h, i, j, k, l, m, n, p, q, s, t, u, v, y, z, B, D, E, G, H, I, L, M, R, S, T, U, V, X, Y, Z, _, a2, a3, a5 } from "./dialog-triggers-CVCxqKg3.js";
1
+ import { $ as resolveConfig, a4 as subscribeConfigChange, a1 as runWithConfig, o as AparteElicitationAbortError, w as buildApprovalPanel, x as buildElicitationPanel, J as escapeAttr, F as cssEscape, a7 as mergeSegmentUpdate, K as escapeHtml, a8 as writeStreamedMarkdown, C as copyText, N as getSegmentRenderer, O as installDefaultRenderersOnce, r 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, e as APARTE_DEFAULT_LOCALE, P as installDialogTriggersOnce } from "./dialog-triggers-D8wNICrT.js";
2
+ import { A, a, b, c, d, f, g, h, i, j, k, l, m, n, p, q, s, t, u, v, y, z, B, D, E, G, H, I, L, M, R, S, T, U, V, X, Y, Z, _, a2, a3, a5 } from "./dialog-triggers-D8wNICrT.js";
3
+ function presenceOn(value) {
4
+ return value === "" ? true : Boolean(value);
5
+ }
3
6
  class AparteOption extends HTMLElement {
4
7
  static get observedAttributes() {
5
8
  return ["value", "disabled", "selected", "data-status"];
@@ -34,21 +37,13 @@ class AparteOption extends HTMLElement {
34
37
  return this.hasAttribute("disabled");
35
38
  }
36
39
  set disabled(val) {
37
- if (val) {
38
- this.setAttribute("disabled", "");
39
- } else {
40
- this.removeAttribute("disabled");
41
- }
40
+ this.toggleAttribute("disabled", presenceOn(val));
42
41
  }
43
42
  get selected() {
44
43
  return this.hasAttribute("selected");
45
44
  }
46
45
  set selected(val) {
47
- if (val) {
48
- this.setAttribute("selected", "");
49
- } else {
50
- this.removeAttribute("selected");
51
- }
46
+ this.toggleAttribute("selected", presenceOn(val));
52
47
  }
53
48
  _updateAriaSelected() {
54
49
  this.setAttribute("aria-selected", this.selected ? "true" : "false");
@@ -104,18 +99,13 @@ class AparteOptgroup extends HTMLElement {
104
99
  return this.hasAttribute("collapsed");
105
100
  }
106
101
  set collapsed(val) {
107
- if (val) {
108
- this.setAttribute("collapsed", "");
109
- } else {
110
- this.removeAttribute("collapsed");
111
- }
102
+ this.toggleAttribute("collapsed", presenceOn(val));
112
103
  }
113
104
  get loading() {
114
105
  return this.hasAttribute("loading");
115
106
  }
116
107
  set loading(val) {
117
- if (val) this.setAttribute("loading", "");
118
- else this.removeAttribute("loading");
108
+ this.toggleAttribute("loading", presenceOn(val));
119
109
  }
120
110
  _render() {
121
111
  if (this.label) {
@@ -152,7 +142,9 @@ class AparteOptgroup extends HTMLElement {
152
142
  if (!loader) {
153
143
  loader = document.createElement("div");
154
144
  loader.className = "aparte-optgroup-loader";
155
- loader.innerHTML = '<span class="aparte-spinner-small"></span> Fetching models...';
145
+ const spinner = document.createElement("span");
146
+ spinner.className = "aparte-spinner-small";
147
+ loader.replaceChildren(spinner, ` ${resolveConfig(this).t("loading") || "Loading…"}`);
156
148
  this.appendChild(loader);
157
149
  }
158
150
  } else if (loader) {
@@ -211,12 +203,13 @@ class AparteSelect extends HTMLElement {
211
203
  _boundHandleDocumentClick = this._handleDocumentClick.bind(this);
212
204
  _boundHandleKeydown = this._handleKeydown.bind(this);
213
205
  static get observedAttributes() {
214
- return ["value", "placeholder", "disabled", "grouped", "searchable", "open"];
206
+ return ["value", "placeholder", "disabled", "searchable", "open"];
215
207
  }
216
208
  connectedCallback() {
217
209
  this._value = this.getAttribute("value") || "";
218
210
  this._isOpen = this.hasAttribute("open");
219
211
  this._render();
212
+ this._applyDisabled();
220
213
  this._setupEventListeners();
221
214
  this._setupMutationObserver();
222
215
  }
@@ -241,6 +234,13 @@ class AparteSelect extends HTMLElement {
241
234
  this._dropdown?.setAttribute("hidden", "");
242
235
  }
243
236
  }
237
+ if (name === "placeholder") {
238
+ this._updateTriggerLabel();
239
+ this._updateNames();
240
+ }
241
+ if (name === "disabled") {
242
+ this._applyDisabled();
243
+ }
244
244
  }
245
245
  // ─────────────────────────────────────────────────────────────────────────
246
246
  // Public API
@@ -260,11 +260,7 @@ class AparteSelect extends HTMLElement {
260
260
  return this._isOpen;
261
261
  }
262
262
  set open(val) {
263
- if (val) {
264
- this.setAttribute("open", "");
265
- } else {
266
- this.removeAttribute("open");
267
- }
263
+ this.toggleAttribute("open", presenceOn(val));
268
264
  }
269
265
  // ─────────────────────────────────────────────────────────────────────────
270
266
  // Rendering
@@ -558,6 +554,34 @@ class AparteSelect extends HTMLElement {
558
554
  this._emitChange(previousValue);
559
555
  this._trigger?.focus();
560
556
  }
557
+ /**
558
+ * The combobox and its listbox are named by the host's `aria-label`, else by the
559
+ * placeholder — the same rule the first render applies, re-applied when the
560
+ * placeholder changes. Written only when different: the mutation observer watches
561
+ * this subtree.
562
+ */
563
+ _updateNames() {
564
+ const name = this.getAttribute("aria-label") || this.getAttribute("placeholder") || "Select...";
565
+ for (const el2 of [this._trigger, this.querySelector(".aparte-select-options")]) {
566
+ if (el2 && el2.getAttribute("aria-label") !== name) el2.setAttribute("aria-label", name);
567
+ }
568
+ }
569
+ /**
570
+ * `disabled` takes the trigger out of the tab order and says so — opacity alone
571
+ * left it focusable and announced as operable. An open dropdown closes.
572
+ */
573
+ _applyDisabled() {
574
+ const trigger = this._trigger;
575
+ if (!trigger) return;
576
+ const disabled = this.hasAttribute("disabled");
577
+ trigger.setAttribute("tabindex", disabled ? "-1" : "0");
578
+ if (disabled) {
579
+ trigger.setAttribute("aria-disabled", "true");
580
+ if (this._isOpen) this._closeDropdown();
581
+ } else {
582
+ trigger.removeAttribute("aria-disabled");
583
+ }
584
+ }
561
585
  _updateTriggerLabel() {
562
586
  const placeholder = this.getAttribute("placeholder") || "Select...";
563
587
  const labelText = this._trigger?.querySelector(".aparte-select-label-text");
@@ -895,7 +919,7 @@ if (typeof customElements !== "undefined" && !customElements.get("aparte-elicita
895
919
  }
896
920
  class AparteChat extends HTMLElement {
897
921
  static get observedAttributes() {
898
- return ["placeholder", "disabled", "center-empty", "attachments"];
922
+ return ["placeholder", "disabled", "submit-on-enter", "center-empty", "attachments"];
899
923
  }
900
924
  _observer = null;
901
925
  /**
@@ -908,6 +932,7 @@ class AparteChat extends HTMLElement {
908
932
  this._render();
909
933
  this._forwardAttr("placeholder");
910
934
  this._forwardAttr("disabled");
935
+ this._forwardAttr("submit-on-enter");
911
936
  this._syncEmptyWatch();
912
937
  }
913
938
  disconnectedCallback() {
@@ -1187,6 +1212,7 @@ class AparteChatBubble extends HTMLElement {
1187
1212
  appendToken(chunk) {
1188
1213
  this._content += chunk;
1189
1214
  this._updateContent();
1215
+ this._syncCopyAction();
1190
1216
  }
1191
1217
  /** Set content directly */
1192
1218
  setContent(content) {
@@ -1194,6 +1220,7 @@ class AparteChatBubble extends HTMLElement {
1194
1220
  this.setAttribute("content", content);
1195
1221
  this._resetMarkdownStream();
1196
1222
  this._updateContent();
1223
+ this._syncCopyAction();
1197
1224
  }
1198
1225
  /**
1199
1226
  * Drop the incremental Markdown parser's state.
@@ -1229,6 +1256,7 @@ class AparteChatBubble extends HTMLElement {
1229
1256
  const updated = mergeSegmentUpdate(this._segments[index], updates);
1230
1257
  this._segments[index] = updated;
1231
1258
  this._applySegmentUpdate(segmentId, updated, updates);
1259
+ this._syncCopyAction();
1232
1260
  }
1233
1261
  }
1234
1262
  /** Append content to a segment */
@@ -1237,6 +1265,7 @@ class AparteChatBubble extends HTMLElement {
1237
1265
  if (segment && "content" in segment) {
1238
1266
  segment.content += content;
1239
1267
  this._applySegmentUpdate(segmentId, segment, { content: segment.content });
1268
+ this._syncCopyAction();
1240
1269
  }
1241
1270
  }
1242
1271
  /** Get all segments */
@@ -1266,6 +1295,7 @@ class AparteChatBubble extends HTMLElement {
1266
1295
  const el2 = this._segmentsEl?.querySelector(`:scope > [data-segment-id="${cssEscape(segmentId)}"]`);
1267
1296
  el2?.remove();
1268
1297
  this._updateWaiting();
1298
+ this._syncCopyAction();
1269
1299
  }
1270
1300
  /** Set attachments (chips shown above message content, user role only) */
1271
1301
  setAttachments(attachments) {
@@ -1420,7 +1450,7 @@ class AparteChatBubble extends HTMLElement {
1420
1450
  </div>
1421
1451
  <div class="aparte-footer">
1422
1452
  <div class="aparte-branch-picker" hidden>
1423
- <button class="aparte-btn aparte-btn--icon aparte-btn--sm aparte-branch-prev" aria-label="${escapeAttr(this._cfg.getLocale().previousResponse ?? "Previous response")}">&#8249;</button>
1453
+ <button type="button" class="aparte-btn aparte-btn--icon aparte-btn--sm aparte-branch-prev" aria-label="${escapeAttr(this._cfg.getLocale().previousResponse ?? "Previous response")}">${this._cfg.getIcon("prevBranch")}</button>
1424
1454
  <span class="aparte-branch-label">1 / 1</span>
1425
1455
  <!-- The move has to be ANNOUNCED. Pressing the arrows deliberately does not
1426
1456
  take focus, so without a live region a screen-reader user gets the new
@@ -1429,7 +1459,7 @@ class AparteChatBubble extends HTMLElement {
1429
1459
  dots, which reads as nothing. No new locale key — the position is
1430
1460
  digits, and the buttons beside it already carry translated labels. -->
1431
1461
  <span class="aparte-sr-only aparte-branch-status" aria-live="polite"></span>
1432
- <button class="aparte-btn aparte-btn--icon aparte-btn--sm aparte-branch-next" aria-label="${escapeAttr(this._cfg.getLocale().nextResponse ?? "Next response")}">&#8250;</button>
1462
+ <button type="button" class="aparte-btn aparte-btn--icon aparte-btn--sm aparte-branch-next" aria-label="${escapeAttr(this._cfg.getLocale().nextResponse ?? "Next response")}">${this._cfg.getIcon("nextBranch")}</button>
1433
1463
  </div>
1434
1464
  <div class="aparte-action-bar" role="toolbar" aria-label="${escapeAttr(this._cfg.getLocale().messageActions ?? "Message actions")}"></div>
1435
1465
  </div>
@@ -1589,6 +1619,7 @@ class AparteChatBubble extends HTMLElement {
1589
1619
  this._contentEl.style.display = this._segments.length > 0 ? "none" : "";
1590
1620
  }
1591
1621
  this._reflectError();
1622
+ this._syncCopyAction();
1592
1623
  }
1593
1624
  /**
1594
1625
  * Reflect the error state on the bubble: `data-error` on `.aparte-message` while
@@ -1644,16 +1675,16 @@ class AparteChatBubble extends HTMLElement {
1644
1675
  this._attachmentsEl.innerHTML = this._attachments.map((a4) => {
1645
1676
  const name = escapeHtml(a4.name);
1646
1677
  if (a4.type.startsWith("image/")) {
1647
- return `<div class="aparte-thumbnail aparte-thumb aparte-thumb--image" title="${name}"><img class="aparte-thumb__img" src="${escapeHtml(a4.url)}" alt="${name}" loading="lazy" /><span class="aparte-thumb__name">${name}</span></div>`;
1678
+ return `<div class="aparte-thumbnail aparte-thumb aparte-thumb--image" title="${name}"><img class="aparte-thumbnail__image" src="${escapeHtml(a4.url)}" alt="${name}" loading="lazy" /><span class="aparte-thumb__name">${name}</span></div>`;
1648
1679
  }
1649
- return `<div class="aparte-thumbnail aparte-thumb aparte-thumb--file" title="${name}"><span class="aparte-thumb__ext">${escapeHtml(this._fileExt(a4.name))}</span><span class="aparte-thumb__name">${name}</span></div>`;
1680
+ return `<div class="aparte-thumbnail aparte-thumb aparte-thumb--file" title="${name}"><span class="aparte-thumbnail__label">${escapeHtml(this._fileExt(a4.name))}</span><span class="aparte-thumb__name">${name}</span></div>`;
1650
1681
  }).join("");
1651
1682
  if (!this._cfg.getHostHandlers().attachmentPreview) return;
1652
1683
  this._attachmentsEl.querySelectorAll(".aparte-thumb--image").forEach((tile) => {
1653
1684
  tile.setAttribute("role", "button");
1654
1685
  tile.setAttribute("tabindex", "0");
1655
1686
  const open = () => {
1656
- const img = tile.querySelector(".aparte-thumb__img");
1687
+ const img = tile.querySelector(".aparte-thumbnail__image");
1657
1688
  if (!img) return;
1658
1689
  this.dispatchEvent(new CustomEvent("aparte-attachment-preview", {
1659
1690
  bubbles: true,
@@ -1790,6 +1821,27 @@ class AparteChatBubble extends HTMLElement {
1790
1821
  // ─────────────────────────────────────────────────────────────────────────
1791
1822
  // Action Bar
1792
1823
  // ─────────────────────────────────────────────────────────────────────────
1824
+ /**
1825
+ * What the copy action copies: the reply, not the reasoning. A `thinking` segment is
1826
+ * the model's scratchpad, and the client already keeps it out of the history it sends
1827
+ * back (`_segmentsToText`) for the same reason — copying it along with the answer
1828
+ * pasted a paragraph of deliberation above every reply, which no assistant on the
1829
+ * market does. Segments with no `content` (a tool call) contribute nothing, so a
1830
+ * turn made only of those copies as `''` — and the action bar reads this to decide
1831
+ * whether to offer the button at all.
1832
+ */
1833
+ _copyableText() {
1834
+ return (this._content || this._segments.filter((s2) => s2.type !== "thinking").map((s2) => s2.content ?? "").join("\n")).trim();
1835
+ }
1836
+ /**
1837
+ * The copy button is for text. `hidden` rather than removed: the bar is rebuilt
1838
+ * rarely and the text changes on every streamed token, so the button is toggled in
1839
+ * place from each path that changes what a copy would paste.
1840
+ */
1841
+ _syncCopyAction() {
1842
+ const btn = this._actionBarEl?.querySelector(".aparte-action-copy");
1843
+ if (btn) btn.hidden = this._copyableText() === "";
1844
+ }
1793
1845
  _updateActionBar() {
1794
1846
  if (!this._actionBarEl) return;
1795
1847
  if (this._editing) {
@@ -1821,6 +1873,7 @@ class AparteChatBubble extends HTMLElement {
1821
1873
  }
1822
1874
  }
1823
1875
  this._actionBarEl.innerHTML = buttons.join("");
1876
+ this._syncCopyAction();
1824
1877
  this._appendCustomActions(icons);
1825
1878
  this._applyTranscriptBusy();
1826
1879
  this._actionBarEl.querySelectorAll(".aparte-action-btn").forEach((btn) => {
@@ -1912,7 +1965,8 @@ class AparteChatBubble extends HTMLElement {
1912
1965
  const roles = a4.bubble?.roles ?? ["user", "assistant"];
1913
1966
  if (!roles.includes(this._role)) continue;
1914
1967
  const btn = document.createElement("button");
1915
- btn.className = "aparte-btn aparte-btn--icon aparte-action-btn aparte-action-custom";
1968
+ btn.type = "button";
1969
+ btn.className = "aparte-btn aparte-btn--icon aparte-btn--sm aparte-action-btn aparte-action-custom";
1916
1970
  btn.dataset["action"] = `custom:${a4.id}`;
1917
1971
  btn.setAttribute("aria-label", a4.label);
1918
1972
  btn.setAttribute("title", a4.label);
@@ -1926,28 +1980,28 @@ class AparteChatBubble extends HTMLElement {
1926
1980
  switch (action) {
1927
1981
  case "copy": {
1928
1982
  const l2 = locale.copy ?? "Copy";
1929
- 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>`;
1983
+ return `<button type="button" class="aparte-btn aparte-btn--icon aparte-btn--sm aparte-action-btn aparte-action-copy" data-action="copy" aria-label="${escapeAttr(l2)}" title="${escapeAttr(l2)}">${icons.copy()}</button>`;
1930
1984
  }
1931
1985
  case "edit": {
1932
1986
  const l2 = locale.edit ?? "Edit message";
1933
- 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>`;
1987
+ return `<button type="button" class="aparte-btn aparte-btn--icon aparte-btn--sm aparte-action-btn aparte-action-edit" data-action="edit" aria-label="${escapeAttr(l2)}" title="${escapeAttr(l2)}">${icons.edit()}</button>`;
1934
1988
  }
1935
1989
  case "retry": {
1936
1990
  const l2 = locale.retry ?? "Retry";
1937
- 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>`;
1991
+ return `<button type="button" class="aparte-btn aparte-btn--icon aparte-btn--sm aparte-action-btn aparte-action-retry" data-action="retry" aria-label="${escapeAttr(l2)}" title="${escapeAttr(l2)}">${icons.retry()}</button>`;
1938
1992
  }
1939
1993
  case "thumbUp": {
1940
1994
  const l2 = locale.feedbackPositive ?? "Good response";
1941
- 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>`;
1995
+ return `<button type="button" class="aparte-btn aparte-btn--icon aparte-btn--sm aparte-action-btn aparte-action-feedback-pos" data-action="feedback-positive" aria-label="${escapeAttr(l2)}" title="${escapeAttr(l2)}">${icons.thumbUp()}</button>`;
1942
1996
  }
1943
1997
  case "thumbDown": {
1944
1998
  const l2 = locale.feedbackNegative ?? "Bad response";
1945
- 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>`;
1999
+ return `<button type="button" class="aparte-btn aparte-btn--icon aparte-btn--sm aparte-action-btn aparte-action-feedback-neg" data-action="feedback-negative" aria-label="${escapeAttr(l2)}" title="${escapeAttr(l2)}">${icons.thumbDown()}</button>`;
1946
2000
  }
1947
2001
  case "info": {
1948
2002
  if (!this._usage) return "";
1949
2003
  const l2 = locale.messageInfo ?? "Details";
1950
- 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>`;
2004
+ return `<button type="button" class="aparte-btn aparte-btn--icon aparte-btn--sm aparte-action-btn aparte-action-info" data-action="info" aria-label="${escapeAttr(l2)}" title="${escapeAttr(l2)}">${this._cfg.getIcon("info")}</button>`;
1951
2005
  }
1952
2006
  default:
1953
2007
  return "";
@@ -1959,7 +2013,7 @@ class AparteChatBubble extends HTMLElement {
1959
2013
  const locale = this._cfg.getLocale();
1960
2014
  const saveLabel = locale.editConfirm ?? "Save";
1961
2015
  const cancelLabel = locale.editCancel ?? "Cancel";
1962
- 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>`;
2016
+ this._actionBarEl.innerHTML = `<button type="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 type="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>`;
1963
2017
  this._actionBarEl.querySelectorAll(".aparte-action-btn").forEach((btn) => {
1964
2018
  btn.addEventListener("click", (e) => this._handleActionClick(e));
1965
2019
  });
@@ -1988,7 +2042,7 @@ class AparteChatBubble extends HTMLElement {
1988
2042
  }
1989
2043
  switch (action) {
1990
2044
  case "copy": {
1991
- const text = this._content || this._segments.filter((s2) => s2.type !== "thinking").map((s2) => s2.content ?? "").join("\n");
2045
+ const text = this._copyableText();
1992
2046
  const icons = this._cfg.getIconProvider();
1993
2047
  const locale = this._cfg.getLocale();
1994
2048
  copyText(text).then(() => {
@@ -2352,6 +2406,8 @@ class AparteChatViewport extends HTMLElement {
2352
2406
  _overlayObserver = null;
2353
2407
  /** Last `--aparte-bottom-inset` written, in px — style writes only when it changes. */
2354
2408
  _overlayInset = -1;
2409
+ /** Last `--aparte-transcript-inset` written on the host, in px — same write-on-change rule. */
2410
+ _transcriptInset = -1;
2355
2411
  _boundResetHandler = null;
2356
2412
  /**
2357
2413
  * When true, _reRenderActivePath() only dispatches aparte-path-changed without
@@ -2372,6 +2428,7 @@ class AparteChatViewport extends HTMLElement {
2372
2428
  this._render();
2373
2429
  this._setupEventListeners();
2374
2430
  this._setupObservers();
2431
+ this._updateTranscriptInset();
2375
2432
  this._boundResetHandler = () => this.clearAll();
2376
2433
  window.addEventListener("aparte-reset", this._boundResetHandler);
2377
2434
  window.addEventListener("aparte-config-change", this._onConfigChange);
@@ -3275,6 +3332,7 @@ class AparteChatViewport extends HTMLElement {
3275
3332
  _setupObservers() {
3276
3333
  this._resizeObserver = new ResizeObserver(() => {
3277
3334
  this._updateOverlayInset();
3335
+ this._updateTranscriptInset();
3278
3336
  if (this._isAutoScrollEnabled) {
3279
3337
  this._scrollToBottom();
3280
3338
  }
@@ -3355,6 +3413,32 @@ class AparteChatViewport extends HTMLElement {
3355
3413
  this._overlayInset = inset;
3356
3414
  this.style.setProperty("--aparte-bottom-inset", `${inset}px`);
3357
3415
  }
3416
+ /**
3417
+ * Publish, on the chat host, how far from the host's inline edge the rows start —
3418
+ * the transcript's padding plus the scrollbar gutter the scroller reserves on both
3419
+ * edges, at whatever step its container query has taken — as
3420
+ * `--aparte-transcript-inset`. The composer pads by it (composer.css), so its box
3421
+ * lands on the row's box at every width. The two used to be independent stacks: the
3422
+ * composer cannot know the gutter and a container query cannot reach it, so they sat
3423
+ * 10px apart at 768 and the gutter's half apart at 1280, and the reading-column demo
3424
+ * showed three left edges 12px apart. Written only when the value changes, for the
3425
+ * same reason `_updateOverlayInset` is. On the HOST, not on this element: the
3426
+ * composer is a sibling, and a custom property only travels down.
3427
+ */
3428
+ _updateTranscriptInset() {
3429
+ const host = this.parentElement;
3430
+ if (!host) return;
3431
+ const ref = this.querySelector(".aparte-messages-wrapper") ?? this._container ?? this;
3432
+ const pad = parseFloat(getComputedStyle(ref).paddingInlineStart) || 0;
3433
+ const rtl = getComputedStyle(host).direction === "rtl";
3434
+ const refRect = ref.getBoundingClientRect();
3435
+ const hostRect = host.getBoundingClientRect();
3436
+ const raw = rtl ? hostRect.right - refRect.right : refRect.left - hostRect.left;
3437
+ const inset = Math.max(0, Math.round((raw + pad) * 10) / 10);
3438
+ if (inset === this._transcriptInset) return;
3439
+ this._transcriptInset = inset;
3440
+ host.style.setProperty("--aparte-transcript-inset", `${inset}px`);
3441
+ }
3358
3442
  /** Is the scroll surface within `_scrollThreshold` of its bottom, right now? */
3359
3443
  _isAtBottom() {
3360
3444
  if (!this._container) return true;
@@ -3617,6 +3701,7 @@ class AparteChatViewport extends HTMLElement {
3617
3701
  this._overlayObserver = null;
3618
3702
  this._overlayRoot = null;
3619
3703
  this._overlayInset = -1;
3704
+ this._transcriptInset = -1;
3620
3705
  this._resizeObserver = null;
3621
3706
  this._mutationObserver = null;
3622
3707
  if (this._spacerRafId !== null) {
@@ -4400,6 +4485,7 @@ class AparteComposerSend extends HTMLElement {
4400
4485
  const root = this._getRoot();
4401
4486
  const disabled = !root || root.disabled || root.value.trim() === "";
4402
4487
  this.innerHTML = `<button
4488
+ type="button"
4403
4489
  class="aparte-btn aparte-btn--primary aparte-btn--solid aparte-btn--icon aparte-cs-button aparte-send-button"
4404
4490
  aria-label="${escapeAttr(label)}"
4405
4491
  title="${escapeAttr(label)}"
@@ -4551,6 +4637,7 @@ class AparteComposerCancel extends HTMLElement {
4551
4637
  const label = resolveConfig(this).t("stopButton") || "Stop";
4552
4638
  const icon = this._getStopIcon();
4553
4639
  this.innerHTML = `<button
4640
+ type="button"
4554
4641
  class="aparte-btn aparte-btn--icon aparte-cc-button"
4555
4642
  aria-label="${escapeAttr(label)}"
4556
4643
  title="${escapeAttr(label)}"
@@ -4633,9 +4720,9 @@ class AparteComposerAttachments extends HTMLElement {
4633
4720
  if (file.type.startsWith("image/")) {
4634
4721
  const url = URL.createObjectURL(file);
4635
4722
  this._objectUrls.push(url);
4636
- return `<div class="aparte-thumbnail aparte-thumb aparte-thumb--image" title="${name}"><img class="aparte-thumb__img" src="${escapeAttr(url)}" alt="${name}" /><span class="aparte-thumb__name">${name}</span>${remove}</div>`;
4723
+ return `<div class="aparte-thumbnail aparte-thumb aparte-thumb--image" title="${name}"><img class="aparte-thumbnail__image" src="${escapeAttr(url)}" alt="${name}" /><span class="aparte-thumb__name">${name}</span>${remove}</div>`;
4637
4724
  }
4638
- return `<div class="aparte-thumbnail aparte-thumb aparte-thumb--file" title="${name}"><span class="aparte-thumb__ext">${escapeHtml(this._ext(file.name))}</span><span class="aparte-thumb__name">${name}</span>${remove}</div>`;
4725
+ return `<div class="aparte-thumbnail aparte-thumb aparte-thumb--file" title="${name}"><span class="aparte-thumbnail__label">${escapeHtml(this._ext(file.name))}</span><span class="aparte-thumb__name">${name}</span>${remove}</div>`;
4639
4726
  }).join("");
4640
4727
  this.querySelectorAll(".aparte-thumb__remove").forEach((btn, i2) => {
4641
4728
  btn.addEventListener("click", (e) => {
@@ -4649,7 +4736,7 @@ class AparteComposerAttachments extends HTMLElement {
4649
4736
  tile.setAttribute("role", "button");
4650
4737
  tile.setAttribute("tabindex", "0");
4651
4738
  const open = () => {
4652
- const img = tile.querySelector(".aparte-thumb__img");
4739
+ const img = tile.querySelector(".aparte-thumbnail__image");
4653
4740
  if (!img) return;
4654
4741
  this.dispatchEvent(new CustomEvent("aparte-attachment-preview", {
4655
4742
  bubbles: true,
@@ -5914,9 +6001,6 @@ class AparteScrollRail extends HTMLElement {
5914
6001
  if (!customElements.get("aparte-scroll-rail")) {
5915
6002
  customElements.define("aparte-scroll-rail", AparteScrollRail);
5916
6003
  }
5917
- function presenceOn(value) {
5918
- return value === "" ? true : Boolean(value);
5919
- }
5920
6004
  const DRAWER_QUERY = "(max-width: 48rem)";
5921
6005
  class AparteSidebar extends HTMLElement {
5922
6006
  static get observedAttributes() {
@@ -5987,6 +6071,8 @@ class AparteSidebar extends HTMLElement {
5987
6071
  this._watchBreakpoint();
5988
6072
  this._syncHidden();
5989
6073
  this._lastCollapsed = this.collapsed;
6074
+ if (!this.id) this.id = `aparte-sidebar-${++sidebarIdSeq}`;
6075
+ this._syncToggles();
5990
6076
  this._ready = true;
5991
6077
  }
5992
6078
  /**
@@ -6030,6 +6116,7 @@ class AparteSidebar extends HTMLElement {
6030
6116
  if (!this._auto && !this.drawer) this._closedByHost = collapsed;
6031
6117
  this._syncScrim();
6032
6118
  this._syncHidden();
6119
+ this._syncToggles();
6033
6120
  if (collapsed && this.drawer && this._opener?.isConnected) {
6034
6121
  this._opener.focus();
6035
6122
  this._opener = null;
@@ -6074,6 +6161,26 @@ class AparteSidebar extends HTMLElement {
6074
6161
  }
6075
6162
  this._syncScrim();
6076
6163
  this._syncHidden();
6164
+ this._syncToggles();
6165
+ }
6166
+ /**
6167
+ * Every `[data-aparte-sidebar-toggle]` that drives THIS sidebar says what it does:
6168
+ * `aria-expanded` follows the state, `aria-controls` names the element. Synced from
6169
+ * the three places the state settles (connect, the attribute, the breakpoint), never
6170
+ * from `open()` alone — a close on Escape would otherwise leave an "expanded" that
6171
+ * lies. The row's own `⋯` button already did this; the sidebar's toggle did not.
6172
+ */
6173
+ _syncToggles() {
6174
+ for (const control of document.querySelectorAll("[data-aparte-sidebar-toggle]")) {
6175
+ if (!this._drives(control)) continue;
6176
+ control.setAttribute("aria-expanded", String(!this.collapsed));
6177
+ control.setAttribute("aria-controls", this.id);
6178
+ }
6179
+ }
6180
+ /** Whether a toggle control targets this sidebar: named by id, else the nearest one. */
6181
+ _drives(control) {
6182
+ const named = control.getAttribute("data-aparte-sidebar-toggle");
6183
+ return named ? named === this.id : nearestSidebar(control) === this;
6077
6184
  }
6078
6185
  /**
6079
6186
  * A collapsed sidebar is invisible — folded to nothing in the flow, slid off the
@@ -6126,21 +6233,35 @@ class AparteSidebar extends HTMLElement {
6126
6233
  }
6127
6234
  }
6128
6235
  _onKeydown = (e) => {
6129
- if (e.key === "Escape" && this.drawer && !this.collapsed) {
6236
+ if (!this.drawer || this.collapsed) return;
6237
+ if (e.key === "Escape") {
6130
6238
  e.preventDefault();
6131
6239
  this.close();
6240
+ return;
6241
+ }
6242
+ if (e.key === "Tab") {
6243
+ const focusable = Array.from(this.querySelectorAll(FOCUSABLE));
6244
+ if (focusable.length === 0) return;
6245
+ const first = focusable[0];
6246
+ const last = focusable[focusable.length - 1];
6247
+ const active = document.activeElement;
6248
+ if (!this.contains(active)) {
6249
+ e.preventDefault();
6250
+ first.focus();
6251
+ } else if (e.shiftKey && active === first) {
6252
+ e.preventDefault();
6253
+ last.focus();
6254
+ } else if (!e.shiftKey && active === last) {
6255
+ e.preventDefault();
6256
+ first.focus();
6257
+ }
6132
6258
  }
6133
6259
  };
6134
6260
  /** A `[data-aparte-sidebar-toggle]` anywhere toggles this sidebar — no script in the host. */
6135
6261
  _onDocumentClick = (e) => {
6136
6262
  const control = e.target?.closest?.("[data-aparte-sidebar-toggle]");
6137
6263
  if (!control) return;
6138
- const named = control.getAttribute("data-aparte-sidebar-toggle");
6139
- if (named) {
6140
- if (named !== this.id) return;
6141
- } else if (nearestSidebar(control) !== this) {
6142
- return;
6143
- }
6264
+ if (!this._drives(control)) return;
6144
6265
  this.toggle(control);
6145
6266
  };
6146
6267
  // ─── Search ───────────────────────────────────────────────────────────
@@ -6174,6 +6295,7 @@ class AparteSidebar extends HTMLElement {
6174
6295
  this.dataset["ownLabel"] = "locale";
6175
6296
  }
6176
6297
  }
6298
+ let sidebarIdSeq = 0;
6177
6299
  const FOCUSABLE = [
6178
6300
  "a[href]",
6179
6301
  "button:not([disabled])",