@aparte/core 0.16.10 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/client/aparte-client.d.ts +42 -3
- package/dist/client/aparte-client.d.ts.map +1 -1
- package/dist/components/bubble/aparte-chat-bubble.d.ts +22 -0
- package/dist/components/bubble/aparte-chat-bubble.d.ts.map +1 -1
- package/dist/components/chat/aparte-chat.d.ts +33 -0
- package/dist/components/chat/aparte-chat.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-action.d.ts +5 -3
- package/dist/components/composer/aparte-composer-action.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-toolbar.d.ts +5 -2
- package/dist/components/composer/aparte-composer-toolbar.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer.d.ts +27 -0
- package/dist/components/composer/aparte-composer.d.ts.map +1 -1
- package/dist/components/context/aparte-context.d.ts +8 -0
- package/dist/components/context/aparte-context.d.ts.map +1 -1
- package/dist/components/conversation-list/aparte-conversation-list.d.ts +82 -17
- package/dist/components/conversation-list/aparte-conversation-list.d.ts.map +1 -1
- package/dist/components/scroll-rail/aparte-scroll-rail.d.ts.map +1 -1
- package/dist/components/sidebar/aparte-sidebar.d.ts.map +1 -1
- package/dist/components/split/aparte-split.d.ts.map +1 -1
- package/dist/components/suggestions/aparte-suggestions.d.ts +10 -1
- package/dist/components/suggestions/aparte-suggestions.d.ts.map +1 -1
- package/dist/components/viewport/aparte-chat-viewport.d.ts +55 -1
- package/dist/components/viewport/aparte-chat-viewport.d.ts.map +1 -1
- package/dist/config/action-provider.d.ts +14 -14
- package/dist/config/action-provider.d.ts.map +1 -1
- package/dist/config/aparte-config.d.ts +82 -13
- package/dist/config/aparte-config.d.ts.map +1 -1
- package/dist/config/bubble-shell-renderer.d.ts +50 -12
- package/dist/config/bubble-shell-renderer.d.ts.map +1 -1
- package/dist/config/locale.d.ts +33 -1
- package/dist/config/locale.d.ts.map +1 -1
- package/dist/config/sanitize.d.ts.map +1 -1
- package/dist/conversations/conversation-controller.d.ts +14 -0
- package/dist/conversations/conversation-controller.d.ts.map +1 -1
- package/dist/conversations/conversation-manager.d.ts +57 -19
- package/dist/conversations/conversation-manager.d.ts.map +1 -1
- package/dist/conversations/types.d.ts +18 -11
- package/dist/conversations/types.d.ts.map +1 -1
- package/dist/custom-elements.json +6841 -6085
- package/dist/{dialog-triggers-cYGTlfZf.js → dialog-triggers-Bb-3EkT4.js} +591 -161
- package/dist/dialog-triggers-Bb-3EkT4.js.map +1 -0
- package/dist/generated/element-attributes.d.ts +10 -4
- package/dist/generated/element-attributes.d.ts.map +1 -1
- package/dist/host/aparte-chat-host.d.ts +30 -0
- package/dist/host/aparte-chat-host.d.ts.map +1 -1
- package/dist/index.css +1 -8393
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +550 -98
- package/dist/index.js.map +1 -1
- package/dist/index.node.d.ts +1 -1
- package/dist/index.node.d.ts.map +1 -1
- package/dist/index.node.js +1 -1
- package/dist/index.node.js.map +1 -1
- package/dist/interop/element-props.d.ts +1 -1
- package/dist/primitives/select/aparte-optgroup.d.ts +1 -0
- package/dist/primitives/select/aparte-optgroup.d.ts.map +1 -1
- package/dist/primitives/select/aparte-select.d.ts +12 -1
- package/dist/primitives/select/aparte-select.d.ts.map +1 -1
- package/dist/renderers/markdown-stream.d.ts.map +1 -1
- package/dist/runtime/message-repository.d.ts +16 -3
- package/dist/runtime/message-repository.d.ts.map +1 -1
- package/dist/types/chat.d.ts +41 -46
- package/dist/types/chat.d.ts.map +1 -1
- package/dist/types/event-map.d.ts +7 -7
- package/dist/types/event-map.d.ts.map +1 -1
- package/dist/types/events.d.ts +7 -6
- package/dist/types/events.d.ts.map +1 -1
- package/dist/types/imperative-api.d.ts +16 -0
- package/dist/types/imperative-api.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/models.d.ts +9 -0
- package/dist/types/models.d.ts.map +1 -1
- package/dist/types/segments.d.ts +56 -13
- package/dist/types/segments.d.ts.map +1 -1
- package/dist/utils/event-target.d.ts +31 -0
- package/dist/utils/event-target.d.ts.map +1 -0
- package/package.json +2 -2
- package/dist/dialog-triggers-cYGTlfZf.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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,
|
|
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,
|
|
1
|
+
import { $ as resolveConfig, a7 as eventOrigin, a8 as activeElementIn, a4 as subscribeConfigChange, a1 as runWithConfig, o as AparteElicitationAbortError, w as buildApprovalPanel, x as buildElicitationPanel, J as escapeAttr, F as cssEscape, a9 as mergeSegmentUpdate, K as escapeHtml, aa as writeStreamedMarkdown, S as isSafeUrl, C as copyText, N as getSegmentRenderer, O as installDefaultRenderersOnce, r as AparteMessageRepository, ab as stampSegmentActivity, ac as stampSegmentOnInsert, ad as stampSegmentOnUpdate, ae as renumberSegments, af as openSegmentIds, ag as isTerminalStatus, ah as adoptMessageSegments, Q as isAwaitingReply, W as populateBubbleFromMessage, a6 as uuid, a0 as revokeAttachmentUrls, V as parseMarkdownToSegments, ai as APARTE_DERIVED_SEGMENTS, e as APARTE_DEFAULT_LOCALE, aj as rootOf, P as installDialogTriggersOnce } from "./dialog-triggers-Bb-3EkT4.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, T, U, X, Y, Z, _, a2, a3, a5 } from "./dialog-triggers-Bb-3EkT4.js";
|
|
3
3
|
function presenceOn(value) {
|
|
4
4
|
return value === "" ? true : Boolean(value);
|
|
5
5
|
}
|
|
@@ -95,6 +95,9 @@ class AparteOptgroup extends HTMLElement {
|
|
|
95
95
|
get collapsible() {
|
|
96
96
|
return this.hasAttribute("collapsible");
|
|
97
97
|
}
|
|
98
|
+
set collapsible(val) {
|
|
99
|
+
this.toggleAttribute("collapsible", presenceOn(val));
|
|
100
|
+
}
|
|
98
101
|
get collapsed() {
|
|
99
102
|
return this.hasAttribute("collapsed");
|
|
100
103
|
}
|
|
@@ -202,6 +205,31 @@ class AparteSelect extends HTMLElement {
|
|
|
202
205
|
_boundHandleOptionClick = this._handleOptionClick.bind(this);
|
|
203
206
|
_boundHandleDocumentClick = this._handleDocumentClick.bind(this);
|
|
204
207
|
_boundHandleKeydown = this._handleKeydown.bind(this);
|
|
208
|
+
/**
|
|
209
|
+
* The trigger's own three, bound for the same reason and left inline for a whole
|
|
210
|
+
* release: they were arrows on `this`, so nothing could ever remove them, and the
|
|
211
|
+
* trigger element survives a re-connect. One re-parent made a click toggle twice —
|
|
212
|
+
* open and shut inside the same click, i.e. a control that looks dead — and two made
|
|
213
|
+
* it announce `aparte-select-open` twice. The docblock above describes exactly this,
|
|
214
|
+
* fixed for the option click and not for its neighbours.
|
|
215
|
+
*/
|
|
216
|
+
_boundHandleTriggerClick = () => {
|
|
217
|
+
this._toggle();
|
|
218
|
+
};
|
|
219
|
+
_boundHandleTriggerKeydown = (e) => {
|
|
220
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
221
|
+
if (this._isOpen) return;
|
|
222
|
+
e.preventDefault();
|
|
223
|
+
this._toggle();
|
|
224
|
+
}
|
|
225
|
+
if (e.key === "ArrowDown" && !this._isOpen) {
|
|
226
|
+
e.preventDefault();
|
|
227
|
+
this._openDropdown();
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
_boundHandleSearchInput = (e) => {
|
|
231
|
+
this._filterOptions(e.target.value.toLowerCase());
|
|
232
|
+
};
|
|
205
233
|
static get observedAttributes() {
|
|
206
234
|
return ["value", "placeholder", "disabled", "searchable", "open"];
|
|
207
235
|
}
|
|
@@ -335,6 +363,7 @@ class AparteSelect extends HTMLElement {
|
|
|
335
363
|
this._updateTriggerLabel();
|
|
336
364
|
}
|
|
337
365
|
_setupMutationObserver() {
|
|
366
|
+
this._observer?.disconnect();
|
|
338
367
|
this._observer = new MutationObserver(() => {
|
|
339
368
|
this._updateDropdownContent();
|
|
340
369
|
this._updateTriggerLabel();
|
|
@@ -382,24 +411,14 @@ class AparteSelect extends HTMLElement {
|
|
|
382
411
|
this._restoreActive();
|
|
383
412
|
}
|
|
384
413
|
_setupEventListeners() {
|
|
385
|
-
this._trigger?.
|
|
386
|
-
this._trigger?.addEventListener("
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
e.preventDefault();
|
|
390
|
-
this._toggle();
|
|
391
|
-
}
|
|
392
|
-
if (e.key === "ArrowDown" && !this._isOpen) {
|
|
393
|
-
e.preventDefault();
|
|
394
|
-
this._openDropdown();
|
|
395
|
-
}
|
|
396
|
-
});
|
|
414
|
+
this._trigger?.removeEventListener("click", this._boundHandleTriggerClick);
|
|
415
|
+
this._trigger?.addEventListener("click", this._boundHandleTriggerClick);
|
|
416
|
+
this._trigger?.removeEventListener("keydown", this._boundHandleTriggerKeydown);
|
|
417
|
+
this._trigger?.addEventListener("keydown", this._boundHandleTriggerKeydown);
|
|
397
418
|
this.removeEventListener("click", this._boundHandleOptionClick);
|
|
398
419
|
this.addEventListener("click", this._boundHandleOptionClick);
|
|
399
|
-
this._searchInput?.
|
|
400
|
-
|
|
401
|
-
this._filterOptions(query);
|
|
402
|
-
});
|
|
420
|
+
this._searchInput?.removeEventListener("input", this._boundHandleSearchInput);
|
|
421
|
+
this._searchInput?.addEventListener("input", this._boundHandleSearchInput);
|
|
403
422
|
document.addEventListener("click", this._boundHandleDocumentClick);
|
|
404
423
|
document.addEventListener("keydown", this._boundHandleKeydown);
|
|
405
424
|
}
|
|
@@ -410,13 +429,14 @@ class AparteSelect extends HTMLElement {
|
|
|
410
429
|
}
|
|
411
430
|
}
|
|
412
431
|
_handleDocumentClick(e) {
|
|
413
|
-
|
|
432
|
+
const origin = eventOrigin(e);
|
|
433
|
+
if (!origin || !this.contains(origin)) {
|
|
414
434
|
this._closeDropdown();
|
|
415
435
|
}
|
|
416
436
|
}
|
|
417
437
|
_handleKeydown(e) {
|
|
418
438
|
if (!this._isOpen) return;
|
|
419
|
-
const inSearch =
|
|
439
|
+
const inSearch = activeElementIn(this) === this._searchInput;
|
|
420
440
|
switch (e.key) {
|
|
421
441
|
case "Escape":
|
|
422
442
|
e.preventDefault();
|
|
@@ -956,6 +976,13 @@ class AparteChat extends HTMLElement {
|
|
|
956
976
|
return ["placeholder", "disabled", "submit-on-enter", "center-empty", "attachments"];
|
|
957
977
|
}
|
|
958
978
|
_observer = null;
|
|
979
|
+
_loadingObserver = null;
|
|
980
|
+
/** Waits for a `<aparte-chat-viewport>` child to appear — see `_watchForViewport`. */
|
|
981
|
+
_childObserver = null;
|
|
982
|
+
/** The viewport is loading and the gate is on — see `_applyLoadingGate`. */
|
|
983
|
+
_gated = false;
|
|
984
|
+
/** The composer's `disabled` is the gate's to remove, not one the consumer set. */
|
|
985
|
+
_gateOwns = false;
|
|
959
986
|
/**
|
|
960
987
|
* True only for the composition THIS element injected. An author-provided
|
|
961
988
|
* composer (or a `framework-managed` host) is never edited by the attachments
|
|
@@ -968,10 +995,16 @@ class AparteChat extends HTMLElement {
|
|
|
968
995
|
this._forwardAttr("disabled");
|
|
969
996
|
this._forwardAttr("submit-on-enter");
|
|
970
997
|
this._syncEmptyWatch();
|
|
998
|
+
this._syncLoadingWatch();
|
|
999
|
+
this._watchForViewport();
|
|
971
1000
|
}
|
|
972
1001
|
disconnectedCallback() {
|
|
973
1002
|
this._observer?.disconnect();
|
|
974
1003
|
this._observer = null;
|
|
1004
|
+
this._loadingObserver?.disconnect();
|
|
1005
|
+
this._loadingObserver = null;
|
|
1006
|
+
this._childObserver?.disconnect();
|
|
1007
|
+
this._childObserver = null;
|
|
975
1008
|
}
|
|
976
1009
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
977
1010
|
if (oldValue === newValue) return;
|
|
@@ -983,6 +1016,10 @@ class AparteChat extends HTMLElement {
|
|
|
983
1016
|
this._syncAttachments();
|
|
984
1017
|
return;
|
|
985
1018
|
}
|
|
1019
|
+
if (name === "disabled" && newValue === null && this._gated) {
|
|
1020
|
+
this._gateOwns = true;
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
986
1023
|
const composer = this.querySelector("aparte-composer");
|
|
987
1024
|
if (!composer) return;
|
|
988
1025
|
if (newValue !== null) composer.setAttribute(name, newValue);
|
|
@@ -1057,11 +1094,70 @@ class AparteChat extends HTMLElement {
|
|
|
1057
1094
|
if (!viewport) return;
|
|
1058
1095
|
this._updateEmpty();
|
|
1059
1096
|
this._observer = new MutationObserver(() => this._updateEmpty());
|
|
1060
|
-
this._observer.observe(viewport, { childList: true, subtree: true });
|
|
1097
|
+
this._observer.observe(viewport, { childList: true, subtree: true, attributes: true, attributeFilter: ["loading"] });
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* A transcript on its way cannot take a message: while the viewport says `loading`
|
|
1101
|
+
* the composer is disabled, whatever else the chat is set to. A send in that window
|
|
1102
|
+
* raced the fetch — the controller appended to a history that `setMessages`
|
|
1103
|
+
* overwrote a moment later. Independent of `center-empty`: every chat gates.
|
|
1104
|
+
*/
|
|
1105
|
+
_syncLoadingWatch() {
|
|
1106
|
+
this._loadingObserver?.disconnect();
|
|
1107
|
+
this._loadingObserver = null;
|
|
1108
|
+
const viewport = this.querySelector("aparte-chat-viewport");
|
|
1109
|
+
if (!viewport) return;
|
|
1110
|
+
this._applyLoadingGate();
|
|
1111
|
+
this._loadingObserver = new MutationObserver(() => this._applyLoadingGate());
|
|
1112
|
+
this._loadingObserver.observe(viewport, { attributes: true, attributeFilter: ["loading"] });
|
|
1113
|
+
}
|
|
1114
|
+
/**
|
|
1115
|
+
* Both watches above need a viewport to observe, and they were installed once, at
|
|
1116
|
+
* connect, when there might not be one yet: `<aparte-chat framework-managed>` IS
|
|
1117
|
+
* Angular's host element — upgraded on insert, filled by the template afterwards — and
|
|
1118
|
+
* hand-written markup upgrades before its children are parsed. Neither ever got its
|
|
1119
|
+
* observer, so the documented "every chat gates" was inert on exactly the chats whose
|
|
1120
|
+
* transcript is fetched. This waits for the child to arrive, installs them, and stops.
|
|
1121
|
+
*/
|
|
1122
|
+
_watchForViewport() {
|
|
1123
|
+
this._childObserver?.disconnect();
|
|
1124
|
+
this._childObserver = null;
|
|
1125
|
+
if (this.querySelector("aparte-chat-viewport")) return;
|
|
1126
|
+
this._childObserver = new MutationObserver(() => {
|
|
1127
|
+
if (!this.querySelector("aparte-chat-viewport")) return;
|
|
1128
|
+
this._childObserver?.disconnect();
|
|
1129
|
+
this._childObserver = null;
|
|
1130
|
+
this._syncEmptyWatch();
|
|
1131
|
+
this._syncLoadingWatch();
|
|
1132
|
+
});
|
|
1133
|
+
this._childObserver.observe(this, { childList: true, subtree: true });
|
|
1134
|
+
}
|
|
1135
|
+
/**
|
|
1136
|
+
* The gate only ever ADDS `disabled` and only ever removes what it added: a `disabled`
|
|
1137
|
+
* the consumer set — on this element, or on a composer of their own — stays.
|
|
1138
|
+
*/
|
|
1139
|
+
_applyLoadingGate() {
|
|
1140
|
+
const composer = this.querySelector("aparte-composer");
|
|
1141
|
+
const loading = this.querySelector("aparte-chat-viewport")?.hasAttribute("loading") ?? false;
|
|
1142
|
+
if (!composer) return;
|
|
1143
|
+
if (loading && !this._gated) {
|
|
1144
|
+
this._gated = true;
|
|
1145
|
+
this._gateOwns = !composer.hasAttribute("disabled");
|
|
1146
|
+
composer.setAttribute("disabled", "");
|
|
1147
|
+
} else if (!loading && this._gated) {
|
|
1148
|
+
this._gated = false;
|
|
1149
|
+
if (this._gateOwns && !this.hasAttribute("disabled")) composer.removeAttribute("disabled");
|
|
1150
|
+
this._gateOwns = false;
|
|
1151
|
+
}
|
|
1061
1152
|
}
|
|
1153
|
+
/**
|
|
1154
|
+
* Empty means no message AND none on its way. A viewport that says `loading` holds
|
|
1155
|
+
* no bubble yet, and it used to count as empty: the welcome and the centred composer
|
|
1156
|
+
* showed while a conversation was being fetched.
|
|
1157
|
+
*/
|
|
1062
1158
|
_updateEmpty() {
|
|
1063
1159
|
const viewport = this.querySelector("aparte-chat-viewport");
|
|
1064
|
-
const empty = !viewport || !viewport.querySelector("aparte-chat-bubble");
|
|
1160
|
+
const empty = !viewport || !viewport.hasAttribute("loading") && !viewport.querySelector("aparte-chat-bubble");
|
|
1065
1161
|
this.toggleAttribute("data-empty", empty);
|
|
1066
1162
|
}
|
|
1067
1163
|
}
|
|
@@ -1507,6 +1603,11 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1507
1603
|
</div>
|
|
1508
1604
|
`;
|
|
1509
1605
|
}
|
|
1606
|
+
const messageEl = this.querySelector(".aparte-message");
|
|
1607
|
+
if (messageEl && !messageEl.hasAttribute("role")) {
|
|
1608
|
+
messageEl.setAttribute("role", "article");
|
|
1609
|
+
messageEl.setAttribute("aria-label", this._getAriaLabel());
|
|
1610
|
+
}
|
|
1510
1611
|
this._contentEl = this.querySelector(".aparte-content");
|
|
1511
1612
|
this._segmentsEl = this.querySelector(".aparte-segments");
|
|
1512
1613
|
this._attachmentsEl = this.querySelector(".aparte-attachments");
|
|
@@ -1578,7 +1679,9 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1578
1679
|
const nameEl = this.querySelector(".aparte-name");
|
|
1579
1680
|
if (message) {
|
|
1580
1681
|
message.setAttribute("data-role", this._role);
|
|
1581
|
-
message.
|
|
1682
|
+
if (message.getAttribute("role") === "article") {
|
|
1683
|
+
message.setAttribute("aria-label", this._getAriaLabel());
|
|
1684
|
+
}
|
|
1582
1685
|
}
|
|
1583
1686
|
if (avatar) {
|
|
1584
1687
|
avatar.setAttribute("data-role", this._role);
|
|
@@ -1714,7 +1817,7 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1714
1817
|
}
|
|
1715
1818
|
this._attachmentsEl.innerHTML = this._attachments.map((a4) => {
|
|
1716
1819
|
const name = escapeHtml(a4.name);
|
|
1717
|
-
if (a4.type.startsWith("image/")) {
|
|
1820
|
+
if (a4.type.startsWith("image/") && this._isShowableUrl(a4.url)) {
|
|
1718
1821
|
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>`;
|
|
1719
1822
|
}
|
|
1720
1823
|
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>`;
|
|
@@ -1742,6 +1845,24 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1742
1845
|
});
|
|
1743
1846
|
}
|
|
1744
1847
|
/** Uppercased file extension (≤4 chars), or 'FILE' when there is none. */
|
|
1848
|
+
/**
|
|
1849
|
+
* The same answer `copyAttributes` gives before it writes an `src`, plus `blob:`.
|
|
1850
|
+
*
|
|
1851
|
+
* An attachment's `url` is documented as "URL or data URI" and a storage adapter
|
|
1852
|
+
* re-mints it when a conversation is restored, so it is the app's value, not the
|
|
1853
|
+
* composer's — it was escaped and written, where every other URL core emits is
|
|
1854
|
+
* asked whether its SCHEME is allowed at all. A picture whose URL is refused is
|
|
1855
|
+
* not half-rendered: it falls back to the file chip, which still names the file.
|
|
1856
|
+
*
|
|
1857
|
+
* `blob:` is added because it is the shape core itself mints — `filesToAttachments`
|
|
1858
|
+
* calls `URL.createObjectURL`, and `AparteMessage.attachments` documents the same
|
|
1859
|
+
* on hydration. `isSafeUrl` refuses it, and rightly: that list governs MODEL
|
|
1860
|
+
* markup, which has no way to make a blob URL resolve.
|
|
1861
|
+
*/
|
|
1862
|
+
_isShowableUrl(url) {
|
|
1863
|
+
if (!url) return false;
|
|
1864
|
+
return isSafeUrl(url, "img") || /^blob:/i.test(url.trim());
|
|
1865
|
+
}
|
|
1745
1866
|
_fileExt(filename) {
|
|
1746
1867
|
const dot = filename.lastIndexOf(".");
|
|
1747
1868
|
return dot > 0 ? filename.slice(dot + 1).toUpperCase().slice(0, 4) : "FILE";
|
|
@@ -2255,7 +2376,7 @@ class AparteChatBubble extends HTMLElement {
|
|
|
2255
2376
|
}
|
|
2256
2377
|
}
|
|
2257
2378
|
this._updateWaiting();
|
|
2258
|
-
if (wasStreaming && !streaming) this.
|
|
2379
|
+
if (wasStreaming && !streaming) this._updateContent();
|
|
2259
2380
|
}
|
|
2260
2381
|
}
|
|
2261
2382
|
if (!customElements.get("aparte-chat-bubble")) {
|
|
@@ -2421,6 +2542,20 @@ class AparteChatStatus extends HTMLElement {
|
|
|
2421
2542
|
if (!customElements.get("aparte-chat-status")) {
|
|
2422
2543
|
customElements.define("aparte-chat-status", AparteChatStatus);
|
|
2423
2544
|
}
|
|
2545
|
+
function withParsedSegments(message, blocks) {
|
|
2546
|
+
if (message.role !== "assistant" || message.segments?.length) return message;
|
|
2547
|
+
if (typeof message.content !== "string" || !message.content.trim()) return message;
|
|
2548
|
+
const segments = parseMarkdownToSegments(message.content, { blocks });
|
|
2549
|
+
const onlyProse = segments.length <= 1 && segments.every((s2) => s2.type === "text");
|
|
2550
|
+
return onlyProse ? message : markDerivedSegments({ ...message, segments });
|
|
2551
|
+
}
|
|
2552
|
+
function markDerivedSegments(message) {
|
|
2553
|
+
Object.defineProperty(message, APARTE_DERIVED_SEGMENTS, { value: true, enumerable: false });
|
|
2554
|
+
return message;
|
|
2555
|
+
}
|
|
2556
|
+
function hasDerivedSegments(message) {
|
|
2557
|
+
return message[APARTE_DERIVED_SEGMENTS] === true;
|
|
2558
|
+
}
|
|
2424
2559
|
class AparteChatViewport extends HTMLElement {
|
|
2425
2560
|
// The scroll surface: an internal `.aparte-viewport-container` div (core mode)
|
|
2426
2561
|
// or the host element itself (framework-managed mode). HTMLElement covers both.
|
|
@@ -2443,6 +2578,11 @@ class AparteChatViewport extends HTMLElement {
|
|
|
2443
2578
|
_lastScrollTop = 0;
|
|
2444
2579
|
/** The scroll height at the last scroll event: how much it moved since bounds what churn can do. */
|
|
2445
2580
|
_lastScrollHeight = 0;
|
|
2581
|
+
/** When the transcript last mutated under our observer — an engine's settle adjustment follows one. */
|
|
2582
|
+
_lastMutationAt = Number.NEGATIVE_INFINITY;
|
|
2583
|
+
/** How long after a mutation an engine's settle adjustment can arrive, and how far it moves. */
|
|
2584
|
+
_settleShadowMs = 100;
|
|
2585
|
+
_settleBoundPx = 100;
|
|
2446
2586
|
/** When this component last moved `scrollTop` itself — see `_handleScroll`. "Never" is
|
|
2447
2587
|
* -Infinity rather than 0: `performance.now()` can be under a second old. */
|
|
2448
2588
|
_ownScrollAt = Number.NEGATIVE_INFINITY;
|
|
@@ -2504,7 +2644,53 @@ class AparteChatViewport extends HTMLElement {
|
|
|
2504
2644
|
*/
|
|
2505
2645
|
_frameworkManagedDOM = false;
|
|
2506
2646
|
static get observedAttributes() {
|
|
2507
|
-
return ["scroll-threshold", "max-rendered-bubbles"];
|
|
2647
|
+
return ["scroll-threshold", "max-rendered-bubbles", "loading"];
|
|
2648
|
+
}
|
|
2649
|
+
/**
|
|
2650
|
+
* The transcript is on its way: a conversation was chosen and its messages are
|
|
2651
|
+
* being fetched. Two skeleton turns stand where they will be, the scroll surface
|
|
2652
|
+
* is `aria-busy`, and a visually hidden line names the wait. Reflected as the
|
|
2653
|
+
* `loading` attribute — `AparteConversationController` sets it through its
|
|
2654
|
+
* binding; a loop of your own sets it by hand. Not the same thing as `data-busy`,
|
|
2655
|
+
* which is a reply streaming into a transcript that is already there.
|
|
2656
|
+
*/
|
|
2657
|
+
get loading() {
|
|
2658
|
+
return this.hasAttribute("loading");
|
|
2659
|
+
}
|
|
2660
|
+
set loading(value) {
|
|
2661
|
+
this.toggleAttribute("loading", presenceOn(value));
|
|
2662
|
+
}
|
|
2663
|
+
setLoading(loading) {
|
|
2664
|
+
this.loading = loading;
|
|
2665
|
+
}
|
|
2666
|
+
/** Draw or remove the wait, from the attribute. Safe before the structure exists. */
|
|
2667
|
+
_syncLoading() {
|
|
2668
|
+
const container = this._container;
|
|
2669
|
+
if (!container) return;
|
|
2670
|
+
const on = this.hasAttribute("loading");
|
|
2671
|
+
container.setAttribute("aria-busy", on ? "true" : "false");
|
|
2672
|
+
if (this._frameworkManagedDOM) return;
|
|
2673
|
+
const existing = container.querySelector(":scope > .aparte-viewport-loading");
|
|
2674
|
+
if (!on) {
|
|
2675
|
+
existing?.remove();
|
|
2676
|
+
container.querySelector(":scope > .aparte-viewport-loading-status")?.remove();
|
|
2677
|
+
return;
|
|
2678
|
+
}
|
|
2679
|
+
if (existing) return;
|
|
2680
|
+
const cfg = resolveConfig(this);
|
|
2681
|
+
const skeleton = document.createElement("div");
|
|
2682
|
+
skeleton.className = "aparte-viewport-loading";
|
|
2683
|
+
skeleton.setAttribute("aria-hidden", "true");
|
|
2684
|
+
skeleton.innerHTML = '<span class="aparte-skeleton aparte-skeleton--rect aparte-viewport-loading__user"></span><span class="aparte-skeleton aparte-skeleton--text"></span><span class="aparte-skeleton aparte-skeleton--text"></span><span class="aparte-skeleton aparte-skeleton--text"></span><span class="aparte-skeleton aparte-skeleton--text aparte-viewport-loading__last"></span>';
|
|
2685
|
+
const status = document.createElement("span");
|
|
2686
|
+
status.className = "aparte-viewport-loading-status aparte-sr-only";
|
|
2687
|
+
status.setAttribute("role", "status");
|
|
2688
|
+
container.prepend(skeleton, status);
|
|
2689
|
+
const say = () => {
|
|
2690
|
+
if (status.isConnected) status.textContent = cfg.t("loadingConversation");
|
|
2691
|
+
};
|
|
2692
|
+
if (typeof requestAnimationFrame === "function") requestAnimationFrame(say);
|
|
2693
|
+
else say();
|
|
2508
2694
|
}
|
|
2509
2695
|
constructor() {
|
|
2510
2696
|
super();
|
|
@@ -2589,6 +2775,9 @@ class AparteChatViewport extends HTMLElement {
|
|
|
2589
2775
|
this._maxRenderedBubbles = parseInt(newValue || "1000", 10);
|
|
2590
2776
|
this._pruneRenderedBubbles();
|
|
2591
2777
|
break;
|
|
2778
|
+
case "loading":
|
|
2779
|
+
this._syncLoading();
|
|
2780
|
+
break;
|
|
2592
2781
|
}
|
|
2593
2782
|
}
|
|
2594
2783
|
/**
|
|
@@ -2670,12 +2859,32 @@ class AparteChatViewport extends HTMLElement {
|
|
|
2670
2859
|
if (streaming !== null && streaming !== head) return null;
|
|
2671
2860
|
return head;
|
|
2672
2861
|
}
|
|
2673
|
-
/**
|
|
2862
|
+
/**
|
|
2863
|
+
* The id of the message currently streaming, if any — searched across the whole
|
|
2864
|
+
* TREE, and answering with an OFF-HEAD one first when there are two.
|
|
2865
|
+
*
|
|
2866
|
+
* It used to scan `getMessages()`, the active path. A retry or an edit on an
|
|
2867
|
+
* earlier bubble calls `addSiblingOf`, which switches the branch to the new pending
|
|
2868
|
+
* reply: the turn still being written leaves the path, the scan found nothing, and
|
|
2869
|
+
* the rest of that turn was appended to the message that had just replaced it.
|
|
2870
|
+
*
|
|
2871
|
+
* A new pending reply is itself marked streaming, so "the first one found" would
|
|
2872
|
+
* answer the head and let the write through — the very case this exists to refuse.
|
|
2873
|
+
* Answering the off-head one instead makes `_activeMessageId` refuse while ANY turn
|
|
2874
|
+
* elsewhere in the tree is still open. That also refuses the new turn's own first
|
|
2875
|
+
* deltas for as long as the superseded one has not settled, which is the trade this
|
|
2876
|
+
* whole path already makes: losing the tail is visible, writing it onto someone
|
|
2877
|
+
* else's message is not.
|
|
2878
|
+
*/
|
|
2674
2879
|
_streamingMessageId() {
|
|
2675
|
-
|
|
2676
|
-
|
|
2880
|
+
const head = this._repo.headId;
|
|
2881
|
+
let atHead = null;
|
|
2882
|
+
for (const message of this._repo.getAllMessages()) {
|
|
2883
|
+
if (!message.isStreaming) continue;
|
|
2884
|
+
if (message.id !== head) return message.id;
|
|
2885
|
+
atHead = message.id;
|
|
2677
2886
|
}
|
|
2678
|
-
return
|
|
2887
|
+
return atHead;
|
|
2679
2888
|
}
|
|
2680
2889
|
/**
|
|
2681
2890
|
* While a reply streams, the transcript is read-only except for Stop.
|
|
@@ -2817,6 +3026,12 @@ class AparteChatViewport extends HTMLElement {
|
|
|
2817
3026
|
/**
|
|
2818
3027
|
* Atomic update for a message by ID
|
|
2819
3028
|
* Supports updating content, status, segments, and other metadata
|
|
3029
|
+
*
|
|
3030
|
+
* An id the tree does not hold is a silent no-op, deliberately: the message may have
|
|
3031
|
+
* been truncated away by an edit while a turn was still writing to it, and inventing
|
|
3032
|
+
* it back would put a message the reader deleted on screen. It does mean a late error
|
|
3033
|
+
* card can have nowhere to land — `aparte-message-error` still fires, so a host that
|
|
3034
|
+
* wants to show one has the event.
|
|
2820
3035
|
*/
|
|
2821
3036
|
updateMessage(messageId, updates) {
|
|
2822
3037
|
const message = this._repo.getMessageById(messageId);
|
|
@@ -2855,6 +3070,7 @@ class AparteChatViewport extends HTMLElement {
|
|
|
2855
3070
|
* the framework owns the DOM and will create the bubble element itself.
|
|
2856
3071
|
*/
|
|
2857
3072
|
appendMessage(message, options) {
|
|
3073
|
+
message = withParsedSegments(message, resolveConfig(this).getStreamBlocks());
|
|
2858
3074
|
const stored = options?.historical ? adoptMessageSegments(message) : message.segments?.length ? { ...message, segments: message.segments.reduce(
|
|
2859
3075
|
(acc, segment) => {
|
|
2860
3076
|
acc.push(stampSegmentOnInsert(
|
|
@@ -2868,6 +3084,7 @@ class AparteChatViewport extends HTMLElement {
|
|
|
2868
3084
|
[]
|
|
2869
3085
|
) } : { ...message };
|
|
2870
3086
|
if (!options?.historical && isAwaitingReply(message)) stored.isStreaming = true;
|
|
3087
|
+
if (hasDerivedSegments(message)) markDerivedSegments(stored);
|
|
2871
3088
|
this._repo.addOrUpdateMessage(this._repo.headId, stored);
|
|
2872
3089
|
if (!this._frameworkManagedDOM) {
|
|
2873
3090
|
const wrapper = this.querySelector(".aparte-messages-wrapper");
|
|
@@ -3050,12 +3267,15 @@ class AparteChatViewport extends HTMLElement {
|
|
|
3050
3267
|
*/
|
|
3051
3268
|
importTree(tree) {
|
|
3052
3269
|
this.clearAll({ revokeAttachments: false });
|
|
3270
|
+
const blocks = resolveConfig(this).getStreamBlocks();
|
|
3053
3271
|
this._repo.import({
|
|
3054
3272
|
...tree,
|
|
3055
|
-
messages: tree.messages.map((entry) =>
|
|
3056
|
-
|
|
3057
|
-
message
|
|
3058
|
-
|
|
3273
|
+
messages: tree.messages.map((entry) => {
|
|
3274
|
+
const parsed = withParsedSegments(entry.message, blocks);
|
|
3275
|
+
const message = adoptMessageSegments(parsed);
|
|
3276
|
+
if (hasDerivedSegments(parsed)) markDerivedSegments(message);
|
|
3277
|
+
return { ...entry, message };
|
|
3278
|
+
})
|
|
3059
3279
|
});
|
|
3060
3280
|
this._reRenderActivePath();
|
|
3061
3281
|
}
|
|
@@ -3070,7 +3290,7 @@ class AparteChatViewport extends HTMLElement {
|
|
|
3070
3290
|
*/
|
|
3071
3291
|
clearAll(options) {
|
|
3072
3292
|
if (options?.revokeAttachments !== false) {
|
|
3073
|
-
for (const message of this._repo.
|
|
3293
|
+
for (const message of this._repo.getAllMessages()) {
|
|
3074
3294
|
revokeAttachmentUrls(message.attachments);
|
|
3075
3295
|
}
|
|
3076
3296
|
}
|
|
@@ -3292,6 +3512,7 @@ class AparteChatViewport extends HTMLElement {
|
|
|
3292
3512
|
container.appendChild(wrapper);
|
|
3293
3513
|
this.appendChild(container);
|
|
3294
3514
|
this._container = container;
|
|
3515
|
+
this._syncLoading();
|
|
3295
3516
|
this._scrollBtn = document.createElement("button");
|
|
3296
3517
|
this._scrollBtn.className = "aparte-btn aparte-btn--surface aparte-btn--circle aparte-btn--lg aparte-scroll-btn aparte-scroll-btn--hidden";
|
|
3297
3518
|
this._scrollBtn.setAttribute("type", "button");
|
|
@@ -3382,6 +3603,19 @@ class AparteChatViewport extends HTMLElement {
|
|
|
3382
3603
|
* 1 run in 6: the rebuild's height churn moved scrollTop within the second after
|
|
3383
3604
|
* the click, and the click made that look like the reader's).
|
|
3384
3605
|
*/
|
|
3606
|
+
/**
|
|
3607
|
+
* Is a pointer pressed on the surface right now? A press on the TEXT is not a scroll
|
|
3608
|
+
* gesture (`_noteReaderInput` counts only the gutter), but a hand that is down and
|
|
3609
|
+
* dragging a selection past the edge scrolls the container all the same — and that
|
|
3610
|
+
* decrease is the reader's. Held from the press to its release, wherever it lands.
|
|
3611
|
+
*/
|
|
3612
|
+
_pointerHeld = false;
|
|
3613
|
+
_holdPointer = () => {
|
|
3614
|
+
this._pointerHeld = true;
|
|
3615
|
+
};
|
|
3616
|
+
_releasePointer = () => {
|
|
3617
|
+
this._pointerHeld = false;
|
|
3618
|
+
};
|
|
3385
3619
|
_noteReaderInput = (e) => {
|
|
3386
3620
|
if (e.type === "keydown" && !this._scrollKeys.has(e.key)) return;
|
|
3387
3621
|
if (e.type === "pointerdown") {
|
|
@@ -3414,6 +3648,9 @@ class AparteChatViewport extends HTMLElement {
|
|
|
3414
3648
|
for (const type of ["wheel", "touchmove", "pointerdown", "keydown"]) {
|
|
3415
3649
|
this._container?.addEventListener(type, this._noteReaderInput, { passive: true });
|
|
3416
3650
|
}
|
|
3651
|
+
this._container?.addEventListener("pointerdown", this._holdPointer, { passive: true });
|
|
3652
|
+
document.addEventListener("pointerup", this._releasePointer, true);
|
|
3653
|
+
document.addEventListener("pointercancel", this._releasePointer, true);
|
|
3417
3654
|
this._scrollBtn?.addEventListener("click", this._onScrollBtnClick);
|
|
3418
3655
|
this.addEventListener("aparte-branch-navigate", this._onBranchNavigate);
|
|
3419
3656
|
}
|
|
@@ -3446,6 +3683,7 @@ class AparteChatViewport extends HTMLElement {
|
|
|
3446
3683
|
this._updateOverlayInset();
|
|
3447
3684
|
}
|
|
3448
3685
|
this._mutationObserver = new MutationObserver((mutations) => {
|
|
3686
|
+
this._lastMutationAt = performance.now();
|
|
3449
3687
|
if (this._frameworkManagedDOM) this._keepScrollButtonLast();
|
|
3450
3688
|
if (this._isAutoScrollEnabled && !this._gliding()) {
|
|
3451
3689
|
const added = mutations.flatMap((m2) => Array.from(m2.addedNodes)).filter((n2) => n2 instanceof Element && n2.tagName === "APARTE-CHAT-BUBBLE");
|
|
@@ -3547,7 +3785,8 @@ class AparteChatViewport extends HTMLElement {
|
|
|
3547
3785
|
const drop = this._lastScrollTop - top;
|
|
3548
3786
|
const churn = Math.abs(height - this._lastScrollHeight);
|
|
3549
3787
|
const now = performance.now();
|
|
3550
|
-
const
|
|
3788
|
+
const afterMutation = now - this._lastMutationAt < this._settleShadowMs && drop <= this._settleBoundPx;
|
|
3789
|
+
const settlingOurs = now - this._ownScrollAt < this._ownScrollWindowMs && now - this._readerInputAt >= this._ownScrollWindowMs && !this._pointerHeld && (drop <= churn + 2 || afterMutation);
|
|
3551
3790
|
const readerWentUp = drop > 1 && !settlingOurs;
|
|
3552
3791
|
const readerHandOnIt = drop > 1 && now - this._readerInputAt < this._ownScrollWindowMs;
|
|
3553
3792
|
this._lastScrollTop = top;
|
|
@@ -3788,6 +4027,10 @@ class AparteChatViewport extends HTMLElement {
|
|
|
3788
4027
|
for (const type of ["wheel", "touchmove", "pointerdown", "keydown"]) {
|
|
3789
4028
|
this._container?.removeEventListener(type, this._noteReaderInput);
|
|
3790
4029
|
}
|
|
4030
|
+
this._container?.removeEventListener("pointerdown", this._holdPointer);
|
|
4031
|
+
document.removeEventListener("pointerup", this._releasePointer, true);
|
|
4032
|
+
document.removeEventListener("pointercancel", this._releasePointer, true);
|
|
4033
|
+
this._pointerHeld = false;
|
|
3791
4034
|
this._scrollBtn?.removeEventListener("click", this._onScrollBtnClick);
|
|
3792
4035
|
this.removeEventListener("aparte-branch-navigate", this._onBranchNavigate);
|
|
3793
4036
|
this._resizeObserver?.disconnect();
|
|
@@ -3934,6 +4177,16 @@ class AparteComposer extends HTMLElement {
|
|
|
3934
4177
|
get disabled() {
|
|
3935
4178
|
return this.hasAttribute("disabled");
|
|
3936
4179
|
}
|
|
4180
|
+
/**
|
|
4181
|
+
* The attribute is the state, so the property writes it. It has to exist: React 19
|
|
4182
|
+
* and Svelte assign the PROPERTY whenever the element has one of that name, and a
|
|
4183
|
+
* getter alone made `<aparte-composer disabled="">` — a spelling the wrappers type
|
|
4184
|
+
* as valid — throw and take the render down. `presenceOn` because `''` is what
|
|
4185
|
+
* those templates hand a presence property (#62).
|
|
4186
|
+
*/
|
|
4187
|
+
set disabled(value) {
|
|
4188
|
+
this.toggleAttribute("disabled", presenceOn(value));
|
|
4189
|
+
}
|
|
3937
4190
|
/**
|
|
3938
4191
|
* When false, Shift+Enter submits and a bare Enter inserts a newline —
|
|
3939
4192
|
* the inverse of the default. Driven by the `submit-on-enter` attribute.
|
|
@@ -3947,6 +4200,11 @@ class AparteComposer extends HTMLElement {
|
|
|
3947
4200
|
get placeholder() {
|
|
3948
4201
|
return this.getAttribute("placeholder") ?? "";
|
|
3949
4202
|
}
|
|
4203
|
+
/** Writes the attribute the inputs read (see `disabled` for why the setter exists). */
|
|
4204
|
+
set placeholder(value) {
|
|
4205
|
+
if (value === null || value === void 0) this.removeAttribute("placeholder");
|
|
4206
|
+
else this.setAttribute("placeholder", String(value));
|
|
4207
|
+
}
|
|
3950
4208
|
get targetId() {
|
|
3951
4209
|
return this.getAttribute("target");
|
|
3952
4210
|
}
|
|
@@ -4287,13 +4545,35 @@ class AparteComposer extends HTMLElement {
|
|
|
4287
4545
|
}
|
|
4288
4546
|
return void 0;
|
|
4289
4547
|
}
|
|
4548
|
+
/**
|
|
4549
|
+
* The message this composer's turn is writing, when the event that started it said.
|
|
4550
|
+
* Kept so the END of a DIFFERENT turn cannot be read as the end of this one — see
|
|
4551
|
+
* `_isForThisTurn`.
|
|
4552
|
+
*/
|
|
4553
|
+
_streamingMessageId = null;
|
|
4554
|
+
/**
|
|
4555
|
+
* Whether a terminal event ends the turn this composer is following.
|
|
4556
|
+
*
|
|
4557
|
+
* `_isForThisComposer` answers "is this chat's?", and both turns of a retry belong
|
|
4558
|
+
* to the same chat: the client aborts the superseded reply, the engine emits
|
|
4559
|
+
* `run-aborted` for it, and that unwind turned Stop back into Send while the new
|
|
4560
|
+
* reply was still streaming — and evicted the open panel, i.e. the approval question
|
|
4561
|
+
* a tool was still waiting on. An event with no `messageId` still ends the turn: a
|
|
4562
|
+
* loop of one's own saying "done" has always meant this one.
|
|
4563
|
+
*/
|
|
4564
|
+
_isForThisTurn(e) {
|
|
4565
|
+
const id = e.detail?.messageId;
|
|
4566
|
+
return !id || this._streamingMessageId === null || id === this._streamingMessageId;
|
|
4567
|
+
}
|
|
4290
4568
|
_handleMessageStart(e) {
|
|
4291
4569
|
if (!this._isForThisComposer(e)) return;
|
|
4570
|
+
this._streamingMessageId = e.detail?.messageId ?? null;
|
|
4292
4571
|
this._streaming = true;
|
|
4293
4572
|
this._emit("streaming-change", { streaming: true });
|
|
4294
4573
|
}
|
|
4295
4574
|
_handleMessageDone(e) {
|
|
4296
|
-
if (!this._isForThisComposer(e)) return;
|
|
4575
|
+
if (!this._isForThisComposer(e) || !this._isForThisTurn(e)) return;
|
|
4576
|
+
this._streamingMessageId = null;
|
|
4297
4577
|
this._streaming = false;
|
|
4298
4578
|
this._emit("streaming-change", { streaming: false });
|
|
4299
4579
|
if (this._panelActive) this._evictPanel();
|
|
@@ -5080,8 +5360,10 @@ class AparteComposerToolbar extends HTMLElement {
|
|
|
5080
5360
|
_observer = null;
|
|
5081
5361
|
connectedCallback() {
|
|
5082
5362
|
this._syncEmpty();
|
|
5083
|
-
this._observer ??= new MutationObserver(() =>
|
|
5084
|
-
|
|
5363
|
+
this._observer ??= new MutationObserver((records) => {
|
|
5364
|
+
if (records.some((r) => !(r.type === "attributes" && r.target === this))) this._syncEmpty();
|
|
5365
|
+
});
|
|
5366
|
+
this._observer.observe(this, { childList: true, subtree: true, attributes: true, attributeFilter: ["hidden", "data-empty"] });
|
|
5085
5367
|
}
|
|
5086
5368
|
disconnectedCallback() {
|
|
5087
5369
|
this._observer?.disconnect();
|
|
@@ -5101,7 +5383,10 @@ class AparteComposerToolbar extends HTMLElement {
|
|
|
5101
5383
|
* not empty, and hiding it would be a twenty-minute mystery for whoever wrote it.
|
|
5102
5384
|
*/
|
|
5103
5385
|
_syncEmpty() {
|
|
5104
|
-
const
|
|
5386
|
+
const visibleChild = Array.from(this.children).some((c2) => !c2.hasAttribute("hidden") && !c2.hasAttribute("data-empty"));
|
|
5387
|
+
const ownText = Array.from(this.childNodes).some((n2) => n2.nodeType === Node.TEXT_NODE && (n2.textContent ?? "").trim() !== "");
|
|
5388
|
+
const hasContent = visibleChild || ownText;
|
|
5389
|
+
if (hasContent === !this.hasAttribute("data-empty")) return;
|
|
5105
5390
|
if (hasContent) this.removeAttribute("data-empty");
|
|
5106
5391
|
else this.setAttribute("data-empty", "");
|
|
5107
5392
|
}
|
|
@@ -5109,6 +5394,9 @@ class AparteComposerToolbar extends HTMLElement {
|
|
|
5109
5394
|
if (!customElements.get("aparte-composer-toolbar")) {
|
|
5110
5395
|
customElements.define("aparte-composer-toolbar", AparteComposerToolbar);
|
|
5111
5396
|
}
|
|
5397
|
+
function rowSelector(attr, id) {
|
|
5398
|
+
return `[${attr}="${cssEscape(id)}"]`;
|
|
5399
|
+
}
|
|
5112
5400
|
const DAY = 864e5;
|
|
5113
5401
|
const MENU_GAP = 4;
|
|
5114
5402
|
const VIEWPORT_MARGIN = 8;
|
|
@@ -5117,8 +5405,37 @@ class AparteConversationList extends HTMLElement {
|
|
|
5117
5405
|
_activeId = null;
|
|
5118
5406
|
_open = null;
|
|
5119
5407
|
_renaming = null;
|
|
5408
|
+
_warnedNoManager = false;
|
|
5120
5409
|
static get observedAttributes() {
|
|
5121
|
-
return ["active-id", "
|
|
5410
|
+
return ["active-id", "flat", "loading", "manage"];
|
|
5411
|
+
}
|
|
5412
|
+
/**
|
|
5413
|
+
* The list is on its way: rows of the kit's skeleton stand where the rows will be,
|
|
5414
|
+
* the element is `aria-busy`, and a visually hidden line names the wait. Reflected
|
|
5415
|
+
* as the `loading` attribute, so a site sets it while its store answers and clears
|
|
5416
|
+
* it when `conversations` is set. A list that is not there yet is not an empty list.
|
|
5417
|
+
*/
|
|
5418
|
+
get loading() {
|
|
5419
|
+
return this.hasAttribute("loading");
|
|
5420
|
+
}
|
|
5421
|
+
set loading(value) {
|
|
5422
|
+
this.toggleAttribute("loading", presenceOn(value));
|
|
5423
|
+
}
|
|
5424
|
+
/**
|
|
5425
|
+
* Let the list finish the gesture through the registered conversation manager.
|
|
5426
|
+
* Reflected as the `manage` attribute.
|
|
5427
|
+
*
|
|
5428
|
+
* The property is not a nicety here: this is the attribute that authorises a WRITE,
|
|
5429
|
+
* and Vue and both Sveltes stringify a bound `false` onto an element that has no
|
|
5430
|
+
* property of that name — `manage="false"`, which `hasAttribute` reads as on. So
|
|
5431
|
+
* turning management off turned it on, and a delete the host meant to handle itself
|
|
5432
|
+
* was also done by the list. `presenceOn`, like `loading` above it.
|
|
5433
|
+
*/
|
|
5434
|
+
get manage() {
|
|
5435
|
+
return this.hasAttribute("manage");
|
|
5436
|
+
}
|
|
5437
|
+
set manage(value) {
|
|
5438
|
+
this.toggleAttribute("manage", presenceOn(value));
|
|
5122
5439
|
}
|
|
5123
5440
|
// ─── Lifecycle ────────────────────────────────────────────────────────
|
|
5124
5441
|
connectedCallback() {
|
|
@@ -5146,7 +5463,7 @@ class AparteConversationList extends HTMLElement {
|
|
|
5146
5463
|
if (name === "active-id") {
|
|
5147
5464
|
this._activeId = newValue;
|
|
5148
5465
|
this._updateActiveState();
|
|
5149
|
-
} else if (name === "
|
|
5466
|
+
} else if ((name === "flat" || name === "loading") && this.isConnected) {
|
|
5150
5467
|
this._render();
|
|
5151
5468
|
}
|
|
5152
5469
|
}
|
|
@@ -5174,7 +5491,42 @@ class AparteConversationList extends HTMLElement {
|
|
|
5174
5491
|
_render() {
|
|
5175
5492
|
this._closeMenu();
|
|
5176
5493
|
this._renaming = null;
|
|
5494
|
+
const focused = activeElementIn(this);
|
|
5495
|
+
const held = focused && this.contains(focused) ? focused.closest("[data-select-id], [data-more-id]") : null;
|
|
5496
|
+
const restore = held?.dataset["selectId"] ? rowSelector("data-select-id", held.dataset["selectId"]) : held?.dataset["moreId"] ? rowSelector("data-more-id", held.dataset["moreId"]) : null;
|
|
5497
|
+
const heldRow = held?.closest("[data-conv-id]") ?? null;
|
|
5498
|
+
const heldIndex = heldRow ? Array.from(this.querySelectorAll("[data-conv-id]")).indexOf(heldRow) : -1;
|
|
5499
|
+
const loading = this.hasAttribute("loading");
|
|
5500
|
+
this.setAttribute("aria-busy", loading ? "true" : "false");
|
|
5501
|
+
if (loading) {
|
|
5502
|
+
const row = '<span class="aparte-skeleton aparte-skeleton--text"></span>';
|
|
5503
|
+
this.innerHTML = '<div class="aparte-conv-list-loading" aria-hidden="true">' + row + row + row + row + row + row + '</div><span class="aparte-conv-list-loading-status aparte-sr-only" role="status"></span>';
|
|
5504
|
+
const status = this.querySelector(".aparte-conv-list-loading-status");
|
|
5505
|
+
const say = () => {
|
|
5506
|
+
if (status?.isConnected) status.textContent = this._t("loadingConversations");
|
|
5507
|
+
};
|
|
5508
|
+
if (typeof requestAnimationFrame === "function") requestAnimationFrame(say);
|
|
5509
|
+
else say();
|
|
5510
|
+
return;
|
|
5511
|
+
}
|
|
5177
5512
|
this.innerHTML = this._groups().map((g2) => this._renderGroup(g2)).join("");
|
|
5513
|
+
if (!restore) return;
|
|
5514
|
+
const back = this.querySelector(restore);
|
|
5515
|
+
if (back) {
|
|
5516
|
+
back.focus();
|
|
5517
|
+
return;
|
|
5518
|
+
}
|
|
5519
|
+
const rows = Array.from(this.querySelectorAll("[data-select-id]"));
|
|
5520
|
+
const next = heldIndex >= 0 ? rows[heldIndex] ?? rows[rows.length - 1] : null;
|
|
5521
|
+
if (next) {
|
|
5522
|
+
next.focus();
|
|
5523
|
+
return;
|
|
5524
|
+
}
|
|
5525
|
+
if (!this.hasAttribute("tabindex")) {
|
|
5526
|
+
this.setAttribute("tabindex", "-1");
|
|
5527
|
+
this.addEventListener("blur", () => this.removeAttribute("tabindex"), { once: true });
|
|
5528
|
+
}
|
|
5529
|
+
this.focus();
|
|
5178
5530
|
}
|
|
5179
5531
|
_t(key) {
|
|
5180
5532
|
const locale = resolveConfig(this).getLocale();
|
|
@@ -5183,12 +5535,12 @@ class AparteConversationList extends HTMLElement {
|
|
|
5183
5535
|
/**
|
|
5184
5536
|
* Pinned first, then the calendar buckets every chat product uses, then a heading
|
|
5185
5537
|
* per month (newest first), then whatever carries no date. Flat when nothing is
|
|
5186
|
-
* dated or the host asked for `
|
|
5538
|
+
* dated or the host asked for `flat`: a heading over the only group would say
|
|
5187
5539
|
* nothing.
|
|
5188
5540
|
*/
|
|
5189
5541
|
_groups() {
|
|
5190
5542
|
const items = this._conversations;
|
|
5191
|
-
if (this.hasAttribute("
|
|
5543
|
+
if (this.hasAttribute("flat") || !items.some((c2) => typeof c2.updatedAt === "number")) {
|
|
5192
5544
|
return [{ key: "all", label: null, items }];
|
|
5193
5545
|
}
|
|
5194
5546
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -5291,12 +5643,16 @@ class AparteConversationList extends HTMLElement {
|
|
|
5291
5643
|
<div class="aparte-menu__separator" role="separator"></div>
|
|
5292
5644
|
<button type="button" class="aparte-menu__item aparte-conv-menu__item--danger" role="menuitem" data-menu-action="delete">${trashGlyph}<span>${remove}</span></button>`;
|
|
5293
5645
|
}
|
|
5646
|
+
/** The question the confirm step asks — the popover's accessible name while it asks it. */
|
|
5647
|
+
_confirmQuestion(conv) {
|
|
5648
|
+
return this._t("deleteConversationConfirm").replace("{title}", conv.title || this._t("newChat"));
|
|
5649
|
+
}
|
|
5294
5650
|
_confirmMarkup(conv) {
|
|
5295
|
-
const question = escapeHtml(this.
|
|
5651
|
+
const question = escapeHtml(this._confirmQuestion(conv));
|
|
5296
5652
|
const cancel = escapeHtml(this._t("cancel"));
|
|
5297
5653
|
const remove = escapeHtml(this._t("deleteConversation"));
|
|
5298
5654
|
return `
|
|
5299
|
-
<div class="aparte-conv-menu__confirm"
|
|
5655
|
+
<div class="aparte-conv-menu__confirm">
|
|
5300
5656
|
<p class="aparte-conv-menu__question">${question}</p>
|
|
5301
5657
|
<div class="aparte-conv-menu__actions">
|
|
5302
5658
|
<button type="button" class="aparte-btn aparte-btn--sm aparte-btn--ghost" data-menu-action="cancel">${cancel}</button>
|
|
@@ -5320,6 +5676,8 @@ class AparteConversationList extends HTMLElement {
|
|
|
5320
5676
|
this._placeMenu(button, menu);
|
|
5321
5677
|
menu.querySelector('[role="menuitem"]')?.focus();
|
|
5322
5678
|
document.addEventListener("pointerdown", this._onDocumentPointerDown, true);
|
|
5679
|
+
document.addEventListener("pointerup", this._onDocumentPointerUp, true);
|
|
5680
|
+
document.addEventListener("pointercancel", this._onDocumentPointerUp, true);
|
|
5323
5681
|
window.addEventListener("scroll", this._onWindowScroll, true);
|
|
5324
5682
|
window.addEventListener("resize", this._onWindowScroll);
|
|
5325
5683
|
}
|
|
@@ -5350,14 +5708,29 @@ class AparteConversationList extends HTMLElement {
|
|
|
5350
5708
|
open.menu.remove();
|
|
5351
5709
|
open.button.setAttribute("aria-expanded", "false");
|
|
5352
5710
|
document.removeEventListener("pointerdown", this._onDocumentPointerDown, true);
|
|
5711
|
+
document.removeEventListener("pointerup", this._onDocumentPointerUp, true);
|
|
5712
|
+
document.removeEventListener("pointercancel", this._onDocumentPointerUp, true);
|
|
5713
|
+
this._pressInMenu = false;
|
|
5353
5714
|
window.removeEventListener("scroll", this._onWindowScroll, true);
|
|
5354
5715
|
window.removeEventListener("resize", this._onWindowScroll);
|
|
5355
5716
|
if (returnFocus && open.button.isConnected) open.button.focus();
|
|
5356
5717
|
}
|
|
5718
|
+
/**
|
|
5719
|
+
* A pointer is down inside the open menu. WebKit does not move the focus to a
|
|
5720
|
+
* button on mousedown: the element that held it is blurred instead — `focusout`
|
|
5721
|
+
* with no `relatedTarget` — before the click reaches the item. `_onFocusOut` used
|
|
5722
|
+
* to read that as the focus leaving the menu and close it on the press, so with a
|
|
5723
|
+
* mouse no item could be chosen at all. Held from the press to its release.
|
|
5724
|
+
*/
|
|
5725
|
+
_pressInMenu = false;
|
|
5726
|
+
_onDocumentPointerUp = () => {
|
|
5727
|
+
this._pressInMenu = false;
|
|
5728
|
+
};
|
|
5357
5729
|
_onDocumentPointerDown = (e) => {
|
|
5358
5730
|
const open = this._open;
|
|
5359
5731
|
if (!open) return;
|
|
5360
|
-
const target = e
|
|
5732
|
+
const target = eventOrigin(e);
|
|
5733
|
+
this._pressInMenu = !!target && open.menu.contains(target);
|
|
5361
5734
|
if (open.menu.contains(target) || open.button.contains(target)) return;
|
|
5362
5735
|
this._closeMenu();
|
|
5363
5736
|
};
|
|
@@ -5368,9 +5741,37 @@ class AparteConversationList extends HTMLElement {
|
|
|
5368
5741
|
const open = this._open;
|
|
5369
5742
|
if (!open) return;
|
|
5370
5743
|
const next = e.relatedTarget;
|
|
5744
|
+
if (!next && this._pressInMenu) return;
|
|
5371
5745
|
if (next && (open.menu.contains(next) || open.button.contains(next))) return;
|
|
5372
5746
|
if (open.menu.contains(e.target)) this._closeMenu();
|
|
5373
5747
|
};
|
|
5748
|
+
/**
|
|
5749
|
+
* The gesture, finished. The event has already gone out — cancelable, so a host
|
|
5750
|
+
* that wants the write for itself calls `preventDefault()` and this stands down —
|
|
5751
|
+
* and what is left is the registered conversation manager doing it. Without
|
|
5752
|
+
* `manage` the element behaves exactly as it did before the attribute existed.
|
|
5753
|
+
*
|
|
5754
|
+
* A `manage` with no manager registered is a menu that does nothing, so the first
|
|
5755
|
+
* gesture says so and names the call that fixes it. Said here rather than at
|
|
5756
|
+
* `connectedCallback` because a list in the page's HTML connects before the script
|
|
5757
|
+
* that registers the manager runs: a warning at connect would fire on a setup that
|
|
5758
|
+
* is perfectly correct one tick later.
|
|
5759
|
+
*/
|
|
5760
|
+
_manage(event, run) {
|
|
5761
|
+
if (!this.hasAttribute("manage") || event.defaultPrevented) return;
|
|
5762
|
+
const manager = resolveConfig(this).getConversationManager();
|
|
5763
|
+
if (!manager) {
|
|
5764
|
+
if (this._warnedNoManager) return;
|
|
5765
|
+
this._warnedNoManager = true;
|
|
5766
|
+
console.warn(
|
|
5767
|
+
"[aparte-conversation-list] `manage` is set but no conversation manager is registered, so the row menu writes nothing. Call setConversationManager(manager) on your config, or drop `manage` and handle the events yourself."
|
|
5768
|
+
);
|
|
5769
|
+
return;
|
|
5770
|
+
}
|
|
5771
|
+
void run(manager).catch((err) => {
|
|
5772
|
+
console.warn("[aparte-conversation-list] the conversation manager refused the change:", err);
|
|
5773
|
+
});
|
|
5774
|
+
}
|
|
5374
5775
|
_onMenuAction(action) {
|
|
5375
5776
|
const open = this._open;
|
|
5376
5777
|
if (!open) return;
|
|
@@ -5392,46 +5793,64 @@ class AparteConversationList extends HTMLElement {
|
|
|
5392
5793
|
case "pin":
|
|
5393
5794
|
this._closeMenu(true);
|
|
5394
5795
|
if (conv.pinnedAt) {
|
|
5395
|
-
|
|
5396
|
-
"aparte-unpin
|
|
5397
|
-
{ detail: { id }, bubbles: true, composed: true }
|
|
5398
|
-
)
|
|
5796
|
+
const unpin = new CustomEvent(
|
|
5797
|
+
"aparte-conversation-unpin",
|
|
5798
|
+
{ detail: { id }, bubbles: true, composed: true, cancelable: true }
|
|
5799
|
+
);
|
|
5800
|
+
this.dispatchEvent(unpin);
|
|
5801
|
+
this._manage(unpin, (m2) => m2.unpin(id));
|
|
5399
5802
|
} else {
|
|
5400
|
-
|
|
5401
|
-
"aparte-pin
|
|
5402
|
-
{ detail: { id }, bubbles: true, composed: true }
|
|
5403
|
-
)
|
|
5803
|
+
const pin = new CustomEvent(
|
|
5804
|
+
"aparte-conversation-pin",
|
|
5805
|
+
{ detail: { id }, bubbles: true, composed: true, cancelable: true }
|
|
5806
|
+
);
|
|
5807
|
+
this.dispatchEvent(pin);
|
|
5808
|
+
this._manage(pin, (m2) => m2.pin(id));
|
|
5404
5809
|
}
|
|
5405
5810
|
return;
|
|
5406
5811
|
case "archive":
|
|
5407
5812
|
this._closeMenu(true);
|
|
5408
5813
|
if (conv.archivedAt) {
|
|
5409
|
-
|
|
5410
|
-
"aparte-unarchive
|
|
5411
|
-
{ detail: { id }, bubbles: true, composed: true }
|
|
5412
|
-
)
|
|
5814
|
+
const unarchive = new CustomEvent(
|
|
5815
|
+
"aparte-conversation-unarchive",
|
|
5816
|
+
{ detail: { id }, bubbles: true, composed: true, cancelable: true }
|
|
5817
|
+
);
|
|
5818
|
+
this.dispatchEvent(unarchive);
|
|
5819
|
+
this._manage(unarchive, (m2) => m2.unarchive(id));
|
|
5413
5820
|
} else {
|
|
5414
|
-
|
|
5415
|
-
"aparte-archive
|
|
5416
|
-
{ detail: { id }, bubbles: true, composed: true }
|
|
5417
|
-
)
|
|
5821
|
+
const archive = new CustomEvent(
|
|
5822
|
+
"aparte-conversation-archive",
|
|
5823
|
+
{ detail: { id }, bubbles: true, composed: true, cancelable: true }
|
|
5824
|
+
);
|
|
5825
|
+
this.dispatchEvent(archive);
|
|
5826
|
+
this._manage(archive, (m2) => m2.archive(id));
|
|
5418
5827
|
}
|
|
5419
5828
|
return;
|
|
5420
|
-
case "delete":
|
|
5421
|
-
open.menu.
|
|
5422
|
-
|
|
5829
|
+
case "delete": {
|
|
5830
|
+
const previous = [...open.menu.childNodes];
|
|
5831
|
+
const holder = document.createElement("div");
|
|
5832
|
+
holder.innerHTML = this._confirmMarkup(conv);
|
|
5833
|
+
open.menu.append(...holder.childNodes);
|
|
5834
|
+
open.menu.setAttribute("role", "dialog");
|
|
5835
|
+
open.menu.setAttribute("aria-label", this._confirmQuestion(conv));
|
|
5423
5836
|
open.menu.querySelector('[data-menu-action="cancel"]')?.focus();
|
|
5837
|
+
for (const node of previous) node.remove();
|
|
5838
|
+
this._placeMenu(open.button, open.menu);
|
|
5424
5839
|
return;
|
|
5840
|
+
}
|
|
5425
5841
|
case "cancel":
|
|
5426
5842
|
this._closeMenu(true);
|
|
5427
5843
|
return;
|
|
5428
|
-
case "confirm-delete":
|
|
5844
|
+
case "confirm-delete": {
|
|
5429
5845
|
this._closeMenu(true);
|
|
5430
|
-
|
|
5431
|
-
"aparte-delete
|
|
5432
|
-
{ detail: { id }, bubbles: true, composed: true }
|
|
5433
|
-
)
|
|
5846
|
+
const remove = new CustomEvent(
|
|
5847
|
+
"aparte-conversation-delete",
|
|
5848
|
+
{ detail: { id }, bubbles: true, composed: true, cancelable: true }
|
|
5849
|
+
);
|
|
5850
|
+
this.dispatchEvent(remove);
|
|
5851
|
+
this._manage(remove, (m2) => m2.delete(id));
|
|
5434
5852
|
return;
|
|
5853
|
+
}
|
|
5435
5854
|
}
|
|
5436
5855
|
}
|
|
5437
5856
|
_menuFocusables() {
|
|
@@ -5441,7 +5860,7 @@ class AparteConversationList extends HTMLElement {
|
|
|
5441
5860
|
}
|
|
5442
5861
|
_onMenuKeydown(e) {
|
|
5443
5862
|
const items = this._menuFocusables();
|
|
5444
|
-
const index = items.indexOf(
|
|
5863
|
+
const index = items.indexOf(activeElementIn(this));
|
|
5445
5864
|
const focusAt = (i2) => {
|
|
5446
5865
|
e.preventDefault();
|
|
5447
5866
|
items[(i2 + items.length) % items.length]?.focus();
|
|
@@ -5507,10 +5926,12 @@ class AparteConversationList extends HTMLElement {
|
|
|
5507
5926
|
const title = renaming.input.value.trim();
|
|
5508
5927
|
this._render();
|
|
5509
5928
|
if (commit && conv && title && title !== conv.title) {
|
|
5510
|
-
|
|
5511
|
-
"aparte-rename
|
|
5512
|
-
{ detail: { id: conv.id, title }, bubbles: true, composed: true }
|
|
5513
|
-
)
|
|
5929
|
+
const rename = new CustomEvent(
|
|
5930
|
+
"aparte-conversation-rename",
|
|
5931
|
+
{ detail: { id: conv.id, title }, bubbles: true, composed: true, cancelable: true }
|
|
5932
|
+
);
|
|
5933
|
+
this.dispatchEvent(rename);
|
|
5934
|
+
this._manage(rename, (m2) => m2.updateTitle(conv.id, title));
|
|
5514
5935
|
}
|
|
5515
5936
|
if (restoreFocus) {
|
|
5516
5937
|
this.querySelector(`[data-select-id="${cssEscape(renaming.id)}"]`)?.focus();
|
|
@@ -5540,7 +5961,7 @@ class AparteConversationList extends HTMLElement {
|
|
|
5540
5961
|
if (select) {
|
|
5541
5962
|
this._closeMenu();
|
|
5542
5963
|
this.dispatchEvent(new CustomEvent(
|
|
5543
|
-
"aparte-select
|
|
5964
|
+
"aparte-conversation-select",
|
|
5544
5965
|
{ detail: { id: select.dataset["selectId"] }, bubbles: true, composed: true }
|
|
5545
5966
|
));
|
|
5546
5967
|
}
|
|
@@ -5574,6 +5995,15 @@ class AparteConversationList extends HTMLElement {
|
|
|
5574
5995
|
}
|
|
5575
5996
|
if (!customElements.get("aparte-conversation-list")) customElements.define("aparte-conversation-list", AparteConversationList);
|
|
5576
5997
|
const isSuggestion = (value) => typeof value === "string" || typeof value === "object" && value !== null && typeof value.label === "string";
|
|
5998
|
+
const parseSuggestions = (raw) => {
|
|
5999
|
+
try {
|
|
6000
|
+
const parsed = JSON.parse(raw);
|
|
6001
|
+
return Array.isArray(parsed) ? parsed.filter(isSuggestion) : [];
|
|
6002
|
+
} catch {
|
|
6003
|
+
console.warn('[aparte-suggestions] `suggestions` is not a JSON array. Use ["…"] or [{"label": "…", "prompt": "…"}].');
|
|
6004
|
+
return [];
|
|
6005
|
+
}
|
|
6006
|
+
};
|
|
5577
6007
|
const labelOf = (s2) => typeof s2 === "string" ? s2 : s2.label;
|
|
5578
6008
|
const promptOf = (s2) => typeof s2 === "string" ? s2 : s2.prompt ?? s2.label;
|
|
5579
6009
|
class AparteSuggestions extends HTMLElement {
|
|
@@ -5593,8 +6023,17 @@ class AparteSuggestions extends HTMLElement {
|
|
|
5593
6023
|
get suggestions() {
|
|
5594
6024
|
return this._suggestions;
|
|
5595
6025
|
}
|
|
6026
|
+
/**
|
|
6027
|
+
* Takes the array, or the JSON string the attribute documents.
|
|
6028
|
+
*
|
|
6029
|
+
* Both, because both arrive: React 19 and Svelte assign the PROPERTY whenever the
|
|
6030
|
+
* element has one of that name, so `<aparte-suggestions suggestions='[…]'>` — the
|
|
6031
|
+
* documented spelling, typed valid by both wrappers — reached this setter as a
|
|
6032
|
+
* string, `Array.isArray` said no, and the row rendered nothing while the attribute
|
|
6033
|
+
* (and so `attributeChangedCallback`) was never written.
|
|
6034
|
+
*/
|
|
5596
6035
|
set suggestions(value) {
|
|
5597
|
-
this._suggestions = Array.isArray(value) ? value.filter(isSuggestion) : [];
|
|
6036
|
+
this._suggestions = typeof value === "string" ? parseSuggestions(value) : Array.isArray(value) ? value.filter(isSuggestion) : [];
|
|
5598
6037
|
if (this.isConnected) this._render();
|
|
5599
6038
|
}
|
|
5600
6039
|
connectedCallback() {
|
|
@@ -5630,13 +6069,7 @@ class AparteSuggestions extends HTMLElement {
|
|
|
5630
6069
|
const raw = this.getAttribute("suggestions");
|
|
5631
6070
|
if (raw === null || raw === this._parsed) return;
|
|
5632
6071
|
this._parsed = raw;
|
|
5633
|
-
|
|
5634
|
-
const parsed = JSON.parse(raw);
|
|
5635
|
-
this._suggestions = Array.isArray(parsed) ? parsed.filter(isSuggestion) : [];
|
|
5636
|
-
} catch {
|
|
5637
|
-
console.warn('[aparte-suggestions] `suggestions` is not a JSON array. Use ["…"] or [{"label": "…", "prompt": "…"}].');
|
|
5638
|
-
this._suggestions = [];
|
|
5639
|
-
}
|
|
6072
|
+
this._suggestions = parseSuggestions(raw);
|
|
5640
6073
|
}
|
|
5641
6074
|
/**
|
|
5642
6075
|
* The composer the click goes through, resolved the way the composer's own parts
|
|
@@ -5778,6 +6211,17 @@ class AparteContext extends HTMLElement {
|
|
|
5778
6211
|
get window() {
|
|
5779
6212
|
return this._window();
|
|
5780
6213
|
}
|
|
6214
|
+
/**
|
|
6215
|
+
* Writes the `window` attribute; `null` gives the model's own window back.
|
|
6216
|
+
*
|
|
6217
|
+
* The setter has to exist: React 19 and Svelte assign the PROPERTY whenever the
|
|
6218
|
+
* element has one of that name, so `<aparte-context window={8000}>` — a spelling
|
|
6219
|
+
* both wrappers type as valid — hit a getter alone and threw.
|
|
6220
|
+
*/
|
|
6221
|
+
set window(value) {
|
|
6222
|
+
if (value === null || value === void 0) this.removeAttribute("window");
|
|
6223
|
+
else this.setAttribute("window", String(value));
|
|
6224
|
+
}
|
|
5781
6225
|
/** The current level, or `null` while nothing is shown. */
|
|
5782
6226
|
get level() {
|
|
5783
6227
|
return this._level;
|
|
@@ -6038,7 +6482,8 @@ class AparteScrollRail extends HTMLElement {
|
|
|
6038
6482
|
_rebuild() {
|
|
6039
6483
|
const bubbles = this._bubbles();
|
|
6040
6484
|
const locale = resolveConfig(this).getLocale();
|
|
6041
|
-
|
|
6485
|
+
const railLabel = locale.scrollRailLabel ?? "Conversation outline";
|
|
6486
|
+
if (this.getAttribute("aria-label") !== railLabel) this.setAttribute("aria-label", railLabel);
|
|
6042
6487
|
if (bubbles.length < 2) {
|
|
6043
6488
|
this.setAttribute("data-empty", "");
|
|
6044
6489
|
this.replaceChildren();
|
|
@@ -6081,8 +6526,11 @@ class AparteScrollRail extends HTMLElement {
|
|
|
6081
6526
|
tick.setAttribute("aria-label", label);
|
|
6082
6527
|
tick.title = label;
|
|
6083
6528
|
}
|
|
6084
|
-
if (id === this._currentId)
|
|
6085
|
-
|
|
6529
|
+
if (id === this._currentId) {
|
|
6530
|
+
if (tick.getAttribute("aria-current") !== "true") tick.setAttribute("aria-current", "true");
|
|
6531
|
+
} else {
|
|
6532
|
+
tick.removeAttribute("aria-current");
|
|
6533
|
+
}
|
|
6086
6534
|
const at = list.children[index] ?? null;
|
|
6087
6535
|
if (at !== item) list.insertBefore(item, at);
|
|
6088
6536
|
index++;
|
|
@@ -6264,7 +6712,7 @@ class AparteScrollRail extends HTMLElement {
|
|
|
6264
6712
|
_onKeydown = (e) => {
|
|
6265
6713
|
if (e.key !== "ArrowDown" && e.key !== "ArrowUp" && e.key !== "Home" && e.key !== "End") return;
|
|
6266
6714
|
const ticks = Array.from(this.querySelectorAll(".aparte-scroll-rail__tick"));
|
|
6267
|
-
const index = ticks.indexOf(
|
|
6715
|
+
const index = ticks.indexOf(activeElementIn(this));
|
|
6268
6716
|
if (index === -1) return;
|
|
6269
6717
|
e.preventDefault();
|
|
6270
6718
|
const next = e.key === "Home" ? 0 : e.key === "End" ? ticks.length - 1 : e.key === "ArrowDown" ? Math.min(ticks.length - 1, index + 1) : Math.max(0, index - 1);
|
|
@@ -6521,7 +6969,10 @@ class AparteSidebar extends HTMLElement {
|
|
|
6521
6969
|
* lies. The row's own `⋯` button already did this; the sidebar's toggle did not.
|
|
6522
6970
|
*/
|
|
6523
6971
|
_syncToggles() {
|
|
6524
|
-
|
|
6972
|
+
const selector = "[data-aparte-sidebar-toggle]";
|
|
6973
|
+
const root = rootOf(this);
|
|
6974
|
+
const controls = root === document ? Array.from(document.querySelectorAll(selector)) : [...root.querySelectorAll(selector), ...document.querySelectorAll(selector)];
|
|
6975
|
+
for (const control of controls) {
|
|
6525
6976
|
if (!this._drives(control)) continue;
|
|
6526
6977
|
control.setAttribute("aria-expanded", String(!this.collapsed));
|
|
6527
6978
|
control.setAttribute("aria-controls", this.id);
|
|
@@ -6594,7 +7045,7 @@ class AparteSidebar extends HTMLElement {
|
|
|
6594
7045
|
if (focusable.length === 0) return;
|
|
6595
7046
|
const first = focusable[0];
|
|
6596
7047
|
const last = focusable[focusable.length - 1];
|
|
6597
|
-
const active =
|
|
7048
|
+
const active = activeElementIn(this);
|
|
6598
7049
|
if (!this.contains(active)) {
|
|
6599
7050
|
e.preventDefault();
|
|
6600
7051
|
first.focus();
|
|
@@ -6609,7 +7060,7 @@ class AparteSidebar extends HTMLElement {
|
|
|
6609
7060
|
};
|
|
6610
7061
|
/** A `[data-aparte-sidebar-toggle]` anywhere toggles this sidebar — no script in the host. */
|
|
6611
7062
|
_onDocumentClick = (e) => {
|
|
6612
|
-
const control = e
|
|
7063
|
+
const control = eventOrigin(e)?.closest?.("[data-aparte-sidebar-toggle]");
|
|
6613
7064
|
if (!control) return;
|
|
6614
7065
|
if (!this._drives(control)) return;
|
|
6615
7066
|
this.toggle(control);
|
|
@@ -6658,7 +7109,7 @@ function visibleFocusable(root) {
|
|
|
6658
7109
|
return Array.from(root.querySelectorAll(FOCUSABLE)).filter((el2) => !el2.closest("[hidden]") && el2.checkVisibility?.() !== false);
|
|
6659
7110
|
}
|
|
6660
7111
|
function nearestSidebar(control) {
|
|
6661
|
-
return control.closest("aparte-sidebar") ?? document.querySelector("aparte-sidebar");
|
|
7112
|
+
return control.closest("aparte-sidebar") ?? rootOf(control).querySelector("aparte-sidebar") ?? document.querySelector("aparte-sidebar");
|
|
6662
7113
|
}
|
|
6663
7114
|
function fold(text) {
|
|
6664
7115
|
return text.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase().trim();
|
|
@@ -7385,15 +7836,16 @@ class AparteSplit extends HTMLElement {
|
|
|
7385
7836
|
* first on the page. Same shape as `[data-aparte-sidebar-toggle]`.
|
|
7386
7837
|
*/
|
|
7387
7838
|
_onDocumentClick = (e) => {
|
|
7388
|
-
const control = e
|
|
7839
|
+
const control = eventOrigin(e)?.closest?.("[data-aparte-split-pane]");
|
|
7389
7840
|
if (!control) return;
|
|
7390
7841
|
const value = (control.getAttribute("data-aparte-split-pane") ?? "").trim();
|
|
7391
7842
|
const which = value === "start" || value === "end" ? value : null;
|
|
7392
7843
|
const named = which ? "" : value;
|
|
7393
7844
|
if (named) {
|
|
7394
7845
|
if (named !== this.id) return;
|
|
7395
|
-
} else
|
|
7396
|
-
|
|
7846
|
+
} else {
|
|
7847
|
+
const nearest = control.closest("aparte-split") ?? rootOf(control).querySelector("aparte-split") ?? document.querySelector("aparte-split");
|
|
7848
|
+
if (nearest !== this) return;
|
|
7397
7849
|
}
|
|
7398
7850
|
this.showPane(which ?? (this.pane === "start" ? "end" : "start"));
|
|
7399
7851
|
};
|
|
@@ -7525,10 +7977,10 @@ export {
|
|
|
7525
7977
|
installDialogTriggersOnce,
|
|
7526
7978
|
isAwaitingReply,
|
|
7527
7979
|
R as isFormatAdapter,
|
|
7528
|
-
|
|
7980
|
+
isSafeUrl,
|
|
7529
7981
|
T as isSegmentSettled,
|
|
7530
7982
|
U as parseAparteEventStream,
|
|
7531
|
-
|
|
7983
|
+
parseMarkdownToSegments,
|
|
7532
7984
|
populateBubbleFromMessage,
|
|
7533
7985
|
X as readableToAsyncIterable,
|
|
7534
7986
|
registerAllComponents,
|