@magicx-eng/ai-autocomplete-angular 0.1.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.
Files changed (57) hide show
  1. package/README.md +582 -0
  2. package/esm2020/lib/ai-autocomplete/ai-autocomplete.component.mjs +365 -0
  3. package/esm2020/lib/ai-autocomplete/index.mjs +2 -0
  4. package/esm2020/lib/ai-autocomplete-dropdown/ai-autocomplete-dropdown.component.mjs +81 -0
  5. package/esm2020/lib/ai-autocomplete-dropdown/index.mjs +2 -0
  6. package/esm2020/lib/ai-autocomplete.module.mjs +26 -0
  7. package/esm2020/lib/controller/ai-autocomplete.controller.mjs +226 -0
  8. package/esm2020/lib/controller/index.mjs +2 -0
  9. package/esm2020/lib/controller/observables.mjs +24 -0
  10. package/esm2020/lib/input/ai-autocomplete-input.directive.mjs +180 -0
  11. package/esm2020/lib/input/index.mjs +2 -0
  12. package/esm2020/lib/internal/editor-host/editor-host.directive.mjs +253 -0
  13. package/esm2020/lib/internal/pill-list/pill-list.component.mjs +65 -0
  14. package/esm2020/lib/internal/suggestion-grid/suggestion-grid.component.mjs +97 -0
  15. package/esm2020/lib/internal/suggestion-item/suggestion-item.component.mjs +76 -0
  16. package/esm2020/lib/shared/types.mjs +2 -0
  17. package/esm2020/magicx-eng-ai-autocomplete-angular.mjs +5 -0
  18. package/esm2020/public-api.mjs +18 -0
  19. package/fesm2015/magicx-eng-ai-autocomplete-angular.mjs +1386 -0
  20. package/fesm2015/magicx-eng-ai-autocomplete-angular.mjs.map +1 -0
  21. package/fesm2020/magicx-eng-ai-autocomplete-angular.mjs +1370 -0
  22. package/fesm2020/magicx-eng-ai-autocomplete-angular.mjs.map +1 -0
  23. package/index.d.ts +6 -0
  24. package/lib/ai-autocomplete/ai-autocomplete.component.d.ts +113 -0
  25. package/lib/ai-autocomplete/ai-autocomplete.component.d.ts.map +1 -0
  26. package/lib/ai-autocomplete/index.d.ts +2 -0
  27. package/lib/ai-autocomplete/index.d.ts.map +1 -0
  28. package/lib/ai-autocomplete-dropdown/ai-autocomplete-dropdown.component.d.ts +51 -0
  29. package/lib/ai-autocomplete-dropdown/ai-autocomplete-dropdown.component.d.ts.map +1 -0
  30. package/lib/ai-autocomplete-dropdown/index.d.ts +2 -0
  31. package/lib/ai-autocomplete-dropdown/index.d.ts.map +1 -0
  32. package/lib/ai-autocomplete.module.d.ts +18 -0
  33. package/lib/ai-autocomplete.module.d.ts.map +1 -0
  34. package/lib/controller/ai-autocomplete.controller.d.ts +131 -0
  35. package/lib/controller/ai-autocomplete.controller.d.ts.map +1 -0
  36. package/lib/controller/index.d.ts +2 -0
  37. package/lib/controller/index.d.ts.map +1 -0
  38. package/lib/controller/observables.d.ts +22 -0
  39. package/lib/controller/observables.d.ts.map +1 -0
  40. package/lib/input/ai-autocomplete-input.directive.d.ts +60 -0
  41. package/lib/input/ai-autocomplete-input.directive.d.ts.map +1 -0
  42. package/lib/input/index.d.ts +2 -0
  43. package/lib/input/index.d.ts.map +1 -0
  44. package/lib/internal/editor-host/editor-host.directive.d.ts +46 -0
  45. package/lib/internal/editor-host/editor-host.directive.d.ts.map +1 -0
  46. package/lib/internal/pill-list/pill-list.component.d.ts +35 -0
  47. package/lib/internal/pill-list/pill-list.component.d.ts.map +1 -0
  48. package/lib/internal/suggestion-grid/suggestion-grid.component.d.ts +37 -0
  49. package/lib/internal/suggestion-grid/suggestion-grid.component.d.ts.map +1 -0
  50. package/lib/internal/suggestion-item/suggestion-item.component.d.ts +33 -0
  51. package/lib/internal/suggestion-item/suggestion-item.component.d.ts.map +1 -0
  52. package/lib/shared/types.d.ts +125 -0
  53. package/lib/shared/types.d.ts.map +1 -0
  54. package/magicx-eng-ai-autocomplete-angular.d.ts.map +1 -0
  55. package/package.json +46 -0
  56. package/public-api.d.ts +15 -0
  57. package/public-api.d.ts.map +1 -0
@@ -0,0 +1,1386 @@
1
+ import * as i1 from '@angular/common';
2
+ import { CommonModule, isPlatformBrowser } from '@angular/common';
3
+ import * as i0 from '@angular/core';
4
+ import { EventEmitter, Component, ChangeDetectionStrategy, ViewEncapsulation, Input, Output, inject, ChangeDetectorRef, NgZone, ViewChild, ElementRef, Renderer2, Directive, PLATFORM_ID, forwardRef, NgModule } from '@angular/core';
5
+ import { NG_VALUE_ACCESSOR } from '@angular/forms';
6
+ import { AIAutocomplete, getCursorOffset, extractPlainText, renderEditableContent, plainTextLength, setCursorOffset, ModeController, buildQuery } from '@magicx-eng/ai-autocomplete-vanilla';
7
+ import { map } from 'rxjs/operators';
8
+ import { map as map$1, distinctUntilChanged, shareReplay, BehaviorSubject, Subject, takeUntil } from 'rxjs';
9
+
10
+ /**
11
+ * Internal pill list used by both the dropdown and Tier 1's inline pillbar.
12
+ * Mirrors `packages/react/src/components/PillList.tsx` one-for-one — including
13
+ * the per-index opacity ramp (0.4 / 0.3 / 0.15) and the skeleton-loading
14
+ * fallback that preserves layout while the next fetch is in flight.
15
+ *
16
+ * @internal
17
+ */
18
+ class PillListComponent {
19
+ constructor() {
20
+ this.pills = [];
21
+ this.activePillIndex = 0;
22
+ this.rounded = false;
23
+ /**
24
+ * When true, pills become non-interactive shimmering skeletons. Real pills
25
+ * (with their text) are rendered when provided so widths/positions match
26
+ * the previous state; with no pills, fixed-width fallback placeholders are
27
+ * rendered instead.
28
+ */
29
+ this.loading = false;
30
+ this.selectPill = new EventEmitter();
31
+ /** Fallback widths when loading with zero cached pills (matches React). */
32
+ this.fallbackSkeletonWidths = [125, 69];
33
+ this.trackByType = (_, pill) => pill.type;
34
+ this.trackByWidth = (_, w) => w;
35
+ }
36
+ /** Per-index opacity ramp — mirrors PillList.tsx `getPillOpacity`. */
37
+ pillOpacity(index) {
38
+ if (index === 0)
39
+ return 0.4;
40
+ if (index === 1)
41
+ return 0.3;
42
+ return 0.15;
43
+ }
44
+ onPillClick(index) {
45
+ if (this.loading)
46
+ return;
47
+ this.selectPill.emit(index);
48
+ }
49
+ onPillMouseDown(event) {
50
+ // Prevent the editor from losing focus when a pill is tapped.
51
+ event.preventDefault();
52
+ }
53
+ }
54
+ PillListComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: PillListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
55
+ PillListComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "15.2.10", type: PillListComponent, isStandalone: true, selector: "aia-pill-list", inputs: { pills: "pills", activePillIndex: "activePillIndex", rounded: "rounded", loading: "loading" }, outputs: { selectPill: "selectPill" }, ngImport: i0, template: "<ng-container *ngIf=\"loading && pills.length === 0; else realPills\">\n <span class=\"aia-pill-list\" data-aia-pill-list-loading=\"\">\n <span\n *ngFor=\"let w of fallbackSkeletonWidths; trackBy: trackByWidth; let i = index\"\n data-aia-pill-skeleton=\"\"\n class=\"aia-pill aia-pill-skeleton\"\n [class.aia-pill-rounded]=\"rounded\"\n [style.width.px]=\"w\"\n [style.opacity]=\"pillOpacity(i)\"\n ></span>\n </span>\n</ng-container>\n\n<ng-template #realPills>\n <span\n class=\"aia-pill-list\"\n [attr.data-aia-pill-list-loading]=\"loading ? '' : null\"\n >\n <button\n *ngFor=\"let pill of pills; trackBy: trackByType; let i = index\"\n type=\"button\"\n data-aia-pill=\"\"\n [attr.data-aia-loading]=\"loading ? '' : null\"\n tabindex=\"-1\"\n class=\"aia-pill\"\n [class.aia-pill-rounded]=\"rounded\"\n [class.aia-pill-active]=\"i === activePillIndex && !loading\"\n [class.aia-pill-skeleton]=\"loading\"\n [style.opacity]=\"pillOpacity(i)\"\n [disabled]=\"loading\"\n (mousedown)=\"onPillMouseDown($event)\"\n (click)=\"onPillClick(i)\"\n >\n {{ pill.text }}\n </button>\n </span>\n</ng-template>\n", styles: [".aia-pill-list{position:relative;z-index:1;pointer-events:auto;display:inline-flex;gap:5px;align-items:center;vertical-align:middle}.aia-pill{display:inline-flex;align-items:center;justify-content:center;height:36px;padding:13px;border:none;border-radius:999px;background:rgba(49,50,85,.25);background:color-mix(in srgb,var(--aia-pill-bg, var(--aia-color-background-supportive, #313255)) 25%,transparent);color:var(--aia-pill-color, var(--aia-color-text-muted, #c1c4cb));font-family:inherit;font-size:var(--aia-pill-font-size, 19px);line-height:30px;cursor:pointer;white-space:nowrap;animation:aiaPillFadeIn .4s cubic-bezier(.4,0,.2,1) forwards;box-shadow:#fff -3.2px -3.2px 3.2px -3.2px inset,#fff 6.4px 6.4px 1.6px -8px inset,var(--aia-dropdown-bg, transparent) -6.4px 6.4px 1.6px -8px inset,var(--aia-dropdown-bg, transparent) 6.4px -6.4px 1.6px -8px inset,#ffffff26 -1.6px 0 0 -1.6px inset,#ffffff26 0 -1.6px 0 -1.6px inset,#ffffff4d 0 1.6px inset,#ffffff4d 1.6px 0 inset,inset 0 0 30px 5px #0000000d,#00000014 0 0 30px 2px;backdrop-filter:blur(30px)}.aia-pill-rounded{border-radius:999px}.aia-pill-skeleton{pointer-events:none;cursor:default;color:transparent;animation:aiaPillSkeletonPulse 1.4s ease-in-out infinite}.aia-pill:hover{filter:brightness(1.2)}.aia-pill-active{outline:1px solid #5a5b8a}@keyframes aiaPillFadeIn{0%{opacity:0}}@keyframes aiaPillSkeletonPulse{0%,to{filter:brightness(1)}50%{filter:brightness(.55)}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
56
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: PillListComponent, decorators: [{
57
+ type: Component,
58
+ args: [{ selector: "aia-pill-list", standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "<ng-container *ngIf=\"loading && pills.length === 0; else realPills\">\n <span class=\"aia-pill-list\" data-aia-pill-list-loading=\"\">\n <span\n *ngFor=\"let w of fallbackSkeletonWidths; trackBy: trackByWidth; let i = index\"\n data-aia-pill-skeleton=\"\"\n class=\"aia-pill aia-pill-skeleton\"\n [class.aia-pill-rounded]=\"rounded\"\n [style.width.px]=\"w\"\n [style.opacity]=\"pillOpacity(i)\"\n ></span>\n </span>\n</ng-container>\n\n<ng-template #realPills>\n <span\n class=\"aia-pill-list\"\n [attr.data-aia-pill-list-loading]=\"loading ? '' : null\"\n >\n <button\n *ngFor=\"let pill of pills; trackBy: trackByType; let i = index\"\n type=\"button\"\n data-aia-pill=\"\"\n [attr.data-aia-loading]=\"loading ? '' : null\"\n tabindex=\"-1\"\n class=\"aia-pill\"\n [class.aia-pill-rounded]=\"rounded\"\n [class.aia-pill-active]=\"i === activePillIndex && !loading\"\n [class.aia-pill-skeleton]=\"loading\"\n [style.opacity]=\"pillOpacity(i)\"\n [disabled]=\"loading\"\n (mousedown)=\"onPillMouseDown($event)\"\n (click)=\"onPillClick(i)\"\n >\n {{ pill.text }}\n </button>\n </span>\n</ng-template>\n", styles: [".aia-pill-list{position:relative;z-index:1;pointer-events:auto;display:inline-flex;gap:5px;align-items:center;vertical-align:middle}.aia-pill{display:inline-flex;align-items:center;justify-content:center;height:36px;padding:13px;border:none;border-radius:999px;background:rgba(49,50,85,.25);background:color-mix(in srgb,var(--aia-pill-bg, var(--aia-color-background-supportive, #313255)) 25%,transparent);color:var(--aia-pill-color, var(--aia-color-text-muted, #c1c4cb));font-family:inherit;font-size:var(--aia-pill-font-size, 19px);line-height:30px;cursor:pointer;white-space:nowrap;animation:aiaPillFadeIn .4s cubic-bezier(.4,0,.2,1) forwards;box-shadow:#fff -3.2px -3.2px 3.2px -3.2px inset,#fff 6.4px 6.4px 1.6px -8px inset,var(--aia-dropdown-bg, transparent) -6.4px 6.4px 1.6px -8px inset,var(--aia-dropdown-bg, transparent) 6.4px -6.4px 1.6px -8px inset,#ffffff26 -1.6px 0 0 -1.6px inset,#ffffff26 0 -1.6px 0 -1.6px inset,#ffffff4d 0 1.6px inset,#ffffff4d 1.6px 0 inset,inset 0 0 30px 5px #0000000d,#00000014 0 0 30px 2px;backdrop-filter:blur(30px)}.aia-pill-rounded{border-radius:999px}.aia-pill-skeleton{pointer-events:none;cursor:default;color:transparent;animation:aiaPillSkeletonPulse 1.4s ease-in-out infinite}.aia-pill:hover{filter:brightness(1.2)}.aia-pill-active{outline:1px solid #5a5b8a}@keyframes aiaPillFadeIn{0%{opacity:0}}@keyframes aiaPillSkeletonPulse{0%,to{filter:brightness(1)}50%{filter:brightness(.55)}}\n"] }]
59
+ }], propDecorators: { pills: [{
60
+ type: Input
61
+ }], activePillIndex: [{
62
+ type: Input
63
+ }], rounded: [{
64
+ type: Input
65
+ }], loading: [{
66
+ type: Input
67
+ }], selectPill: [{
68
+ type: Output
69
+ }] } });
70
+
71
+ /**
72
+ * Internal single-option cell used inside the suggestion grid. Mirrors
73
+ * `packages/react/src/components/SuggestionItem.tsx`. Handles:
74
+ *
75
+ * - Tappable vs non-tappable cursor + hover styling
76
+ * - Pressed-state streak animation (500ms, single-flight)
77
+ * - Keyboard activation (Enter/Space)
78
+ * - ARIA `role="option"` + `aria-selected` for the listbox pattern
79
+ * - Loading skeleton when `loading=true`
80
+ *
81
+ * @internal
82
+ */
83
+ class SuggestionItemComponent {
84
+ constructor() {
85
+ this.isHighlighted = false;
86
+ this.id = "";
87
+ this.loading = false;
88
+ this.select = new EventEmitter();
89
+ this.highlight = new EventEmitter();
90
+ this.pressed = false;
91
+ this.cdr = inject(ChangeDetectorRef);
92
+ }
93
+ ngOnDestroy() {
94
+ if (this.pressedTimer !== undefined)
95
+ clearTimeout(this.pressedTimer);
96
+ }
97
+ onSelect() {
98
+ if (this.loading || !this.option.is_tappable || this.pressed)
99
+ return;
100
+ this.pressed = true;
101
+ this.cdr.markForCheck();
102
+ this.select.emit(this.option);
103
+ if (this.pressedTimer !== undefined)
104
+ clearTimeout(this.pressedTimer);
105
+ this.pressedTimer = setTimeout(() => {
106
+ this.pressed = false;
107
+ this.cdr.markForCheck();
108
+ }, 500);
109
+ }
110
+ onMouseEnter() {
111
+ if (this.loading || !this.option.is_tappable)
112
+ return;
113
+ this.highlight.emit();
114
+ }
115
+ onKeyDown(event) {
116
+ if (this.loading || !this.option.is_tappable)
117
+ return;
118
+ if (event.key === "Enter" || event.key === " ") {
119
+ event.preventDefault();
120
+ this.onSelect();
121
+ }
122
+ }
123
+ }
124
+ SuggestionItemComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: SuggestionItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
125
+ SuggestionItemComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "15.2.10", type: SuggestionItemComponent, isStandalone: true, selector: "aia-suggestion-item", inputs: { option: "option", isHighlighted: "isHighlighted", id: "id", loading: "loading" }, outputs: { select: "select", highlight: "highlight" }, ngImport: i0, template: "<div\n [id]=\"id\"\n role=\"option\"\n data-aia-option=\"\"\n [attr.data-aia-loading]=\"loading ? '' : null\"\n [attr.aria-selected]=\"isHighlighted\"\n class=\"aia-suggestion-item\"\n [class.aia-suggestion-item-highlighted]=\"isHighlighted && !loading\"\n [class.aia-suggestion-item-tappable]=\"option.is_tappable\"\n [class.aia-suggestion-item-non-tappable]=\"!option.is_tappable\"\n [class.aia-suggestion-item-pressed]=\"pressed\"\n [attr.tabindex]=\"loading || !option.is_tappable ? -1 : 0\"\n (click)=\"onSelect()\"\n (keydown)=\"onKeyDown($event)\"\n (mouseenter)=\"onMouseEnter()\"\n>\n <div class=\"aia-suggestion-streaks\"></div>\n <div class=\"aia-suggestion-streaks-vert\"></div>\n <span class=\"aia-suggestion-content\">\n <span class=\"aia-suggestion-text\">\n <ng-container *ngIf=\"option.icon; else noIcon\"\n >{{ option.icon }} {{ option.text }}</ng-container\n >\n <ng-template #noIcon>{{ option.text }}</ng-template>\n </span>\n <span *ngIf=\"option.tag\" class=\"aia-suggestion-tag\">{{ option.tag }}</span>\n </span>\n</div>\n", styles: [".aia-suggestion-item{position:relative;overflow:visible;display:flex;align-items:flex-start;font-family:inherit;font-size:var(--aia-option-font-size, 19px);font-weight:250;line-height:24px;color:var(--aia-option-color, var(--aia-color-text-muted, #c1c4cb));white-space:normal;word-break:break-word;border-radius:12px;padding:13px;animation:aiaSuggestionFadeIn .5s cubic-bezier(.4,0,.2,1) forwards}@keyframes aiaSuggestionFadeIn{0%{opacity:0}}.aia-suggestion-content{position:relative;z-index:2}.aia-suggestion-item-tappable{cursor:pointer}.aia-suggestion-item-tappable:hover{color:var(--aia-option-color-selected, var(--aia-color-text-default, #fff))}.aia-suggestion-item-non-tappable{cursor:default}.aia-suggestion-item-highlighted{color:var(--aia-option-color-selected, var(--aia-color-text-default, #fff));background:var(--aia-option-bg, transparent)}.aia-suggestion-tag{font-size:11px;margin-left:6px;opacity:.5}.aia-suggestion-item-pressed{opacity:.8;color:var(--aia-color-text-default, #fff);background:rgba(var(--aia-streak-rgb, 255, 255, 255),.06);animation:aiaGlassFade .5s ease forwards,aiaTapDown .5s ease forwards}@keyframes aiaTapDown{0%{transform:scale(1)}30%{transform:scale(.97)}to{transform:scale(1)}}@keyframes aiaGlassFade{0%{background:var(--aia-streak-glass-bg, rgba(255, 255, 255, .1))}to{background:transparent}}.aia-suggestion-streaks{position:absolute;inset:0;z-index:1;pointer-events:none;border-radius:inherit;overflow:hidden}.aia-suggestion-streaks:before{content:\"\";position:absolute;bottom:-3px;left:60%;width:0;height:6px;opacity:0;background:radial-gradient(ellipse at center,rgba(var(--aia-streak-rgb, 255, 255, 255),.5) 0%,rgba(var(--aia-streak-rgb, 255, 255, 255),.2) 40%,transparent 70%);box-shadow:0 0 12px 4px rgba(var(--aia-streak-rgb, 255, 255, 255),.2);filter:blur(1px);border-radius:50%}.aia-suggestion-streaks:after{content:\"\";position:absolute;top:-3px;right:60%;width:0;height:6px;opacity:0;background:radial-gradient(ellipse at center,rgba(var(--aia-streak-rgb, 255, 255, 255),.5) 0%,rgba(var(--aia-streak-rgb, 255, 255, 255),.2) 40%,transparent 70%);box-shadow:0 0 12px 4px rgba(var(--aia-streak-rgb, 255, 255, 255),.2);filter:blur(1px);border-radius:50%}.aia-suggestion-streaks-vert{position:absolute;inset:0;z-index:1;pointer-events:none;border-radius:inherit;overflow:hidden}.aia-suggestion-streaks-vert:before{content:\"\";position:absolute;bottom:0;right:-3px;width:6px;height:0;opacity:0;background:radial-gradient(ellipse at center,rgba(var(--aia-streak-rgb, 255, 255, 255),.4) 0%,rgba(var(--aia-streak-rgb, 255, 255, 255),.15) 40%,transparent 70%);box-shadow:0 0 12px 4px rgba(var(--aia-streak-rgb, 255, 255, 255),.15);filter:blur(1px);border-radius:50%}.aia-suggestion-streaks-vert:after{content:\"\";position:absolute;top:0;left:-3px;width:6px;height:0;opacity:0;background:radial-gradient(ellipse at center,rgba(var(--aia-streak-rgb, 255, 255, 255),.4) 0%,rgba(var(--aia-streak-rgb, 255, 255, 255),.15) 40%,transparent 70%);box-shadow:0 0 12px 4px rgba(var(--aia-streak-rgb, 255, 255, 255),.15);filter:blur(1px);border-radius:50%}.aia-suggestion-item-pressed .aia-suggestion-streaks:before{animation:aiaStreakHorizRight .5s cubic-bezier(.4,0,.2,1) forwards}.aia-suggestion-item-pressed .aia-suggestion-streaks:after{animation:aiaStreakHorizLeft .5s cubic-bezier(.4,0,.2,1) forwards}.aia-suggestion-item-pressed .aia-suggestion-streaks-vert:before{animation:aiaStreakVertUp .3s cubic-bezier(.3,0,.2,1) .2s forwards}.aia-suggestion-item-pressed .aia-suggestion-streaks-vert:after{animation:aiaStreakVertDown .3s cubic-bezier(.3,0,.2,1) .2s forwards}@keyframes aiaStreakHorizRight{0%{width:0;height:4px;opacity:0;filter:blur(1px);box-shadow:0 0 8px 3px rgba(var(--aia-streak-rgb, 255, 255, 255),.3)}15%{height:4px;opacity:1;filter:blur(1px);box-shadow:0 0 10px 4px rgba(var(--aia-streak-rgb, 255, 255, 255),.3)}80%{width:50%;height:10px;opacity:.8;filter:blur(3px);box-shadow:0 0 16px 6px rgba(var(--aia-streak-rgb, 255, 255, 255),.1)}to{width:50%;height:12px;opacity:0;filter:blur(5px);box-shadow:0 0 20px 8px rgba(var(--aia-streak-rgb, 255, 255, 255),.03)}}@keyframes aiaStreakHorizLeft{0%{width:0;height:4px;opacity:0;filter:blur(1px);box-shadow:0 0 8px 3px rgba(var(--aia-streak-rgb, 255, 255, 255),.3)}15%{height:4px;opacity:1;filter:blur(1px);box-shadow:0 0 10px 4px rgba(var(--aia-streak-rgb, 255, 255, 255),.3)}80%{width:50%;height:10px;opacity:.8;filter:blur(3px);box-shadow:0 0 16px 6px rgba(var(--aia-streak-rgb, 255, 255, 255),.1)}to{width:50%;height:12px;opacity:0;filter:blur(5px);box-shadow:0 0 20px 8px rgba(var(--aia-streak-rgb, 255, 255, 255),.03)}}@keyframes aiaStreakVertUp{0%{height:0;width:6px;opacity:.9;filter:blur(1.8px);box-shadow:0 0 12px 5px rgba(var(--aia-streak-rgb, 255, 255, 255),.25)}75%{height:100%;width:10px;opacity:.4;filter:blur(3px);box-shadow:0 0 18px 8px rgba(var(--aia-streak-rgb, 255, 255, 255),.06)}to{height:100%;width:14px;opacity:0;filter:blur(5px);box-shadow:0 0 24px 10px rgba(var(--aia-streak-rgb, 255, 255, 255),.02)}}@keyframes aiaStreakVertDown{0%{height:0;width:6px;opacity:.9;filter:blur(1.8px);box-shadow:0 0 12px 5px rgba(var(--aia-streak-rgb, 255, 255, 255),.25)}75%{height:100%;width:10px;opacity:.4;filter:blur(3px);box-shadow:0 0 18px 8px rgba(var(--aia-streak-rgb, 255, 255, 255),.06)}to{height:100%;width:14px;opacity:0;filter:blur(5px);box-shadow:0 0 24px 10px rgba(var(--aia-streak-rgb, 255, 255, 255),.02)}}.aia-suggestion-item[data-aia-loading]{cursor:default;animation:aiaSuggestionSkeletonPulse 1.4s ease-in-out infinite}.aia-suggestion-item[data-aia-loading] .aia-suggestion-text{color:transparent;background:var(--aia-skeleton-bg, var(--aia-pill-bg, rgba(189, 189, 189, .51)));border-radius:999px;-webkit-box-decoration-break:clone;box-decoration-break:clone}.aia-suggestion-item[data-aia-loading] .aia-suggestion-tag{display:none}@keyframes aiaSuggestionSkeletonPulse{0%,to{filter:brightness(1)}50%{filter:brightness(.55)}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
126
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: SuggestionItemComponent, decorators: [{
127
+ type: Component,
128
+ args: [{ selector: "aia-suggestion-item", standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "<div\n [id]=\"id\"\n role=\"option\"\n data-aia-option=\"\"\n [attr.data-aia-loading]=\"loading ? '' : null\"\n [attr.aria-selected]=\"isHighlighted\"\n class=\"aia-suggestion-item\"\n [class.aia-suggestion-item-highlighted]=\"isHighlighted && !loading\"\n [class.aia-suggestion-item-tappable]=\"option.is_tappable\"\n [class.aia-suggestion-item-non-tappable]=\"!option.is_tappable\"\n [class.aia-suggestion-item-pressed]=\"pressed\"\n [attr.tabindex]=\"loading || !option.is_tappable ? -1 : 0\"\n (click)=\"onSelect()\"\n (keydown)=\"onKeyDown($event)\"\n (mouseenter)=\"onMouseEnter()\"\n>\n <div class=\"aia-suggestion-streaks\"></div>\n <div class=\"aia-suggestion-streaks-vert\"></div>\n <span class=\"aia-suggestion-content\">\n <span class=\"aia-suggestion-text\">\n <ng-container *ngIf=\"option.icon; else noIcon\"\n >{{ option.icon }} {{ option.text }}</ng-container\n >\n <ng-template #noIcon>{{ option.text }}</ng-template>\n </span>\n <span *ngIf=\"option.tag\" class=\"aia-suggestion-tag\">{{ option.tag }}</span>\n </span>\n</div>\n", styles: [".aia-suggestion-item{position:relative;overflow:visible;display:flex;align-items:flex-start;font-family:inherit;font-size:var(--aia-option-font-size, 19px);font-weight:250;line-height:24px;color:var(--aia-option-color, var(--aia-color-text-muted, #c1c4cb));white-space:normal;word-break:break-word;border-radius:12px;padding:13px;animation:aiaSuggestionFadeIn .5s cubic-bezier(.4,0,.2,1) forwards}@keyframes aiaSuggestionFadeIn{0%{opacity:0}}.aia-suggestion-content{position:relative;z-index:2}.aia-suggestion-item-tappable{cursor:pointer}.aia-suggestion-item-tappable:hover{color:var(--aia-option-color-selected, var(--aia-color-text-default, #fff))}.aia-suggestion-item-non-tappable{cursor:default}.aia-suggestion-item-highlighted{color:var(--aia-option-color-selected, var(--aia-color-text-default, #fff));background:var(--aia-option-bg, transparent)}.aia-suggestion-tag{font-size:11px;margin-left:6px;opacity:.5}.aia-suggestion-item-pressed{opacity:.8;color:var(--aia-color-text-default, #fff);background:rgba(var(--aia-streak-rgb, 255, 255, 255),.06);animation:aiaGlassFade .5s ease forwards,aiaTapDown .5s ease forwards}@keyframes aiaTapDown{0%{transform:scale(1)}30%{transform:scale(.97)}to{transform:scale(1)}}@keyframes aiaGlassFade{0%{background:var(--aia-streak-glass-bg, rgba(255, 255, 255, .1))}to{background:transparent}}.aia-suggestion-streaks{position:absolute;inset:0;z-index:1;pointer-events:none;border-radius:inherit;overflow:hidden}.aia-suggestion-streaks:before{content:\"\";position:absolute;bottom:-3px;left:60%;width:0;height:6px;opacity:0;background:radial-gradient(ellipse at center,rgba(var(--aia-streak-rgb, 255, 255, 255),.5) 0%,rgba(var(--aia-streak-rgb, 255, 255, 255),.2) 40%,transparent 70%);box-shadow:0 0 12px 4px rgba(var(--aia-streak-rgb, 255, 255, 255),.2);filter:blur(1px);border-radius:50%}.aia-suggestion-streaks:after{content:\"\";position:absolute;top:-3px;right:60%;width:0;height:6px;opacity:0;background:radial-gradient(ellipse at center,rgba(var(--aia-streak-rgb, 255, 255, 255),.5) 0%,rgba(var(--aia-streak-rgb, 255, 255, 255),.2) 40%,transparent 70%);box-shadow:0 0 12px 4px rgba(var(--aia-streak-rgb, 255, 255, 255),.2);filter:blur(1px);border-radius:50%}.aia-suggestion-streaks-vert{position:absolute;inset:0;z-index:1;pointer-events:none;border-radius:inherit;overflow:hidden}.aia-suggestion-streaks-vert:before{content:\"\";position:absolute;bottom:0;right:-3px;width:6px;height:0;opacity:0;background:radial-gradient(ellipse at center,rgba(var(--aia-streak-rgb, 255, 255, 255),.4) 0%,rgba(var(--aia-streak-rgb, 255, 255, 255),.15) 40%,transparent 70%);box-shadow:0 0 12px 4px rgba(var(--aia-streak-rgb, 255, 255, 255),.15);filter:blur(1px);border-radius:50%}.aia-suggestion-streaks-vert:after{content:\"\";position:absolute;top:0;left:-3px;width:6px;height:0;opacity:0;background:radial-gradient(ellipse at center,rgba(var(--aia-streak-rgb, 255, 255, 255),.4) 0%,rgba(var(--aia-streak-rgb, 255, 255, 255),.15) 40%,transparent 70%);box-shadow:0 0 12px 4px rgba(var(--aia-streak-rgb, 255, 255, 255),.15);filter:blur(1px);border-radius:50%}.aia-suggestion-item-pressed .aia-suggestion-streaks:before{animation:aiaStreakHorizRight .5s cubic-bezier(.4,0,.2,1) forwards}.aia-suggestion-item-pressed .aia-suggestion-streaks:after{animation:aiaStreakHorizLeft .5s cubic-bezier(.4,0,.2,1) forwards}.aia-suggestion-item-pressed .aia-suggestion-streaks-vert:before{animation:aiaStreakVertUp .3s cubic-bezier(.3,0,.2,1) .2s forwards}.aia-suggestion-item-pressed .aia-suggestion-streaks-vert:after{animation:aiaStreakVertDown .3s cubic-bezier(.3,0,.2,1) .2s forwards}@keyframes aiaStreakHorizRight{0%{width:0;height:4px;opacity:0;filter:blur(1px);box-shadow:0 0 8px 3px rgba(var(--aia-streak-rgb, 255, 255, 255),.3)}15%{height:4px;opacity:1;filter:blur(1px);box-shadow:0 0 10px 4px rgba(var(--aia-streak-rgb, 255, 255, 255),.3)}80%{width:50%;height:10px;opacity:.8;filter:blur(3px);box-shadow:0 0 16px 6px rgba(var(--aia-streak-rgb, 255, 255, 255),.1)}to{width:50%;height:12px;opacity:0;filter:blur(5px);box-shadow:0 0 20px 8px rgba(var(--aia-streak-rgb, 255, 255, 255),.03)}}@keyframes aiaStreakHorizLeft{0%{width:0;height:4px;opacity:0;filter:blur(1px);box-shadow:0 0 8px 3px rgba(var(--aia-streak-rgb, 255, 255, 255),.3)}15%{height:4px;opacity:1;filter:blur(1px);box-shadow:0 0 10px 4px rgba(var(--aia-streak-rgb, 255, 255, 255),.3)}80%{width:50%;height:10px;opacity:.8;filter:blur(3px);box-shadow:0 0 16px 6px rgba(var(--aia-streak-rgb, 255, 255, 255),.1)}to{width:50%;height:12px;opacity:0;filter:blur(5px);box-shadow:0 0 20px 8px rgba(var(--aia-streak-rgb, 255, 255, 255),.03)}}@keyframes aiaStreakVertUp{0%{height:0;width:6px;opacity:.9;filter:blur(1.8px);box-shadow:0 0 12px 5px rgba(var(--aia-streak-rgb, 255, 255, 255),.25)}75%{height:100%;width:10px;opacity:.4;filter:blur(3px);box-shadow:0 0 18px 8px rgba(var(--aia-streak-rgb, 255, 255, 255),.06)}to{height:100%;width:14px;opacity:0;filter:blur(5px);box-shadow:0 0 24px 10px rgba(var(--aia-streak-rgb, 255, 255, 255),.02)}}@keyframes aiaStreakVertDown{0%{height:0;width:6px;opacity:.9;filter:blur(1.8px);box-shadow:0 0 12px 5px rgba(var(--aia-streak-rgb, 255, 255, 255),.25)}75%{height:100%;width:10px;opacity:.4;filter:blur(3px);box-shadow:0 0 18px 8px rgba(var(--aia-streak-rgb, 255, 255, 255),.06)}to{height:100%;width:14px;opacity:0;filter:blur(5px);box-shadow:0 0 24px 10px rgba(var(--aia-streak-rgb, 255, 255, 255),.02)}}.aia-suggestion-item[data-aia-loading]{cursor:default;animation:aiaSuggestionSkeletonPulse 1.4s ease-in-out infinite}.aia-suggestion-item[data-aia-loading] .aia-suggestion-text{color:transparent;background:var(--aia-skeleton-bg, var(--aia-pill-bg, rgba(189, 189, 189, .51)));border-radius:999px;-webkit-box-decoration-break:clone;box-decoration-break:clone}.aia-suggestion-item[data-aia-loading] .aia-suggestion-tag{display:none}@keyframes aiaSuggestionSkeletonPulse{0%,to{filter:brightness(1)}50%{filter:brightness(.55)}}\n"] }]
129
+ }], propDecorators: { option: [{
130
+ type: Input
131
+ }], isHighlighted: [{
132
+ type: Input
133
+ }], id: [{
134
+ type: Input
135
+ }], loading: [{
136
+ type: Input
137
+ }], select: [{
138
+ type: Output
139
+ }], highlight: [{
140
+ type: Output
141
+ }] } });
142
+
143
+ /**
144
+ * Internal scrollable grid of suggestion options. Mirrors
145
+ * `packages/react/src/components/SuggestionGrid.tsx` — including the bottom
146
+ * fade overlay that appears when there's vertical overflow, driven by a
147
+ * ResizeObserver + scroll listener. ResizeObserver runs outside Angular's
148
+ * zone (§16.1) so high-frequency layout events don't trip change detection.
149
+ *
150
+ * @internal
151
+ */
152
+ class SuggestionGridComponent {
153
+ constructor() {
154
+ this.options = [];
155
+ this.activeIndex = -1;
156
+ this.listboxId = "";
157
+ this.loading = false;
158
+ this.select = new EventEmitter();
159
+ this.highlight = new EventEmitter();
160
+ this.hasBottomOverflow = false;
161
+ this.zone = inject(NgZone);
162
+ this.cdr = inject(ChangeDetectorRef);
163
+ this.trackByOptionText = (_, opt) => opt.text;
164
+ }
165
+ ngAfterViewInit() {
166
+ this.bindOverflowTracking();
167
+ this.measureOverflow();
168
+ }
169
+ ngOnChanges() {
170
+ // Synchronously re-measure when options change — matches React's
171
+ // useLayoutEffect on [options]. ResizeObserver alone misses this case
172
+ // because the observed box doesn't grow when the grid hits max-height.
173
+ queueMicrotask(() => this.measureOverflow());
174
+ }
175
+ ngOnDestroy() {
176
+ var _a;
177
+ (_a = this.resizeObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
178
+ if (this.scrollListener && this.gridRef) {
179
+ this.gridRef.nativeElement.removeEventListener("scroll", this.scrollListener);
180
+ }
181
+ }
182
+ onSelectItem(option) {
183
+ this.select.emit(option);
184
+ }
185
+ onHighlightItem(index) {
186
+ this.highlight.emit(index);
187
+ }
188
+ bindOverflowTracking() {
189
+ var _a;
190
+ const grid = (_a = this.gridRef) === null || _a === void 0 ? void 0 : _a.nativeElement;
191
+ if (!grid)
192
+ return;
193
+ this.zone.runOutsideAngular(() => {
194
+ this.scrollListener = () => this.measureOverflow();
195
+ grid.addEventListener("scroll", this.scrollListener, { passive: true });
196
+ if (typeof ResizeObserver !== "undefined") {
197
+ this.resizeObserver = new ResizeObserver(() => this.measureOverflow());
198
+ this.resizeObserver.observe(grid);
199
+ }
200
+ });
201
+ }
202
+ measureOverflow() {
203
+ var _a;
204
+ const grid = (_a = this.gridRef) === null || _a === void 0 ? void 0 : _a.nativeElement;
205
+ if (!grid)
206
+ return;
207
+ const next = grid.scrollHeight - grid.scrollTop - grid.clientHeight > 1;
208
+ if (next !== this.hasBottomOverflow) {
209
+ this.zone.run(() => {
210
+ this.hasBottomOverflow = next;
211
+ this.cdr.markForCheck();
212
+ });
213
+ }
214
+ }
215
+ }
216
+ SuggestionGridComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: SuggestionGridComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
217
+ SuggestionGridComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "15.2.10", type: SuggestionGridComponent, isStandalone: true, selector: "aia-suggestion-grid", inputs: { options: "options", activeIndex: "activeIndex", listboxId: "listboxId", loading: "loading" }, outputs: { select: "select", highlight: "highlight" }, viewQueries: [{ propertyName: "gridRef", first: true, predicate: ["grid"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div\n class=\"aia-suggestion-grid-scroll\"\n [attr.data-fade]=\"hasBottomOverflow ? '' : null\"\n>\n <div #grid class=\"aia-suggestion-grid\">\n <aia-suggestion-item\n *ngFor=\"let option of options; trackBy: trackByOptionText; let i = index\"\n [option]=\"option\"\n [isHighlighted]=\"i === activeIndex\"\n [id]=\"listboxId + '-option-' + i\"\n [loading]=\"loading\"\n (select)=\"onSelectItem($event)\"\n (highlight)=\"onHighlightItem(i)\"\n >\n </aia-suggestion-item>\n </div>\n</div>\n", styles: [".aia-suggestion-grid-scroll{position:relative}.aia-suggestion-grid-scroll:after{content:\"\";position:absolute;left:0;right:0;bottom:0;height:50%;pointer-events:none;opacity:0;transition:opacity .15s ease-out;backdrop-filter:blur(12px);mask-image:linear-gradient(to bottom,transparent,white)}.aia-suggestion-grid-scroll[data-fade]:after{opacity:1}.aia-suggestion-grid{display:grid;grid-template-columns:minmax(0,250px);grid-auto-rows:min-content;align-content:start;max-width:100cqi;padding:8px;max-height:200px;overflow-y:auto;scrollbar-width:thin;scrollbar-color:var(--aia-scrollbar-thumb, rgba(0, 0, 0, .3)) transparent}@container (min-width: 516px){.aia-suggestion-grid{grid-template-columns:repeat(2,minmax(0,250px));justify-content:start}}.aia-suggestion-grid::-webkit-scrollbar{width:6px}.aia-suggestion-grid::-webkit-scrollbar-track{background:transparent}.aia-suggestion-grid::-webkit-scrollbar-thumb{background:var(--aia-scrollbar-thumb, rgba(0, 0, 0, .3));border-radius:3px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "component", type: SuggestionItemComponent, selector: "aia-suggestion-item", inputs: ["option", "isHighlighted", "id", "loading"], outputs: ["select", "highlight"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
218
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: SuggestionGridComponent, decorators: [{
219
+ type: Component,
220
+ args: [{ selector: "aia-suggestion-grid", standalone: true, imports: [CommonModule, SuggestionItemComponent], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "<div\n class=\"aia-suggestion-grid-scroll\"\n [attr.data-fade]=\"hasBottomOverflow ? '' : null\"\n>\n <div #grid class=\"aia-suggestion-grid\">\n <aia-suggestion-item\n *ngFor=\"let option of options; trackBy: trackByOptionText; let i = index\"\n [option]=\"option\"\n [isHighlighted]=\"i === activeIndex\"\n [id]=\"listboxId + '-option-' + i\"\n [loading]=\"loading\"\n (select)=\"onSelectItem($event)\"\n (highlight)=\"onHighlightItem(i)\"\n >\n </aia-suggestion-item>\n </div>\n</div>\n", styles: [".aia-suggestion-grid-scroll{position:relative}.aia-suggestion-grid-scroll:after{content:\"\";position:absolute;left:0;right:0;bottom:0;height:50%;pointer-events:none;opacity:0;transition:opacity .15s ease-out;backdrop-filter:blur(12px);mask-image:linear-gradient(to bottom,transparent,white)}.aia-suggestion-grid-scroll[data-fade]:after{opacity:1}.aia-suggestion-grid{display:grid;grid-template-columns:minmax(0,250px);grid-auto-rows:min-content;align-content:start;max-width:100cqi;padding:8px;max-height:200px;overflow-y:auto;scrollbar-width:thin;scrollbar-color:var(--aia-scrollbar-thumb, rgba(0, 0, 0, .3)) transparent}@container (min-width: 516px){.aia-suggestion-grid{grid-template-columns:repeat(2,minmax(0,250px));justify-content:start}}.aia-suggestion-grid::-webkit-scrollbar{width:6px}.aia-suggestion-grid::-webkit-scrollbar-track{background:transparent}.aia-suggestion-grid::-webkit-scrollbar-thumb{background:var(--aia-scrollbar-thumb, rgba(0, 0, 0, .3));border-radius:3px}\n"] }]
221
+ }], propDecorators: { options: [{
222
+ type: Input
223
+ }], activeIndex: [{
224
+ type: Input
225
+ }], listboxId: [{
226
+ type: Input
227
+ }], loading: [{
228
+ type: Input
229
+ }], select: [{
230
+ type: Output
231
+ }], highlight: [{
232
+ type: Output
233
+ }], gridRef: [{
234
+ type: ViewChild,
235
+ args: ["grid", { static: false }]
236
+ }] } });
237
+
238
+ /**
239
+ * Tier 2 dropdown component — the Angular equivalent of React's
240
+ * `<AIAutocompleteDropdown />`. Renders the pillbar, options grid, and
241
+ * loading skeleton based on the supplied controller's `dropdown$` snapshot.
242
+ *
243
+ * ```html
244
+ * <ai-autocomplete-dropdown [controller]="ac"></ai-autocomplete-dropdown>
245
+ * ```
246
+ *
247
+ * All actions (pill click, option select, option highlight) are forwarded to
248
+ * the controller. Consumers who need to observe these events can subscribe
249
+ * to the controller's per-field observables.
250
+ *
251
+ * @public
252
+ */
253
+ class AIAutocompleteDropdownComponent {
254
+ constructor() {
255
+ /** Whether to render the pillbar above the options grid. Default: true. */
256
+ this.showPills = true;
257
+ /** Fallback skeleton-bar widths used when loading with zero cached options. */
258
+ this.fallbackSkeletonBarWidths = [159, 119, 164];
259
+ this.trackByWidth = (_, w) => w;
260
+ }
261
+ /**
262
+ * View-model derived from the controller's `dropdown$`. Adds three booleans
263
+ * (`showsPills`, `showsOptions`, `showsFallbackSkeleton`, `isVisible`) that
264
+ * keep template-level logic to a property read per §19.6.
265
+ */
266
+ get vm$() {
267
+ return this.controller.dropdown$.pipe(map((dd) => {
268
+ var _a, _b;
269
+ const options = (_b = (_a = dd.suggestions[0]) === null || _a === void 0 ? void 0 : _a.options) !== null && _b !== void 0 ? _b : [];
270
+ const showsPills = this.showPills && (dd.pills.length > 0 || dd.isLoading);
271
+ const showsOptions = options.length > 0;
272
+ const showsFallbackSkeleton = dd.isLoading && !showsOptions;
273
+ const isVisible = dd.isOpen && (showsOptions || showsPills || dd.isLoading);
274
+ return {
275
+ pills: dd.pills,
276
+ options,
277
+ activeIndex: dd.activeIndex,
278
+ isLoading: dd.isLoading,
279
+ showsPills,
280
+ showsOptions,
281
+ showsFallbackSkeleton,
282
+ isVisible,
283
+ };
284
+ }));
285
+ }
286
+ onPillClick(index) {
287
+ this.controller.setActivePill(index);
288
+ }
289
+ onSelect(option) {
290
+ this.controller.selectOption(option);
291
+ }
292
+ onHighlight(index) {
293
+ this.controller.setActiveDropdownIndex(index);
294
+ }
295
+ preventBlur(event) {
296
+ // Prevent the editor from losing focus when interacting with the dropdown.
297
+ event.preventDefault();
298
+ }
299
+ }
300
+ AIAutocompleteDropdownComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: AIAutocompleteDropdownComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
301
+ AIAutocompleteDropdownComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "15.2.10", type: AIAutocompleteDropdownComponent, isStandalone: true, selector: "ai-autocomplete-dropdown", inputs: { controller: "controller", showPills: "showPills", className: "className" }, ngImport: i0, template: "<div\n *ngIf=\"vm$ | async as vm\"\n [id]=\"controller.listboxId\"\n role=\"listbox\"\n data-aia-dropdown=\"\"\n [attr.data-aia-loading]=\"vm.isLoading ? '' : null\"\n class=\"magicx-aia-dropdown\"\n [class.magicx-aia-dropdown-visible]=\"vm.isVisible\"\n [class]=\"className ? 'magicx-aia-dropdown ' + className : 'magicx-aia-dropdown'\"\n (mousedown)=\"preventBlur($event)\"\n>\n <div\n *ngIf=\"vm.showsPills\"\n class=\"magicx-aia-dropdown-pillbar\"\n data-aia-pillbar=\"\"\n >\n <aia-pill-list\n [pills]=\"vm.pills\"\n [activePillIndex]=\"0\"\n [rounded]=\"true\"\n [loading]=\"vm.isLoading\"\n (selectPill)=\"onPillClick($event)\"\n ></aia-pill-list>\n </div>\n\n <aia-suggestion-grid\n *ngIf=\"vm.showsOptions\"\n [options]=\"vm.options\"\n [activeIndex]=\"vm.activeIndex\"\n [listboxId]=\"controller.listboxId\"\n [loading]=\"vm.isLoading\"\n (select)=\"onSelect($event)\"\n (highlight)=\"onHighlight($event)\"\n ></aia-suggestion-grid>\n\n <div\n *ngIf=\"vm.showsFallbackSkeleton\"\n class=\"magicx-aia-dropdown-skeleton-bars\"\n data-aia-skeleton-bars=\"\"\n >\n <span\n *ngFor=\"let w of fallbackSkeletonBarWidths; trackBy: trackByWidth\"\n class=\"magicx-aia-dropdown-skeleton-bar\"\n [style.width.px]=\"w\"\n ></span>\n </div>\n</div>\n", styles: [".magicx-aia-dropdown{position:absolute;left:0;right:0;top:100%;max-width:516px;margin-top:6px;display:flex;flex-direction:column;overflow:hidden;container-type:inline-size;z-index:10;opacity:0;pointer-events:none;transition:opacity .4s cubic-bezier(.4,0,.2,1);box-shadow:#fff -3.2px -3.2px 3.2px -3.2px inset,#fff 6.4px 6.4px 1.6px -8px inset,var(--aia-dropdown-bg, transparent) -6.4px 6.4px 1.6px -8px inset,var(--aia-dropdown-bg, transparent) 6.4px -6.4px 1.6px -8px inset,#ffffff26 -1.6px 0 0 -1.6px inset,#ffffff26 0 -1.6px 0 -1.6px inset,#ffffff4d 0 1.6px inset,#ffffff4d 1.6px 0 inset,inset 0 0 30px 5px #0000000d,#00000014 0 0 30px 2px;backdrop-filter:blur(30px);border-radius:28px}.magicx-aia-dropdown-visible{opacity:1;pointer-events:auto}.magicx-aia-dropdown-pillbar{padding:27px 27px 3px;overflow-x:auto;overflow-y:hidden;scrollbar-width:none;mask-image:linear-gradient(to right,#000 0,#000 calc(100% - 48px),transparent 100%);-webkit-mask-image:linear-gradient(to right,#000 0,#000 calc(100% - 48px),transparent 100%)}.magicx-aia-dropdown-pillbar::-webkit-scrollbar{display:none}.magicx-aia-dropdown-skeleton-bars{display:flex;flex-direction:column;gap:20px;padding:25px}.magicx-aia-dropdown-skeleton-bar{display:block;height:19px;border-radius:999px;background:var(--aia-skeleton-bg, var(--aia-pill-bg, rgba(189, 189, 189, .51)));opacity:.5;animation:aiaDropdownSkeletonPulse 1.4s ease-in-out infinite}.magicx-aia-dropdown-skeleton-bar:nth-child(2){animation-delay:.15s}.magicx-aia-dropdown-skeleton-bar:nth-child(3){animation-delay:.3s}@keyframes aiaDropdownSkeletonPulse{0%,to{opacity:.5}50%{opacity:.25}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }, { kind: "component", type: PillListComponent, selector: "aia-pill-list", inputs: ["pills", "activePillIndex", "rounded", "loading"], outputs: ["selectPill"] }, { kind: "component", type: SuggestionGridComponent, selector: "aia-suggestion-grid", inputs: ["options", "activeIndex", "listboxId", "loading"], outputs: ["select", "highlight"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
302
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: AIAutocompleteDropdownComponent, decorators: [{
303
+ type: Component,
304
+ args: [{ selector: "ai-autocomplete-dropdown", standalone: true, imports: [CommonModule, PillListComponent, SuggestionGridComponent], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "<div\n *ngIf=\"vm$ | async as vm\"\n [id]=\"controller.listboxId\"\n role=\"listbox\"\n data-aia-dropdown=\"\"\n [attr.data-aia-loading]=\"vm.isLoading ? '' : null\"\n class=\"magicx-aia-dropdown\"\n [class.magicx-aia-dropdown-visible]=\"vm.isVisible\"\n [class]=\"className ? 'magicx-aia-dropdown ' + className : 'magicx-aia-dropdown'\"\n (mousedown)=\"preventBlur($event)\"\n>\n <div\n *ngIf=\"vm.showsPills\"\n class=\"magicx-aia-dropdown-pillbar\"\n data-aia-pillbar=\"\"\n >\n <aia-pill-list\n [pills]=\"vm.pills\"\n [activePillIndex]=\"0\"\n [rounded]=\"true\"\n [loading]=\"vm.isLoading\"\n (selectPill)=\"onPillClick($event)\"\n ></aia-pill-list>\n </div>\n\n <aia-suggestion-grid\n *ngIf=\"vm.showsOptions\"\n [options]=\"vm.options\"\n [activeIndex]=\"vm.activeIndex\"\n [listboxId]=\"controller.listboxId\"\n [loading]=\"vm.isLoading\"\n (select)=\"onSelect($event)\"\n (highlight)=\"onHighlight($event)\"\n ></aia-suggestion-grid>\n\n <div\n *ngIf=\"vm.showsFallbackSkeleton\"\n class=\"magicx-aia-dropdown-skeleton-bars\"\n data-aia-skeleton-bars=\"\"\n >\n <span\n *ngFor=\"let w of fallbackSkeletonBarWidths; trackBy: trackByWidth\"\n class=\"magicx-aia-dropdown-skeleton-bar\"\n [style.width.px]=\"w\"\n ></span>\n </div>\n</div>\n", styles: [".magicx-aia-dropdown{position:absolute;left:0;right:0;top:100%;max-width:516px;margin-top:6px;display:flex;flex-direction:column;overflow:hidden;container-type:inline-size;z-index:10;opacity:0;pointer-events:none;transition:opacity .4s cubic-bezier(.4,0,.2,1);box-shadow:#fff -3.2px -3.2px 3.2px -3.2px inset,#fff 6.4px 6.4px 1.6px -8px inset,var(--aia-dropdown-bg, transparent) -6.4px 6.4px 1.6px -8px inset,var(--aia-dropdown-bg, transparent) 6.4px -6.4px 1.6px -8px inset,#ffffff26 -1.6px 0 0 -1.6px inset,#ffffff26 0 -1.6px 0 -1.6px inset,#ffffff4d 0 1.6px inset,#ffffff4d 1.6px 0 inset,inset 0 0 30px 5px #0000000d,#00000014 0 0 30px 2px;backdrop-filter:blur(30px);border-radius:28px}.magicx-aia-dropdown-visible{opacity:1;pointer-events:auto}.magicx-aia-dropdown-pillbar{padding:27px 27px 3px;overflow-x:auto;overflow-y:hidden;scrollbar-width:none;mask-image:linear-gradient(to right,#000 0,#000 calc(100% - 48px),transparent 100%);-webkit-mask-image:linear-gradient(to right,#000 0,#000 calc(100% - 48px),transparent 100%)}.magicx-aia-dropdown-pillbar::-webkit-scrollbar{display:none}.magicx-aia-dropdown-skeleton-bars{display:flex;flex-direction:column;gap:20px;padding:25px}.magicx-aia-dropdown-skeleton-bar{display:block;height:19px;border-radius:999px;background:var(--aia-skeleton-bg, var(--aia-pill-bg, rgba(189, 189, 189, .51)));opacity:.5;animation:aiaDropdownSkeletonPulse 1.4s ease-in-out infinite}.magicx-aia-dropdown-skeleton-bar:nth-child(2){animation-delay:.15s}.magicx-aia-dropdown-skeleton-bar:nth-child(3){animation-delay:.3s}@keyframes aiaDropdownSkeletonPulse{0%,to{opacity:.5}50%{opacity:.25}}\n"] }]
305
+ }], propDecorators: { controller: [{
306
+ type: Input
307
+ }], showPills: [{
308
+ type: Input
309
+ }], className: [{
310
+ type: Input
311
+ }] } });
312
+
313
+ /**
314
+ * Per-field observable derivation factory. Enforces the §19.4 invariant:
315
+ *
316
+ * - `distinctUntilChanged` — prevents re-emission storms when the upstream
317
+ * state object ticks but the field a consumer cares about hasn't changed.
318
+ * Without this, every keystroke would re-render every async-pipe binding
319
+ * in the consumer's template even when nothing changed for that binding.
320
+ * - `shareReplay({ bufferSize: 1, refCount: true })` — guarantees that
321
+ * multiple subscribers (multiple `async` pipes in a template, e.g.
322
+ * `*ngIf="isLoading$ | async"` and `[disabled]="isLoading$ | async"`)
323
+ * share a single upstream subscription. `refCount: true` is non-obvious
324
+ * but essential: without it, the inner subscription survives after the
325
+ * last subscriber unmounts, leaking the chain back to `state$`.
326
+ *
327
+ * Every public observable on {@link AIAutocompleteController} must go through
328
+ * this factory — no exceptions.
329
+ *
330
+ * @internal
331
+ */
332
+ function deriveField(source$, selector, equalityFn) {
333
+ return source$.pipe(map$1(selector), distinctUntilChanged(equalityFn), shareReplay({ bufferSize: 1, refCount: true }));
334
+ }
335
+
336
+ /**
337
+ * Tier 2 logic surface for `@magicx-eng/ai-autocomplete-angular` — the Angular
338
+ * equivalent of React's `useAIAutocomplete()` hook. Wraps a headless
339
+ * `@magicx-eng/ai-autocomplete-vanilla` core and exposes its state as RxJS
340
+ * observables plus thin action methods.
341
+ *
342
+ * Per TDB §3.3, the controller is consumer-managed — the consumer creates it
343
+ * with `new`, holds the reference, and **must call `destroy()`** in their
344
+ * own `ngOnDestroy`. The Tier 1 `<ai-autocomplete>` component does this
345
+ * automatically; Tier 2 consumers do not get auto-cleanup.
346
+ *
347
+ * @public
348
+ */
349
+ class AIAutocompleteController {
350
+ /**
351
+ * Throws if invoked outside a browser environment. For SSR / Angular
352
+ * Universal, gate construction with `isPlatformBrowser` — see TDB §16.3.
353
+ */
354
+ constructor(opts = {}) {
355
+ var _a, _b;
356
+ this.isDestroyed = false;
357
+ if (typeof document === "undefined") {
358
+ throw new Error("AIAutocompleteController requires a browser environment. " +
359
+ "For SSR / Angular Universal, gate construction with isPlatformBrowser.");
360
+ }
361
+ // Headless mode never renders to the host, so a throwaway div is fine —
362
+ // it's required by the core's signature for symmetry with full mode.
363
+ const host = document.createElement("div");
364
+ this.core = new AIAutocomplete(host, Object.assign(Object.assign({}, opts), { renderMode: "headless", source: (_a = opts.source) !== null && _a !== void 0 ? _a : "headless-sdk",
365
+ // Headless mode never renders the dropdown DOM, but the keyboard
366
+ // controller still reads `optionsPosition` to decide which vertical
367
+ // arrow opens the dropdown from the input.
368
+ optionsPosition: (_b = opts.optionsPosition) !== null && _b !== void 0 ? _b : "below" }));
369
+ this.rawState$ = new BehaviorSubject(this.core.getState());
370
+ this.unsubscribeCore = this.core.subscribe((state) => {
371
+ if (!this.isDestroyed)
372
+ this.rawState$.next(state);
373
+ });
374
+ // === Per-field observables ===
375
+ this.text$ = deriveField(this.rawState$, (s) => s.text);
376
+ this.completedParams$ = deriveField(this.rawState$, (s) => s.completedParams);
377
+ this.suggestionPills$ = deriveField(this.rawState$, (s) => s.actionableSuggestions);
378
+ this.segments$ = deriveField(this.rawState$, (s) => s.segments);
379
+ this.newParamId$ = deriveField(this.rawState$, (s) => s.newParamId);
380
+ this.suggestions$ = deriveField(this.rawState$, (s) => s.suggestions);
381
+ this.activeIndex$ = deriveField(this.rawState$, (s) => s.activeDropdownIndex);
382
+ this.isLoading$ = deriveField(this.rawState$, computeUiLoading);
383
+ this.isReady$ = deriveField(this.rawState$, (s) => s.isReady);
384
+ this.isFocused$ = deriveField(this.rawState$, (s) => s.isFocused);
385
+ this.isDropdownOpen$ = deriveField(this.rawState$, (s) => s.isDropdownOpen);
386
+ this.placeholderText$ = deriveField(this.rawState$, (s) => s.placeholderText);
387
+ this.error$ = deriveField(this.rawState$, (s) => s.error);
388
+ this.editingParam$ = deriveField(this.rawState$, (s) => s.editingParam);
389
+ this.editingAnchor$ = deriveField(this.rawState$, (s) => s.editingAnchor);
390
+ this.caretOffset$ = deriveField(this.rawState$, (s) => s.caretOffset);
391
+ // === Composite observables ===
392
+ // Project directly off `rawState$` with a single map() so each store
393
+ // tick produces exactly ONE coherent state$ emission. Using
394
+ // `combineLatest` over the per-field observables here would fire one
395
+ // emission per inner-source tick (up to N per store update), and the
396
+ // editor-host directive's post-promote refocus would run against an
397
+ // intermediate state where `newParamId` was set but `segments` /
398
+ // `caretOffset` still held their old values — leaving the caret stuck
399
+ // at the pre-click position.
400
+ this.state$ = this.rawState$.pipe(map$1(computeState), shareReplay({ bufferSize: 1, refCount: true }));
401
+ this.dropdown$ = deriveField(this.rawState$, computeDropdown);
402
+ }
403
+ // === Read-only accessors ===
404
+ /** Stable ARIA listbox ID for the dropdown. Pass to `<textarea aria-controls="...">`. */
405
+ get listboxId() {
406
+ return this.core.listboxId;
407
+ }
408
+ /** Whether `destroy()` has been called. After destruction all action methods no-op. */
409
+ get destroyed() {
410
+ return this.isDestroyed;
411
+ }
412
+ /**
413
+ * Synchronous snapshot of the controller's current public state. Mirrors
414
+ * the latest emission of `state$`. Useful in callbacks where you can't
415
+ * `subscribe` (e.g. inside an event handler).
416
+ */
417
+ getState() {
418
+ return computeState(this.rawState$.value);
419
+ }
420
+ // === Action methods (pass-through to the core) ===
421
+ /** Clear all state, rotate `session_id`, and re-fetch. Call after submit. */
422
+ reset() {
423
+ this.core.reset();
424
+ }
425
+ /** Pick a dropdown option (or replace the re-edited param if in re-edit mode). */
426
+ selectOption(option) {
427
+ this.core.selectOption(option);
428
+ }
429
+ /** Promote the pill at `index` to active (front of the list). */
430
+ setActivePill(index) {
431
+ this.core.setActivePill(index);
432
+ }
433
+ /** Drop the last completed param's "completed" status. Text stays as plain text. */
434
+ removeLastParam() {
435
+ this.core.removeLastParam();
436
+ }
437
+ /** Clear the shimmer animation state for the most-recently-added param. */
438
+ clearNewParamId() {
439
+ this.core.clearNewParamId();
440
+ }
441
+ /** Controlled-mode setter for editor text. */
442
+ setValue(text) {
443
+ this.core.setValue(text);
444
+ }
445
+ /** Controlled-mode setter for completed params. */
446
+ setCompletedParams(params) {
447
+ this.core.setCompletedParams(params);
448
+ }
449
+ /** Forward plain-text input from the consumer's `<textarea>` to the core. */
450
+ handleTextChange(value) {
451
+ this.core.handleTextChange(value);
452
+ }
453
+ /** Forward keyboard events from the consumer's `<textarea>` to the core. */
454
+ handleKeyDown(event) {
455
+ this.core.handleKeyDown(event);
456
+ }
457
+ /** Notify the core of focus state. */
458
+ setFocused(focused) {
459
+ this.core.setFocused(focused);
460
+ }
461
+ /** Enter re-edit mode for the bold param with the given id. */
462
+ startEditingParam(paramId) {
463
+ this.core.startEditingParam(paramId);
464
+ }
465
+ /** Exit re-edit mode without changing the bold param. */
466
+ exitEditMode() {
467
+ this.core.exitEditMode();
468
+ }
469
+ /** Forward post-input caret position (extends the edit tail). */
470
+ handleCaretAfterInput(offset) {
471
+ this.core.handleCaretAfterInput(offset);
472
+ }
473
+ /** Forward selection-only caret moves (may exit re-edit). */
474
+ handleCaretMove(offset) {
475
+ this.core.handleCaretMove(offset);
476
+ }
477
+ /** Replace the bold param being re-edited. Returns true when applied. */
478
+ replaceEditingRange(replacement) {
479
+ return this.core.replaceEditingRange(replacement);
480
+ }
481
+ /** Set the highlighted option index (typically driven by hover). */
482
+ setActiveDropdownIndex(index) {
483
+ this.core.setActiveDropdownIndex(index);
484
+ }
485
+ // === Lifecycle ===
486
+ /**
487
+ * Apply runtime changes to a subset of options. Event callbacks
488
+ * (`onSubmit`, `onError`, etc.) cannot be swapped this way — they are
489
+ * registered once at construction (matches the React hook's design). For
490
+ * dynamic event reactivity, subscribe to the corresponding observable
491
+ * (e.g. `text$` instead of `onChange`).
492
+ */
493
+ update(opts) {
494
+ this.core.update(opts);
495
+ }
496
+ /**
497
+ * Release all resources — unsubscribes from the core, destroys the core,
498
+ * and completes the internal state subject. Safe to call multiple times.
499
+ * After destruction, action methods no-op silently.
500
+ */
501
+ destroy() {
502
+ if (this.isDestroyed)
503
+ return;
504
+ this.isDestroyed = true;
505
+ this.unsubscribeCore();
506
+ this.core.destroy();
507
+ this.rawState$.complete();
508
+ }
509
+ }
510
+ // ===== Pure helpers — not part of the public surface =====
511
+ function computeUiLoading(s) {
512
+ return s.isLoading && !s.editingParam && !s.inSelectionAnimation;
513
+ }
514
+ function computeState(s) {
515
+ return {
516
+ text: s.text,
517
+ completedParams: s.completedParams,
518
+ suggestionPills: s.actionableSuggestions,
519
+ segments: s.segments,
520
+ newParamId: s.newParamId,
521
+ suggestions: s.suggestions,
522
+ activeIndex: s.activeDropdownIndex,
523
+ isLoading: computeUiLoading(s),
524
+ isReady: s.isReady,
525
+ isFocused: s.isFocused,
526
+ isDropdownOpen: s.isDropdownOpen,
527
+ placeholderText: s.placeholderText,
528
+ error: s.error,
529
+ editingParam: s.editingParam,
530
+ editingAnchor: s.editingAnchor,
531
+ caretOffset: s.caretOffset,
532
+ };
533
+ }
534
+ function computeDropdown(s) {
535
+ const editingParam = s.editingParam;
536
+ const activeSuggestion = s.actionableSuggestions[0];
537
+ const dropdownPill = editingParam
538
+ ? {
539
+ type: editingParam.suggestionType,
540
+ text: editingParam.suggestionPlaceholder,
541
+ required: true,
542
+ options: editingParam.options,
543
+ }
544
+ : null;
545
+ const dropdownActivePill = dropdownPill !== null && dropdownPill !== void 0 ? dropdownPill : activeSuggestion;
546
+ const dropdownPills = dropdownPill ? [dropdownPill] : s.actionableSuggestions;
547
+ return {
548
+ suggestions: dropdownActivePill ? [Object.assign(Object.assign({}, dropdownActivePill), { options: s.filteredOptions })] : [],
549
+ activeIndex: s.activeDropdownIndex,
550
+ isOpen: s.isDropdownOpen,
551
+ pills: dropdownPills,
552
+ isLoading: computeUiLoading(s),
553
+ };
554
+ }
555
+
556
+ /**
557
+ * Module-level cache for the one-time plaintext-only feature detection.
558
+ * Mirrors `supportsPlaintextOnly` in useContentEditableEditor.ts.
559
+ */
560
+ let plaintextOnlyCache;
561
+ function supportsPlaintextOnly() {
562
+ if (plaintextOnlyCache !== undefined)
563
+ return plaintextOnlyCache;
564
+ if (typeof document === "undefined")
565
+ return false;
566
+ const probe = document.createElement("div");
567
+ probe.setAttribute("contenteditable", "plaintext-only");
568
+ plaintextOnlyCache = probe.contentEditable === "plaintext-only";
569
+ return plaintextOnlyCache;
570
+ }
571
+ /**
572
+ * Internal directive that owns every concern specific to running the
573
+ * autocomplete on top of a contentEditable host: composition state, caret
574
+ * tracking, selectionchange handling, `beforeinput` interception, paste
575
+ * sanitization, plaintext-only feature detection, segment rendering,
576
+ * post-promote refocus, and re-edit caret park.
577
+ *
578
+ * Mirrors `packages/react/src/hooks/useContentEditableEditor.ts` one-for-one.
579
+ * Used only by the Tier 1 `<ai-autocomplete>` component — never exported.
580
+ *
581
+ * @internal
582
+ */
583
+ class AIAutocompleteEditorHostDirective {
584
+ constructor() {
585
+ this.autoFocus = true;
586
+ this.el = inject(ElementRef);
587
+ this.renderer = inject(Renderer2);
588
+ this.zone = inject(NgZone);
589
+ this.destroy$ = new Subject();
590
+ this.composing = false;
591
+ this.lastInputAt = 0;
592
+ this.lastSeenParamId = "";
593
+ this.lastEditingId = "";
594
+ this.editingParamId = null;
595
+ }
596
+ ngOnInit() {
597
+ if (!this.controller) {
598
+ throw new Error("[aiaEditorHost] requires a controller");
599
+ }
600
+ const el = this.el.nativeElement;
601
+ const ceMode = supportsPlaintextOnly() ? "plaintext-only" : "true";
602
+ this.renderer.setAttribute(el, "contenteditable", ceMode);
603
+ this.renderer.setAttribute(el, "aria-controls", this.controller.listboxId);
604
+ this.renderer.setAttribute(el, "suppress-content-editable-warning", "true");
605
+ // Auto-focus on mount. If the editor already has focus (e.g. from a
606
+ // click that fired before our lifecycle ran), notify the controller
607
+ // instead of stealing focus.
608
+ if (this.autoFocus) {
609
+ queueMicrotask(() => {
610
+ if (typeof document === "undefined")
611
+ return;
612
+ if (document.activeElement === el) {
613
+ this.controller.setFocused(true);
614
+ }
615
+ else {
616
+ el.focus();
617
+ }
618
+ });
619
+ }
620
+ this.bindDocumentSelectionListener();
621
+ this.bindBeforeInputListener();
622
+ this.bindStateSubscription();
623
+ }
624
+ ngOnDestroy() {
625
+ this.destroy$.next();
626
+ this.destroy$.complete();
627
+ if (this.documentSelectionListener && typeof document !== "undefined") {
628
+ document.removeEventListener("selectionchange", this.documentSelectionListener);
629
+ }
630
+ if (this.beforeInputListener) {
631
+ this.el.nativeElement.removeEventListener("beforeinput", this.beforeInputListener);
632
+ }
633
+ }
634
+ // === Host event handlers ===
635
+ onCompositionStart() {
636
+ this.composing = true;
637
+ }
638
+ onCompositionEnd() {
639
+ this.composing = false;
640
+ this.fireInput();
641
+ }
642
+ onInput() {
643
+ this.lastInputAt = performance.now();
644
+ this.fireInput();
645
+ this.controller.handleCaretAfterInput(getCursorOffset(this.el.nativeElement));
646
+ }
647
+ onKeyDown(event) {
648
+ this.controller.handleKeyDown(event);
649
+ }
650
+ onPaste(event) {
651
+ var _a, _b, _c;
652
+ event.preventDefault();
653
+ const el = this.el.nativeElement;
654
+ const text = ((_b = (_a = event.clipboardData) === null || _a === void 0 ? void 0 : _a.getData("text/plain")) !== null && _b !== void 0 ? _b : "").replace(/\r?\n/g, " ");
655
+ if (!text)
656
+ return;
657
+ const doc = (_c = el.ownerDocument) !== null && _c !== void 0 ? _c : document;
658
+ const sel = doc.getSelection();
659
+ if (!sel || sel.rangeCount === 0)
660
+ return;
661
+ const range = sel.getRangeAt(0);
662
+ if (!el.contains(range.startContainer))
663
+ return;
664
+ range.deleteContents();
665
+ const node = doc.createTextNode(text);
666
+ range.insertNode(node);
667
+ range.setStartAfter(node);
668
+ range.collapse(true);
669
+ sel.removeAllRanges();
670
+ sel.addRange(range);
671
+ this.fireInput();
672
+ }
673
+ onFocus() {
674
+ this.controller.setFocused(true);
675
+ }
676
+ onBlur() {
677
+ this.controller.setFocused(false);
678
+ }
679
+ // === Internal helpers ===
680
+ fireInput() {
681
+ if (this.composing)
682
+ return;
683
+ const el = this.el.nativeElement;
684
+ const raw = extractPlainText(el);
685
+ const shouldCapitalize = raw.length > 0 && raw[0] !== raw[0].toUpperCase();
686
+ const next = shouldCapitalize ? raw[0].toUpperCase() + raw.slice(1) : raw;
687
+ this.controller.handleTextChange(next);
688
+ }
689
+ bindDocumentSelectionListener() {
690
+ if (typeof document === "undefined")
691
+ return;
692
+ const el = this.el.nativeElement;
693
+ // selectionchange fires on every keystroke and caret move — run outside
694
+ // Angular's zone (§16.1) and re-enter only when we actually need to
695
+ // notify the controller of a meaningful change.
696
+ this.zone.runOutsideAngular(() => {
697
+ const handler = () => {
698
+ var _a;
699
+ const sel = document.getSelection();
700
+ if (!sel || sel.rangeCount === 0)
701
+ return;
702
+ if (!sel.anchorNode || !el.contains(sel.anchorNode))
703
+ return;
704
+ const anchor = sel.anchorNode;
705
+ const startEl = anchor.nodeType === Node.ELEMENT_NODE ? anchor : anchor.parentElement;
706
+ const strong = startEl === null || startEl === void 0 ? void 0 : startEl.closest('strong[data-seg="completed"][data-param-id]');
707
+ const enclosing = (_a = strong === null || strong === void 0 ? void 0 : strong.dataset.paramId) !== null && _a !== void 0 ? _a : null;
708
+ if (enclosing && enclosing !== this.editingParamId) {
709
+ this.zone.run(() => this.controller.startEditingParam(enclosing));
710
+ return;
711
+ }
712
+ if (performance.now() - this.lastInputAt < 50)
713
+ return;
714
+ this.zone.run(() => this.controller.handleCaretMove(getCursorOffset(el)));
715
+ };
716
+ this.documentSelectionListener = handler;
717
+ document.addEventListener("selectionchange", handler);
718
+ });
719
+ }
720
+ bindBeforeInputListener() {
721
+ const el = this.el.nativeElement;
722
+ // Native beforeinput — React's synthetic onBeforeInput is wired to the
723
+ // legacy textInput event and doesn't fire reliably for delete* types.
724
+ const handler = (event) => {
725
+ var _a;
726
+ const inputEvent = event;
727
+ const t = inputEvent.inputType;
728
+ if (t === "insertParagraph" || t === "insertLineBreak" || t === "insertFromDrop") {
729
+ event.preventDefault();
730
+ return;
731
+ }
732
+ if (t.startsWith("insert") || t.startsWith("delete")) {
733
+ const replacement = t.startsWith("delete") ? "" : ((_a = inputEvent.data) !== null && _a !== void 0 ? _a : "");
734
+ if (this.controller.replaceEditingRange(replacement)) {
735
+ event.preventDefault();
736
+ }
737
+ }
738
+ };
739
+ this.beforeInputListener = handler;
740
+ el.addEventListener("beforeinput", handler);
741
+ }
742
+ bindStateSubscription() {
743
+ const el = this.el.nativeElement;
744
+ this.controller.state$.pipe(takeUntil(this.destroy$)).subscribe((state) => {
745
+ var _a, _b, _c, _d, _e, _f, _g, _h;
746
+ this.editingParamId = (_b = (_a = state.editingParam) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : null;
747
+ renderEditableContent({
748
+ input: el,
749
+ segments: state.segments,
750
+ newParamId: state.newParamId,
751
+ editingParamId: (_d = (_c = state.editingParam) === null || _c === void 0 ? void 0 : _c.id) !== null && _d !== void 0 ? _d : null,
752
+ placeholderText: state.placeholderText,
753
+ isFocused: state.isFocused,
754
+ });
755
+ // Post-promote refocus + caret-after-bold placement (mirrors React's
756
+ // useLayoutEffect on newParamId).
757
+ const currentParamId = (_e = state.newParamId) !== null && _e !== void 0 ? _e : "";
758
+ if (currentParamId && currentParamId !== this.lastSeenParamId) {
759
+ this.lastSeenParamId = currentParamId;
760
+ el.focus();
761
+ const desired = (_f = state.caretOffset) !== null && _f !== void 0 ? _f : plainTextLength(el);
762
+ setCursorOffset(el, desired);
763
+ }
764
+ else if (!currentParamId) {
765
+ this.lastSeenParamId = "";
766
+ }
767
+ // Re-edit caret park (mirrors React's useLayoutEffect on editingParam).
768
+ const editingId = (_h = (_g = state.editingParam) === null || _g === void 0 ? void 0 : _g.id) !== null && _h !== void 0 ? _h : "";
769
+ if (editingId && editingId !== this.lastEditingId && state.editingAnchor != null) {
770
+ this.lastEditingId = editingId;
771
+ setCursorOffset(el, state.editingAnchor);
772
+ }
773
+ else if (!editingId) {
774
+ this.lastEditingId = "";
775
+ }
776
+ });
777
+ }
778
+ }
779
+ AIAutocompleteEditorHostDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: AIAutocompleteEditorHostDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
780
+ AIAutocompleteEditorHostDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "15.2.10", type: AIAutocompleteEditorHostDirective, isStandalone: true, selector: "[aiaEditorHost]", inputs: { controller: ["aiaEditorHost", "controller"], autoFocus: "autoFocus" }, host: { attributes: { "role": "combobox", "aria-autocomplete": "list", "aria-haspopup": "listbox", "spellcheck": "true", "enterkeyhint": "send", "tabindex": "0" }, listeners: { "input": "onInput()", "keydown": "onKeyDown($event)", "compositionstart": "onCompositionStart()", "compositionend": "onCompositionEnd()", "paste": "onPaste($event)", "focus": "onFocus()", "blur": "onBlur()" } }, ngImport: i0 });
781
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: AIAutocompleteEditorHostDirective, decorators: [{
782
+ type: Directive,
783
+ args: [{
784
+ selector: "[aiaEditorHost]",
785
+ standalone: true,
786
+ host: {
787
+ role: "combobox",
788
+ "aria-autocomplete": "list",
789
+ "aria-haspopup": "listbox",
790
+ spellcheck: "true",
791
+ enterkeyhint: "send",
792
+ tabindex: "0",
793
+ "(input)": "onInput()",
794
+ "(keydown)": "onKeyDown($event)",
795
+ "(compositionstart)": "onCompositionStart()",
796
+ "(compositionend)": "onCompositionEnd()",
797
+ "(paste)": "onPaste($event)",
798
+ "(focus)": "onFocus()",
799
+ "(blur)": "onBlur()",
800
+ },
801
+ }]
802
+ }], propDecorators: { controller: [{
803
+ type: Input,
804
+ args: ["aiaEditorHost"]
805
+ }], autoFocus: [{
806
+ type: Input
807
+ }] } });
808
+
809
+ function resolveInitialMode(mode) {
810
+ if (mode !== "auto")
811
+ return mode;
812
+ if (typeof window === "undefined")
813
+ return "dark";
814
+ return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
815
+ }
816
+ /**
817
+ * Tier 1 component — `<ai-autocomplete>`. The Angular equivalent of React's
818
+ * `<AIAutocomplete />`. Owns the contentEditable editor, inline pills, mode
819
+ * controller, submit button slot, and an embedded
820
+ * {@link AIAutocompleteController} that drives the dropdown.
821
+ *
822
+ * @public
823
+ */
824
+ class AIAutocompleteComponent {
825
+ constructor() {
826
+ this.maskCompletedText = false;
827
+ /** Where unfilled pills render. Default: "inline". */
828
+ this.pillPlacement = "inline";
829
+ /** Color mode. "auto" follows prefers-color-scheme. Default: "auto". */
830
+ this.mode = "auto";
831
+ this.optionsPosition = "below";
832
+ this.animations = true;
833
+ /** Focus the editor on mount. Default: true. */
834
+ this.autoFocus = true;
835
+ // === Outputs ===
836
+ /** Called on Enter or submit-button click. */
837
+ this.submitted = new EventEmitter();
838
+ this.errored = new EventEmitter();
839
+ this.valueChange = new EventEmitter();
840
+ this.completedParamsChange = new EventEmitter();
841
+ this.focused = new EventEmitter();
842
+ this.blurred = new EventEmitter();
843
+ this.resolvedMode = "dark";
844
+ this.onChangeFn = noop$1;
845
+ this.onTouchedFn = noop$1;
846
+ // §16.9: prefer `inject()` over constructor-parameter DI for components.
847
+ this.platformId = inject(PLATFORM_ID);
848
+ this.el = inject(ElementRef);
849
+ this.cdr = inject(ChangeDetectorRef);
850
+ this.zone = inject(NgZone);
851
+ }
852
+ ngOnInit() {
853
+ if (!isPlatformBrowser(this.platformId))
854
+ return;
855
+ this.resolvedMode = resolveInitialMode(this.mode);
856
+ this.controller = new AIAutocompleteController({
857
+ apiConfig: this.apiConfig,
858
+ optionOverrides: this.optionOverrides,
859
+ maskCompletedText: this.maskCompletedText,
860
+ columns: this.columns,
861
+ dropdownTrigger: this.dropdownTrigger,
862
+ optionsPosition: this.optionsPosition,
863
+ closeDropdownOnBlur: this.closeDropdownOnBlur,
864
+ showNonTappableOptions: this.showNonTappableOptions,
865
+ source: "full-sdk",
866
+ value: this.value,
867
+ completedParams: this.completedParams,
868
+ onSubmit: (result) => this.handleSubmit(result),
869
+ onError: (error) => this.errored.emit(error),
870
+ onChange: (text) => {
871
+ this.valueChange.emit(text);
872
+ this.onChangeFn(text);
873
+ },
874
+ onParamsChange: (params) => this.completedParamsChange.emit(params),
875
+ onFocus: () => this.focused.emit(),
876
+ onBlur: () => {
877
+ this.blurred.emit();
878
+ this.onTouchedFn();
879
+ },
880
+ setCursor: (offset) => {
881
+ var _a;
882
+ const editor = (_a = this.editorRef) === null || _a === void 0 ? void 0 : _a.nativeElement;
883
+ if (!editor)
884
+ return;
885
+ editor.focus();
886
+ setCursorOffset(editor, offset);
887
+ },
888
+ });
889
+ }
890
+ ngAfterViewInit() {
891
+ if (!isPlatformBrowser(this.platformId))
892
+ return;
893
+ const container = this.el.nativeElement;
894
+ this.modeController = new ModeController(container, this.mode);
895
+ // Pill-wrap detection — when the pill list wraps below the editor's last
896
+ // line, drop its 8px left margin via a data attribute the CSS keys off.
897
+ // Mirrors the React component's useLayoutEffect.
898
+ if (typeof ResizeObserver !== "undefined") {
899
+ this.zone.runOutsideAngular(() => {
900
+ var _a;
901
+ this.resizeObserver = new ResizeObserver(() => this.measurePillWrap());
902
+ const editor = (_a = this.editorRef) === null || _a === void 0 ? void 0 : _a.nativeElement;
903
+ if (editor)
904
+ this.resizeObserver.observe(editor);
905
+ });
906
+ this.measurePillWrap();
907
+ }
908
+ }
909
+ ngOnChanges(changes) {
910
+ var _a;
911
+ if (!this.controller)
912
+ return;
913
+ // Controlled-mode propagation
914
+ if (changes.value && this.value !== undefined) {
915
+ this.controller.setValue(this.value);
916
+ }
917
+ if (changes.completedParams && this.completedParams !== undefined) {
918
+ this.controller.setCompletedParams(this.completedParams);
919
+ }
920
+ // Reactive options
921
+ if (changes.apiConfig ||
922
+ changes.optionOverrides ||
923
+ changes.dropdownTrigger ||
924
+ changes.optionsPosition ||
925
+ changes.closeDropdownOnBlur ||
926
+ changes.showNonTappableOptions) {
927
+ this.controller.update({
928
+ apiConfig: this.apiConfig,
929
+ optionOverrides: this.optionOverrides,
930
+ dropdownTrigger: this.dropdownTrigger,
931
+ optionsPosition: this.optionsPosition,
932
+ closeDropdownOnBlur: this.closeDropdownOnBlur,
933
+ showNonTappableOptions: this.showNonTappableOptions,
934
+ });
935
+ }
936
+ if (changes.mode) {
937
+ (_a = this.modeController) === null || _a === void 0 ? void 0 : _a.setMode(this.mode);
938
+ this.resolvedMode = resolveInitialMode(this.mode);
939
+ }
940
+ }
941
+ ngOnDestroy() {
942
+ var _a, _b, _c;
943
+ (_a = this.resizeObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
944
+ (_b = this.modeController) === null || _b === void 0 ? void 0 : _b.destroy();
945
+ (_c = this.controller) === null || _c === void 0 ? void 0 : _c.destroy();
946
+ }
947
+ // === Imperative API (exposed on the component instance via @ViewChild) ===
948
+ /** Programmatically focus the editor. */
949
+ focus() {
950
+ var _a;
951
+ (_a = this.editorRef) === null || _a === void 0 ? void 0 : _a.nativeElement.focus();
952
+ }
953
+ /** Programmatically blur the editor. */
954
+ blur() {
955
+ var _a;
956
+ (_a = this.editorRef) === null || _a === void 0 ? void 0 : _a.nativeElement.blur();
957
+ }
958
+ /** Clear all state, rotate session, and re-fetch. */
959
+ reset() {
960
+ var _a;
961
+ (_a = this.controller) === null || _a === void 0 ? void 0 : _a.reset();
962
+ }
963
+ /** Switch color mode at runtime. */
964
+ setMode(mode) {
965
+ var _a;
966
+ this.mode = mode;
967
+ (_a = this.modeController) === null || _a === void 0 ? void 0 : _a.setMode(mode);
968
+ this.resolvedMode = resolveInitialMode(mode);
969
+ this.cdr.markForCheck();
970
+ }
971
+ // === Template helpers ===
972
+ /**
973
+ * 'default' → render the built-in arrow button.
974
+ * 'custom' → project the consumer-supplied TemplateRef.
975
+ * 'hidden' → no button.
976
+ */
977
+ get submitButtonState() {
978
+ if (this.submitButton === null)
979
+ return "hidden";
980
+ if (this.submitButton === undefined)
981
+ return "default";
982
+ return "custom";
983
+ }
984
+ /** Narrowed accessor for *ngTemplateOutlet — never undefined. */
985
+ get customSubmitTemplate() {
986
+ var _a;
987
+ return (_a = this.submitButton) !== null && _a !== void 0 ? _a : null;
988
+ }
989
+ get showInlinePills() {
990
+ return this.pillPlacement === "inline";
991
+ }
992
+ get showDropdownPills() {
993
+ return this.pillPlacement === "dropdown";
994
+ }
995
+ /**
996
+ * `true` when there's something to submit — driven by current segments
997
+ * and completed params. Read off the live editor via the controller state.
998
+ */
999
+ canSubmit(state) {
1000
+ return state.segments.length > 0 || state.completedParams.length > 0;
1001
+ }
1002
+ onWrapperClick(event) {
1003
+ // Clicking a pill should activate it without stealing focus elsewhere.
1004
+ const target = event.target;
1005
+ if (target === null || target === void 0 ? void 0 : target.closest("[data-aia-pill]"))
1006
+ return;
1007
+ this.focus();
1008
+ }
1009
+ onSubmitButtonClick(event) {
1010
+ event.stopPropagation();
1011
+ this.handleSubmitFromUserAction();
1012
+ }
1013
+ onSubmitSlotClick(event) {
1014
+ event.stopPropagation();
1015
+ this.handleSubmitFromUserAction();
1016
+ }
1017
+ onSelectPill(index) {
1018
+ this.controller.setActivePill(index);
1019
+ }
1020
+ // === ControlValueAccessor ===
1021
+ writeValue(value) {
1022
+ if (!this.controller)
1023
+ return;
1024
+ this.controller.setValue(value !== null && value !== void 0 ? value : "");
1025
+ }
1026
+ registerOnChange(fn) {
1027
+ this.onChangeFn = fn;
1028
+ }
1029
+ registerOnTouched(fn) {
1030
+ this.onTouchedFn = fn;
1031
+ }
1032
+ setDisabledState(_disabled) {
1033
+ // The contentEditable host doesn't have a native disabled attribute. For
1034
+ // v0.1 we accept the call so Angular's forms layer doesn't throw, but
1035
+ // the editor stays interactive. A future enhancement can toggle
1036
+ // contentEditable + ARIA disabled here.
1037
+ }
1038
+ // === Submit flow ===
1039
+ /**
1040
+ * Called by the controller's onSubmit callback (which fires from the
1041
+ * keyboard controller on Enter). Reads the live editor text — same trick
1042
+ * the React component uses — so the emitted query reflects whatever's in
1043
+ * the DOM right now, not a possibly-stale state snapshot.
1044
+ */
1045
+ handleSubmit(_result) {
1046
+ var _a, _b;
1047
+ const editor = (_a = this.editorRef) === null || _a === void 0 ? void 0 : _a.nativeElement;
1048
+ if (!editor)
1049
+ return;
1050
+ const text = extractPlainText(editor);
1051
+ const params = (_b = this.completedParams) !== null && _b !== void 0 ? _b : this.controller.getState().completedParams;
1052
+ const built = buildQuery(text, params);
1053
+ this.submitted.emit({
1054
+ query: text.trim(),
1055
+ raw_query: built.rawQuery,
1056
+ completed_params: built.completedParams,
1057
+ });
1058
+ this.controller.reset();
1059
+ }
1060
+ handleSubmitFromUserAction() {
1061
+ const state = this.controller.getState();
1062
+ if (!this.canSubmit(state))
1063
+ return;
1064
+ // Synthesize the result for the submit-button click path; same shape
1065
+ // the controller would deliver from a keyboard Enter, so handleSubmit
1066
+ // can take either entry point.
1067
+ this.handleSubmit({
1068
+ query: state.text.trim(),
1069
+ raw_query: "",
1070
+ completed_params: [],
1071
+ });
1072
+ }
1073
+ measurePillWrap() {
1074
+ var _a, _b;
1075
+ const container = (_a = this.pillContainerRef) === null || _a === void 0 ? void 0 : _a.nativeElement;
1076
+ const editor = (_b = this.editorRef) === null || _b === void 0 ? void 0 : _b.nativeElement;
1077
+ if (!container || !editor)
1078
+ return;
1079
+ const inner = container.firstElementChild;
1080
+ if (!inner)
1081
+ return;
1082
+ // Wrap detection: the pill row's top vs the editor's bottom. When the
1083
+ // pills sit on (or above) the editor's last line they're NOT wrapped —
1084
+ // keep the 8px margin so they don't butt against typed text or the
1085
+ // placeholder. This handles the empty-editor + placeholder case
1086
+ // naturally: the placeholder still occupies the editor's line box.
1087
+ const cRect = inner.getBoundingClientRect();
1088
+ const eRect = editor.getBoundingClientRect();
1089
+ const wrapped = cRect.top >= eRect.bottom - 2;
1090
+ this.zone.run(() => {
1091
+ if (wrapped)
1092
+ container.setAttribute("data-aia-pill-wrapped", "");
1093
+ else
1094
+ container.removeAttribute("data-aia-pill-wrapped");
1095
+ });
1096
+ }
1097
+ }
1098
+ AIAutocompleteComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: AIAutocompleteComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1099
+ AIAutocompleteComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "15.2.10", type: AIAutocompleteComponent, isStandalone: true, selector: "ai-autocomplete", inputs: { apiConfig: "apiConfig", optionOverrides: "optionOverrides", maskCompletedText: "maskCompletedText", columns: "columns", pillPlacement: "pillPlacement", mode: "mode", optionsPosition: "optionsPosition", animations: "animations", dropdownTrigger: "dropdownTrigger", closeDropdownOnBlur: "closeDropdownOnBlur", showNonTappableOptions: "showNonTappableOptions", autoFocus: "autoFocus", value: "value", completedParams: "completedParams", submitButton: "submitButton" }, outputs: { submitted: "submitted", errored: "errored", valueChange: "valueChange", completedParamsChange: "completedParamsChange", focused: "focused", blurred: "blurred" }, providers: [
1100
+ {
1101
+ provide: NG_VALUE_ACCESSOR,
1102
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
1103
+ useExisting: forwardRef(() => AIAutocompleteComponent),
1104
+ multi: true,
1105
+ },
1106
+ ], viewQueries: [{ propertyName: "editorRef", first: true, predicate: ["editor"], descendants: true }, { propertyName: "pillContainerRef", first: true, predicate: ["pillContainer"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div\n class=\"magicx-aia magicx-aia-container\"\n [attr.data-pill-placement]=\"pillPlacement\"\n [attr.data-options-position]=\"optionsPosition\"\n [attr.data-animations]=\"animations ? 'on' : 'off'\"\n [attr.data-mode]=\"resolvedMode\"\n>\n <ai-autocomplete-dropdown\n *ngIf=\"controller\"\n [controller]=\"controller\"\n [showPills]=\"showDropdownPills\"\n ></ai-autocomplete-dropdown>\n\n <div\n class=\"magicx-aia-input-wrapper\"\n (click)=\"onWrapperClick($event)\"\n (keyup)=\"$event.stopPropagation()\"\n >\n <div class=\"magicx-aia-editor-area\" data-aia-editor=\"\">\n <div\n #editor\n *ngIf=\"controller\"\n [aiaEditorHost]=\"controller\"\n [autoFocus]=\"autoFocus\"\n class=\"magicx-aia-input\"\n data-aia-input=\"\"\n ></div>\n\n <ng-container *ngIf=\"controller && showInlinePills\">\n <span\n #pillContainer\n class=\"magicx-aia-pill-list-container\"\n data-aia-pill-list-container=\"\"\n *ngIf=\"\n (controller.isLoading$ | async) ||\n ((controller.suggestionPills$ | async)?.length ?? 0) > 0\n \"\n >\n <aia-pill-list\n [pills]=\"(controller.suggestionPills$ | async) ?? []\"\n [activePillIndex]=\"0\"\n [loading]=\"(controller.isLoading$ | async) ?? false\"\n (selectPill)=\"onSelectPill($event)\"\n ></aia-pill-list>\n </span>\n </ng-container>\n </div>\n\n <ng-container [ngSwitch]=\"submitButtonState\">\n <button\n *ngSwitchCase=\"'default'\"\n type=\"button\"\n data-aia-submit=\"\"\n class=\"magicx-aia-submit-button\"\n [disabled]=\"\n !(\n controller &&\n canSubmit({\n segments: (controller.segments$ | async) ?? [],\n completedParams: (controller.completedParams$ | async) ?? []\n })\n )\n \"\n aria-label=\"Submit\"\n (click)=\"onSubmitButtonClick($event)\"\n >\n <svg\n width=\"18\"\n height=\"18\"\n viewBox=\"0 0 18 18\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Submit\"\n >\n <path\n d=\"M9 14V4M9 4L4 9M9 4L14 9\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n </button>\n\n <span\n *ngSwitchCase=\"'custom'\"\n data-aia-submit=\"\"\n class=\"magicx-aia-submit-slot\"\n (click)=\"onSubmitSlotClick($event)\"\n >\n <ng-container *ngTemplateOutlet=\"customSubmitTemplate\"></ng-container>\n </span>\n </ng-container>\n </div>\n</div>\n", styles: [":where(.magicx-aia,.magicx-aia *,.magicx-aia *::before,.magicx-aia *::after){box-sizing:border-box}:where(.magicx-aia),:where(.magicx-aia[data-mode=\"light\"]){--aia-pill-bg: #bdbdbd;--aia-pill-color: #000000;--aia-pill-font-size: 19px;--aia-option-bg: transparent;--aia-option-color: #4f4f4f;--aia-option-color-selected: #000000;--aia-option-font-size: 19px;--aia-written-text-color: #000000;--aia-written-text-font-size: 19px;--aia-caret-color: var(--aia-written-text-color, #000000);--aia-submit-bg: #000000;--aia-submit-color: #ffffff;--aia-color-text-muted: #6b7280;--aia-skeleton-bg: rgba(189, 189, 189, .51);--aia-streak-rgb: 99, 102, 241;--aia-streak-glass-bg: rgba(99, 102, 241, .1)}:where(.magicx-aia[data-mode=\"dark\"]){--aia-pill-bg: #bdbdbd;--aia-pill-color: #ffffff;--aia-pill-font-size: 19px;--aia-option-bg: transparent;--aia-option-color: #b0b0b0;--aia-option-color-selected: #ffffff;--aia-option-font-size: 19px;--aia-written-text-color: #ffffff;--aia-written-text-font-size: 19px;--aia-caret-color: var(--aia-written-text-color, #ffffff);--aia-submit-bg: #ffffff;--aia-submit-color: #000000;--aia-color-text-muted: #c1c4cb;--aia-skeleton-bg: #333539;--aia-streak-rgb: 255, 255, 255;--aia-streak-glass-bg: rgba(255, 255, 255, .1)}:where(.magicx-aia[data-options-position=\"above\"]) [data-aia-dropdown]{top:auto;bottom:100%;margin-top:0;margin-bottom:6px;flex-direction:column-reverse}:where(.magicx-aia[data-options-position=\"above\"]) [data-aia-pillbar]{padding:0 27px 27px}:where(.magicx-aia[data-animations=\"off\"]) *,:where(.magicx-aia[data-animations=\"off\"]) *:before,:where(.magicx-aia[data-animations=\"off\"]) *:after{animation-duration:0s!important;transition-duration:0s!important}.magicx-aia-container{position:relative;font-family:var(--aia-font-family, inherit);container-type:inline-size}.magicx-aia-input-wrapper{padding:20px;border:1px solid var(--aia-color-border-default, #9ea5b2);border-radius:23px;background:transparent;overflow:hidden;display:flex;align-items:center;gap:4px}.magicx-aia-editor-area{position:relative;flex:1;min-width:0;min-height:26.6px;line-height:26.6px;font-family:inherit;font-size:var(--aia-written-text-font-size, 19px);white-space:pre-wrap;word-break:break-word;overflow-wrap:anywhere}.magicx-aia-input{display:inline;outline:none;background:transparent;color:var(--aia-written-text-color, var(--aia-color-text-default, #fff));caret-color:var( --aia-caret-color, var(--aia-written-text-color, var(--aia-color-text-default, #fff)) );font-weight:250;vertical-align:middle}:where(.magicx-aia-input) strong{font-weight:500;letter-spacing:-.01em}:where(.magicx-aia-input) strong[class~=magicx-aia-segment--editing]{background-color:color-mix(in srgb,var(--aia-edit-outline, currentColor) 20%,transparent);border-radius:6px;padding:2px 3px;margin:0 -1.5px;caret-color:transparent}.magicx-aia-input:has(strong[class~=\"magicx-aia-segment--editing\"]){caret-color:transparent}:where(.magicx-aia-input) strong[class~=magicx-aia-segment--editing]::selection{background:transparent;color:inherit}:where(.magicx-aia-input) strong[class~=magicx-aia-segment--editing]::-moz-selection{background:transparent;color:inherit}.magicx-aia-input[data-aia-empty=true][data-placeholder]:before{content:attr(data-placeholder);color:var(--aia-color-text-muted, #c1c4cb);opacity:.7;pointer-events:none}.magicx-aia-input[data-aia-empty=true]{display:inline-block;min-width:1px}.magicx-aia-pill-list-container{display:inline;margin-left:8px}.magicx-aia-pill-list-container:empty,.magicx-aia-pill-list-container[data-aia-pill-wrapped]{margin-left:0}.magicx-aia-submit-button{flex-shrink:0;width:36px;height:36px;border-radius:50%;border:none;background:var(--aia-submit-bg, var(--aia-color-text-default, #fff));color:var(--aia-submit-color, var(--aia-color-bg-default, #000));cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0;transition:opacity .2s ease}.magicx-aia-submit-button:hover{opacity:.85}.magicx-aia-submit-slot{display:contents}:where(.magicx-aia-input) strong[class~=magicx-aia-shimmer-revealed]{color:transparent;background:linear-gradient(120deg,var(--aia-written-text-color, var(--aia-color-text-default, #fff)) 0%,var(--aia-written-text-color, var(--aia-color-text-default, #fff)) 44%,#b0b0b0 48%,#b0b0b0 52%,var(--aia-written-text-color, var(--aia-color-text-default, #fff)) 56%,var(--aia-written-text-color, var(--aia-color-text-default, #fff)) 100%);background-size:200% 100%;-webkit-background-clip:text;background-clip:text}:where(.magicx-aia-input) strong[class~=magicx-aia-shimmer-sweep]{animation:aiaTextShimmer .65s ease-out forwards}@keyframes aiaTextShimmer{0%{background-position:100% 0}to{background-position:-50% 0}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i1.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }, { kind: "component", type: AIAutocompleteDropdownComponent, selector: "ai-autocomplete-dropdown", inputs: ["controller", "showPills", "className"] }, { kind: "directive", type: AIAutocompleteEditorHostDirective, selector: "[aiaEditorHost]", inputs: ["aiaEditorHost", "autoFocus"] }, { kind: "component", type: PillListComponent, selector: "aia-pill-list", inputs: ["pills", "activePillIndex", "rounded", "loading"], outputs: ["selectPill"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1107
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: AIAutocompleteComponent, decorators: [{
1108
+ type: Component,
1109
+ args: [{ selector: "ai-autocomplete", standalone: true, imports: [
1110
+ CommonModule,
1111
+ AIAutocompleteDropdownComponent,
1112
+ AIAutocompleteEditorHostDirective,
1113
+ PillListComponent,
1114
+ ], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, providers: [
1115
+ {
1116
+ provide: NG_VALUE_ACCESSOR,
1117
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
1118
+ useExisting: forwardRef(() => AIAutocompleteComponent),
1119
+ multi: true,
1120
+ },
1121
+ ], template: "<div\n class=\"magicx-aia magicx-aia-container\"\n [attr.data-pill-placement]=\"pillPlacement\"\n [attr.data-options-position]=\"optionsPosition\"\n [attr.data-animations]=\"animations ? 'on' : 'off'\"\n [attr.data-mode]=\"resolvedMode\"\n>\n <ai-autocomplete-dropdown\n *ngIf=\"controller\"\n [controller]=\"controller\"\n [showPills]=\"showDropdownPills\"\n ></ai-autocomplete-dropdown>\n\n <div\n class=\"magicx-aia-input-wrapper\"\n (click)=\"onWrapperClick($event)\"\n (keyup)=\"$event.stopPropagation()\"\n >\n <div class=\"magicx-aia-editor-area\" data-aia-editor=\"\">\n <div\n #editor\n *ngIf=\"controller\"\n [aiaEditorHost]=\"controller\"\n [autoFocus]=\"autoFocus\"\n class=\"magicx-aia-input\"\n data-aia-input=\"\"\n ></div>\n\n <ng-container *ngIf=\"controller && showInlinePills\">\n <span\n #pillContainer\n class=\"magicx-aia-pill-list-container\"\n data-aia-pill-list-container=\"\"\n *ngIf=\"\n (controller.isLoading$ | async) ||\n ((controller.suggestionPills$ | async)?.length ?? 0) > 0\n \"\n >\n <aia-pill-list\n [pills]=\"(controller.suggestionPills$ | async) ?? []\"\n [activePillIndex]=\"0\"\n [loading]=\"(controller.isLoading$ | async) ?? false\"\n (selectPill)=\"onSelectPill($event)\"\n ></aia-pill-list>\n </span>\n </ng-container>\n </div>\n\n <ng-container [ngSwitch]=\"submitButtonState\">\n <button\n *ngSwitchCase=\"'default'\"\n type=\"button\"\n data-aia-submit=\"\"\n class=\"magicx-aia-submit-button\"\n [disabled]=\"\n !(\n controller &&\n canSubmit({\n segments: (controller.segments$ | async) ?? [],\n completedParams: (controller.completedParams$ | async) ?? []\n })\n )\n \"\n aria-label=\"Submit\"\n (click)=\"onSubmitButtonClick($event)\"\n >\n <svg\n width=\"18\"\n height=\"18\"\n viewBox=\"0 0 18 18\"\n fill=\"none\"\n role=\"img\"\n aria-label=\"Submit\"\n >\n <path\n d=\"M9 14V4M9 4L4 9M9 4L14 9\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n </button>\n\n <span\n *ngSwitchCase=\"'custom'\"\n data-aia-submit=\"\"\n class=\"magicx-aia-submit-slot\"\n (click)=\"onSubmitSlotClick($event)\"\n >\n <ng-container *ngTemplateOutlet=\"customSubmitTemplate\"></ng-container>\n </span>\n </ng-container>\n </div>\n</div>\n", styles: [":where(.magicx-aia,.magicx-aia *,.magicx-aia *::before,.magicx-aia *::after){box-sizing:border-box}:where(.magicx-aia),:where(.magicx-aia[data-mode=\"light\"]){--aia-pill-bg: #bdbdbd;--aia-pill-color: #000000;--aia-pill-font-size: 19px;--aia-option-bg: transparent;--aia-option-color: #4f4f4f;--aia-option-color-selected: #000000;--aia-option-font-size: 19px;--aia-written-text-color: #000000;--aia-written-text-font-size: 19px;--aia-caret-color: var(--aia-written-text-color, #000000);--aia-submit-bg: #000000;--aia-submit-color: #ffffff;--aia-color-text-muted: #6b7280;--aia-skeleton-bg: rgba(189, 189, 189, .51);--aia-streak-rgb: 99, 102, 241;--aia-streak-glass-bg: rgba(99, 102, 241, .1)}:where(.magicx-aia[data-mode=\"dark\"]){--aia-pill-bg: #bdbdbd;--aia-pill-color: #ffffff;--aia-pill-font-size: 19px;--aia-option-bg: transparent;--aia-option-color: #b0b0b0;--aia-option-color-selected: #ffffff;--aia-option-font-size: 19px;--aia-written-text-color: #ffffff;--aia-written-text-font-size: 19px;--aia-caret-color: var(--aia-written-text-color, #ffffff);--aia-submit-bg: #ffffff;--aia-submit-color: #000000;--aia-color-text-muted: #c1c4cb;--aia-skeleton-bg: #333539;--aia-streak-rgb: 255, 255, 255;--aia-streak-glass-bg: rgba(255, 255, 255, .1)}:where(.magicx-aia[data-options-position=\"above\"]) [data-aia-dropdown]{top:auto;bottom:100%;margin-top:0;margin-bottom:6px;flex-direction:column-reverse}:where(.magicx-aia[data-options-position=\"above\"]) [data-aia-pillbar]{padding:0 27px 27px}:where(.magicx-aia[data-animations=\"off\"]) *,:where(.magicx-aia[data-animations=\"off\"]) *:before,:where(.magicx-aia[data-animations=\"off\"]) *:after{animation-duration:0s!important;transition-duration:0s!important}.magicx-aia-container{position:relative;font-family:var(--aia-font-family, inherit);container-type:inline-size}.magicx-aia-input-wrapper{padding:20px;border:1px solid var(--aia-color-border-default, #9ea5b2);border-radius:23px;background:transparent;overflow:hidden;display:flex;align-items:center;gap:4px}.magicx-aia-editor-area{position:relative;flex:1;min-width:0;min-height:26.6px;line-height:26.6px;font-family:inherit;font-size:var(--aia-written-text-font-size, 19px);white-space:pre-wrap;word-break:break-word;overflow-wrap:anywhere}.magicx-aia-input{display:inline;outline:none;background:transparent;color:var(--aia-written-text-color, var(--aia-color-text-default, #fff));caret-color:var( --aia-caret-color, var(--aia-written-text-color, var(--aia-color-text-default, #fff)) );font-weight:250;vertical-align:middle}:where(.magicx-aia-input) strong{font-weight:500;letter-spacing:-.01em}:where(.magicx-aia-input) strong[class~=magicx-aia-segment--editing]{background-color:color-mix(in srgb,var(--aia-edit-outline, currentColor) 20%,transparent);border-radius:6px;padding:2px 3px;margin:0 -1.5px;caret-color:transparent}.magicx-aia-input:has(strong[class~=\"magicx-aia-segment--editing\"]){caret-color:transparent}:where(.magicx-aia-input) strong[class~=magicx-aia-segment--editing]::selection{background:transparent;color:inherit}:where(.magicx-aia-input) strong[class~=magicx-aia-segment--editing]::-moz-selection{background:transparent;color:inherit}.magicx-aia-input[data-aia-empty=true][data-placeholder]:before{content:attr(data-placeholder);color:var(--aia-color-text-muted, #c1c4cb);opacity:.7;pointer-events:none}.magicx-aia-input[data-aia-empty=true]{display:inline-block;min-width:1px}.magicx-aia-pill-list-container{display:inline;margin-left:8px}.magicx-aia-pill-list-container:empty,.magicx-aia-pill-list-container[data-aia-pill-wrapped]{margin-left:0}.magicx-aia-submit-button{flex-shrink:0;width:36px;height:36px;border-radius:50%;border:none;background:var(--aia-submit-bg, var(--aia-color-text-default, #fff));color:var(--aia-submit-color, var(--aia-color-bg-default, #000));cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0;transition:opacity .2s ease}.magicx-aia-submit-button:hover{opacity:.85}.magicx-aia-submit-slot{display:contents}:where(.magicx-aia-input) strong[class~=magicx-aia-shimmer-revealed]{color:transparent;background:linear-gradient(120deg,var(--aia-written-text-color, var(--aia-color-text-default, #fff)) 0%,var(--aia-written-text-color, var(--aia-color-text-default, #fff)) 44%,#b0b0b0 48%,#b0b0b0 52%,var(--aia-written-text-color, var(--aia-color-text-default, #fff)) 56%,var(--aia-written-text-color, var(--aia-color-text-default, #fff)) 100%);background-size:200% 100%;-webkit-background-clip:text;background-clip:text}:where(.magicx-aia-input) strong[class~=magicx-aia-shimmer-sweep]{animation:aiaTextShimmer .65s ease-out forwards}@keyframes aiaTextShimmer{0%{background-position:100% 0}to{background-position:-50% 0}}\n"] }]
1122
+ }], propDecorators: { apiConfig: [{
1123
+ type: Input
1124
+ }], optionOverrides: [{
1125
+ type: Input
1126
+ }], maskCompletedText: [{
1127
+ type: Input
1128
+ }], columns: [{
1129
+ type: Input
1130
+ }], pillPlacement: [{
1131
+ type: Input
1132
+ }], mode: [{
1133
+ type: Input
1134
+ }], optionsPosition: [{
1135
+ type: Input
1136
+ }], animations: [{
1137
+ type: Input
1138
+ }], dropdownTrigger: [{
1139
+ type: Input
1140
+ }], closeDropdownOnBlur: [{
1141
+ type: Input
1142
+ }], showNonTappableOptions: [{
1143
+ type: Input
1144
+ }], autoFocus: [{
1145
+ type: Input
1146
+ }], value: [{
1147
+ type: Input
1148
+ }], completedParams: [{
1149
+ type: Input
1150
+ }], submitButton: [{
1151
+ type: Input
1152
+ }], submitted: [{
1153
+ type: Output
1154
+ }], errored: [{
1155
+ type: Output
1156
+ }], valueChange: [{
1157
+ type: Output
1158
+ }], completedParamsChange: [{
1159
+ type: Output
1160
+ }], focused: [{
1161
+ type: Output
1162
+ }], blurred: [{
1163
+ type: Output
1164
+ }], editorRef: [{
1165
+ type: ViewChild,
1166
+ args: ["editor", { static: false }]
1167
+ }], pillContainerRef: [{
1168
+ type: ViewChild,
1169
+ args: ["pillContainer"]
1170
+ }] } });
1171
+ function noop$1() {
1172
+ // Default no-op for CVA callbacks.
1173
+ }
1174
+
1175
+ /**
1176
+ * Tier 2 input binder. Attach to a `<textarea>` or `<input>` and pass an
1177
+ * {@link AIAutocompleteController}:
1178
+ *
1179
+ * ```html
1180
+ * <textarea [aiaInput]="ac"></textarea>
1181
+ * ```
1182
+ *
1183
+ * The directive wires the input's events (input/keydown/focus/blur) to the
1184
+ * controller, mirrors controller state into ARIA attributes
1185
+ * (`role="combobox"`, `aria-expanded`, `aria-activedescendant`,
1186
+ * `aria-autocomplete`, `aria-controls`), and implements
1187
+ * {@link ControlValueAccessor} so the host element participates in Reactive
1188
+ * Forms out of the box:
1189
+ *
1190
+ * ```html
1191
+ * <textarea [aiaInput]="ac" [formControl]="myControl"></textarea>
1192
+ * ```
1193
+ *
1194
+ * Per the React hook's behavior, the first typed character is auto-capitalized
1195
+ * — typing "h" becomes "H" before being forwarded to the controller. The
1196
+ * cursor position is preserved across that rewrite.
1197
+ *
1198
+ * @public
1199
+ */
1200
+ class AIAutocompleteInputDirective {
1201
+ constructor() {
1202
+ this.el = inject(ElementRef);
1203
+ this.renderer = inject(Renderer2);
1204
+ this.destroy$ = new Subject();
1205
+ this.onChangeFn = noop;
1206
+ this.onTouchedFn = noop;
1207
+ }
1208
+ ngOnInit() {
1209
+ if (!this.controller) {
1210
+ throw new Error('[aiaInput] requires a controller — use `[aiaInput]="myController"`.');
1211
+ }
1212
+ const el = this.el.nativeElement;
1213
+ this.renderer.setAttribute(el, "aria-controls", this.controller.listboxId);
1214
+ // Sync editor text → DOM. Skips the write when values already match, so
1215
+ // user keystrokes don't fight with our writeback. Preserves the caret
1216
+ // position when we do write (and the element still has focus).
1217
+ this.controller.text$.pipe(takeUntil(this.destroy$)).subscribe((text) => {
1218
+ if (el.value === text)
1219
+ return;
1220
+ const wasFocused = typeof document !== "undefined" && document.activeElement === el;
1221
+ const start = el.selectionStart;
1222
+ const end = el.selectionEnd;
1223
+ this.renderer.setProperty(el, "value", text);
1224
+ if (wasFocused && start != null && end != null) {
1225
+ const clampedStart = Math.min(start, text.length);
1226
+ const clampedEnd = Math.min(end, text.length);
1227
+ try {
1228
+ el.setSelectionRange(clampedStart, clampedEnd);
1229
+ }
1230
+ catch (_a) {
1231
+ // `setSelectionRange` throws on some input types (e.g. "number");
1232
+ // safe to ignore — those inputs don't have a meaningful selection.
1233
+ }
1234
+ }
1235
+ });
1236
+ this.controller.isDropdownOpen$.pipe(takeUntil(this.destroy$)).subscribe((isOpen) => {
1237
+ this.renderer.setAttribute(el, "aria-expanded", String(isOpen));
1238
+ });
1239
+ this.controller.activeIndex$.pipe(takeUntil(this.destroy$)).subscribe((idx) => {
1240
+ if (idx >= 0) {
1241
+ this.renderer.setAttribute(el, "aria-activedescendant", `${this.controller.listboxId}-option-${idx}`);
1242
+ }
1243
+ else {
1244
+ this.renderer.removeAttribute(el, "aria-activedescendant");
1245
+ }
1246
+ });
1247
+ }
1248
+ ngOnDestroy() {
1249
+ this.destroy$.next();
1250
+ this.destroy$.complete();
1251
+ }
1252
+ // === Event handlers (wired via host metadata above) ===
1253
+ /**
1254
+ * Auto-capitalizes the first character on the user's first keystroke so the
1255
+ * server receives "Create" instead of "create". Matches React's
1256
+ * `handleChange` in `useAIAutocomplete`. Skipped during IME composition so
1257
+ * we don't disrupt multi-keystroke input methods (Pinyin, Hangul, etc.).
1258
+ */
1259
+ onInput(event) {
1260
+ const target = event.target;
1261
+ const raw = target.value;
1262
+ const isComposing = event.isComposing === true;
1263
+ const shouldCapitalize = raw.length > 0 && !isComposing && raw[0] !== raw[0].toUpperCase();
1264
+ let next = raw;
1265
+ if (shouldCapitalize) {
1266
+ const start = target.selectionStart;
1267
+ const end = target.selectionEnd;
1268
+ next = raw[0].toUpperCase() + raw.slice(1);
1269
+ this.renderer.setProperty(target, "value", next);
1270
+ if (start != null && end != null) {
1271
+ try {
1272
+ target.setSelectionRange(start, end);
1273
+ }
1274
+ catch (_a) {
1275
+ // See note in ngOnInit's text$ subscriber.
1276
+ }
1277
+ }
1278
+ }
1279
+ this.controller.handleTextChange(next);
1280
+ this.onChangeFn(next);
1281
+ }
1282
+ onKeyDown(event) {
1283
+ this.controller.handleKeyDown(event);
1284
+ }
1285
+ onFocus() {
1286
+ this.controller.setFocused(true);
1287
+ }
1288
+ onBlur() {
1289
+ this.controller.setFocused(false);
1290
+ this.onTouchedFn();
1291
+ }
1292
+ // === ControlValueAccessor ===
1293
+ writeValue(value) {
1294
+ const next = value !== null && value !== void 0 ? value : "";
1295
+ this.renderer.setProperty(this.el.nativeElement, "value", next);
1296
+ // The controller may be undefined transiently if Angular calls
1297
+ // writeValue() before ngOnInit (rare but possible during dynamic form
1298
+ // construction). Guard so we don't throw in that window.
1299
+ if (this.controller)
1300
+ this.controller.setValue(next);
1301
+ }
1302
+ registerOnChange(fn) {
1303
+ this.onChangeFn = fn;
1304
+ }
1305
+ registerOnTouched(fn) {
1306
+ this.onTouchedFn = fn;
1307
+ }
1308
+ setDisabledState(disabled) {
1309
+ this.renderer.setProperty(this.el.nativeElement, "disabled", disabled);
1310
+ }
1311
+ }
1312
+ AIAutocompleteInputDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: AIAutocompleteInputDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1313
+ AIAutocompleteInputDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "15.2.10", type: AIAutocompleteInputDirective, isStandalone: true, selector: "[aiaInput]", inputs: { controller: ["aiaInput", "controller"] }, host: { attributes: { "role": "combobox", "aria-autocomplete": "list" }, listeners: { "input": "onInput($event)", "keydown": "onKeyDown($event)", "focus": "onFocus()", "blur": "onBlur()" } }, providers: [
1314
+ {
1315
+ provide: NG_VALUE_ACCESSOR,
1316
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
1317
+ useExisting: forwardRef(() => AIAutocompleteInputDirective),
1318
+ multi: true,
1319
+ },
1320
+ ], ngImport: i0 });
1321
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: AIAutocompleteInputDirective, decorators: [{
1322
+ type: Directive,
1323
+ args: [{
1324
+ selector: "[aiaInput]",
1325
+ standalone: true,
1326
+ host: {
1327
+ role: "combobox",
1328
+ "aria-autocomplete": "list",
1329
+ "(input)": "onInput($event)",
1330
+ "(keydown)": "onKeyDown($event)",
1331
+ "(focus)": "onFocus()",
1332
+ "(blur)": "onBlur()",
1333
+ },
1334
+ providers: [
1335
+ {
1336
+ provide: NG_VALUE_ACCESSOR,
1337
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
1338
+ useExisting: forwardRef(() => AIAutocompleteInputDirective),
1339
+ multi: true,
1340
+ },
1341
+ ],
1342
+ }]
1343
+ }], propDecorators: { controller: [{
1344
+ type: Input,
1345
+ args: ["aiaInput"]
1346
+ }] } });
1347
+ function noop() {
1348
+ // Default no-op callback for CVA — overwritten by Angular's form bridge.
1349
+ }
1350
+
1351
+ /**
1352
+ * Convenience NgModule that re-exports every public component and directive
1353
+ * in this package. Standalone-first consumers can import the individual
1354
+ * symbols directly; legacy NgModule-based codebases import this once and
1355
+ * get everything.
1356
+ *
1357
+ * @public
1358
+ */
1359
+ class AIAutocompleteModule {
1360
+ }
1361
+ AIAutocompleteModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: AIAutocompleteModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
1362
+ AIAutocompleteModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "15.2.10", ngImport: i0, type: AIAutocompleteModule, imports: [AIAutocompleteComponent, AIAutocompleteDropdownComponent, AIAutocompleteInputDirective], exports: [AIAutocompleteComponent, AIAutocompleteDropdownComponent, AIAutocompleteInputDirective] });
1363
+ AIAutocompleteModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: AIAutocompleteModule, imports: [AIAutocompleteComponent, AIAutocompleteDropdownComponent] });
1364
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: AIAutocompleteModule, decorators: [{
1365
+ type: NgModule,
1366
+ args: [{
1367
+ imports: [AIAutocompleteComponent, AIAutocompleteDropdownComponent, AIAutocompleteInputDirective],
1368
+ exports: [AIAutocompleteComponent, AIAutocompleteDropdownComponent, AIAutocompleteInputDirective],
1369
+ }]
1370
+ }] });
1371
+
1372
+ /**
1373
+ * Public API surface for @magicx-eng/ai-autocomplete-angular.
1374
+ *
1375
+ * Populated incrementally as features land — see TDB.md §6.6 for the full
1376
+ * planned export list. Only symbols re-exported here are considered part of
1377
+ * the package's public contract (TDB §19.2).
1378
+ */
1379
+ // === Tier 1: Full component ===
1380
+
1381
+ /**
1382
+ * Generated bundle index. Do not edit.
1383
+ */
1384
+
1385
+ export { AIAutocompleteComponent, AIAutocompleteController, AIAutocompleteDropdownComponent, AIAutocompleteInputDirective, AIAutocompleteModule };
1386
+ //# sourceMappingURL=magicx-eng-ai-autocomplete-angular.mjs.map