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