@aparte/core 0.16.8 → 0.16.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/aparte-client.d.ts +57 -2
- package/dist/client/aparte-client.d.ts.map +1 -1
- package/dist/client/lifecycle-events.d.ts +8 -1
- package/dist/client/lifecycle-events.d.ts.map +1 -1
- package/dist/client/stream-adapter.d.ts +8 -0
- package/dist/client/stream-adapter.d.ts.map +1 -1
- package/dist/components/bubble/aparte-chat-bubble.d.ts +6 -0
- package/dist/components/bubble/aparte-chat-bubble.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-attachments.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-input.d.ts +1 -1
- package/dist/components/composer/aparte-composer.d.ts.map +1 -1
- package/dist/components/conversation-list/aparte-conversation-list.d.ts +5 -0
- package/dist/components/conversation-list/aparte-conversation-list.d.ts.map +1 -1
- package/dist/components/scroll-rail/aparte-scroll-rail.d.ts +3 -2
- package/dist/components/scroll-rail/aparte-scroll-rail.d.ts.map +1 -1
- package/dist/components/status/aparte-chat-status.d.ts +41 -3
- package/dist/components/status/aparte-chat-status.d.ts.map +1 -1
- package/dist/custom-elements.json +12484 -12344
- package/dist/{dialog-triggers-D8wNICrT.js → dialog-triggers-CApC7K1I.js} +96 -26
- package/dist/{dialog-triggers-D8wNICrT.js.map → dialog-triggers-CApC7K1I.js.map} +1 -1
- package/dist/elicitation/panel.d.ts.map +1 -1
- package/dist/index.css +119 -31
- package/dist/index.js +157 -47
- package/dist/index.js.map +1 -1
- package/dist/index.node.js +1 -1
- package/dist/interop/element-props.d.ts.map +1 -1
- package/dist/primitives/select/aparte-select.d.ts +25 -3
- package/dist/primitives/select/aparte-select.d.ts.map +1 -1
- package/dist/types/events.d.ts +4 -2
- package/dist/types/events.d.ts.map +1 -1
- package/package.json +2 -2
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, 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-
|
|
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-
|
|
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-CApC7K1I.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-CApC7K1I.js";
|
|
3
3
|
function presenceOn(value) {
|
|
4
4
|
return value === "" ? true : Boolean(value);
|
|
5
5
|
}
|
|
@@ -207,11 +207,12 @@ class AparteSelect extends HTMLElement {
|
|
|
207
207
|
}
|
|
208
208
|
connectedCallback() {
|
|
209
209
|
this._value = this.getAttribute("value") || "";
|
|
210
|
-
|
|
210
|
+
const wantsOpen = this.hasAttribute("open") && !this._isOpen;
|
|
211
211
|
this._render();
|
|
212
212
|
this._applyDisabled();
|
|
213
213
|
this._setupEventListeners();
|
|
214
214
|
this._setupMutationObserver();
|
|
215
|
+
if (wantsOpen) this._openDropdown();
|
|
215
216
|
}
|
|
216
217
|
disconnectedCallback() {
|
|
217
218
|
this.removeEventListener("click", this._boundHandleOptionClick);
|
|
@@ -226,20 +227,18 @@ class AparteSelect extends HTMLElement {
|
|
|
226
227
|
this._updateTriggerLabel();
|
|
227
228
|
}
|
|
228
229
|
if (name === "open") {
|
|
229
|
-
|
|
230
|
-
if (this._isOpen) {
|
|
231
|
-
this.
|
|
232
|
-
this.
|
|
233
|
-
} else {
|
|
234
|
-
this._dropdown?.setAttribute("hidden", "");
|
|
230
|
+
const wanted = this.hasAttribute("open");
|
|
231
|
+
if (wanted !== this._isOpen) {
|
|
232
|
+
if (wanted) this._openDropdown();
|
|
233
|
+
else this._closeDropdown();
|
|
235
234
|
}
|
|
236
235
|
}
|
|
237
236
|
if (name === "placeholder") {
|
|
238
237
|
this._updateTriggerLabel();
|
|
239
|
-
this._updateNames();
|
|
240
238
|
}
|
|
241
239
|
if (name === "disabled") {
|
|
242
240
|
this._applyDisabled();
|
|
241
|
+
if (!this.hasAttribute("disabled") && this.hasAttribute("open")) this._openDropdown();
|
|
243
242
|
}
|
|
244
243
|
}
|
|
245
244
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -277,7 +276,7 @@ class AparteSelect extends HTMLElement {
|
|
|
277
276
|
const trigger = document.createElement("div");
|
|
278
277
|
trigger.className = "aparte-select-trigger";
|
|
279
278
|
trigger.setAttribute("tabindex", "0");
|
|
280
|
-
trigger.setAttribute("role", "combobox");
|
|
279
|
+
trigger.setAttribute("role", searchable ? "button" : "combobox");
|
|
281
280
|
trigger.setAttribute("aria-haspopup", "listbox");
|
|
282
281
|
trigger.setAttribute("aria-expanded", "false");
|
|
283
282
|
trigger.setAttribute("aria-label", this.getAttribute("aria-label") || placeholder);
|
|
@@ -297,12 +296,17 @@ class AparteSelect extends HTMLElement {
|
|
|
297
296
|
const dropdown = document.createElement("div");
|
|
298
297
|
dropdown.className = "aparte-select-dropdown";
|
|
299
298
|
dropdown.hidden = !this._isOpen;
|
|
299
|
+
let searchInput = null;
|
|
300
300
|
if (searchable) {
|
|
301
|
-
|
|
301
|
+
searchInput = document.createElement("input");
|
|
302
302
|
searchInput.type = "text";
|
|
303
303
|
searchInput.className = "aparte-select-search";
|
|
304
304
|
searchInput.placeholder = cfg.t("selectSearchPlaceholder") || "Search…";
|
|
305
305
|
searchInput.setAttribute("aria-label", cfg.t("selectSearchLabel") || "Search options");
|
|
306
|
+
searchInput.setAttribute("role", "combobox");
|
|
307
|
+
searchInput.setAttribute("aria-haspopup", "listbox");
|
|
308
|
+
searchInput.setAttribute("aria-autocomplete", "list");
|
|
309
|
+
searchInput.setAttribute("aria-expanded", String(this._isOpen));
|
|
306
310
|
dropdown.appendChild(searchInput);
|
|
307
311
|
}
|
|
308
312
|
const optionsContainer = document.createElement("div");
|
|
@@ -311,6 +315,7 @@ class AparteSelect extends HTMLElement {
|
|
|
311
315
|
optionsContainer.setAttribute("aria-label", trigger.getAttribute("aria-label") ?? placeholder);
|
|
312
316
|
optionsContainer.id = this.id ? `${this.id}-listbox` : `aparte-listbox-${++AparteSelect._listboxSeq}`;
|
|
313
317
|
trigger.setAttribute("aria-controls", optionsContainer.id);
|
|
318
|
+
searchInput?.setAttribute("aria-controls", optionsContainer.id);
|
|
314
319
|
slottedChildren.forEach((child) => {
|
|
315
320
|
if (child.tagName === "APARTE-OPTION" || child.tagName === "APARTE-OPTGROUP") {
|
|
316
321
|
optionsContainer.appendChild(child);
|
|
@@ -459,6 +464,20 @@ class AparteSelect extends HTMLElement {
|
|
|
459
464
|
const base = this._activeIndex < 0 ? delta > 0 ? -1 : 0 : this._activeIndex;
|
|
460
465
|
this._setActive(base + delta);
|
|
461
466
|
}
|
|
467
|
+
/**
|
|
468
|
+
* The element that holds DOM focus while the list is being driven, and therefore
|
|
469
|
+
* the one that must carry the combobox state a reader follows: the filter field
|
|
470
|
+
* when the select is `searchable` (it is focused on open), the trigger otherwise.
|
|
471
|
+
*/
|
|
472
|
+
_activeHost() {
|
|
473
|
+
return this._searchInput ?? this._trigger;
|
|
474
|
+
}
|
|
475
|
+
/** `aria-expanded` belongs to the combobox — write it wherever the combobox is. */
|
|
476
|
+
_setExpanded(open) {
|
|
477
|
+
const flag = String(open);
|
|
478
|
+
this._trigger?.setAttribute("aria-expanded", flag);
|
|
479
|
+
this._searchInput?.setAttribute("aria-expanded", flag);
|
|
480
|
+
}
|
|
462
481
|
/** Highlight the option at `index` (clamped) and point aria-activedescendant at it. */
|
|
463
482
|
_setActive(index) {
|
|
464
483
|
const all = this.querySelectorAll("aparte-option");
|
|
@@ -466,7 +485,7 @@ class AparteSelect extends HTMLElement {
|
|
|
466
485
|
const opts = this._visibleOptions();
|
|
467
486
|
if (opts.length === 0) {
|
|
468
487
|
this._activeIndex = -1;
|
|
469
|
-
this.
|
|
488
|
+
this._activeHost()?.removeAttribute("aria-activedescendant");
|
|
470
489
|
return;
|
|
471
490
|
}
|
|
472
491
|
const clamped = Math.max(0, Math.min(index, opts.length - 1));
|
|
@@ -474,13 +493,13 @@ class AparteSelect extends HTMLElement {
|
|
|
474
493
|
const active = opts[clamped];
|
|
475
494
|
if (!active.id) active.id = `aparte-option-${++AparteSelect._optIdSeq}`;
|
|
476
495
|
active.setAttribute("data-active", "");
|
|
477
|
-
this.
|
|
496
|
+
this._activeHost()?.setAttribute("aria-activedescendant", active.id);
|
|
478
497
|
active.scrollIntoView?.({ block: "nearest" });
|
|
479
498
|
}
|
|
480
499
|
/** Clear the keyboard highlight (on close). */
|
|
481
500
|
_clearActive() {
|
|
482
501
|
this._activeIndex = -1;
|
|
483
|
-
this.
|
|
502
|
+
this._activeHost()?.removeAttribute("aria-activedescendant");
|
|
484
503
|
this.querySelectorAll("aparte-option").forEach((o) => o.removeAttribute("data-active"));
|
|
485
504
|
}
|
|
486
505
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -494,10 +513,11 @@ class AparteSelect extends HTMLElement {
|
|
|
494
513
|
}
|
|
495
514
|
}
|
|
496
515
|
_openDropdown() {
|
|
516
|
+
if (this._isOpen) return;
|
|
497
517
|
if (this.hasAttribute("disabled")) return;
|
|
498
518
|
this._isOpen = true;
|
|
499
519
|
this._dropdown?.removeAttribute("hidden");
|
|
500
|
-
this.
|
|
520
|
+
this._setExpanded(true);
|
|
501
521
|
this.setAttribute("open", "");
|
|
502
522
|
this._updatePosition();
|
|
503
523
|
this._searchInput?.focus();
|
|
@@ -510,7 +530,7 @@ class AparteSelect extends HTMLElement {
|
|
|
510
530
|
this._isOpen = false;
|
|
511
531
|
this._clearActive();
|
|
512
532
|
this._dropdown?.setAttribute("hidden", "");
|
|
513
|
-
this.
|
|
533
|
+
this._setExpanded(false);
|
|
514
534
|
this.removeAttribute("open");
|
|
515
535
|
this.removeAttribute("position");
|
|
516
536
|
if (this._dropdown) {
|
|
@@ -556,15 +576,28 @@ class AparteSelect extends HTMLElement {
|
|
|
556
576
|
}
|
|
557
577
|
/**
|
|
558
578
|
* 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
|
|
560
|
-
* placeholder changes. Written only when different: the mutation
|
|
561
|
-
* this subtree.
|
|
579
|
+
* placeholder — the same rule the first render applies, re-applied whenever the
|
|
580
|
+
* placeholder or the selection changes. Written only when different: the mutation
|
|
581
|
+
* observer watches this subtree.
|
|
582
|
+
*
|
|
583
|
+
* A `combobox` is NAMED by that attribute and VALUED by its visible label span, so
|
|
584
|
+
* the name is the control and nothing else. A `button` — the shape a `searchable`
|
|
585
|
+
* trigger takes, since the filter field is then the combobox — is named by its
|
|
586
|
+
* CONTENT, which this very attribute overrides: the searchable trigger announced
|
|
587
|
+
* "Pick a model, button" and never said which model was selected. So the button's
|
|
588
|
+
* name carries both halves, and drops the second when it would only repeat the
|
|
589
|
+
* first (nothing selected: the value IS the placeholder).
|
|
562
590
|
*/
|
|
563
591
|
_updateNames() {
|
|
564
|
-
const
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
592
|
+
const placeholder = this.getAttribute("placeholder") || "Select...";
|
|
593
|
+
const name = this.getAttribute("aria-label") || placeholder;
|
|
594
|
+
const list = this.querySelector(".aparte-select-options");
|
|
595
|
+
if (list && list.getAttribute("aria-label") !== name) list.setAttribute("aria-label", name);
|
|
596
|
+
const trigger = this._trigger;
|
|
597
|
+
if (!trigger) return;
|
|
598
|
+
const value = this._getSelectedLabel() || placeholder;
|
|
599
|
+
const wanted = trigger.getAttribute("role") === "button" && value !== name ? `${name}: ${value}` : name;
|
|
600
|
+
if (trigger.getAttribute("aria-label") !== wanted) trigger.setAttribute("aria-label", wanted);
|
|
568
601
|
}
|
|
569
602
|
/**
|
|
570
603
|
* `disabled` takes the trigger out of the tab order and says so — opacity alone
|
|
@@ -601,6 +634,7 @@ class AparteSelect extends HTMLElement {
|
|
|
601
634
|
}));
|
|
602
635
|
}
|
|
603
636
|
}
|
|
637
|
+
this._updateNames();
|
|
604
638
|
const options = this.querySelectorAll("aparte-option");
|
|
605
639
|
options.forEach((opt) => {
|
|
606
640
|
const isSelected = opt.getAttribute("value") === this._value;
|
|
@@ -1089,6 +1123,12 @@ class AparteChatBubble extends HTMLElement {
|
|
|
1089
1123
|
_editing = false;
|
|
1090
1124
|
/** The live inline editor (the composer's contenteditable primitive), present only while `_editing`. */
|
|
1091
1125
|
_editInput = null;
|
|
1126
|
+
/**
|
|
1127
|
+
* The `data-action` of the button the editor was opened from, so leaving it can hand
|
|
1128
|
+
* the keyboard back. The STRING, not the node: entering and leaving edit mode rewrite
|
|
1129
|
+
* the action bar's `innerHTML`, so the opener's identity is destroyed twice.
|
|
1130
|
+
*/
|
|
1131
|
+
_editReturnAction = null;
|
|
1092
1132
|
static get observedAttributes() {
|
|
1093
1133
|
return ["data-role", "content", "timestamp", "message-id", "streaming", "name"];
|
|
1094
1134
|
}
|
|
@@ -2126,6 +2166,8 @@ class AparteChatBubble extends HTMLElement {
|
|
|
2126
2166
|
*/
|
|
2127
2167
|
_enterEditMode() {
|
|
2128
2168
|
if (this._editing || !this._contentEl) return;
|
|
2169
|
+
const opener = document.activeElement;
|
|
2170
|
+
this._editReturnAction = opener instanceof HTMLElement && this.contains(opener) ? opener.getAttribute("data-action") ?? "edit" : null;
|
|
2129
2171
|
this._editing = true;
|
|
2130
2172
|
this.querySelector(".aparte-message")?.setAttribute("data-editing", "");
|
|
2131
2173
|
const input = document.createElement("aparte-composer-input");
|
|
@@ -2159,6 +2201,16 @@ class AparteChatBubble extends HTMLElement {
|
|
|
2159
2201
|
this.querySelector(".aparte-message")?.removeAttribute("data-editing");
|
|
2160
2202
|
this._editing = false;
|
|
2161
2203
|
this._updateActionBar();
|
|
2204
|
+
const returnTo = this._editReturnAction;
|
|
2205
|
+
this._editReturnAction = null;
|
|
2206
|
+
if (returnTo && this._actionBarEl) {
|
|
2207
|
+
const back = Array.from(this._actionBarEl.querySelectorAll("[data-action]")).find((btn) => btn.getAttribute("data-action") === returnTo);
|
|
2208
|
+
const target = back && !back.disabled ? back : this._actionBarEl.querySelector("button:not([disabled])");
|
|
2209
|
+
if (target) {
|
|
2210
|
+
for (const button of this._rovingButtons()) button.tabIndex = button === target ? 0 : -1;
|
|
2211
|
+
target.focus();
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2162
2214
|
if (save && newContent && newContent !== original) {
|
|
2163
2215
|
const messageId = this.getAttribute("message-id");
|
|
2164
2216
|
if (messageId) {
|
|
@@ -2306,27 +2358,63 @@ class AparteChatStatus extends HTMLElement {
|
|
|
2306
2358
|
<span class="aparte-dot"></span>
|
|
2307
2359
|
</div>
|
|
2308
2360
|
<span class="aparte-status-text"></span>
|
|
2361
|
+
<span class="aparte-sr-only aparte-status-sr"></span>
|
|
2309
2362
|
</div>
|
|
2310
2363
|
</div>
|
|
2311
2364
|
</div>
|
|
2312
2365
|
`;
|
|
2313
2366
|
this.querySelector(".aparte-status-container")?.setAttribute("aria-label", text);
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2367
|
+
this._writeText(this.getAttribute("text"));
|
|
2368
|
+
}
|
|
2369
|
+
/**
|
|
2370
|
+
* The one writer for both text nodes, so exactly ONE of them is ever populated.
|
|
2371
|
+
*
|
|
2372
|
+
* The container is a polite live region, and a live region announces its CONTENT.
|
|
2373
|
+
* The dots-only default had none — an `aria-hidden` dot and an empty span — so the
|
|
2374
|
+
* whole state rode on `aria-label`, which names the region rather than reporting it,
|
|
2375
|
+
* and nothing was announced. The screen-reader span carries the fallback word in that
|
|
2376
|
+
* case; when `text` is set the visible span already carries the same string, and
|
|
2377
|
+
* populating both would have the region read it twice.
|
|
2378
|
+
*/
|
|
2379
|
+
_writeText(text) {
|
|
2380
|
+
const line = text || null;
|
|
2381
|
+
const textEl = this.querySelector(".aparte-status-text");
|
|
2382
|
+
if (textEl) textEl.textContent = line ?? "";
|
|
2383
|
+
this._syncLiveWord();
|
|
2384
|
+
}
|
|
2385
|
+
/**
|
|
2386
|
+
* The fallback word enters and leaves the live region WITH the element's visibility.
|
|
2387
|
+
*
|
|
2388
|
+
* Writing it at render time put it in the region while the host was still
|
|
2389
|
+
* `display: none` — every wrapper mounts `<aparte-chat-status>` once and flips
|
|
2390
|
+
* `visible` — so by the time the region was exposed its content had not changed, and
|
|
2391
|
+
* on the second and third turn there was not even a reveal to notice: the text was
|
|
2392
|
+
* byte-identical to what was already sitting there. Revealing a region that already
|
|
2393
|
+
* holds its text is the path assistive tech is documented not to announce reliably;
|
|
2394
|
+
* mutating one that is already exposed is the path that works. So the word is written
|
|
2395
|
+
* when `visible` arrives and cleared when it leaves, which makes every turn a real
|
|
2396
|
+
* change.
|
|
2397
|
+
*
|
|
2398
|
+
* With `text` set the visible span is the region's content and the screen-reader span
|
|
2399
|
+
* stays empty — one copy, read once.
|
|
2400
|
+
*/
|
|
2401
|
+
_syncLiveWord() {
|
|
2402
|
+
const srEl = this.querySelector(".aparte-status-sr");
|
|
2403
|
+
if (!srEl) return;
|
|
2404
|
+
const dotsOnly = !this.getAttribute("text");
|
|
2405
|
+
srEl.textContent = dotsOnly && this.hasAttribute("visible") ? "Typing" : "";
|
|
2318
2406
|
}
|
|
2319
2407
|
_updateVisibility(visible) {
|
|
2320
2408
|
const container = this.querySelector(".aparte-status-container");
|
|
2321
2409
|
if (container) {
|
|
2322
2410
|
container.setAttribute("data-visible", String(visible));
|
|
2323
2411
|
}
|
|
2412
|
+
this._syncLiveWord();
|
|
2324
2413
|
}
|
|
2325
2414
|
_updateText(text) {
|
|
2326
|
-
const textEl = this.querySelector(".aparte-status-text");
|
|
2327
2415
|
const container = this.querySelector(".aparte-status-container");
|
|
2328
2416
|
if (!container) return;
|
|
2329
|
-
|
|
2417
|
+
this._writeText(text);
|
|
2330
2418
|
container.setAttribute("aria-label", text || "Typing");
|
|
2331
2419
|
}
|
|
2332
2420
|
}
|
|
@@ -4012,6 +4100,8 @@ class AparteComposer extends HTMLElement {
|
|
|
4012
4100
|
onEvict?.();
|
|
4013
4101
|
}
|
|
4014
4102
|
_teardownPanel() {
|
|
4103
|
+
const active = document.activeElement;
|
|
4104
|
+
const hadFocus = active instanceof Node && this.contains(active);
|
|
4015
4105
|
const existing = this.querySelector("[data-aparte-panel]");
|
|
4016
4106
|
if (existing) existing.remove();
|
|
4017
4107
|
this.removeAttribute("data-panel-active");
|
|
@@ -4023,7 +4113,7 @@ class AparteComposer extends HTMLElement {
|
|
|
4023
4113
|
this._panelOnEvict = null;
|
|
4024
4114
|
this._panelToken = null;
|
|
4025
4115
|
this._emit("panel-change", { active: false, submitEnabled: false, mode: "submit" });
|
|
4026
|
-
this.focus();
|
|
4116
|
+
if (hadFocus) this.focus();
|
|
4027
4117
|
}
|
|
4028
4118
|
/**
|
|
4029
4119
|
* Update the send button's state while a panel is active.
|
|
@@ -4085,7 +4175,14 @@ class AparteComposer extends HTMLElement {
|
|
|
4085
4175
|
const detail = {
|
|
4086
4176
|
content: value,
|
|
4087
4177
|
timestamp: Date.now(),
|
|
4088
|
-
|
|
4178
|
+
/*
|
|
4179
|
+
* `_ownTargetId()`, not the bare attribute — the same resolution `cancel()`
|
|
4180
|
+
* uses, and for the same reason. The wrappers all set `target`, so this is
|
|
4181
|
+
* byte-identical there; in RAW core nothing writes it, so every send carried
|
|
4182
|
+
* `targetId: undefined` and a page with two hand-written chats delivered the
|
|
4183
|
+
* reply to whichever one the host resolved first.
|
|
4184
|
+
*/
|
|
4185
|
+
targetId: this._ownTargetId(),
|
|
4089
4186
|
files: this._attachments.length > 0 ? [...this._attachments] : void 0
|
|
4090
4187
|
};
|
|
4091
4188
|
this.dispatchEvent(new CustomEvent("aparte-send", {
|
|
@@ -4732,20 +4829,22 @@ class AparteComposerAttachments extends HTMLElement {
|
|
|
4732
4829
|
});
|
|
4733
4830
|
});
|
|
4734
4831
|
if (!resolveConfig(this).getHostHandlers().attachmentPreview) return;
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
tile.
|
|
4832
|
+
const images = files.filter((f2) => f2.type.startsWith("image/"));
|
|
4833
|
+
this.querySelectorAll(".aparte-thumb--image").forEach((tile, i2) => {
|
|
4834
|
+
const img = tile.querySelector(".aparte-thumbnail__image");
|
|
4835
|
+
if (!img) return;
|
|
4836
|
+
img.setAttribute("role", "button");
|
|
4837
|
+
img.setAttribute("tabindex", "0");
|
|
4838
|
+
img.setAttribute("aria-label", images[i2]?.name ?? "");
|
|
4738
4839
|
const open = () => {
|
|
4739
|
-
const img = tile.querySelector(".aparte-thumbnail__image");
|
|
4740
|
-
if (!img) return;
|
|
4741
4840
|
this.dispatchEvent(new CustomEvent("aparte-attachment-preview", {
|
|
4742
4841
|
bubbles: true,
|
|
4743
4842
|
composed: true,
|
|
4744
|
-
detail: { url: img.src, name:
|
|
4843
|
+
detail: { url: img.src, name: images[i2]?.name ?? "" }
|
|
4745
4844
|
}));
|
|
4746
4845
|
};
|
|
4747
|
-
|
|
4748
|
-
|
|
4846
|
+
img.addEventListener("click", open);
|
|
4847
|
+
img.addEventListener("keydown", (e) => {
|
|
4749
4848
|
const key = e.key;
|
|
4750
4849
|
if (key !== "Enter" && key !== " ") return;
|
|
4751
4850
|
e.preventDefault();
|
|
@@ -5375,7 +5474,10 @@ class AparteConversationList extends HTMLElement {
|
|
|
5375
5474
|
input.dataset["renameId"] = conv.id;
|
|
5376
5475
|
select.replaceWith(input);
|
|
5377
5476
|
this._renaming = { id: conv.id, input, done: false };
|
|
5378
|
-
input.addEventListener("blur", () =>
|
|
5477
|
+
input.addEventListener("blur", (e) => {
|
|
5478
|
+
const next = e.relatedTarget;
|
|
5479
|
+
this._finishRename(true, !next || this.contains(next));
|
|
5480
|
+
});
|
|
5379
5481
|
input.focus();
|
|
5380
5482
|
input.select();
|
|
5381
5483
|
}
|
|
@@ -5384,23 +5486,28 @@ class AparteConversationList extends HTMLElement {
|
|
|
5384
5486
|
* the blur a removal causes cannot commit a second time. The row is re-rendered
|
|
5385
5487
|
* from the array either way: the list shows data, and the host's re-assignment is
|
|
5386
5488
|
* what makes the new title appear.
|
|
5489
|
+
*
|
|
5490
|
+
* `restoreFocus` puts the keyboard back on the row: every exit destroys the field
|
|
5491
|
+
* the reader was in, so without it the focus lands on `<body>` and the next Tab
|
|
5492
|
+
* restarts at the top of the page. The caller decides — the blur path says no when
|
|
5493
|
+
* the focus is already heading somewhere live outside this list.
|
|
5387
5494
|
*/
|
|
5388
|
-
_finishRename(commit,
|
|
5495
|
+
_finishRename(commit, restoreFocus) {
|
|
5389
5496
|
const renaming = this._renaming;
|
|
5390
5497
|
if (!renaming || renaming.done) return;
|
|
5391
5498
|
renaming.done = true;
|
|
5392
5499
|
const conv = this._conversations.find((c2) => c2.id === renaming.id);
|
|
5393
5500
|
const title = renaming.input.value.trim();
|
|
5394
5501
|
this._render();
|
|
5395
|
-
if (viaKeyboard) {
|
|
5396
|
-
this.querySelector(`[data-select-id="${cssEscape(renaming.id)}"]`)?.focus();
|
|
5397
|
-
}
|
|
5398
5502
|
if (commit && conv && title && title !== conv.title) {
|
|
5399
5503
|
this.dispatchEvent(new CustomEvent(
|
|
5400
5504
|
"aparte-rename-conversation",
|
|
5401
5505
|
{ detail: { id: conv.id, title }, bubbles: true, composed: true }
|
|
5402
5506
|
));
|
|
5403
5507
|
}
|
|
5508
|
+
if (restoreFocus) {
|
|
5509
|
+
this.querySelector(`[data-select-id="${cssEscape(renaming.id)}"]`)?.focus();
|
|
5510
|
+
}
|
|
5404
5511
|
}
|
|
5405
5512
|
// ─── Events ───────────────────────────────────────────────────────────
|
|
5406
5513
|
_onClick = (e) => {
|
|
@@ -6217,7 +6324,7 @@ class AparteSidebar extends HTMLElement {
|
|
|
6217
6324
|
* nothing focusable in it keeps the focus where it was, which is the honest outcome.
|
|
6218
6325
|
*/
|
|
6219
6326
|
_focusFirst() {
|
|
6220
|
-
this
|
|
6327
|
+
visibleFocusable(this)[0]?.focus();
|
|
6221
6328
|
}
|
|
6222
6329
|
_syncScrim() {
|
|
6223
6330
|
const wanted = this.drawer && !this.collapsed;
|
|
@@ -6240,7 +6347,7 @@ class AparteSidebar extends HTMLElement {
|
|
|
6240
6347
|
return;
|
|
6241
6348
|
}
|
|
6242
6349
|
if (e.key === "Tab") {
|
|
6243
|
-
const focusable =
|
|
6350
|
+
const focusable = visibleFocusable(this);
|
|
6244
6351
|
if (focusable.length === 0) return;
|
|
6245
6352
|
const first = focusable[0];
|
|
6246
6353
|
const last = focusable[focusable.length - 1];
|
|
@@ -6304,6 +6411,9 @@ const FOCUSABLE = [
|
|
|
6304
6411
|
"textarea:not([disabled])",
|
|
6305
6412
|
'[tabindex]:not([tabindex="-1"])'
|
|
6306
6413
|
].join(",");
|
|
6414
|
+
function visibleFocusable(root) {
|
|
6415
|
+
return Array.from(root.querySelectorAll(FOCUSABLE)).filter((el2) => !el2.closest("[hidden]") && el2.checkVisibility?.() !== false);
|
|
6416
|
+
}
|
|
6307
6417
|
function nearestSidebar(control) {
|
|
6308
6418
|
return control.closest("aparte-sidebar") ?? document.querySelector("aparte-sidebar");
|
|
6309
6419
|
}
|