@aparte/angular 0.16.11 → 0.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, inject, Injectable, makeEnvironmentProviders, provideAppInitializer, DestroyRef, effect, ElementRef, signal, output, computed, numberAttribute, booleanAttribute, ViewChildren, ViewChild, Input, ChangeDetectionStrategy, CUSTOM_ELEMENTS_SCHEMA, Component, Renderer2, Directive, EventEmitter, HostListener, Output } from '@angular/core';
2
+ import { InjectionToken, inject, Injectable, makeEnvironmentProviders, provideAppInitializer, DestroyRef, effect, ElementRef, signal, computed, output, numberAttribute, booleanAttribute, ViewChildren, ViewChild, Input, ChangeDetectionStrategy, CUSTOM_ELEMENTS_SCHEMA, Component, Renderer2, Directive, EventEmitter, HostListener, Output } from '@angular/core';
3
3
  import { AparteClient, aparteGlobalConfig, isAwaitingReply, uuid, AparteChatHost, APARTE_DEFAULT_UI_EVENTS, applyElementProps, AparteConversationManager } from '@aparte/core';
4
4
  export { aparteGlobalConfig } from '@aparte/core';
5
5
  import { DOCUMENT, NgTemplateOutlet } from '@angular/common';
@@ -18,6 +18,15 @@ const APARTE_CLIENT_OPTIONS = new InjectionToken('APARTE_CLIENT_OPTIONS');
18
18
  class AparteAiService {
19
19
  _clientOptions = inject(APARTE_CLIENT_OPTIONS, { optional: true });
20
20
  _client = new AparteClient(this._clientOptions ?? {});
21
+ /**
22
+ * The `AparteClient` this service drives — the same object React's
23
+ * `useAparteClient`, Vue's and Svelte's `createAparteClient` return as
24
+ * `{ client, abort }`. Angular alone kept it private, so the instance was
25
+ * unreachable while each service's docblock called itself the others' equivalent.
26
+ */
27
+ get client() {
28
+ return this._client;
29
+ }
21
30
  /**
22
31
  * Start listening to `aparte-send` events globally.
23
32
  * This connects the <aparte-chat> components to the AI Providers.
@@ -30,7 +39,9 @@ class AparteAiService {
30
39
  this._client.start();
31
40
  }
32
41
  /**
33
- * Stop listening.
42
+ * Stop listening. The client also takes its `keyResolver` back off the config,
43
+ * so a paused bridge no longer answers for keys — a model list refreshed while
44
+ * disconnected sees the config's other key sources only.
34
45
  */
35
46
  disconnect() {
36
47
  this._client.stop();
@@ -139,8 +150,22 @@ function applyThemeMode(mode, doc, destroyRef) {
139
150
  * `AparteAiService.connect()` needed.
140
151
  *
141
152
  * The components (`AparteChatComponent`, `AparteUiComponent`) are standalone and
142
- * work WITHOUT this — it is config sugar. You can equally call `aparteGlobalConfig.*`
143
- * yourself, exactly like the React/Vue/Svelte wrappers do.
153
+ * work WITHOUT this — it is config sugar. Angular is the only wrapper that has it,
154
+ * and the other three lose nothing: what this provider exists for is an initializer
155
+ * that runs before the first component and a `DestroyRef` to release the theme
156
+ * listener. Everything it configures is a plain call — bar `theme: 'auto'`, which is a
157
+ * `matchMedia` listener you own — so React, Vue and Svelte make the same ones at module
158
+ * scope, before the app mounts:
159
+ *
160
+ * ```ts
161
+ * aparteGlobalConfig.registerAIProvider(createOpenAICompatProvider(presets.OPENROUTER));
162
+ * aparteGlobalConfig.setModelConfig({ defaultModel: 'openai/gpt-4o-mini' });
163
+ * aparteGlobalConfig.setLocale(fr);
164
+ * document.documentElement.setAttribute('data-aparte-theme', 'dark');
165
+ * // 'auto' is the one option with no one-liner: match prefers-color-scheme, rewrite the
166
+ * // attribute on change, and drop the listener when your app tears down.
167
+ * // The client stays the wrapper's own: useAparteClient(), createAparteClient().
168
+ * ```
144
169
  *
145
170
  * @example
146
171
  * bootstrapApplication(App, {
@@ -200,15 +225,19 @@ class AparteChatComponent {
200
225
  // Debounced with requestAnimationFrame so rapid token bursts only trigger
201
226
  // one sync per paint frame instead of N syncs for N tokens.
202
227
  effect(() => {
203
- const msgs = this.messages();
204
- if (msgs.length > 0) {
205
- if (this._syncRafId !== null)
206
- return;
207
- this._syncRafId = requestAnimationFrame(() => {
208
- this._syncRafId = null;
209
- this._host?.syncBubbles();
210
- });
211
- }
228
+ // Read the signal so the effect tracks it — and react to EVERY value,
229
+ // the empty list included. React, Vue and Svelte all re-sync
230
+ // unconditionally, and emptying the transcript is exactly when the host
231
+ // has to let go of the bubbles that just went away. With the default
232
+ // bubbles `bubbleRefs.changes` hid the divergence; under a custom
233
+ // `[bubbleTemplate]` that query never fires and this was the only path.
234
+ this.messages();
235
+ if (this._syncRafId !== null)
236
+ return;
237
+ this._syncRafId = requestAnimationFrame(() => {
238
+ this._syncRafId = null;
239
+ this._host?.syncBubbles();
240
+ });
212
241
  });
213
242
  }
214
243
  elementRef = inject(ElementRef);
@@ -248,6 +277,35 @@ class AparteChatComponent {
248
277
  */
249
278
  set centerWhenEmptyInput(val) { this.centerWhenEmpty.set(val); }
250
279
  centerWhenEmpty = signal(false);
280
+ /**
281
+ * The conversation is on its way: the viewport draws two skeleton turns, the empty
282
+ * state stays off and the composer is disabled until it lands. The conversation
283
+ * controller sets this itself while it fetches through your adapter's `loadFull()`;
284
+ * the input is for a wait of your own. Off by default.
285
+ */
286
+ set loadingInput(val) { this.loading.set(val); }
287
+ loading = signal(false);
288
+ /** The controller's wait (a conversation fetched through the adapter). */
289
+ hostLoading = signal(false);
290
+ waiting = computed(() => this.loading() || this.hostLoading());
291
+ get loadingText() { return (this.config ?? aparteGlobalConfig).t('loadingConversation'); }
292
+ /**
293
+ * Class(es) merged onto the INNER `.aparte-chat-container`, not onto this
294
+ * component's own `<aparte-chat>` host.
295
+ *
296
+ * The other three wrappers have no such host: their root IS the container, so a
297
+ * consumer's `className` / `class` lands on the div that carries
298
+ * `.aparte-chat-container`, `[overlay-composer]` and `[data-aparte-empty]` — the
299
+ * three selectors core's shell recipe keys on. Here a class written on the tag
300
+ * lands one level above them, so overriding the shell needed a descendant
301
+ * selector on Angular alone. This input is that parity. Additive: the recipe's
302
+ * own classes stay.
303
+ */
304
+ set containerClassInput(val) { this.containerClass.set(val ?? ''); }
305
+ containerClass = signal('');
306
+ /** Inline style for the same div — a string or a `{ prop: value }` map. */
307
+ set containerStyleInput(val) { this.containerStyle.set(val ?? null); }
308
+ containerStyle = signal(null);
251
309
  /**
252
310
  * Overlay the composer on the transcript (the ChatGPT anatomy): the scroll
253
311
  * surface spans the whole column, the scrollbar runs edge to edge, and the
@@ -377,6 +435,7 @@ class AparteChatComponent {
377
435
  onMessageAppended: (msg) => this.messageAppended.emit(msg),
378
436
  onTypingChange: (typing) => { this.isTyping.set(typing); this.typingChange.emit(typing); },
379
437
  onStreamingChange: (id) => this._streamingId.set(id),
438
+ onLoadingChange: (on) => this.hostLoading.set(on),
380
439
  // Sibling-info / deferred work runs after Angular has re-rendered.
381
440
  afterRender: (cb) => { setTimeout(cb, 0); },
382
441
  resetComposer: () => {
@@ -443,8 +502,11 @@ class AparteChatComponent {
443
502
  }
444
503
  /** Read the current message list. */
445
504
  getMessages() { return this._host?.getMessages() ?? this.messages(); }
446
- /** Clear all messages + reset state. */
447
- clearMessages() { this._host?.clearMessages(); }
505
+ /**
506
+ * Clear all messages + reset state. `{ revokeAttachments: false }` keeps the
507
+ * attachments' object URLs alive — pass it when the same messages are going back.
508
+ */
509
+ clearMessages(options) { this._host?.clearMessages(options); }
448
510
  /** Scroll the viewport to the latest message. */
449
511
  scrollToBottom() {
450
512
  this.viewportRef?.nativeElement?.scrollToBottom?.();
@@ -571,15 +633,29 @@ class AparteChatComponent {
571
633
  this.messageSent.emit(detail);
572
634
  }
573
635
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: AparteChatComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
574
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.25", type: AparteChatComponent, isStandalone: true, selector: "aparte-chat", inputs: { messagesInput: ["messages", "messagesInput"], placeholderInput: ["placeholder", "placeholderInput"], disabledInput: ["disabled", "disabledInput", booleanAttribute], submitOnEnterInput: ["submitOnEnter", "submitOnEnterInput", booleanAttribute], centerWhenEmptyInput: ["centerWhenEmpty", "centerWhenEmptyInput", booleanAttribute], overlayComposerInput: ["overlayComposer", "overlayComposerInput", booleanAttribute], attachmentsInput: ["attachments", "attachmentsInput", booleanAttribute], elicitationInput: ["elicitation", "elicitationInput", booleanAttribute], isTypingInput: ["isTyping", "isTypingInput", booleanAttribute], typingTextInput: ["typingText", "typingTextInput"], layoutTransitionMs: ["layoutTransitionMs", "layoutTransitionMs", numberAttribute], conversationIdInput: ["conversationId", "conversationIdInput"], config: "config", bubbleTemplate: "bubbleTemplate" }, outputs: { messageSent: "messageSent", action: "action", messagesChange: "messagesChange", messageAppended: "messageAppended", typingChange: "typingChange", conversationCreated: "conversationCreated" }, host: { attributes: { "framework-managed": "" } }, viewQueries: [{ propertyName: "viewportRef", first: true, predicate: ["viewport"], descendants: true }, { propertyName: "inputRef", first: true, predicate: ["input"], descendants: true }, { propertyName: "bubbleRefs", predicate: ["bubble"], descendants: true }], ngImport: i0, template: `
636
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.25", type: AparteChatComponent, isStandalone: true, selector: "aparte-chat", inputs: { messagesInput: ["messages", "messagesInput"], placeholderInput: ["placeholder", "placeholderInput"], disabledInput: ["disabled", "disabledInput", booleanAttribute], submitOnEnterInput: ["submitOnEnter", "submitOnEnterInput", booleanAttribute], centerWhenEmptyInput: ["centerWhenEmpty", "centerWhenEmptyInput", booleanAttribute], loadingInput: ["loading", "loadingInput", booleanAttribute], containerClassInput: ["containerClass", "containerClassInput"], containerStyleInput: ["containerStyle", "containerStyleInput"], overlayComposerInput: ["overlayComposer", "overlayComposerInput", booleanAttribute], attachmentsInput: ["attachments", "attachmentsInput", booleanAttribute], elicitationInput: ["elicitation", "elicitationInput", booleanAttribute], isTypingInput: ["isTyping", "isTypingInput", booleanAttribute], typingTextInput: ["typingText", "typingTextInput"], layoutTransitionMs: ["layoutTransitionMs", "layoutTransitionMs", numberAttribute], conversationIdInput: ["conversationId", "conversationIdInput"], config: "config", bubbleTemplate: "bubbleTemplate" }, outputs: { messageSent: "messageSent", action: "action", messagesChange: "messagesChange", messageAppended: "messageAppended", typingChange: "typingChange", conversationCreated: "conversationCreated" }, host: { attributes: { "framework-managed": "" } }, viewQueries: [{ propertyName: "viewportRef", first: true, predicate: ["viewport"], descendants: true }, { propertyName: "inputRef", first: true, predicate: ["input"], descendants: true }, { propertyName: "bubbleRefs", predicate: ["bubble"], descendants: true }], ngImport: i0, template: `
575
637
  <div
576
638
  class="aparte-chat-container"
639
+ [class]="containerClass()"
640
+ [style]="containerStyle()"
577
641
  [class.aparte-chat-container--auto-center]="centerWhenEmpty()"
578
642
  [attr.overlay-composer]="overlayComposer() ? '' : null"
579
- [attr.data-aparte-empty]="centerWhenEmpty() && messages().length === 0 ? '' : null"
643
+ [attr.data-aparte-empty]="centerWhenEmpty() && messages().length === 0 && !waiting() ? '' : null"
580
644
  >
581
- <aparte-chat-viewport #viewport framework-managed="">
582
- @if (messages().length === 0) {
645
+ <aparte-chat-viewport #viewport framework-managed="" [attr.loading]="waiting() ? '' : null">
646
+ @if (waiting()) {
647
+ <!-- The wait, drawn here because this DOM is Angular's: the kit's skeleton
648
+ recipe, and a line for a screen reader. -->
649
+ <div class="aparte-viewport-loading" aria-hidden="true">
650
+ <span class="aparte-skeleton aparte-skeleton--rect aparte-viewport-loading__user"></span>
651
+ <span class="aparte-skeleton aparte-skeleton--text"></span>
652
+ <span class="aparte-skeleton aparte-skeleton--text"></span>
653
+ <span class="aparte-skeleton aparte-skeleton--text"></span>
654
+ <span class="aparte-skeleton aparte-skeleton--text aparte-viewport-loading__last"></span>
655
+ </div>
656
+ <span class="aparte-viewport-loading-status aparte-sr-only" role="status">{{ loadingText }}</span>
657
+ }
658
+ @if (messages().length === 0 && !waiting()) {
583
659
  <!-- Welcome / placeholder shown inside the viewport while empty. -->
584
660
  <ng-content select="[slot='empty-state']"></ng-content>
585
661
  }
@@ -620,7 +696,7 @@ class AparteChatComponent {
620
696
  <aparte-composer
621
697
  #input
622
698
  [attr.placeholder]="placeholder()"
623
- [attr.disabled]="disabled() ? '' : null"
699
+ [attr.disabled]="disabled() || waiting() ? '' : null"
624
700
  [attr.submit-on-enter]="submitOnEnter() ? null : 'false'"
625
701
  (aparte-send)="onAparteSend($event)"
626
702
  >
@@ -662,12 +738,26 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImpo
662
738
  args: [{ selector: 'aparte-chat', standalone: true, imports: [NgTemplateOutlet], schemas: [CUSTOM_ELEMENTS_SCHEMA], changeDetection: ChangeDetectionStrategy.OnPush, host: { 'framework-managed': '' }, template: `
663
739
  <div
664
740
  class="aparte-chat-container"
741
+ [class]="containerClass()"
742
+ [style]="containerStyle()"
665
743
  [class.aparte-chat-container--auto-center]="centerWhenEmpty()"
666
744
  [attr.overlay-composer]="overlayComposer() ? '' : null"
667
- [attr.data-aparte-empty]="centerWhenEmpty() && messages().length === 0 ? '' : null"
745
+ [attr.data-aparte-empty]="centerWhenEmpty() && messages().length === 0 && !waiting() ? '' : null"
668
746
  >
669
- <aparte-chat-viewport #viewport framework-managed="">
670
- @if (messages().length === 0) {
747
+ <aparte-chat-viewport #viewport framework-managed="" [attr.loading]="waiting() ? '' : null">
748
+ @if (waiting()) {
749
+ <!-- The wait, drawn here because this DOM is Angular's: the kit's skeleton
750
+ recipe, and a line for a screen reader. -->
751
+ <div class="aparte-viewport-loading" aria-hidden="true">
752
+ <span class="aparte-skeleton aparte-skeleton--rect aparte-viewport-loading__user"></span>
753
+ <span class="aparte-skeleton aparte-skeleton--text"></span>
754
+ <span class="aparte-skeleton aparte-skeleton--text"></span>
755
+ <span class="aparte-skeleton aparte-skeleton--text"></span>
756
+ <span class="aparte-skeleton aparte-skeleton--text aparte-viewport-loading__last"></span>
757
+ </div>
758
+ <span class="aparte-viewport-loading-status aparte-sr-only" role="status">{{ loadingText }}</span>
759
+ }
760
+ @if (messages().length === 0 && !waiting()) {
671
761
  <!-- Welcome / placeholder shown inside the viewport while empty. -->
672
762
  <ng-content select="[slot='empty-state']"></ng-content>
673
763
  }
@@ -708,7 +798,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImpo
708
798
  <aparte-composer
709
799
  #input
710
800
  [attr.placeholder]="placeholder()"
711
- [attr.disabled]="disabled() ? '' : null"
801
+ [attr.disabled]="disabled() || waiting() ? '' : null"
712
802
  [attr.submit-on-enter]="submitOnEnter() ? null : 'false'"
713
803
  (aparte-send)="onAparteSend($event)"
714
804
  >
@@ -759,6 +849,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImpo
759
849
  }], centerWhenEmptyInput: [{
760
850
  type: Input,
761
851
  args: [{ alias: 'centerWhenEmpty', transform: booleanAttribute }]
852
+ }], loadingInput: [{
853
+ type: Input,
854
+ args: [{ alias: 'loading', transform: booleanAttribute }]
855
+ }], containerClassInput: [{
856
+ type: Input,
857
+ args: ['containerClass']
858
+ }], containerStyleInput: [{
859
+ type: Input,
860
+ args: ['containerStyle']
762
861
  }], overlayComposerInput: [{
763
862
  type: Input,
764
863
  args: [{ alias: 'overlayComposer', transform: booleanAttribute }]
@@ -1034,7 +1133,7 @@ class AparteChatBubbleDirective extends AparteElementBase {
1034
1133
  edit = new EventEmitter();
1035
1134
  /** The `‹1/2›` picker moved between sibling versions. */
1036
1135
  branchNavigate = new EventEmitter();
1037
- /** A link in the message body is about to be followed. Cancelable: `preventDefault()` keeps the browser from navigating, so a host can route the link itself. */
1136
+ /** A link in the message body is about to be followed. Cancelable: `preventDefault()` keeps the browser from navigating, so a host can route the link itself. Angular: this Output emits the detail, so preventDefault() is not on it — bind the DOM event for that: (aparte-link-click)="$event.preventDefault()". */
1038
1137
  linkClick = new EventEmitter();
1039
1138
  onAttachmentPreview(e) { this.attachmentPreview.emit(e.detail); }
1040
1139
  onAction(e) { this.action.emit(e.detail); }
@@ -1128,6 +1227,8 @@ class AparteChatViewportDirective extends AparteElementBase {
1128
1227
  set scrollThreshold(v) { this.write('scroll-threshold', v); }
1129
1228
  /** Caps how many bubbles stay in the DOM; older ones are released. */
1130
1229
  set maxRenderedBubbles(v) { this.write('max-rendered-bubbles', v); }
1230
+ /** The transcript is on its way: a conversation was chosen and its messages are being fetched. The element draws two skeleton turns, marks its scroll surface `aria-busy` and names the wait for a screen reader; `aparte-chat[center-empty]` does not count a loading viewport as empty. Set by `AparteConversationController` through its binding, or by hand from a loop of your own (`setLoading(true)`). */
1231
+ set loading(v) { this.write('loading', v); }
1131
1232
  /** The wrapper's explicit hands-off signal: set it and this element builds no wrapper of its own and relocates none of the nodes the FRAMEWORK renders into it, because the framework owns them. Not "none of its children": core's own scroll-to-bottom button is re-appended whenever it stops being last, and that path runs in this mode only. All four wrappers set it. */
1132
1233
  set frameworkManaged(v) { this.write('framework-managed', v); }
1133
1234
  /** Reflected BY the element while a turn streams: the transcript is read-only meanwhile, and every bubble inside reads it (at connect, and when it changes). The vanilla path derives it from the repository; a framework host sets it through `setTranscriptBusy()`. Read-only from the outside. */
@@ -1142,7 +1243,7 @@ class AparteChatViewportDirective extends AparteElementBase {
1142
1243
  onResetDone() { this.resetDone.emit(); }
1143
1244
  onPathChanged(e) { this.pathChanged.emit(e.detail); }
1144
1245
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: AparteChatViewportDirective, deps: null, target: i0.ɵɵFactoryTarget.Directive });
1145
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "16.1.0", version: "19.2.25", type: AparteChatViewportDirective, isStandalone: true, selector: "aparte-chat-viewport", inputs: { scrollThreshold: ["scrollThreshold", "scrollThreshold", numberAttribute], maxRenderedBubbles: ["maxRenderedBubbles", "maxRenderedBubbles", numberAttribute], frameworkManaged: ["frameworkManaged", "frameworkManaged", booleanAttribute], dataBusy: ["dataBusy", "dataBusy", booleanAttribute] }, outputs: { segmentUpdate: "segmentUpdate", resetDone: "resetDone", pathChanged: "pathChanged" }, host: { listeners: { "aparte-segment-update": "onSegmentUpdate($event)", "aparte-reset-done": "onResetDone()", "aparte-path-changed": "onPathChanged($event)" } }, usesInheritance: true, ngImport: i0 });
1246
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "16.1.0", version: "19.2.25", type: AparteChatViewportDirective, isStandalone: true, selector: "aparte-chat-viewport", inputs: { scrollThreshold: ["scrollThreshold", "scrollThreshold", numberAttribute], maxRenderedBubbles: ["maxRenderedBubbles", "maxRenderedBubbles", numberAttribute], loading: ["loading", "loading", booleanAttribute], frameworkManaged: ["frameworkManaged", "frameworkManaged", booleanAttribute], dataBusy: ["dataBusy", "dataBusy", booleanAttribute] }, outputs: { segmentUpdate: "segmentUpdate", resetDone: "resetDone", pathChanged: "pathChanged" }, host: { listeners: { "aparte-segment-update": "onSegmentUpdate($event)", "aparte-reset-done": "onResetDone()", "aparte-path-changed": "onPathChanged($event)" } }, usesInheritance: true, ngImport: i0 });
1146
1247
  }
1147
1248
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: AparteChatViewportDirective, decorators: [{
1148
1249
  type: Directive,
@@ -1153,6 +1254,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImpo
1153
1254
  }], maxRenderedBubbles: [{
1154
1255
  type: Input,
1155
1256
  args: [{ transform: numberAttribute }]
1257
+ }], loading: [{
1258
+ type: Input,
1259
+ args: [{ transform: booleanAttribute }]
1156
1260
  }], frameworkManaged: [{
1157
1261
  type: Input,
1158
1262
  args: [{ transform: booleanAttribute }]
@@ -1242,7 +1346,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImpo
1242
1346
  }] } });
1243
1347
  /** `<aparte-composer-action>` — Generic action button primitive for <aparte-composer>. */
1244
1348
  class AparteComposerActionDirective extends AparteElementBase {
1245
- /** Icon key for aparteGlobalConfig.getIcon(), or raw SVG/HTML starting with `<` */
1349
+ /** Icon key for aparteGlobalConfig.getIcon(), or raw SVG/HTML starting with `<`. A key core's glyph set does not hold renders AS TEXT (`icon="star"` draws the word *star*), so bring your own set through `setIconProvider` or pass the SVG. */
1246
1350
  set icon(v) { this.write('icon', v); }
1247
1351
  /** Accessible label (also used as tooltip) */
1248
1352
  set label(v) { this.write('label', v); }
@@ -1457,79 +1561,89 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImpo
1457
1561
  type: HostListener,
1458
1562
  args: ['aparte-compact', ['$event']]
1459
1563
  }] } });
1460
- /** `<aparte-conversation-list>` — Conversation-history sidebar — a framework-agnostic web component. The host sets the `conversations` JS property and the `active-id` attribute; this renders the rows and fires the user's intent, never acting on it itself. */
1564
+ /** `<aparte-conversation-list>` — Conversation-history sidebar — a framework-agnostic web component. The host sets the `conversations` JS property and the `active-id` attribute; this renders the rows and fires the user's intent. Acting on it is the host's, unless `manage` hands the row menu's writes to the registered conversation manager. */
1461
1565
  class AparteConversationListDirective extends AparteElementBase {
1462
1566
  /** The id of the conversation to render as selected. */
1463
1567
  set activeId(v) { this.write('active-id', v); }
1464
1568
  /** Render the rows flat, in host order, with no date headings. */
1465
- set noGroups(v) { this.write('no-groups', v); }
1466
- /** The same item on a pinned row; same detail shape, opposite intent. */
1467
- unpinConversation = new EventEmitter();
1468
- /** The pin item was chosen on an unpinned row. */
1469
- pinConversation = new EventEmitter();
1470
- /** The same item on an already-archived one; same detail shape, opposite intent. */
1471
- unarchiveConversation = new EventEmitter();
1472
- /** The archive item was chosen on a live conversation. */
1473
- archiveConversation = new EventEmitter();
1474
- /** The delete was confirmed. Nothing is removed here. */
1475
- deleteConversation = new EventEmitter();
1476
- /** A rename was committed with a new, non-empty title. Nothing is renamed here. */
1477
- renameConversation = new EventEmitter();
1569
+ set flat(v) { this.write('flat', v); }
1570
+ /** The list is on its way: six skeleton rows stand where the rows will be, the element is `aria-busy` and a visually hidden line names the wait. A list that is not there yet is not an empty list. Set it while your store answers, clear it when you assign `conversations`. */
1571
+ set loading(v) { this.write('loading', v); }
1572
+ /** Let the list finish the gesture: rename, pin, unpin, archive, unarchive and delete are performed on the conversation manager registered with `setConversationManager()`, instead of leaving you six listeners to write. The event still fires first, and `preventDefault()` on it takes the gesture back. Off by default, and selecting stays yours either way. */
1573
+ set manage(v) { this.write('manage', v); }
1478
1574
  /** A row's title was activated; the host loads that conversation. */
1479
- selectConversation = new EventEmitter();
1480
- onUnpinConversation(e) { this.unpinConversation.emit(e.detail); }
1481
- onPinConversation(e) { this.pinConversation.emit(e.detail); }
1482
- onUnarchiveConversation(e) { this.unarchiveConversation.emit(e.detail); }
1483
- onArchiveConversation(e) { this.archiveConversation.emit(e.detail); }
1484
- onDeleteConversation(e) { this.deleteConversation.emit(e.detail); }
1485
- onRenameConversation(e) { this.renameConversation.emit(e.detail); }
1486
- onSelectConversation(e) { this.selectConversation.emit(e.detail); }
1575
+ conversationSelect = new EventEmitter();
1576
+ /** A rename was committed with a new, non-empty title. Cancelable: `preventDefault()` takes the gesture back from `manage`. Angular: this Output emits the detail, so preventDefault() is not on it — bind the DOM event for that: (aparte-conversation-rename)="$event.preventDefault()". */
1577
+ conversationRename = new EventEmitter();
1578
+ /** The pin item was chosen on an unpinned row. Cancelable: `preventDefault()` takes the gesture back from `manage`. Angular: this Output emits the detail, so preventDefault() is not on it — bind the DOM event for that: (aparte-conversation-pin)="$event.preventDefault()". */
1579
+ conversationPin = new EventEmitter();
1580
+ /** The same item on a pinned row; same detail shape, opposite intent. Cancelable, like the rest. Angular: this Output emits the detail, so preventDefault() is not on it — bind the DOM event for that: (aparte-conversation-unpin)="$event.preventDefault()". */
1581
+ conversationUnpin = new EventEmitter();
1582
+ /** The archive item was chosen on a live conversation. Cancelable: `preventDefault()` takes the gesture back from `manage`. Angular: this Output emits the detail, so preventDefault() is not on it — bind the DOM event for that: (aparte-conversation-archive)="$event.preventDefault()". */
1583
+ conversationArchive = new EventEmitter();
1584
+ /** The same item on an already-archived one; same detail shape, opposite intent. Cancelable, like the rest. Angular: this Output emits the detail, so preventDefault() is not on it — bind the DOM event for that: (aparte-conversation-unarchive)="$event.preventDefault()". */
1585
+ conversationUnarchive = new EventEmitter();
1586
+ /** The delete was confirmed. Cancelable: `preventDefault()` takes the gesture back from `manage`. Angular: this Output emits the detail, so preventDefault() is not on it — bind the DOM event for that: (aparte-conversation-delete)="$event.preventDefault()". */
1587
+ conversationDelete = new EventEmitter();
1588
+ onConversationSelect(e) { this.conversationSelect.emit(e.detail); }
1589
+ onConversationRename(e) { this.conversationRename.emit(e.detail); }
1590
+ onConversationPin(e) { this.conversationPin.emit(e.detail); }
1591
+ onConversationUnpin(e) { this.conversationUnpin.emit(e.detail); }
1592
+ onConversationArchive(e) { this.conversationArchive.emit(e.detail); }
1593
+ onConversationUnarchive(e) { this.conversationUnarchive.emit(e.detail); }
1594
+ onConversationDelete(e) { this.conversationDelete.emit(e.detail); }
1487
1595
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: AparteConversationListDirective, deps: null, target: i0.ɵɵFactoryTarget.Directive });
1488
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "16.1.0", version: "19.2.25", type: AparteConversationListDirective, isStandalone: true, selector: "aparte-conversation-list", inputs: { activeId: "activeId", noGroups: ["noGroups", "noGroups", booleanAttribute] }, outputs: { unpinConversation: "unpinConversation", pinConversation: "pinConversation", unarchiveConversation: "unarchiveConversation", archiveConversation: "archiveConversation", deleteConversation: "deleteConversation", renameConversation: "renameConversation", selectConversation: "selectConversation" }, host: { listeners: { "aparte-unpin-conversation": "onUnpinConversation($event)", "aparte-pin-conversation": "onPinConversation($event)", "aparte-unarchive-conversation": "onUnarchiveConversation($event)", "aparte-archive-conversation": "onArchiveConversation($event)", "aparte-delete-conversation": "onDeleteConversation($event)", "aparte-rename-conversation": "onRenameConversation($event)", "aparte-select-conversation": "onSelectConversation($event)" } }, usesInheritance: true, ngImport: i0 });
1596
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "16.1.0", version: "19.2.25", type: AparteConversationListDirective, isStandalone: true, selector: "aparte-conversation-list", inputs: { activeId: "activeId", flat: ["flat", "flat", booleanAttribute], loading: ["loading", "loading", booleanAttribute], manage: ["manage", "manage", booleanAttribute] }, outputs: { conversationSelect: "conversationSelect", conversationRename: "conversationRename", conversationPin: "conversationPin", conversationUnpin: "conversationUnpin", conversationArchive: "conversationArchive", conversationUnarchive: "conversationUnarchive", conversationDelete: "conversationDelete" }, host: { listeners: { "aparte-conversation-select": "onConversationSelect($event)", "aparte-conversation-rename": "onConversationRename($event)", "aparte-conversation-pin": "onConversationPin($event)", "aparte-conversation-unpin": "onConversationUnpin($event)", "aparte-conversation-archive": "onConversationArchive($event)", "aparte-conversation-unarchive": "onConversationUnarchive($event)", "aparte-conversation-delete": "onConversationDelete($event)" } }, usesInheritance: true, ngImport: i0 });
1489
1597
  }
1490
1598
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: AparteConversationListDirective, decorators: [{
1491
1599
  type: Directive,
1492
1600
  args: [{ selector: 'aparte-conversation-list', standalone: true }]
1493
1601
  }], propDecorators: { activeId: [{
1494
1602
  type: Input
1495
- }], noGroups: [{
1603
+ }], flat: [{
1496
1604
  type: Input,
1497
1605
  args: [{ transform: booleanAttribute }]
1498
- }], unpinConversation: [{
1606
+ }], loading: [{
1607
+ type: Input,
1608
+ args: [{ transform: booleanAttribute }]
1609
+ }], manage: [{
1610
+ type: Input,
1611
+ args: [{ transform: booleanAttribute }]
1612
+ }], conversationSelect: [{
1499
1613
  type: Output
1500
- }], pinConversation: [{
1614
+ }], conversationRename: [{
1501
1615
  type: Output
1502
- }], unarchiveConversation: [{
1616
+ }], conversationPin: [{
1503
1617
  type: Output
1504
- }], archiveConversation: [{
1618
+ }], conversationUnpin: [{
1505
1619
  type: Output
1506
- }], deleteConversation: [{
1620
+ }], conversationArchive: [{
1507
1621
  type: Output
1508
- }], renameConversation: [{
1622
+ }], conversationUnarchive: [{
1509
1623
  type: Output
1510
- }], selectConversation: [{
1624
+ }], conversationDelete: [{
1511
1625
  type: Output
1512
- }], onUnpinConversation: [{
1626
+ }], onConversationSelect: [{
1513
1627
  type: HostListener,
1514
- args: ['aparte-unpin-conversation', ['$event']]
1515
- }], onPinConversation: [{
1628
+ args: ['aparte-conversation-select', ['$event']]
1629
+ }], onConversationRename: [{
1516
1630
  type: HostListener,
1517
- args: ['aparte-pin-conversation', ['$event']]
1518
- }], onUnarchiveConversation: [{
1631
+ args: ['aparte-conversation-rename', ['$event']]
1632
+ }], onConversationPin: [{
1519
1633
  type: HostListener,
1520
- args: ['aparte-unarchive-conversation', ['$event']]
1521
- }], onArchiveConversation: [{
1634
+ args: ['aparte-conversation-pin', ['$event']]
1635
+ }], onConversationUnpin: [{
1522
1636
  type: HostListener,
1523
- args: ['aparte-archive-conversation', ['$event']]
1524
- }], onDeleteConversation: [{
1637
+ args: ['aparte-conversation-unpin', ['$event']]
1638
+ }], onConversationArchive: [{
1525
1639
  type: HostListener,
1526
- args: ['aparte-delete-conversation', ['$event']]
1527
- }], onRenameConversation: [{
1640
+ args: ['aparte-conversation-archive', ['$event']]
1641
+ }], onConversationUnarchive: [{
1528
1642
  type: HostListener,
1529
- args: ['aparte-rename-conversation', ['$event']]
1530
- }], onSelectConversation: [{
1643
+ args: ['aparte-conversation-unarchive', ['$event']]
1644
+ }], onConversationDelete: [{
1531
1645
  type: HostListener,
1532
- args: ['aparte-select-conversation', ['$event']]
1646
+ args: ['aparte-conversation-delete', ['$event']]
1533
1647
  }] } });
1534
1648
  /** `<aparte-elicitation>` — The default presenter for a request to the human. It renders nothing itself: it registers as the presenter for the config governing its subtree, and mounts a panel inside the nearest `<aparte-composer>` when something asks — a tool handler calling `requestUserInput`, or core's own approval gate. */
1535
1649
  /**
@@ -1644,7 +1758,7 @@ class AparteScrollRailDirective extends AparteElementBase {
1644
1758
  set every(v) { this.write('every', v); }
1645
1759
  /** Reflected BY the element while it has fewer than two ticks. Read-only. */
1646
1760
  set dataEmpty(v) { this.write('data-empty', v); }
1647
- /** A tick was activated. Bubbles, cancelable: `preventDefault()` leaves the transcript where it is. */
1761
+ /** A tick was activated. Bubbles, cancelable: `preventDefault()` leaves the transcript where it is. Angular: this Output emits the detail, so preventDefault() is not on it — bind the DOM event for that: (aparte-scroll-rail-jump)="$event.preventDefault()". */
1648
1762
  scrollRailJump = new EventEmitter();
1649
1763
  onScrollRailJump(e) { this.scrollRailJump.emit(e.detail); }
1650
1764
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: AparteScrollRailDirective, deps: null, target: i0.ɵɵFactoryTarget.Directive });
@@ -1821,17 +1935,17 @@ class AparteSuggestionsDirective extends AparteElementBase {
1821
1935
  set suggestions(v) { this.write('suggestions', v); }
1822
1936
  /** The id of the `<aparte-chat>` whose composer should receive the click, when the element is not inside that composer. */
1823
1937
  set target(v) { this.write('target', v); }
1938
+ /** Shows the row only while the conversation it serves is empty: hidden (`hidden`) once a message is in its transcript or on its way — a thread being loaded counts — and shown again when the transcript is cleared, by a new chat for instance. The composer's send hides it at once, before its message lands. The element sets and lifts `hidden` itself and never lifts one you set. With no transcript to read — a composer outside any chat — the first send hides it until the attribute is removed. */
1939
+ set emptyOnly(v) { this.write('empty-only', v); }
1824
1940
  /** `send` (default): the click fills the composer and submits. `fill`: the click fills the composer and focuses it, so the reader edits first. */
1825
1941
  set mode(v) { this.write('mode', v); }
1826
- /** Hides the row (`hidden`) once its composer has sent something. Remove the attribute, or `hidden`, to show it again. */
1827
- set emptyOnly(v) { this.write('empty-only', v); }
1828
1942
  /** Reflected BY the element while it has no suggestion to show; the stylesheet hides it then. Read-only, do not set it yourself. */
1829
1943
  set dataEmpty(v) { this.write('data-empty', v); }
1830
- /** A starter was clicked. Bubbles, and is cancelable: `preventDefault()` stops the composer from being filled or submitted. */
1944
+ /** A starter was clicked. Bubbles, and is cancelable: `preventDefault()` stops the composer from being filled or submitted. Angular: this Output emits the detail, so preventDefault() is not on it — bind the DOM event for that: (aparte-suggestion)="$event.preventDefault()". */
1831
1945
  suggestion = new EventEmitter();
1832
1946
  onSuggestion(e) { this.suggestion.emit(e.detail); }
1833
1947
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: AparteSuggestionsDirective, deps: null, target: i0.ɵɵFactoryTarget.Directive });
1834
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "16.1.0", version: "19.2.25", type: AparteSuggestionsDirective, isStandalone: true, selector: "aparte-suggestions", inputs: { suggestions: "suggestions", target: "target", mode: "mode", emptyOnly: ["emptyOnly", "emptyOnly", booleanAttribute], dataEmpty: ["dataEmpty", "dataEmpty", booleanAttribute] }, outputs: { suggestion: "suggestion" }, host: { listeners: { "aparte-suggestion": "onSuggestion($event)" } }, usesInheritance: true, ngImport: i0 });
1948
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "16.1.0", version: "19.2.25", type: AparteSuggestionsDirective, isStandalone: true, selector: "aparte-suggestions", inputs: { suggestions: "suggestions", target: "target", emptyOnly: ["emptyOnly", "emptyOnly", booleanAttribute], mode: "mode", dataEmpty: ["dataEmpty", "dataEmpty", booleanAttribute] }, outputs: { suggestion: "suggestion" }, host: { listeners: { "aparte-suggestion": "onSuggestion($event)" } }, usesInheritance: true, ngImport: i0 });
1835
1949
  }
1836
1950
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: AparteSuggestionsDirective, decorators: [{
1837
1951
  type: Directive,
@@ -1840,11 +1954,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImpo
1840
1954
  type: Input
1841
1955
  }], target: [{
1842
1956
  type: Input
1843
- }], mode: [{
1844
- type: Input
1845
1957
  }], emptyOnly: [{
1846
1958
  type: Input,
1847
1959
  args: [{ transform: booleanAttribute }]
1960
+ }], mode: [{
1961
+ type: Input
1848
1962
  }], dataEmpty: [{
1849
1963
  type: Input,
1850
1964
  args: [{ transform: booleanAttribute }]
@@ -1965,7 +2079,7 @@ class ConversationManagerService {
1965
2079
  // The active conversation is owned exclusively by AparteConversationController
1966
2080
  // (inside <aparte-chat>). To switch conversations, either bind
1967
2081
  // `[conversationId]` on <aparte-chat> or dispatch a window event:
1968
- // window.dispatchEvent(new CustomEvent('aparte-select-conversation', {
2082
+ // window.dispatchEvent(new CustomEvent('aparte-conversation-select', {
1969
2083
  // detail: { id, targetId? },
1970
2084
  // }));
1971
2085
  // The `activeId` signal here is read-only and stays in sync via subscribe().