ngx-com 0.0.1 → 0.0.3

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 (58) hide show
  1. package/fesm2022/ngx-com-components-badge.mjs +138 -0
  2. package/fesm2022/ngx-com-components-badge.mjs.map +1 -0
  3. package/fesm2022/ngx-com-components-button.mjs +146 -0
  4. package/fesm2022/ngx-com-components-button.mjs.map +1 -0
  5. package/fesm2022/ngx-com-components-calendar.mjs +5046 -0
  6. package/fesm2022/ngx-com-components-calendar.mjs.map +1 -0
  7. package/fesm2022/ngx-com-components-card.mjs +590 -0
  8. package/fesm2022/ngx-com-components-card.mjs.map +1 -0
  9. package/fesm2022/ngx-com-components-checkbox.mjs +344 -0
  10. package/fesm2022/ngx-com-components-checkbox.mjs.map +1 -0
  11. package/fesm2022/ngx-com-components-collapsible.mjs +612 -0
  12. package/fesm2022/ngx-com-components-collapsible.mjs.map +1 -0
  13. package/fesm2022/ngx-com-components-dropdown-testing.mjs +255 -0
  14. package/fesm2022/ngx-com-components-dropdown-testing.mjs.map +1 -0
  15. package/fesm2022/ngx-com-components-dropdown.mjs +2598 -0
  16. package/fesm2022/ngx-com-components-dropdown.mjs.map +1 -0
  17. package/fesm2022/ngx-com-components-form-field.mjs +923 -0
  18. package/fesm2022/ngx-com-components-form-field.mjs.map +1 -0
  19. package/fesm2022/ngx-com-components-icon.mjs +183 -0
  20. package/fesm2022/ngx-com-components-icon.mjs.map +1 -0
  21. package/fesm2022/ngx-com-components-menu.mjs +1200 -0
  22. package/fesm2022/ngx-com-components-menu.mjs.map +1 -0
  23. package/fesm2022/ngx-com-components-popover.mjs +901 -0
  24. package/fesm2022/ngx-com-components-popover.mjs.map +1 -0
  25. package/fesm2022/ngx-com-components-radio.mjs +621 -0
  26. package/fesm2022/ngx-com-components-radio.mjs.map +1 -0
  27. package/fesm2022/ngx-com-components-sort.mjs +368 -0
  28. package/fesm2022/ngx-com-components-sort.mjs.map +1 -0
  29. package/fesm2022/ngx-com-components-tabs.mjs +1522 -0
  30. package/fesm2022/ngx-com-components-tabs.mjs.map +1 -0
  31. package/fesm2022/ngx-com-components.mjs +17 -0
  32. package/fesm2022/ngx-com-components.mjs.map +1 -0
  33. package/fesm2022/ngx-com-tokens.mjs +12 -0
  34. package/fesm2022/ngx-com-tokens.mjs.map +1 -0
  35. package/fesm2022/ngx-com-utils.mjs +601 -0
  36. package/fesm2022/ngx-com-utils.mjs.map +1 -0
  37. package/fesm2022/ngx-com.mjs +9 -23
  38. package/fesm2022/ngx-com.mjs.map +1 -1
  39. package/package.json +73 -1
  40. package/types/ngx-com-components-badge.d.ts +97 -0
  41. package/types/ngx-com-components-button.d.ts +69 -0
  42. package/types/ngx-com-components-calendar.d.ts +1665 -0
  43. package/types/ngx-com-components-card.d.ts +373 -0
  44. package/types/ngx-com-components-checkbox.d.ts +116 -0
  45. package/types/ngx-com-components-collapsible.d.ts +379 -0
  46. package/types/ngx-com-components-dropdown-testing.d.ts +116 -0
  47. package/types/ngx-com-components-dropdown.d.ts +914 -0
  48. package/types/ngx-com-components-form-field.d.ts +531 -0
  49. package/types/ngx-com-components-icon.d.ts +94 -0
  50. package/types/ngx-com-components-menu.d.ts +479 -0
  51. package/types/ngx-com-components-popover.d.ts +309 -0
  52. package/types/ngx-com-components-radio.d.ts +258 -0
  53. package/types/ngx-com-components-sort.d.ts +133 -0
  54. package/types/ngx-com-components-tabs.d.ts +396 -0
  55. package/types/ngx-com-components.d.ts +12 -0
  56. package/types/ngx-com-tokens.d.ts +7 -0
  57. package/types/ngx-com-utils.d.ts +424 -0
  58. package/types/ngx-com.d.ts +10 -7
@@ -0,0 +1,2598 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, ElementRef, viewChild, input, output, computed, ChangeDetectionStrategy, Component, DestroyRef, signal, TemplateRef, Directive, ViewContainerRef, DOCUMENT, contentChild, linkedSignal } from '@angular/core';
3
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
4
+ import { NgTemplateOutlet } from '@angular/common';
5
+ import { NgControl } from '@angular/forms';
6
+ import { Overlay, OverlayModule } from '@angular/cdk/overlay';
7
+ import { TemplatePortal } from '@angular/cdk/portal';
8
+ import { LiveAnnouncer } from '@angular/cdk/a11y';
9
+ import { cva } from 'class-variance-authority';
10
+ import { mergeClasses } from 'ngx-com/utils';
11
+ import { CdkVirtualScrollViewport, CdkFixedSizeVirtualScroll, CdkVirtualForOf } from '@angular/cdk/scrolling';
12
+ import { Subject } from 'rxjs';
13
+ import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
14
+
15
+ /**
16
+ * CVA variants for the dropdown trigger button.
17
+ * Uses semantic theme tokens for consistent cross-theme styling.
18
+ *
19
+ * @tokens `--color-input-background`, `--color-input-foreground`, `--color-input-border`,
20
+ * `--color-input-placeholder`, `--color-ring`, `--color-muted`, `--color-muted-hover`,
21
+ * `--color-warn`, `--color-success`, `--color-primary`, `--color-border`, `--radius-input`
22
+ */
23
+ const dropdownTriggerVariants = cva([
24
+ 'inline-flex',
25
+ 'items-center',
26
+ 'justify-between',
27
+ 'w-full',
28
+ 'rounded-input',
29
+ 'border',
30
+ 'bg-input-background',
31
+ 'text-input-foreground',
32
+ 'ring-offset-background',
33
+ 'transition-colors',
34
+ 'duration-150',
35
+ 'placeholder:text-input-placeholder',
36
+ 'focus:outline-none',
37
+ 'focus:ring-2',
38
+ 'focus:ring-offset-2',
39
+ 'focus:ring-ring',
40
+ 'disabled:cursor-not-allowed',
41
+ 'disabled:bg-disabled',
42
+ 'disabled:text-disabled-foreground',
43
+ ], {
44
+ variants: {
45
+ variant: {
46
+ default: [
47
+ 'border-input-border',
48
+ 'hover:border-border',
49
+ ],
50
+ outline: [
51
+ 'border-2',
52
+ 'border-input-border',
53
+ 'hover:border-foreground',
54
+ ],
55
+ ghost: [
56
+ 'border-transparent',
57
+ 'bg-transparent',
58
+ 'hover:bg-muted',
59
+ ],
60
+ filled: [
61
+ 'border-transparent',
62
+ 'bg-muted',
63
+ 'hover:bg-muted-hover',
64
+ ],
65
+ },
66
+ size: {
67
+ sm: ['h-8', 'px-2', 'text-xs', 'gap-1'],
68
+ default: ['h-10', 'px-3', 'text-sm', 'gap-2'],
69
+ lg: ['h-12', 'px-4', 'text-base', 'gap-3'],
70
+ },
71
+ state: {
72
+ default: [],
73
+ error: [
74
+ 'border-warn',
75
+ 'focus:ring-warn',
76
+ ],
77
+ success: [
78
+ 'border-success',
79
+ 'focus:ring-success',
80
+ ],
81
+ },
82
+ open: {
83
+ true: ['ring-2', 'ring-ring', 'border-primary'],
84
+ false: [],
85
+ },
86
+ },
87
+ compoundVariants: [
88
+ {
89
+ open: true,
90
+ variant: 'default',
91
+ class: ['border-primary'],
92
+ },
93
+ {
94
+ open: true,
95
+ variant: 'outline',
96
+ class: ['border-primary'],
97
+ },
98
+ ],
99
+ defaultVariants: {
100
+ variant: 'default',
101
+ size: 'default',
102
+ state: 'default',
103
+ open: false,
104
+ },
105
+ });
106
+ /**
107
+ * CVA variants for the dropdown panel (overlay).
108
+ *
109
+ * @tokens `--color-popover`, `--color-popover-foreground`, `--color-border-subtle`, `--radius-overlay`
110
+ */
111
+ const dropdownPanelVariants = cva([
112
+ 'z-50',
113
+ 'overflow-hidden',
114
+ 'rounded-overlay',
115
+ 'border',
116
+ 'border-border-subtle',
117
+ 'bg-popover',
118
+ 'text-popover-foreground',
119
+ 'shadow-lg',
120
+ 'outline-none',
121
+ ], {
122
+ variants: {
123
+ size: {
124
+ sm: ['text-xs'],
125
+ default: ['text-sm'],
126
+ lg: ['text-base'],
127
+ },
128
+ },
129
+ defaultVariants: {
130
+ size: 'default',
131
+ },
132
+ });
133
+ /**
134
+ * CVA variants for individual dropdown options.
135
+ *
136
+ * @tokens `--color-popover-foreground`, `--color-muted`, `--color-primary-subtle`,
137
+ * `--color-primary-subtle-foreground`, `--color-disabled-foreground`
138
+ */
139
+ const dropdownOptionVariants = cva([
140
+ 'relative',
141
+ 'flex',
142
+ 'w-full',
143
+ 'cursor-pointer',
144
+ 'select-none',
145
+ 'items-center',
146
+ 'outline-none',
147
+ 'transition-colors',
148
+ 'duration-100',
149
+ ], {
150
+ variants: {
151
+ size: {
152
+ sm: ['px-2', 'py-1.5', 'text-xs'],
153
+ default: ['px-3', 'py-2', 'text-sm'],
154
+ lg: ['px-4', 'py-3', 'text-base'],
155
+ },
156
+ state: {
157
+ default: [
158
+ 'text-popover-foreground',
159
+ 'hover:bg-muted',
160
+ ],
161
+ active: [
162
+ 'bg-muted',
163
+ 'text-popover-foreground',
164
+ ],
165
+ selected: [
166
+ 'bg-primary-subtle',
167
+ 'text-primary-subtle-foreground',
168
+ ],
169
+ 'selected-active': [
170
+ 'bg-primary-subtle',
171
+ 'text-primary-subtle-foreground',
172
+ 'brightness-95',
173
+ ],
174
+ disabled: [
175
+ 'cursor-not-allowed',
176
+ 'text-disabled-foreground',
177
+ 'hover:bg-transparent',
178
+ ],
179
+ },
180
+ },
181
+ defaultVariants: {
182
+ size: 'default',
183
+ state: 'default',
184
+ },
185
+ });
186
+ /**
187
+ * CVA variants for the search input.
188
+ *
189
+ * @tokens `--color-border-subtle`, `--color-input-placeholder`, `--color-disabled-foreground`
190
+ */
191
+ const dropdownSearchVariants = cva([
192
+ 'flex',
193
+ 'h-10',
194
+ 'w-full',
195
+ 'border-b',
196
+ 'border-border-subtle',
197
+ 'bg-transparent',
198
+ 'px-3',
199
+ 'py-2',
200
+ 'text-sm',
201
+ 'placeholder:text-input-placeholder',
202
+ 'outline-none',
203
+ 'disabled:cursor-not-allowed',
204
+ 'disabled:text-disabled-foreground',
205
+ ], {
206
+ variants: {
207
+ size: {
208
+ sm: ['h-8', 'px-2', 'text-xs'],
209
+ default: ['h-10', 'px-3', 'text-sm'],
210
+ lg: ['h-12', 'px-4', 'text-base'],
211
+ },
212
+ },
213
+ defaultVariants: {
214
+ size: 'default',
215
+ },
216
+ });
217
+ /**
218
+ * CVA variants for multi-select tags.
219
+ *
220
+ * @tokens `--color-muted`, `--color-muted-foreground`, `--color-muted-hover`,
221
+ * `--color-primary-subtle`, `--color-primary-subtle-foreground`, `--radius-tag`
222
+ */
223
+ const dropdownTagVariants = cva([
224
+ 'inline-flex',
225
+ 'items-center',
226
+ 'gap-1',
227
+ 'rounded-tag',
228
+ 'font-medium',
229
+ 'transition-colors',
230
+ 'duration-100',
231
+ ], {
232
+ variants: {
233
+ size: {
234
+ sm: ['h-5', 'px-1.5', 'text-xs'],
235
+ default: ['h-6', 'px-2', 'text-xs'],
236
+ lg: ['h-7', 'px-2.5', 'text-sm'],
237
+ },
238
+ variant: {
239
+ default: [
240
+ 'bg-muted',
241
+ 'text-muted-foreground',
242
+ 'hover:bg-muted-hover',
243
+ ],
244
+ primary: [
245
+ 'bg-primary-subtle',
246
+ 'text-primary-subtle-foreground',
247
+ 'hover:brightness-95',
248
+ ],
249
+ },
250
+ },
251
+ defaultVariants: {
252
+ size: 'default',
253
+ variant: 'primary',
254
+ },
255
+ });
256
+ /**
257
+ * CVA variants for the tag remove button.
258
+ *
259
+ * @tokens `--color-ring`, `--radius-interactive-sm`
260
+ */
261
+ const dropdownTagRemoveVariants = cva([
262
+ 'inline-flex',
263
+ 'items-center',
264
+ 'justify-center',
265
+ 'rounded-interactive-sm',
266
+ 'opacity-70',
267
+ 'transition-opacity',
268
+ 'hover:opacity-100',
269
+ 'focus:outline-none',
270
+ 'focus:ring-1',
271
+ 'focus:ring-ring',
272
+ ], {
273
+ variants: {
274
+ size: {
275
+ sm: ['h-3', 'w-3'],
276
+ default: ['h-3.5', 'w-3.5'],
277
+ lg: ['h-4', 'w-4'],
278
+ },
279
+ },
280
+ defaultVariants: {
281
+ size: 'default',
282
+ },
283
+ });
284
+ /**
285
+ * CVA variants for the overflow badge (+N indicator).
286
+ *
287
+ * @tokens `--color-muted`, `--color-muted-foreground`, `--radius-tag`
288
+ */
289
+ const dropdownOverflowBadgeVariants = cva([
290
+ 'inline-flex',
291
+ 'items-center',
292
+ 'justify-center',
293
+ 'rounded-tag',
294
+ 'font-medium',
295
+ 'text-muted-foreground',
296
+ 'bg-muted',
297
+ ], {
298
+ variants: {
299
+ size: {
300
+ sm: ['h-5', 'px-1.5', 'text-xs'],
301
+ default: ['h-6', 'px-2', 'text-xs'],
302
+ lg: ['h-7', 'px-2.5', 'text-sm'],
303
+ },
304
+ },
305
+ defaultVariants: {
306
+ size: 'default',
307
+ },
308
+ });
309
+ /**
310
+ * CVA variants for group headers.
311
+ *
312
+ * @tokens `--color-muted-foreground`
313
+ */
314
+ const dropdownGroupVariants = cva([
315
+ 'flex',
316
+ 'items-center',
317
+ 'px-3',
318
+ 'py-2',
319
+ 'text-xs',
320
+ 'font-semibold',
321
+ 'uppercase',
322
+ 'tracking-wider',
323
+ 'text-muted-foreground',
324
+ ], {
325
+ variants: {
326
+ size: {
327
+ sm: ['px-2', 'py-1.5'],
328
+ default: ['px-3', 'py-2'],
329
+ lg: ['px-4', 'py-2.5'],
330
+ },
331
+ },
332
+ defaultVariants: {
333
+ size: 'default',
334
+ },
335
+ });
336
+ /**
337
+ * CVA variants for the empty state.
338
+ *
339
+ * @tokens `--color-muted-foreground`
340
+ */
341
+ const dropdownEmptyVariants = cva([
342
+ 'flex',
343
+ 'items-center',
344
+ 'justify-center',
345
+ 'px-3',
346
+ 'py-6',
347
+ 'text-muted-foreground',
348
+ ], {
349
+ variants: {
350
+ size: {
351
+ sm: ['px-2', 'py-4', 'text-xs'],
352
+ default: ['px-3', 'py-6', 'text-sm'],
353
+ lg: ['px-4', 'py-8', 'text-base'],
354
+ },
355
+ },
356
+ defaultVariants: {
357
+ size: 'default',
358
+ },
359
+ });
360
+ /**
361
+ * CVA variants for the clear button.
362
+ *
363
+ * @tokens `--color-ring`, `--radius-interactive-sm`
364
+ */
365
+ const dropdownClearVariants = cva([
366
+ 'inline-flex',
367
+ 'items-center',
368
+ 'justify-center',
369
+ 'rounded-interactive-sm',
370
+ 'opacity-50',
371
+ 'transition-opacity',
372
+ 'hover:opacity-100',
373
+ 'focus:outline-none',
374
+ 'focus:ring-1',
375
+ 'focus:ring-ring',
376
+ ], {
377
+ variants: {
378
+ size: {
379
+ sm: ['h-4', 'w-4'],
380
+ default: ['h-5', 'w-5'],
381
+ lg: ['h-6', 'w-6'],
382
+ },
383
+ },
384
+ defaultVariants: {
385
+ size: 'default',
386
+ },
387
+ });
388
+ /**
389
+ * CVA variants for the chevron icon.
390
+ */
391
+ const dropdownChevronVariants = cva([
392
+ 'shrink-0',
393
+ 'opacity-50',
394
+ 'transition-transform',
395
+ 'duration-200',
396
+ ], {
397
+ variants: {
398
+ size: {
399
+ sm: ['h-3', 'w-3'],
400
+ default: ['h-4', 'w-4'],
401
+ lg: ['h-5', 'w-5'],
402
+ },
403
+ open: {
404
+ true: ['rotate-180'],
405
+ false: ['rotate-0'],
406
+ },
407
+ },
408
+ defaultVariants: {
409
+ size: 'default',
410
+ open: false,
411
+ },
412
+ });
413
+
414
+ /**
415
+ * A single option in the dropdown list.
416
+ * Implements CDK's Highlightable interface for keyboard navigation.
417
+ *
418
+ * @example
419
+ * ```html
420
+ * <com-dropdown-option
421
+ * [value]="user"
422
+ * [selected]="isSelected(user)"
423
+ * [disabled]="user.inactive"
424
+ * (select)="onSelect(user)"
425
+ * />
426
+ * ```
427
+ *
428
+ * @tokens `--color-popover-foreground`, `--color-muted`, `--color-primary-subtle`,
429
+ * `--color-primary-subtle-foreground`, `--color-disabled-foreground`
430
+ */
431
+ class ComDropdownOption {
432
+ elementRef = inject(ElementRef);
433
+ /** Reference to the option element for focus management. */
434
+ optionRef = viewChild('optionElement', ...(ngDevMode ? [{ debugName: "optionRef" }] : []));
435
+ /** The value this option represents. */
436
+ value = input.required(...(ngDevMode ? [{ debugName: "value" }] : []));
437
+ /** Display text for this option (when no template is provided). */
438
+ displayText = input('', ...(ngDevMode ? [{ debugName: "displayText" }] : []));
439
+ /** Unique identifier for this option. */
440
+ id = input.required(...(ngDevMode ? [{ debugName: "id" }] : []));
441
+ /** Index of this option in the list. */
442
+ index = input(0, ...(ngDevMode ? [{ debugName: "index" }] : []));
443
+ /** Whether this option is currently selected. */
444
+ selected = input(false, ...(ngDevMode ? [{ debugName: "selected" }] : []));
445
+ /** Whether this option is currently active (keyboard focused). */
446
+ active = input(false, ...(ngDevMode ? [{ debugName: "active" }] : []));
447
+ /** Whether this option is disabled. */
448
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
449
+ /** Size variant for styling. */
450
+ size = input('default', ...(ngDevMode ? [{ debugName: "size" }] : []));
451
+ /** Custom template for rendering the option content. */
452
+ optionTemplate = input(null, ...(ngDevMode ? [{ debugName: "optionTemplate" }] : []));
453
+ /** Additional CSS classes to apply. */
454
+ userClass = input('', { ...(ngDevMode ? { debugName: "userClass" } : {}), alias: 'class' });
455
+ /** Emitted when the option is selected. */
456
+ select = output();
457
+ /** Emitted when the mouse enters the option. */
458
+ hover = output();
459
+ /** Computed option state for CVA styling. */
460
+ optionState = computed(() => {
461
+ if (this.disabled())
462
+ return 'disabled';
463
+ if (this.selected() && this.active())
464
+ return 'selected-active';
465
+ if (this.selected())
466
+ return 'selected';
467
+ if (this.active())
468
+ return 'active';
469
+ return 'default';
470
+ }, ...(ngDevMode ? [{ debugName: "optionState" }] : []));
471
+ /** Computed CSS classes for the option. */
472
+ optionClasses = computed(() => {
473
+ const baseClasses = dropdownOptionVariants({
474
+ size: this.size(),
475
+ state: this.optionState(),
476
+ });
477
+ return mergeClasses(baseClasses, this.userClass());
478
+ }, ...(ngDevMode ? [{ debugName: "optionClasses" }] : []));
479
+ /** Template context for custom option templates. */
480
+ templateContext = computed(() => ({
481
+ $implicit: this.value(),
482
+ index: this.index(),
483
+ selected: this.selected(),
484
+ active: this.active(),
485
+ disabled: this.disabled(),
486
+ }), ...(ngDevMode ? [{ debugName: "templateContext" }] : []));
487
+ onOptionClick(event) {
488
+ if (this.disabled()) {
489
+ event.preventDefault();
490
+ event.stopPropagation();
491
+ return;
492
+ }
493
+ this.select.emit(this.value());
494
+ }
495
+ onMouseEnter() {
496
+ if (!this.disabled()) {
497
+ this.hover.emit(this.value());
498
+ }
499
+ }
500
+ /** Gets the host element. */
501
+ getHostElement() {
502
+ return this.elementRef.nativeElement;
503
+ }
504
+ /** Scrolls this option into view. */
505
+ scrollIntoView() {
506
+ this.optionRef()?.nativeElement.scrollIntoView({
507
+ block: 'nearest',
508
+ inline: 'nearest',
509
+ });
510
+ }
511
+ /** Focuses this option element. */
512
+ focus() {
513
+ this.optionRef()?.nativeElement.focus();
514
+ }
515
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDropdownOption, deps: [], target: i0.ɵɵFactoryTarget.Component });
516
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: ComDropdownOption, isStandalone: true, selector: "com-dropdown-option", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, displayText: { classPropertyName: "displayText", publicName: "displayText", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: true, transformFunction: null }, index: { classPropertyName: "index", publicName: "index", isSignal: true, isRequired: false, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, optionTemplate: { classPropertyName: "optionTemplate", publicName: "optionTemplate", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { select: "select", hover: "hover" }, host: { classAttribute: "com-dropdown-option-host block" }, viewQueries: [{ propertyName: "optionRef", first: true, predicate: ["optionElement"], descendants: true, isSignal: true }], exportAs: ["comDropdownOption"], ngImport: i0, template: `
517
+ <div
518
+ #optionElement
519
+ [class]="optionClasses()"
520
+ [attr.role]="'option'"
521
+ [attr.id]="id()"
522
+ [attr.aria-selected]="selected()"
523
+ [attr.aria-disabled]="disabled() || null"
524
+ [attr.data-active]="active() || null"
525
+ [attr.data-selected]="selected() || null"
526
+ [attr.data-disabled]="disabled() || null"
527
+ (click)="onOptionClick($event)"
528
+ (mouseenter)="onMouseEnter()"
529
+ >
530
+ @if (optionTemplate()) {
531
+ <ng-container
532
+ [ngTemplateOutlet]="optionTemplate()!"
533
+ [ngTemplateOutletContext]="templateContext()"
534
+ />
535
+ } @else {
536
+ <span class="truncate">{{ displayText() }}</span>
537
+ @if (selected()) {
538
+ <svg
539
+ class="ml-auto h-4 w-4 shrink-0"
540
+ viewBox="0 0 24 24"
541
+ fill="none"
542
+ stroke="currentColor"
543
+ stroke-width="2"
544
+ stroke-linecap="round"
545
+ stroke-linejoin="round"
546
+ >
547
+ <polyline points="20 6 9 17 4 12" />
548
+ </svg>
549
+ }
550
+ }
551
+ </div>
552
+ `, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
553
+ }
554
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDropdownOption, decorators: [{
555
+ type: Component,
556
+ args: [{
557
+ selector: 'com-dropdown-option',
558
+ exportAs: 'comDropdownOption',
559
+ template: `
560
+ <div
561
+ #optionElement
562
+ [class]="optionClasses()"
563
+ [attr.role]="'option'"
564
+ [attr.id]="id()"
565
+ [attr.aria-selected]="selected()"
566
+ [attr.aria-disabled]="disabled() || null"
567
+ [attr.data-active]="active() || null"
568
+ [attr.data-selected]="selected() || null"
569
+ [attr.data-disabled]="disabled() || null"
570
+ (click)="onOptionClick($event)"
571
+ (mouseenter)="onMouseEnter()"
572
+ >
573
+ @if (optionTemplate()) {
574
+ <ng-container
575
+ [ngTemplateOutlet]="optionTemplate()!"
576
+ [ngTemplateOutletContext]="templateContext()"
577
+ />
578
+ } @else {
579
+ <span class="truncate">{{ displayText() }}</span>
580
+ @if (selected()) {
581
+ <svg
582
+ class="ml-auto h-4 w-4 shrink-0"
583
+ viewBox="0 0 24 24"
584
+ fill="none"
585
+ stroke="currentColor"
586
+ stroke-width="2"
587
+ stroke-linecap="round"
588
+ stroke-linejoin="round"
589
+ >
590
+ <polyline points="20 6 9 17 4 12" />
591
+ </svg>
592
+ }
593
+ }
594
+ </div>
595
+ `,
596
+ imports: [NgTemplateOutlet],
597
+ changeDetection: ChangeDetectionStrategy.OnPush,
598
+ host: {
599
+ class: 'com-dropdown-option-host block',
600
+ },
601
+ }]
602
+ }], propDecorators: { optionRef: [{ type: i0.ViewChild, args: ['optionElement', { isSignal: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }], displayText: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayText", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: true }] }], index: [{ type: i0.Input, args: [{ isSignal: true, alias: "index", required: false }] }], selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }], active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], optionTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionTemplate", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], select: [{ type: i0.Output, args: ["select"] }], hover: [{ type: i0.Output, args: ["hover"] }] } });
603
+
604
+ /**
605
+ * The overlay panel containing the dropdown options.
606
+ * Supports virtual scrolling for large lists.
607
+ *
608
+ * @example
609
+ * ```html
610
+ * <com-dropdown-panel
611
+ * [options]="filteredOptions()"
612
+ * [maxHeight]="'300px'"
613
+ * [virtualScrollEnabled]="true"
614
+ * >
615
+ * <ng-content />
616
+ * </com-dropdown-panel>
617
+ * ```
618
+ *
619
+ * @tokens `--color-popover`, `--color-popover-foreground`, `--color-border-subtle`,
620
+ * `--color-muted-foreground`, `--radius-overlay`
621
+ */
622
+ class ComDropdownPanel {
623
+ elementRef = inject(ElementRef);
624
+ /** Reference to the panel element. */
625
+ panelRef = viewChild('panelElement', ...(ngDevMode ? [{ debugName: "panelRef" }] : []));
626
+ /** Reference to the virtual scroll viewport (when enabled). */
627
+ viewport = viewChild('viewport', ...(ngDevMode ? [{ debugName: "viewport" }] : []));
628
+ /** Unique identifier for the panel. */
629
+ panelId = input('', ...(ngDevMode ? [{ debugName: "panelId" }] : []));
630
+ /** The processed options to display. */
631
+ options = input([], ...(ngDevMode ? [{ debugName: "options" }] : []));
632
+ /** Maximum height of the panel. */
633
+ maxHeight = input('256px', ...(ngDevMode ? [{ debugName: "maxHeight" }] : []));
634
+ /** Whether multiple selection is enabled. */
635
+ multiselectable = input(false, ...(ngDevMode ? [{ debugName: "multiselectable" }] : []));
636
+ /** Size variant for styling. */
637
+ size = input('default', ...(ngDevMode ? [{ debugName: "size" }] : []));
638
+ /** Additional CSS classes to apply to the panel. */
639
+ panelClass = input('', ...(ngDevMode ? [{ debugName: "panelClass" }] : []));
640
+ /** Whether virtual scrolling is enabled. */
641
+ virtualScrollEnabled = input(false, ...(ngDevMode ? [{ debugName: "virtualScrollEnabled" }] : []));
642
+ /** Item size for virtual scrolling (in pixels). */
643
+ itemSize = input(40, ...(ngDevMode ? [{ debugName: "itemSize" }] : []));
644
+ /** The current search query (for empty state context). */
645
+ searchQuery = input('', ...(ngDevMode ? [{ debugName: "searchQuery" }] : []));
646
+ /** Custom empty state text. */
647
+ emptyText = input('No options available', ...(ngDevMode ? [{ debugName: "emptyText" }] : []));
648
+ /** Template for rendering each option. */
649
+ optionTemplate = input.required(...(ngDevMode ? [{ debugName: "optionTemplate" }] : []));
650
+ /** Custom template for the empty state. */
651
+ emptyTemplate = input(null, ...(ngDevMode ? [{ debugName: "emptyTemplate" }] : []));
652
+ /** Emitted when the panel is scrolled. */
653
+ scrolled = output();
654
+ /** Whether to show the empty state. */
655
+ showEmpty = computed(() => this.options().length === 0, ...(ngDevMode ? [{ debugName: "showEmpty" }] : []));
656
+ /** Computed CSS classes for the panel. */
657
+ panelClasses = computed(() => {
658
+ const baseClasses = dropdownPanelVariants({ size: this.size() });
659
+ return mergeClasses(baseClasses, this.panelClass());
660
+ }, ...(ngDevMode ? [{ debugName: "panelClasses" }] : []));
661
+ /** Computed CSS classes for the empty state. */
662
+ emptyClasses = computed(() => {
663
+ return dropdownEmptyVariants({ size: this.size() });
664
+ }, ...(ngDevMode ? [{ debugName: "emptyClasses" }] : []));
665
+ /** Template context for the empty state. */
666
+ emptyContext = computed(() => ({
667
+ $implicit: this.searchQuery(),
668
+ }), ...(ngDevMode ? [{ debugName: "emptyContext" }] : []));
669
+ /** Track function for options. */
670
+ trackByFn(_index, option) {
671
+ return option.id;
672
+ }
673
+ /** Gets the host element. */
674
+ getHostElement() {
675
+ return this.elementRef.nativeElement;
676
+ }
677
+ /** Scrolls to a specific index. */
678
+ scrollToIndex(index) {
679
+ const vp = this.viewport();
680
+ if (this.virtualScrollEnabled() && vp) {
681
+ vp.scrollToIndex(index);
682
+ }
683
+ else {
684
+ const panelEl = this.panelRef()?.nativeElement;
685
+ const optionEl = panelEl?.querySelector(`[data-index="${index}"]`);
686
+ optionEl?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
687
+ }
688
+ }
689
+ /** Scrolls an option into view. */
690
+ scrollOptionIntoView(optionElement) {
691
+ optionElement.scrollIntoView({ block: 'nearest', inline: 'nearest' });
692
+ }
693
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDropdownPanel, deps: [], target: i0.ɵɵFactoryTarget.Component });
694
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: ComDropdownPanel, isStandalone: true, selector: "com-dropdown-panel", inputs: { panelId: { classPropertyName: "panelId", publicName: "panelId", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, maxHeight: { classPropertyName: "maxHeight", publicName: "maxHeight", isSignal: true, isRequired: false, transformFunction: null }, multiselectable: { classPropertyName: "multiselectable", publicName: "multiselectable", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, panelClass: { classPropertyName: "panelClass", publicName: "panelClass", isSignal: true, isRequired: false, transformFunction: null }, virtualScrollEnabled: { classPropertyName: "virtualScrollEnabled", publicName: "virtualScrollEnabled", isSignal: true, isRequired: false, transformFunction: null }, itemSize: { classPropertyName: "itemSize", publicName: "itemSize", isSignal: true, isRequired: false, transformFunction: null }, searchQuery: { classPropertyName: "searchQuery", publicName: "searchQuery", isSignal: true, isRequired: false, transformFunction: null }, emptyText: { classPropertyName: "emptyText", publicName: "emptyText", isSignal: true, isRequired: false, transformFunction: null }, optionTemplate: { classPropertyName: "optionTemplate", publicName: "optionTemplate", isSignal: true, isRequired: true, transformFunction: null }, emptyTemplate: { classPropertyName: "emptyTemplate", publicName: "emptyTemplate", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { scrolled: "scrolled" }, host: { classAttribute: "com-dropdown-panel-host block" }, viewQueries: [{ propertyName: "panelRef", first: true, predicate: ["panelElement"], descendants: true, isSignal: true }, { propertyName: "viewport", first: true, predicate: ["viewport"], descendants: true, isSignal: true }], exportAs: ["comDropdownPanel"], ngImport: i0, template: `
695
+ <div
696
+ #panelElement
697
+ [class]="panelClasses()"
698
+ [attr.role]="'listbox'"
699
+ [attr.aria-multiselectable]="multiselectable() || null"
700
+ [attr.id]="panelId()"
701
+ >
702
+ <!-- Search slot -->
703
+ <ng-content select="[comDropdownSearch]" />
704
+
705
+ <!-- Options container -->
706
+ @if (virtualScrollEnabled()) {
707
+ <cdk-virtual-scroll-viewport
708
+ [itemSize]="itemSize()"
709
+ [maxBufferPx]="400"
710
+ [minBufferPx]="200"
711
+ [style.height]="maxHeight()"
712
+ class="overflow-auto"
713
+ >
714
+ <div
715
+ *cdkVirtualFor="let option of options(); trackBy: trackByFn; let i = index"
716
+ [attr.data-index]="i"
717
+ >
718
+ <ng-container
719
+ [ngTemplateOutlet]="optionTemplate()"
720
+ [ngTemplateOutletContext]="{ $implicit: option, index: i }"
721
+ />
722
+ </div>
723
+ </cdk-virtual-scroll-viewport>
724
+ } @else {
725
+ <div
726
+ class="overflow-auto"
727
+ [style.maxHeight]="maxHeight()"
728
+ >
729
+ @for (option of options(); track option.id; let i = $index) {
730
+ <ng-container
731
+ [ngTemplateOutlet]="optionTemplate()"
732
+ [ngTemplateOutletContext]="{ $implicit: option, index: i }"
733
+ />
734
+ }
735
+ </div>
736
+ }
737
+
738
+ <!-- Empty state -->
739
+ @if (showEmpty()) {
740
+ @if (emptyTemplate()) {
741
+ <ng-container
742
+ [ngTemplateOutlet]="emptyTemplate()!"
743
+ [ngTemplateOutletContext]="emptyContext()"
744
+ />
745
+ } @else {
746
+ <div [class]="emptyClasses()">
747
+ {{ emptyText() }}
748
+ </div>
749
+ }
750
+ }
751
+ </div>
752
+ `, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: CdkVirtualScrollViewport, selector: "cdk-virtual-scroll-viewport", inputs: ["orientation", "appendOnly"], outputs: ["scrolledIndexChange"] }, { kind: "directive", type: CdkFixedSizeVirtualScroll, selector: "cdk-virtual-scroll-viewport[itemSize]", inputs: ["itemSize", "minBufferPx", "maxBufferPx"] }, { kind: "directive", type: CdkVirtualForOf, selector: "[cdkVirtualFor][cdkVirtualForOf]", inputs: ["cdkVirtualForOf", "cdkVirtualForTrackBy", "cdkVirtualForTemplate", "cdkVirtualForTemplateCacheSize"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
753
+ }
754
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDropdownPanel, decorators: [{
755
+ type: Component,
756
+ args: [{
757
+ selector: 'com-dropdown-panel',
758
+ exportAs: 'comDropdownPanel',
759
+ template: `
760
+ <div
761
+ #panelElement
762
+ [class]="panelClasses()"
763
+ [attr.role]="'listbox'"
764
+ [attr.aria-multiselectable]="multiselectable() || null"
765
+ [attr.id]="panelId()"
766
+ >
767
+ <!-- Search slot -->
768
+ <ng-content select="[comDropdownSearch]" />
769
+
770
+ <!-- Options container -->
771
+ @if (virtualScrollEnabled()) {
772
+ <cdk-virtual-scroll-viewport
773
+ [itemSize]="itemSize()"
774
+ [maxBufferPx]="400"
775
+ [minBufferPx]="200"
776
+ [style.height]="maxHeight()"
777
+ class="overflow-auto"
778
+ >
779
+ <div
780
+ *cdkVirtualFor="let option of options(); trackBy: trackByFn; let i = index"
781
+ [attr.data-index]="i"
782
+ >
783
+ <ng-container
784
+ [ngTemplateOutlet]="optionTemplate()"
785
+ [ngTemplateOutletContext]="{ $implicit: option, index: i }"
786
+ />
787
+ </div>
788
+ </cdk-virtual-scroll-viewport>
789
+ } @else {
790
+ <div
791
+ class="overflow-auto"
792
+ [style.maxHeight]="maxHeight()"
793
+ >
794
+ @for (option of options(); track option.id; let i = $index) {
795
+ <ng-container
796
+ [ngTemplateOutlet]="optionTemplate()"
797
+ [ngTemplateOutletContext]="{ $implicit: option, index: i }"
798
+ />
799
+ }
800
+ </div>
801
+ }
802
+
803
+ <!-- Empty state -->
804
+ @if (showEmpty()) {
805
+ @if (emptyTemplate()) {
806
+ <ng-container
807
+ [ngTemplateOutlet]="emptyTemplate()!"
808
+ [ngTemplateOutletContext]="emptyContext()"
809
+ />
810
+ } @else {
811
+ <div [class]="emptyClasses()">
812
+ {{ emptyText() }}
813
+ </div>
814
+ }
815
+ }
816
+ </div>
817
+ `,
818
+ imports: [
819
+ NgTemplateOutlet,
820
+ CdkVirtualScrollViewport,
821
+ CdkFixedSizeVirtualScroll,
822
+ CdkVirtualForOf,
823
+ ],
824
+ changeDetection: ChangeDetectionStrategy.OnPush,
825
+ host: {
826
+ class: 'com-dropdown-panel-host block',
827
+ },
828
+ }]
829
+ }], propDecorators: { panelRef: [{ type: i0.ViewChild, args: ['panelElement', { isSignal: true }] }], viewport: [{ type: i0.ViewChild, args: ['viewport', { isSignal: true }] }], panelId: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelId", required: false }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], maxHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxHeight", required: false }] }], multiselectable: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiselectable", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], panelClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelClass", required: false }] }], virtualScrollEnabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "virtualScrollEnabled", required: false }] }], itemSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemSize", required: false }] }], searchQuery: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchQuery", required: false }] }], emptyText: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyText", required: false }] }], optionTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionTemplate", required: true }] }], emptyTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyTemplate", required: false }] }], scrolled: [{ type: i0.Output, args: ["scrolled"] }] } });
830
+
831
+ /**
832
+ * Search input component for filtering dropdown options.
833
+ * Includes debouncing for better performance.
834
+ *
835
+ * @example
836
+ * ```html
837
+ * <com-dropdown-search
838
+ * [placeholder]="'Search...'"
839
+ * [debounceTime]="300"
840
+ * (searchChange)="onSearch($event)"
841
+ * />
842
+ * ```
843
+ *
844
+ * @tokens `--color-border-subtle`, `--color-input-placeholder`, `--color-disabled-foreground`,
845
+ * `--color-ring`, `--radius-interactive-sm`
846
+ */
847
+ class ComDropdownSearch {
848
+ destroyRef = inject(DestroyRef);
849
+ /** Reference to the input element. */
850
+ inputRef = viewChild('searchInput', ...(ngDevMode ? [{ debugName: "inputRef" }] : []));
851
+ /** Subject for debounced search. */
852
+ searchSubject = new Subject();
853
+ /** Current internal search value. */
854
+ internalValue = signal('', ...(ngDevMode ? [{ debugName: "internalValue" }] : []));
855
+ /** Placeholder text for the search input. */
856
+ placeholder = input('Search...', ...(ngDevMode ? [{ debugName: "placeholder" }] : []));
857
+ /** Aria label for accessibility. */
858
+ ariaLabel = input('Search options', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
859
+ /** Whether the search input is disabled. */
860
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
861
+ /** Debounce time in milliseconds. */
862
+ debounceMs = input(300, ...(ngDevMode ? [{ debugName: "debounceMs" }] : []));
863
+ /** Size variant for styling. */
864
+ size = input('default', ...(ngDevMode ? [{ debugName: "size" }] : []));
865
+ /** Additional CSS classes to apply. */
866
+ userClass = input('', { ...(ngDevMode ? { debugName: "userClass" } : {}), alias: 'class' });
867
+ /** Emitted when the search value changes (after debounce). */
868
+ searchChange = output();
869
+ /** Emitted when a navigation key is pressed (for focus management). */
870
+ keyNav = output();
871
+ /** Whether to show the clear button. */
872
+ showClear = computed(() => this.internalValue().length > 0, ...(ngDevMode ? [{ debugName: "showClear" }] : []));
873
+ /** Computed CSS classes for the search input. */
874
+ searchClasses = computed(() => {
875
+ const baseClasses = dropdownSearchVariants({ size: this.size() });
876
+ // Add left padding for the search icon
877
+ return mergeClasses(baseClasses, 'pl-9 pr-8', this.userClass());
878
+ }, ...(ngDevMode ? [{ debugName: "searchClasses" }] : []));
879
+ ngOnInit() {
880
+ // Set up debounced search
881
+ this.searchSubject
882
+ .pipe(debounceTime(this.debounceMs()), distinctUntilChanged(), takeUntilDestroyed(this.destroyRef))
883
+ .subscribe((value) => {
884
+ this.searchChange.emit(value);
885
+ });
886
+ }
887
+ onInput(event) {
888
+ const value = event.target.value;
889
+ this.internalValue.set(value);
890
+ this.searchSubject.next(value);
891
+ }
892
+ onKeydown(event) {
893
+ // Let parent handle navigation keys
894
+ if (['ArrowDown', 'ArrowUp', 'Enter', 'Escape'].includes(event.key)) {
895
+ this.keyNav.emit(event);
896
+ }
897
+ }
898
+ clearSearch() {
899
+ this.internalValue.set('');
900
+ this.searchSubject.next('');
901
+ this.focus();
902
+ }
903
+ /** Focuses the search input. */
904
+ focus() {
905
+ this.inputRef()?.nativeElement.focus();
906
+ }
907
+ /** Gets the current search value. */
908
+ getValue() {
909
+ return this.internalValue();
910
+ }
911
+ /** Sets the search value programmatically. */
912
+ setValue(value) {
913
+ this.internalValue.set(value);
914
+ const inputEl = this.inputRef()?.nativeElement;
915
+ if (inputEl) {
916
+ inputEl.value = value;
917
+ }
918
+ this.searchSubject.next(value);
919
+ }
920
+ /** Clears the search input. */
921
+ clear() {
922
+ this.clearSearch();
923
+ }
924
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDropdownSearch, deps: [], target: i0.ɵɵFactoryTarget.Component });
925
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: ComDropdownSearch, isStandalone: true, selector: "com-dropdown-search", inputs: { placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, debounceMs: { classPropertyName: "debounceMs", publicName: "debounceMs", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { searchChange: "searchChange", keyNav: "keyNav" }, host: { properties: { "attr.comDropdownSearch": "true" }, classAttribute: "com-dropdown-search-host block" }, viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["searchInput"], descendants: true, isSignal: true }], exportAs: ["comDropdownSearch"], ngImport: i0, template: `
926
+ <div class="relative flex items-center">
927
+ <!-- Search icon -->
928
+ <svg
929
+ class="pointer-events-none absolute left-3 h-4 w-4 text-placeholder"
930
+ viewBox="0 0 24 24"
931
+ fill="none"
932
+ stroke="currentColor"
933
+ stroke-width="2"
934
+ stroke-linecap="round"
935
+ stroke-linejoin="round"
936
+ >
937
+ <circle cx="11" cy="11" r="8" />
938
+ <line x1="21" y1="21" x2="16.65" y2="16.65" />
939
+ </svg>
940
+
941
+ <!-- Input -->
942
+ <input
943
+ #searchInput
944
+ type="text"
945
+ [class]="searchClasses()"
946
+ [placeholder]="placeholder()"
947
+ [disabled]="disabled()"
948
+ [attr.aria-label]="ariaLabel()"
949
+ [attr.autocomplete]="'off'"
950
+ [attr.autocapitalize]="'off'"
951
+ [attr.autocorrect]="'off'"
952
+ [attr.spellcheck]="'false'"
953
+ (input)="onInput($event)"
954
+ (keydown)="onKeydown($event)"
955
+ />
956
+
957
+ <!-- Clear button -->
958
+ @if (showClear()) {
959
+ <button
960
+ type="button"
961
+ class="absolute right-3 flex h-4 w-4 items-center justify-center rounded-interactive-sm text-placeholder opacity-70 hover:opacity-100 focus:outline-none focus:ring-1 focus:ring-ring"
962
+ [attr.aria-label]="'Clear search'"
963
+ (click)="clearSearch()"
964
+ >
965
+ <svg
966
+ class="h-3 w-3"
967
+ viewBox="0 0 24 24"
968
+ fill="none"
969
+ stroke="currentColor"
970
+ stroke-width="2"
971
+ stroke-linecap="round"
972
+ stroke-linejoin="round"
973
+ >
974
+ <line x1="18" y1="6" x2="6" y2="18" />
975
+ <line x1="6" y1="6" x2="18" y2="18" />
976
+ </svg>
977
+ </button>
978
+ }
979
+ </div>
980
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
981
+ }
982
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDropdownSearch, decorators: [{
983
+ type: Component,
984
+ args: [{
985
+ selector: 'com-dropdown-search',
986
+ exportAs: 'comDropdownSearch',
987
+ template: `
988
+ <div class="relative flex items-center">
989
+ <!-- Search icon -->
990
+ <svg
991
+ class="pointer-events-none absolute left-3 h-4 w-4 text-placeholder"
992
+ viewBox="0 0 24 24"
993
+ fill="none"
994
+ stroke="currentColor"
995
+ stroke-width="2"
996
+ stroke-linecap="round"
997
+ stroke-linejoin="round"
998
+ >
999
+ <circle cx="11" cy="11" r="8" />
1000
+ <line x1="21" y1="21" x2="16.65" y2="16.65" />
1001
+ </svg>
1002
+
1003
+ <!-- Input -->
1004
+ <input
1005
+ #searchInput
1006
+ type="text"
1007
+ [class]="searchClasses()"
1008
+ [placeholder]="placeholder()"
1009
+ [disabled]="disabled()"
1010
+ [attr.aria-label]="ariaLabel()"
1011
+ [attr.autocomplete]="'off'"
1012
+ [attr.autocapitalize]="'off'"
1013
+ [attr.autocorrect]="'off'"
1014
+ [attr.spellcheck]="'false'"
1015
+ (input)="onInput($event)"
1016
+ (keydown)="onKeydown($event)"
1017
+ />
1018
+
1019
+ <!-- Clear button -->
1020
+ @if (showClear()) {
1021
+ <button
1022
+ type="button"
1023
+ class="absolute right-3 flex h-4 w-4 items-center justify-center rounded-interactive-sm text-placeholder opacity-70 hover:opacity-100 focus:outline-none focus:ring-1 focus:ring-ring"
1024
+ [attr.aria-label]="'Clear search'"
1025
+ (click)="clearSearch()"
1026
+ >
1027
+ <svg
1028
+ class="h-3 w-3"
1029
+ viewBox="0 0 24 24"
1030
+ fill="none"
1031
+ stroke="currentColor"
1032
+ stroke-width="2"
1033
+ stroke-linecap="round"
1034
+ stroke-linejoin="round"
1035
+ >
1036
+ <line x1="18" y1="6" x2="6" y2="18" />
1037
+ <line x1="6" y1="6" x2="18" y2="18" />
1038
+ </svg>
1039
+ </button>
1040
+ }
1041
+ </div>
1042
+ `,
1043
+ changeDetection: ChangeDetectionStrategy.OnPush,
1044
+ host: {
1045
+ class: 'com-dropdown-search-host block',
1046
+ '[attr.comDropdownSearch]': 'true',
1047
+ },
1048
+ }]
1049
+ }], propDecorators: { inputRef: [{ type: i0.ViewChild, args: ['searchInput', { isSignal: true }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], debounceMs: [{ type: i0.Input, args: [{ isSignal: true, alias: "debounceMs", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], searchChange: [{ type: i0.Output, args: ["searchChange"] }], keyNav: [{ type: i0.Output, args: ["keyNav"] }] } });
1050
+
1051
+ /**
1052
+ * Tag component for displaying selected items in multi-select mode.
1053
+ * Includes a remove button for deselection.
1054
+ *
1055
+ * @example
1056
+ * ```html
1057
+ * <com-dropdown-tag
1058
+ * [value]="user"
1059
+ * [displayText]="user.name"
1060
+ * (remove)="onRemove(user)"
1061
+ * />
1062
+ * ```
1063
+ *
1064
+ * @tokens `--color-muted`, `--color-muted-foreground`, `--color-muted-hover`,
1065
+ * `--color-primary-subtle`, `--color-primary-subtle-foreground`,
1066
+ * `--color-ring`, `--radius-tag`, `--radius-interactive-sm`
1067
+ */
1068
+ class ComDropdownTag {
1069
+ /** The value this tag represents. */
1070
+ value = input.required(...(ngDevMode ? [{ debugName: "value" }] : []));
1071
+ /** Display text for this tag. */
1072
+ displayText = input('', ...(ngDevMode ? [{ debugName: "displayText" }] : []));
1073
+ /** Index of this tag in the list. */
1074
+ index = input(0, ...(ngDevMode ? [{ debugName: "index" }] : []));
1075
+ /** Whether the tag (and its remove button) is disabled. */
1076
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
1077
+ /** Size variant for styling. */
1078
+ size = input('default', ...(ngDevMode ? [{ debugName: "size" }] : []));
1079
+ /** Tag variant for styling. */
1080
+ variant = input('primary', ...(ngDevMode ? [{ debugName: "variant" }] : []));
1081
+ /** Additional CSS classes for the tag. */
1082
+ userClass = input('', { ...(ngDevMode ? { debugName: "userClass" } : {}), alias: 'class' });
1083
+ /** Custom template for rendering the tag. */
1084
+ tagTemplate = input(null, ...(ngDevMode ? [{ debugName: "tagTemplate" }] : []));
1085
+ /** Emitted when the remove button is clicked. */
1086
+ remove = output();
1087
+ /** Computed CSS classes for the tag. */
1088
+ tagClasses = computed(() => {
1089
+ const baseClasses = dropdownTagVariants({
1090
+ size: this.size(),
1091
+ variant: this.variant(),
1092
+ });
1093
+ return mergeClasses(baseClasses, this.userClass());
1094
+ }, ...(ngDevMode ? [{ debugName: "tagClasses" }] : []));
1095
+ /** Computed CSS classes for the remove button. */
1096
+ removeClasses = computed(() => {
1097
+ return dropdownTagRemoveVariants({ size: this.size() });
1098
+ }, ...(ngDevMode ? [{ debugName: "removeClasses" }] : []));
1099
+ /** Template context for custom tag templates. */
1100
+ templateContext = computed(() => ({
1101
+ $implicit: this.value(),
1102
+ index: this.index(),
1103
+ remove: () => this.emitRemove(),
1104
+ }), ...(ngDevMode ? [{ debugName: "templateContext" }] : []));
1105
+ onRemove(event) {
1106
+ event.preventDefault();
1107
+ event.stopPropagation();
1108
+ if (!this.disabled()) {
1109
+ this.emitRemove();
1110
+ }
1111
+ }
1112
+ emitRemove() {
1113
+ this.remove.emit(this.value());
1114
+ }
1115
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDropdownTag, deps: [], target: i0.ɵɵFactoryTarget.Component });
1116
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: ComDropdownTag, isStandalone: true, selector: "com-dropdown-tag", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, displayText: { classPropertyName: "displayText", publicName: "displayText", isSignal: true, isRequired: false, transformFunction: null }, index: { classPropertyName: "index", publicName: "index", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, tagTemplate: { classPropertyName: "tagTemplate", publicName: "tagTemplate", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { remove: "remove" }, host: { classAttribute: "com-dropdown-tag-host inline-flex" }, exportAs: ["comDropdownTag"], ngImport: i0, template: `
1117
+ @if (tagTemplate()) {
1118
+ <ng-container
1119
+ [ngTemplateOutlet]="tagTemplate()!"
1120
+ [ngTemplateOutletContext]="templateContext()"
1121
+ />
1122
+ } @else {
1123
+ <span [class]="tagClasses()">
1124
+ <span class="truncate">{{ displayText() }}</span>
1125
+ @if (!disabled()) {
1126
+ <button
1127
+ type="button"
1128
+ [class]="removeClasses()"
1129
+ [attr.aria-label]="'Remove ' + displayText()"
1130
+ (click)="onRemove($event)"
1131
+ (keydown.enter)="onRemove($event)"
1132
+ (keydown.space)="onRemove($event)"
1133
+ >
1134
+ <svg
1135
+ viewBox="0 0 24 24"
1136
+ fill="none"
1137
+ stroke="currentColor"
1138
+ stroke-width="2"
1139
+ stroke-linecap="round"
1140
+ stroke-linejoin="round"
1141
+ class="h-full w-full"
1142
+ >
1143
+ <line x1="18" y1="6" x2="6" y2="18" />
1144
+ <line x1="6" y1="6" x2="18" y2="18" />
1145
+ </svg>
1146
+ </button>
1147
+ }
1148
+ </span>
1149
+ }
1150
+ `, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1151
+ }
1152
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDropdownTag, decorators: [{
1153
+ type: Component,
1154
+ args: [{
1155
+ selector: 'com-dropdown-tag',
1156
+ exportAs: 'comDropdownTag',
1157
+ template: `
1158
+ @if (tagTemplate()) {
1159
+ <ng-container
1160
+ [ngTemplateOutlet]="tagTemplate()!"
1161
+ [ngTemplateOutletContext]="templateContext()"
1162
+ />
1163
+ } @else {
1164
+ <span [class]="tagClasses()">
1165
+ <span class="truncate">{{ displayText() }}</span>
1166
+ @if (!disabled()) {
1167
+ <button
1168
+ type="button"
1169
+ [class]="removeClasses()"
1170
+ [attr.aria-label]="'Remove ' + displayText()"
1171
+ (click)="onRemove($event)"
1172
+ (keydown.enter)="onRemove($event)"
1173
+ (keydown.space)="onRemove($event)"
1174
+ >
1175
+ <svg
1176
+ viewBox="0 0 24 24"
1177
+ fill="none"
1178
+ stroke="currentColor"
1179
+ stroke-width="2"
1180
+ stroke-linecap="round"
1181
+ stroke-linejoin="round"
1182
+ class="h-full w-full"
1183
+ >
1184
+ <line x1="18" y1="6" x2="6" y2="18" />
1185
+ <line x1="6" y1="6" x2="18" y2="18" />
1186
+ </svg>
1187
+ </button>
1188
+ }
1189
+ </span>
1190
+ }
1191
+ `,
1192
+ imports: [NgTemplateOutlet],
1193
+ changeDetection: ChangeDetectionStrategy.OnPush,
1194
+ host: {
1195
+ class: 'com-dropdown-tag-host inline-flex',
1196
+ },
1197
+ }]
1198
+ }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }], displayText: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayText", required: false }] }], index: [{ type: i0.Input, args: [{ isSignal: true, alias: "index", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], tagTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "tagTemplate", required: false }] }], remove: [{ type: i0.Output, args: ["remove"] }] } });
1199
+
1200
+ /**
1201
+ * Group header component for categorized dropdown options.
1202
+ *
1203
+ * @example
1204
+ * ```html
1205
+ * <com-dropdown-group
1206
+ * [label]="'Fruits'"
1207
+ * [count]="5"
1208
+ * />
1209
+ * ```
1210
+ *
1211
+ * @tokens `--color-muted-foreground`
1212
+ */
1213
+ class ComDropdownGroup {
1214
+ /** The group label/key. */
1215
+ label = input.required(...(ngDevMode ? [{ debugName: "label" }] : []));
1216
+ /** The number of options in this group. */
1217
+ count = input(0, ...(ngDevMode ? [{ debugName: "count" }] : []));
1218
+ /** Whether the group is expanded (for collapsible groups). */
1219
+ expanded = input(true, ...(ngDevMode ? [{ debugName: "expanded" }] : []));
1220
+ /** Whether to show the count badge. */
1221
+ showCount = input(false, ...(ngDevMode ? [{ debugName: "showCount" }] : []));
1222
+ /** Size variant for styling. */
1223
+ size = input('default', ...(ngDevMode ? [{ debugName: "size" }] : []));
1224
+ /** Additional CSS classes for the group header. */
1225
+ userClass = input('', { ...(ngDevMode ? { debugName: "userClass" } : {}), alias: 'class' });
1226
+ /** Custom template for rendering the group header. */
1227
+ groupTemplate = input(null, ...(ngDevMode ? [{ debugName: "groupTemplate" }] : []));
1228
+ /** Computed CSS classes for the group header. */
1229
+ groupClasses = computed(() => {
1230
+ const baseClasses = dropdownGroupVariants({ size: this.size() });
1231
+ return mergeClasses(baseClasses, this.userClass());
1232
+ }, ...(ngDevMode ? [{ debugName: "groupClasses" }] : []));
1233
+ /** Template context for custom group templates. */
1234
+ templateContext = computed(() => ({
1235
+ $implicit: this.label(),
1236
+ expanded: this.expanded(),
1237
+ count: this.count(),
1238
+ }), ...(ngDevMode ? [{ debugName: "templateContext" }] : []));
1239
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDropdownGroup, deps: [], target: i0.ɵɵFactoryTarget.Component });
1240
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: ComDropdownGroup, isStandalone: true, selector: "com-dropdown-group", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, count: { classPropertyName: "count", publicName: "count", isSignal: true, isRequired: false, transformFunction: null }, expanded: { classPropertyName: "expanded", publicName: "expanded", isSignal: true, isRequired: false, transformFunction: null }, showCount: { classPropertyName: "showCount", publicName: "showCount", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, groupTemplate: { classPropertyName: "groupTemplate", publicName: "groupTemplate", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "presentation" }, classAttribute: "com-dropdown-group-host block" }, exportAs: ["comDropdownGroup"], ngImport: i0, template: `
1241
+ @if (groupTemplate()) {
1242
+ <ng-container
1243
+ [ngTemplateOutlet]="groupTemplate()!"
1244
+ [ngTemplateOutletContext]="templateContext()"
1245
+ />
1246
+ } @else {
1247
+ <div [class]="groupClasses()">
1248
+ <span>{{ label() }}</span>
1249
+ @if (showCount()) {
1250
+ <span class="ml-auto text-xs text-muted-foreground">
1251
+ ({{ count() }})
1252
+ </span>
1253
+ }
1254
+ </div>
1255
+ }
1256
+ `, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1257
+ }
1258
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDropdownGroup, decorators: [{
1259
+ type: Component,
1260
+ args: [{
1261
+ selector: 'com-dropdown-group',
1262
+ exportAs: 'comDropdownGroup',
1263
+ template: `
1264
+ @if (groupTemplate()) {
1265
+ <ng-container
1266
+ [ngTemplateOutlet]="groupTemplate()!"
1267
+ [ngTemplateOutletContext]="templateContext()"
1268
+ />
1269
+ } @else {
1270
+ <div [class]="groupClasses()">
1271
+ <span>{{ label() }}</span>
1272
+ @if (showCount()) {
1273
+ <span class="ml-auto text-xs text-muted-foreground">
1274
+ ({{ count() }})
1275
+ </span>
1276
+ }
1277
+ </div>
1278
+ }
1279
+ `,
1280
+ imports: [NgTemplateOutlet],
1281
+ changeDetection: ChangeDetectionStrategy.OnPush,
1282
+ host: {
1283
+ class: 'com-dropdown-group-host block',
1284
+ role: 'presentation',
1285
+ },
1286
+ }]
1287
+ }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], count: [{ type: i0.Input, args: [{ isSignal: true, alias: "count", required: false }] }], expanded: [{ type: i0.Input, args: [{ isSignal: true, alias: "expanded", required: false }] }], showCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "showCount", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], groupTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "groupTemplate", required: false }] }] } });
1288
+
1289
+ /**
1290
+ * Directive to mark a template as the custom option template.
1291
+ *
1292
+ * @example
1293
+ * ```html
1294
+ * <com-dropdown [options]="users()">
1295
+ * <ng-template comDropdownOption let-user let-selected="selected">
1296
+ * <div class="flex items-center gap-2">
1297
+ * <span>{{ user.name }}</span>
1298
+ * @if (selected) {
1299
+ * <svg class="h-4 w-4"><!-- check --></svg>
1300
+ * }
1301
+ * </div>
1302
+ * </ng-template>
1303
+ * </com-dropdown>
1304
+ * ```
1305
+ */
1306
+ class ComDropdownOptionTpl {
1307
+ /** Reference to the template. */
1308
+ templateRef = inject(TemplateRef);
1309
+ /**
1310
+ * Static type guard for template type checking.
1311
+ * Enables type-safe access to context properties in templates.
1312
+ */
1313
+ static ngTemplateContextGuard(_dir, ctx) {
1314
+ return true;
1315
+ }
1316
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDropdownOptionTpl, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1317
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.0", type: ComDropdownOptionTpl, isStandalone: true, selector: "ng-template[comDropdownOption]", ngImport: i0 });
1318
+ }
1319
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDropdownOptionTpl, decorators: [{
1320
+ type: Directive,
1321
+ args: [{
1322
+ selector: 'ng-template[comDropdownOption]',
1323
+ }]
1324
+ }] });
1325
+
1326
+ /**
1327
+ * Directive to mark a template as the custom selected value template.
1328
+ *
1329
+ * @example
1330
+ * ```html
1331
+ * <com-dropdown [options]="users()">
1332
+ * <ng-template comDropdownSelected let-user>
1333
+ * @if (user) {
1334
+ * <div class="flex items-center gap-2">
1335
+ * <img [src]="user.avatar" class="h-5 w-5 rounded-full" />
1336
+ * {{ user.name }}
1337
+ * </div>
1338
+ * } @else {
1339
+ * <span class="text-input-placeholder">Select user...</span>
1340
+ * }
1341
+ * </ng-template>
1342
+ * </com-dropdown>
1343
+ * ```
1344
+ */
1345
+ class ComDropdownSelectedTpl {
1346
+ /** Reference to the template. */
1347
+ templateRef = inject(TemplateRef);
1348
+ /**
1349
+ * Static type guard for template type checking.
1350
+ * Enables type-safe access to context properties in templates.
1351
+ */
1352
+ static ngTemplateContextGuard(_dir, ctx) {
1353
+ return true;
1354
+ }
1355
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDropdownSelectedTpl, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1356
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.0", type: ComDropdownSelectedTpl, isStandalone: true, selector: "ng-template[comDropdownSelected]", ngImport: i0 });
1357
+ }
1358
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDropdownSelectedTpl, decorators: [{
1359
+ type: Directive,
1360
+ args: [{
1361
+ selector: 'ng-template[comDropdownSelected]',
1362
+ }]
1363
+ }] });
1364
+
1365
+ /**
1366
+ * Directive to mark a template as the custom empty state template.
1367
+ *
1368
+ * @example
1369
+ * ```html
1370
+ * <com-dropdown [options]="users()" [searchable]="true">
1371
+ * <ng-template comDropdownEmpty let-query>
1372
+ * <div class="flex flex-col items-center gap-2 py-6">
1373
+ * <svg class="h-8 w-8 text-muted-foreground"><!-- search icon --></svg>
1374
+ * <span>No results for "{{ query }}"</span>
1375
+ * </div>
1376
+ * </ng-template>
1377
+ * </com-dropdown>
1378
+ * ```
1379
+ */
1380
+ class ComDropdownEmptyTpl {
1381
+ /** Reference to the template. */
1382
+ templateRef = inject(TemplateRef);
1383
+ /**
1384
+ * Static type guard for template type checking.
1385
+ * Enables type-safe access to context properties in templates.
1386
+ */
1387
+ static ngTemplateContextGuard(_dir, ctx) {
1388
+ return true;
1389
+ }
1390
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDropdownEmptyTpl, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1391
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.0", type: ComDropdownEmptyTpl, isStandalone: true, selector: "ng-template[comDropdownEmpty]", ngImport: i0 });
1392
+ }
1393
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDropdownEmptyTpl, decorators: [{
1394
+ type: Directive,
1395
+ args: [{
1396
+ selector: 'ng-template[comDropdownEmpty]',
1397
+ }]
1398
+ }] });
1399
+
1400
+ /**
1401
+ * Directive to mark a template as the custom group header template.
1402
+ *
1403
+ * @example
1404
+ * ```html
1405
+ * <com-dropdown [options]="users()" [groupBy]="groupByDepartment">
1406
+ * <ng-template comDropdownGroup let-group let-count="count">
1407
+ * <div class="flex items-center justify-between">
1408
+ * <span class="font-semibold">{{ group }}</span>
1409
+ * <span class="text-xs text-muted-foreground">({{ count }})</span>
1410
+ * </div>
1411
+ * </ng-template>
1412
+ * </com-dropdown>
1413
+ * ```
1414
+ */
1415
+ class ComDropdownGroupTpl {
1416
+ /** Reference to the template. */
1417
+ templateRef = inject(TemplateRef);
1418
+ /**
1419
+ * Static type guard for template type checking.
1420
+ * Enables type-safe access to context properties in templates.
1421
+ */
1422
+ static ngTemplateContextGuard(_dir, ctx) {
1423
+ return true;
1424
+ }
1425
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDropdownGroupTpl, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1426
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.0", type: ComDropdownGroupTpl, isStandalone: true, selector: "ng-template[comDropdownGroup]", ngImport: i0 });
1427
+ }
1428
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDropdownGroupTpl, decorators: [{
1429
+ type: Directive,
1430
+ args: [{
1431
+ selector: 'ng-template[comDropdownGroup]',
1432
+ }]
1433
+ }] });
1434
+
1435
+ /**
1436
+ * Directive to mark a template as the custom tag template (multi-select mode).
1437
+ *
1438
+ * @example
1439
+ * ```html
1440
+ * <com-dropdown [options]="users()" [multiple]="true">
1441
+ * <ng-template comDropdownTag let-user let-remove="remove">
1442
+ * <div class="flex items-center gap-1 rounded bg-primary-subtle px-2 py-0.5 text-primary-subtle-foreground">
1443
+ * <img [src]="user.avatar" class="h-4 w-4 rounded-full" />
1444
+ * <span>{{ user.name }}</span>
1445
+ * <button type="button" (click)="remove()" class="ml-1">
1446
+ * <svg class="h-3 w-3"><!-- x icon --></svg>
1447
+ * </button>
1448
+ * </div>
1449
+ * </ng-template>
1450
+ * </com-dropdown>
1451
+ * ```
1452
+ */
1453
+ class ComDropdownTagTpl {
1454
+ /** Reference to the template. */
1455
+ templateRef = inject(TemplateRef);
1456
+ /**
1457
+ * Static type guard for template type checking.
1458
+ * Enables type-safe access to context properties in templates.
1459
+ */
1460
+ static ngTemplateContextGuard(_dir, ctx) {
1461
+ return true;
1462
+ }
1463
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDropdownTagTpl, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1464
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.0", type: ComDropdownTagTpl, isStandalone: true, selector: "ng-template[comDropdownTag]", ngImport: i0 });
1465
+ }
1466
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDropdownTagTpl, decorators: [{
1467
+ type: Directive,
1468
+ args: [{
1469
+ selector: 'ng-template[comDropdownTag]',
1470
+ }]
1471
+ }] });
1472
+
1473
+ /**
1474
+ * Default compare function for primitive values.
1475
+ * @param a First value.
1476
+ * @param b Second value.
1477
+ * @returns Whether the values are equal.
1478
+ */
1479
+ function defaultCompareWith(a, b) {
1480
+ return a === b;
1481
+ }
1482
+ /**
1483
+ * Default display function for converting values to strings.
1484
+ * @param value The value to display.
1485
+ * @returns The string representation.
1486
+ */
1487
+ function defaultDisplayWith(value) {
1488
+ if (value === null || value === undefined) {
1489
+ return '';
1490
+ }
1491
+ return String(value);
1492
+ }
1493
+ /**
1494
+ * Default filter function for search.
1495
+ * @param option The option to test.
1496
+ * @param query The search query.
1497
+ * @param displayWith The display function to get the searchable text.
1498
+ * @returns Whether the option matches the query.
1499
+ */
1500
+ function defaultFilterWith(option, query, displayWith) {
1501
+ const text = displayWith(option).toLowerCase();
1502
+ return text.includes(query.toLowerCase());
1503
+ }
1504
+ /**
1505
+ * Unique ID counter for generating option IDs.
1506
+ */
1507
+ let uniqueIdCounter = 0;
1508
+ /**
1509
+ * Generates a unique ID for dropdown options.
1510
+ * @param prefix Optional prefix for the ID.
1511
+ * @returns A unique ID string.
1512
+ */
1513
+ function generateDropdownId(prefix = 'com-dropdown') {
1514
+ return `${prefix}-${uniqueIdCounter++}`;
1515
+ }
1516
+
1517
+ /** Default position for the dropdown panel. */
1518
+ const DEFAULT_POSITIONS = [
1519
+ // Below trigger, aligned start
1520
+ { originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 4 },
1521
+ // Above trigger, aligned start
1522
+ { originX: 'start', originY: 'top', overlayX: 'start', overlayY: 'bottom', offsetY: -4 },
1523
+ // Below trigger, aligned end
1524
+ { originX: 'end', originY: 'bottom', overlayX: 'end', overlayY: 'top', offsetY: 4 },
1525
+ // Above trigger, aligned end
1526
+ { originX: 'end', originY: 'top', overlayX: 'end', overlayY: 'bottom', offsetY: -4 },
1527
+ ];
1528
+ /** Threshold for enabling virtual scrolling. */
1529
+ const VIRTUAL_SCROLL_THRESHOLD = 50;
1530
+ /**
1531
+ * Reusable dropdown/select component with full accessibility support.
1532
+ * Implements ControlValueAccessor for Reactive Forms integration.
1533
+ *
1534
+ * @tokens `--color-input-background`, `--color-input-foreground`, `--color-input-border`,
1535
+ * `--color-input-placeholder`, `--color-ring`, `--color-muted`, `--color-muted-foreground`,
1536
+ * `--color-popover`, `--color-popover-foreground`, `--color-border-subtle`,
1537
+ * `--color-primary`, `--color-primary-subtle`, `--color-primary-subtle-foreground`,
1538
+ * `--color-warn`, `--color-success`, `--color-disabled`, `--color-disabled-foreground`,
1539
+ * `--color-placeholder`
1540
+ *
1541
+ * @example
1542
+ * ```html
1543
+ * <com-dropdown
1544
+ * [options]="users()"
1545
+ * [compareWith]="compareById"
1546
+ * formControlName="assignee"
1547
+ * placeholder="Select user..."
1548
+ * [searchable]="true"
1549
+ * >
1550
+ * <ng-template comDropdownOption let-user let-selected="selected">
1551
+ * <span>{{ user.name }}</span>
1552
+ * </ng-template>
1553
+ * </com-dropdown>
1554
+ * ```
1555
+ */
1556
+ class ComDropdown {
1557
+ elementRef = inject(ElementRef);
1558
+ destroyRef = inject(DestroyRef);
1559
+ overlay = inject(Overlay);
1560
+ viewContainerRef = inject(ViewContainerRef);
1561
+ liveAnnouncer = inject(LiveAnnouncer);
1562
+ document = inject(DOCUMENT);
1563
+ /** Optional NgControl for form integration. */
1564
+ ngControl = inject(NgControl, { optional: true, self: true });
1565
+ /** Reference to the trigger button element. */
1566
+ triggerRef = viewChild.required('triggerElement');
1567
+ /** Reference to the panel template. */
1568
+ panelTemplateRef = viewChild.required('panelTemplate');
1569
+ /** Content query for custom option template. */
1570
+ optionTemplate = contentChild((ComDropdownOptionTpl), ...(ngDevMode ? [{ debugName: "optionTemplate" }] : []));
1571
+ /** Content query for custom selected template. */
1572
+ selectedTemplate = contentChild((ComDropdownSelectedTpl), ...(ngDevMode ? [{ debugName: "selectedTemplate" }] : []));
1573
+ /** Content query for custom empty template. */
1574
+ emptyTemplate = contentChild(ComDropdownEmptyTpl, ...(ngDevMode ? [{ debugName: "emptyTemplate" }] : []));
1575
+ /** Content query for custom group template. */
1576
+ groupTemplate = contentChild(ComDropdownGroupTpl, ...(ngDevMode ? [{ debugName: "groupTemplate" }] : []));
1577
+ /** Content query for custom tag template. */
1578
+ tagTemplate = contentChild((ComDropdownTagTpl), ...(ngDevMode ? [{ debugName: "tagTemplate" }] : []));
1579
+ /** Overlay reference. */
1580
+ overlayRef = null;
1581
+ /** Unique ID for the dropdown. */
1582
+ dropdownId = generateDropdownId();
1583
+ // ============ INPUTS ============
1584
+ /** Array of options to display. */
1585
+ options = input([], ...(ngDevMode ? [{ debugName: "options" }] : []));
1586
+ /** Current value (single or array for multiple). */
1587
+ value = input(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
1588
+ /** Placeholder text when no value is selected. */
1589
+ placeholder = input('Select...', ...(ngDevMode ? [{ debugName: "placeholder" }] : []));
1590
+ /** Enable multi-select mode. */
1591
+ multiple = input(false, ...(ngDevMode ? [{ debugName: "multiple" }] : []));
1592
+ /** Enable search/filter input. */
1593
+ searchable = input(false, ...(ngDevMode ? [{ debugName: "searchable" }] : []));
1594
+ /** Search input placeholder. */
1595
+ searchPlaceholder = input('Search...', ...(ngDevMode ? [{ debugName: "searchPlaceholder" }] : []));
1596
+ /** Disable the dropdown. */
1597
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
1598
+ /** Mark as required. */
1599
+ required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : []));
1600
+ /** Show clear button. */
1601
+ clearable = input(false, ...(ngDevMode ? [{ debugName: "clearable" }] : []));
1602
+ /** Custom equality function for comparing values. */
1603
+ compareWith = input(defaultCompareWith, ...(ngDevMode ? [{ debugName: "compareWith" }] : []));
1604
+ /** Function to get display text from a value. */
1605
+ displayWith = input(defaultDisplayWith, ...(ngDevMode ? [{ debugName: "displayWith" }] : []));
1606
+ /** Custom filter function for search. */
1607
+ filterWith = input(null, ...(ngDevMode ? [{ debugName: "filterWith" }] : []));
1608
+ /** Function to group options by a key. */
1609
+ groupBy = input(null, ...(ngDevMode ? [{ debugName: "groupBy" }] : []));
1610
+ /** CVA variant for trigger styling. */
1611
+ variant = input('default', ...(ngDevMode ? [{ debugName: "variant" }] : []));
1612
+ /** Size variant. */
1613
+ size = input('default', ...(ngDevMode ? [{ debugName: "size" }] : []));
1614
+ /** Validation state. */
1615
+ state = input('default', ...(ngDevMode ? [{ debugName: "state" }] : []));
1616
+ /** Additional CSS classes for the trigger. */
1617
+ userClass = input('', { ...(ngDevMode ? { debugName: "userClass" } : {}), alias: 'class' });
1618
+ /** Additional CSS classes for the panel. */
1619
+ panelClass = input('', ...(ngDevMode ? [{ debugName: "panelClass" }] : []));
1620
+ /** Maximum height of the panel. */
1621
+ maxHeight = input('256px', ...(ngDevMode ? [{ debugName: "maxHeight" }] : []));
1622
+ /** Panel width strategy. */
1623
+ panelWidth = input('trigger', ...(ngDevMode ? [{ debugName: "panelWidth" }] : []));
1624
+ /** Debounce time for search (ms). */
1625
+ searchDebounceMs = input(300, ...(ngDevMode ? [{ debugName: "searchDebounceMs" }] : []));
1626
+ /** Virtual scroll threshold. */
1627
+ virtualScrollThreshold = input(VIRTUAL_SCROLL_THRESHOLD, ...(ngDevMode ? [{ debugName: "virtualScrollThreshold" }] : []));
1628
+ /** Maximum number of tags to display in multi-select mode. Set to null for no limit. */
1629
+ maxVisibleTags = input(2, ...(ngDevMode ? [{ debugName: "maxVisibleTags" }] : []));
1630
+ // ============ OUTPUTS ============
1631
+ /** Emitted when the value changes. */
1632
+ valueChange = output();
1633
+ /** Emitted when search query changes. */
1634
+ searchChange = output();
1635
+ /** Emitted when panel opens. */
1636
+ opened = output();
1637
+ /** Emitted when panel closes. */
1638
+ closed = output();
1639
+ // ============ INTERNAL STATE ============
1640
+ /** Whether the panel is open. */
1641
+ isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : []));
1642
+ /** Current search query. */
1643
+ searchQuery = signal('', ...(ngDevMode ? [{ debugName: "searchQuery" }] : []));
1644
+ /** Currently active (keyboard focused) option ID. */
1645
+ activeOptionId = signal(null, ...(ngDevMode ? [{ debugName: "activeOptionId" }] : []));
1646
+ /** Internal value state (managed by CVA or input). */
1647
+ internalValue = linkedSignal(() => this.value() ?? null, ...(ngDevMode ? [{ debugName: "internalValue" }] : []));
1648
+ /** Live announcements for screen readers. */
1649
+ liveAnnouncement = signal('', ...(ngDevMode ? [{ debugName: "liveAnnouncement" }] : []));
1650
+ // ============ COMPUTED STATE ============
1651
+ /** Trigger element ID. */
1652
+ triggerId = computed(() => `${this.dropdownId}-trigger`, ...(ngDevMode ? [{ debugName: "triggerId" }] : []));
1653
+ /** Panel element ID. */
1654
+ panelId = computed(() => `${this.dropdownId}-panel`, ...(ngDevMode ? [{ debugName: "panelId" }] : []));
1655
+ /** Currently active descendant ID (for ARIA). */
1656
+ activeDescendant = computed(() => {
1657
+ return this.isOpen() ? this.activeOptionId() : null;
1658
+ }, ...(ngDevMode ? [{ debugName: "activeDescendant" }] : []));
1659
+ /** Whether the dropdown has a value. */
1660
+ hasValue = computed(() => {
1661
+ const val = this.internalValue();
1662
+ if (this.multiple()) {
1663
+ return Array.isArray(val) && val.length > 0;
1664
+ }
1665
+ return val !== null && val !== undefined;
1666
+ }, ...(ngDevMode ? [{ debugName: "hasValue" }] : []));
1667
+ /** Selected value (single mode). */
1668
+ selectedValue = computed(() => {
1669
+ const val = this.internalValue();
1670
+ if (this.multiple() || Array.isArray(val)) {
1671
+ return null;
1672
+ }
1673
+ return val;
1674
+ }, ...(ngDevMode ? [{ debugName: "selectedValue" }] : []));
1675
+ /** Selected values (multiple mode). */
1676
+ selectedValues = computed(() => {
1677
+ const val = this.internalValue();
1678
+ if (!this.multiple()) {
1679
+ return [];
1680
+ }
1681
+ return Array.isArray(val) ? val : val !== null ? [val] : [];
1682
+ }, ...(ngDevMode ? [{ debugName: "selectedValues" }] : []));
1683
+ /** Tags visible in the trigger (limited by maxVisibleTags). */
1684
+ visibleTags = computed(() => {
1685
+ const all = this.selectedValues();
1686
+ const max = this.maxVisibleTags();
1687
+ if (max === null || all.length <= max) {
1688
+ return all;
1689
+ }
1690
+ return all.slice(0, max);
1691
+ }, ...(ngDevMode ? [{ debugName: "visibleTags" }] : []));
1692
+ /** Count of hidden tags (for +N badge). */
1693
+ hiddenTagsCount = computed(() => {
1694
+ const all = this.selectedValues();
1695
+ const max = this.maxVisibleTags();
1696
+ if (max === null || all.length <= max) {
1697
+ return 0;
1698
+ }
1699
+ return all.length - max;
1700
+ }, ...(ngDevMode ? [{ debugName: "hiddenTagsCount" }] : []));
1701
+ /** Processed options with display text and IDs. */
1702
+ processedOptions = computed(() => {
1703
+ const opts = this.options();
1704
+ const display = this.displayWith();
1705
+ return opts.map((opt, index) => ({
1706
+ value: opt,
1707
+ displayText: display(opt),
1708
+ disabled: false, // Could be extended with disabledWith input
1709
+ id: `${this.dropdownId}-option-${index}`,
1710
+ }));
1711
+ }, ...(ngDevMode ? [{ debugName: "processedOptions" }] : []));
1712
+ /** Filtered options based on search query. */
1713
+ filteredOptions = computed(() => {
1714
+ const opts = this.processedOptions();
1715
+ const query = this.searchQuery().trim();
1716
+ if (!query) {
1717
+ return opts;
1718
+ }
1719
+ const filterFn = this.filterWith();
1720
+ const display = this.displayWith();
1721
+ return opts.filter((opt) => {
1722
+ if (filterFn) {
1723
+ return filterFn(opt.value, query);
1724
+ }
1725
+ return defaultFilterWith(opt.value, query, display);
1726
+ });
1727
+ }, ...(ngDevMode ? [{ debugName: "filteredOptions" }] : []));
1728
+ /** Grouped options (when groupBy is provided). */
1729
+ groupedOptions = computed(() => {
1730
+ const groupFn = this.groupBy();
1731
+ if (!groupFn) {
1732
+ return [];
1733
+ }
1734
+ const opts = this.filteredOptions();
1735
+ const groups = new Map();
1736
+ for (const opt of opts) {
1737
+ const key = groupFn(opt.value);
1738
+ const existing = groups.get(key) || [];
1739
+ existing.push(opt);
1740
+ groups.set(key, existing);
1741
+ }
1742
+ return Array.from(groups.entries()).map(([key, options]) => ({
1743
+ key,
1744
+ options,
1745
+ expanded: true,
1746
+ }));
1747
+ }, ...(ngDevMode ? [{ debugName: "groupedOptions" }] : []));
1748
+ /** Whether virtual scrolling should be enabled. */
1749
+ virtualScrollEnabled = computed(() => {
1750
+ return this.filteredOptions().length > this.virtualScrollThreshold();
1751
+ }, ...(ngDevMode ? [{ debugName: "virtualScrollEnabled" }] : []));
1752
+ /** Context for selected template. */
1753
+ selectedContext = computed(() => ({
1754
+ $implicit: this.internalValue(),
1755
+ placeholder: this.placeholder(),
1756
+ multiple: this.multiple(),
1757
+ }), ...(ngDevMode ? [{ debugName: "selectedContext" }] : []));
1758
+ /** Context for empty template. */
1759
+ emptyContext = computed(() => ({
1760
+ $implicit: this.searchQuery(),
1761
+ }), ...(ngDevMode ? [{ debugName: "emptyContext" }] : []));
1762
+ /** Computed trigger classes. */
1763
+ triggerClasses = computed(() => {
1764
+ const baseClasses = dropdownTriggerVariants({
1765
+ variant: this.variant(),
1766
+ size: this.size(),
1767
+ state: this.state(),
1768
+ open: this.isOpen(),
1769
+ });
1770
+ return mergeClasses(baseClasses, this.userClass());
1771
+ }, ...(ngDevMode ? [{ debugName: "triggerClasses" }] : []));
1772
+ /** Computed panel classes. */
1773
+ panelClasses = computed(() => {
1774
+ return mergeClasses('w-full z-50 overflow-hidden rounded-overlay border border-border-subtle bg-popover text-popover-foreground shadow-lg outline-none', this.panelClass());
1775
+ }, ...(ngDevMode ? [{ debugName: "panelClasses" }] : []));
1776
+ /** Computed chevron classes. */
1777
+ chevronClasses = computed(() => {
1778
+ return dropdownChevronVariants({
1779
+ size: this.size(),
1780
+ open: this.isOpen(),
1781
+ });
1782
+ }, ...(ngDevMode ? [{ debugName: "chevronClasses" }] : []));
1783
+ /** Computed clear button classes. */
1784
+ clearClasses = computed(() => {
1785
+ return dropdownClearVariants({ size: this.size() });
1786
+ }, ...(ngDevMode ? [{ debugName: "clearClasses" }] : []));
1787
+ /** Computed overflow badge classes. */
1788
+ overflowBadgeClasses = computed(() => {
1789
+ return dropdownOverflowBadgeVariants({ size: this.size() });
1790
+ }, ...(ngDevMode ? [{ debugName: "overflowBadgeClasses" }] : []));
1791
+ // ============ CVA CALLBACKS ============
1792
+ onChange = () => { };
1793
+ onTouched = () => { };
1794
+ constructor() {
1795
+ // Wire up NgControl if present
1796
+ if (this.ngControl) {
1797
+ this.ngControl.valueAccessor = this;
1798
+ }
1799
+ }
1800
+ ngOnInit() {
1801
+ // Nothing special needed here since we use effects
1802
+ }
1803
+ // ============ CVA IMPLEMENTATION ============
1804
+ writeValue(value) {
1805
+ this.internalValue.set(value);
1806
+ }
1807
+ registerOnChange(fn) {
1808
+ this.onChange = fn;
1809
+ }
1810
+ registerOnTouched(fn) {
1811
+ this.onTouched = fn;
1812
+ }
1813
+ setDisabledState(isDisabled) {
1814
+ // Disabled state is handled via the disabled input
1815
+ // When using forms, the form control's disabled state takes precedence
1816
+ }
1817
+ // ============ PUBLIC METHODS ============
1818
+ /** Opens the dropdown panel. */
1819
+ open() {
1820
+ if (this.disabled() || this.isOpen()) {
1821
+ return;
1822
+ }
1823
+ this.createOverlay();
1824
+ this.isOpen.set(true);
1825
+ this.opened.emit();
1826
+ // Set initial active option
1827
+ const currentValue = this.internalValue();
1828
+ if (currentValue !== null && !this.multiple()) {
1829
+ const option = this.filteredOptions().find((opt) => this.compareWith()(opt.value, currentValue));
1830
+ if (option) {
1831
+ this.activeOptionId.set(option.id);
1832
+ }
1833
+ }
1834
+ else {
1835
+ const firstOption = this.filteredOptions()[0];
1836
+ if (firstOption) {
1837
+ this.activeOptionId.set(firstOption.id);
1838
+ }
1839
+ }
1840
+ // Announce opening
1841
+ this.announce(`${this.placeholder()} dropdown opened, ${this.filteredOptions().length} options available`);
1842
+ }
1843
+ /** Closes the dropdown panel. */
1844
+ close() {
1845
+ if (!this.isOpen()) {
1846
+ return;
1847
+ }
1848
+ this.destroyOverlay();
1849
+ this.isOpen.set(false);
1850
+ this.searchQuery.set('');
1851
+ this.activeOptionId.set(null);
1852
+ this.closed.emit();
1853
+ this.onTouched();
1854
+ }
1855
+ /** Toggles the dropdown panel. */
1856
+ toggle() {
1857
+ if (this.isOpen()) {
1858
+ this.close();
1859
+ }
1860
+ else {
1861
+ this.open();
1862
+ }
1863
+ }
1864
+ /** Clears the selection. */
1865
+ clear(event) {
1866
+ event?.preventDefault();
1867
+ event?.stopPropagation();
1868
+ const newValue = this.multiple() ? [] : null;
1869
+ this.updateValue(newValue);
1870
+ this.announce('Selection cleared');
1871
+ }
1872
+ /** Checks if a value is selected. */
1873
+ isSelected(value) {
1874
+ const current = this.internalValue();
1875
+ const compare = this.compareWith();
1876
+ if (this.multiple()) {
1877
+ const arr = Array.isArray(current) ? current : [];
1878
+ return arr.some((v) => compare(v, value));
1879
+ }
1880
+ return current !== null && compare(current, value);
1881
+ }
1882
+ /** Checks if an option ID is the active one. */
1883
+ isActive(optionId) {
1884
+ return this.activeOptionId() === optionId;
1885
+ }
1886
+ // ============ EVENT HANDLERS ============
1887
+ onTriggerKeydown(event) {
1888
+ switch (event.key) {
1889
+ case 'ArrowDown':
1890
+ case 'ArrowUp':
1891
+ event.preventDefault();
1892
+ if (!this.isOpen()) {
1893
+ this.open();
1894
+ }
1895
+ else {
1896
+ this.navigateOptions(event.key === 'ArrowDown' ? 1 : -1);
1897
+ }
1898
+ break;
1899
+ case 'Enter':
1900
+ case ' ':
1901
+ event.preventDefault();
1902
+ if (this.isOpen() && this.activeOptionId()) {
1903
+ this.selectActiveOption();
1904
+ }
1905
+ else {
1906
+ this.toggle();
1907
+ }
1908
+ break;
1909
+ case 'Escape':
1910
+ if (this.isOpen()) {
1911
+ event.preventDefault();
1912
+ this.close();
1913
+ this.triggerRef().nativeElement.focus();
1914
+ }
1915
+ break;
1916
+ case 'Home':
1917
+ if (this.isOpen()) {
1918
+ event.preventDefault();
1919
+ this.navigateToFirst();
1920
+ }
1921
+ break;
1922
+ case 'End':
1923
+ if (this.isOpen()) {
1924
+ event.preventDefault();
1925
+ this.navigateToLast();
1926
+ }
1927
+ break;
1928
+ case 'Tab':
1929
+ // Close panel when tabbing away from trigger
1930
+ if (this.isOpen()) {
1931
+ this.close();
1932
+ }
1933
+ // Don't prevent default - let Tab naturally move focus
1934
+ break;
1935
+ default:
1936
+ // Type-ahead search
1937
+ if (event.key.length === 1 && !event.ctrlKey && !event.metaKey) {
1938
+ this.typeAhead(event.key);
1939
+ }
1940
+ break;
1941
+ }
1942
+ }
1943
+ onPanelKeydown(event) {
1944
+ switch (event.key) {
1945
+ case 'ArrowDown':
1946
+ event.preventDefault();
1947
+ this.navigateOptions(1);
1948
+ break;
1949
+ case 'ArrowUp':
1950
+ event.preventDefault();
1951
+ this.navigateOptions(-1);
1952
+ break;
1953
+ case 'Enter':
1954
+ case ' ':
1955
+ event.preventDefault();
1956
+ this.selectActiveOption();
1957
+ break;
1958
+ case 'Escape':
1959
+ event.preventDefault();
1960
+ this.close();
1961
+ this.triggerRef().nativeElement.focus();
1962
+ break;
1963
+ case 'Tab':
1964
+ this.close();
1965
+ break;
1966
+ case 'Home':
1967
+ event.preventDefault();
1968
+ this.navigateToFirst();
1969
+ break;
1970
+ case 'End':
1971
+ event.preventDefault();
1972
+ this.navigateToLast();
1973
+ break;
1974
+ }
1975
+ }
1976
+ onSearchChange(query) {
1977
+ this.searchQuery.set(query);
1978
+ this.searchChange.emit(query);
1979
+ // Reset active option to first result
1980
+ const filtered = this.filteredOptions();
1981
+ const firstFiltered = filtered[0];
1982
+ if (firstFiltered) {
1983
+ this.activeOptionId.set(firstFiltered.id);
1984
+ }
1985
+ else {
1986
+ this.activeOptionId.set(null);
1987
+ }
1988
+ }
1989
+ onSearchKeyNav(event) {
1990
+ // Delegate to panel keydown handler
1991
+ this.onPanelKeydown(event);
1992
+ }
1993
+ onOptionHover(optionId) {
1994
+ this.activeOptionId.set(optionId);
1995
+ }
1996
+ selectOption(value) {
1997
+ if (this.multiple()) {
1998
+ this.toggleMultipleValue(value);
1999
+ }
2000
+ else {
2001
+ this.updateValue(value);
2002
+ this.close();
2003
+ this.triggerRef().nativeElement.focus();
2004
+ }
2005
+ }
2006
+ removeValue(value) {
2007
+ const current = this.selectedValues();
2008
+ const compare = this.compareWith();
2009
+ const newValues = current.filter((v) => !compare(v, value));
2010
+ this.updateValue(newValues.length > 0 ? newValues : []);
2011
+ this.announce(`${this.displayWith()(value)} removed`);
2012
+ }
2013
+ trackByValue(item, _index) {
2014
+ return item;
2015
+ }
2016
+ getGlobalIndex(groupKey, localIndex) {
2017
+ const groups = this.groupedOptions();
2018
+ let globalIndex = 0;
2019
+ for (const group of groups) {
2020
+ if (group.key === groupKey) {
2021
+ return globalIndex + localIndex;
2022
+ }
2023
+ globalIndex += group.options.length;
2024
+ }
2025
+ return localIndex;
2026
+ }
2027
+ // ============ PRIVATE METHODS ============
2028
+ createOverlay() {
2029
+ if (this.overlayRef) {
2030
+ return;
2031
+ }
2032
+ const hostEl = this.elementRef.nativeElement;
2033
+ const positionStrategy = this.overlay
2034
+ .position()
2035
+ .flexibleConnectedTo(hostEl)
2036
+ .withPositions(DEFAULT_POSITIONS)
2037
+ .withFlexibleDimensions(false)
2038
+ .withPush(true);
2039
+ const hostWidth = hostEl.getBoundingClientRect().width;
2040
+ this.overlayRef = this.overlay.create({
2041
+ positionStrategy,
2042
+ scrollStrategy: this.overlay.scrollStrategies.reposition(),
2043
+ ...this.getPanelWidthConfig(hostWidth),
2044
+ hasBackdrop: true,
2045
+ backdropClass: 'cdk-overlay-transparent-backdrop',
2046
+ });
2047
+ // Attach panel template
2048
+ const portal = new TemplatePortal(this.panelTemplateRef(), this.viewContainerRef);
2049
+ this.overlayRef.attach(portal);
2050
+ // Close on backdrop click
2051
+ this.overlayRef
2052
+ .backdropClick()
2053
+ .pipe(takeUntilDestroyed(this.destroyRef))
2054
+ .subscribe(() => this.close());
2055
+ // Close on outside click
2056
+ this.overlayRef
2057
+ .outsidePointerEvents()
2058
+ .pipe(takeUntilDestroyed(this.destroyRef))
2059
+ .subscribe(() => this.close());
2060
+ }
2061
+ destroyOverlay() {
2062
+ if (this.overlayRef) {
2063
+ this.overlayRef.dispose();
2064
+ this.overlayRef = null;
2065
+ }
2066
+ }
2067
+ /**
2068
+ * Returns overlay width configuration based on panelWidth setting.
2069
+ * - 'trigger': min-width equals host, can grow wider
2070
+ * - 'auto': no width constraint
2071
+ * - specific value: exact width
2072
+ */
2073
+ getPanelWidthConfig(hostWidth) {
2074
+ const config = this.panelWidth();
2075
+ if (config === 'trigger') {
2076
+ return { minWidth: hostWidth };
2077
+ }
2078
+ if (config === 'auto') {
2079
+ return {};
2080
+ }
2081
+ return { width: config };
2082
+ }
2083
+ updateValue(value) {
2084
+ this.internalValue.set(value);
2085
+ this.onChange(value);
2086
+ this.valueChange.emit(value);
2087
+ // Announce selection
2088
+ if (value !== null && !Array.isArray(value)) {
2089
+ this.announce(`${this.displayWith()(value)} selected`);
2090
+ }
2091
+ }
2092
+ toggleMultipleValue(value) {
2093
+ const current = this.selectedValues();
2094
+ const compare = this.compareWith();
2095
+ const isAlreadySelected = current.some((v) => compare(v, value));
2096
+ let newValues;
2097
+ if (isAlreadySelected) {
2098
+ newValues = current.filter((v) => !compare(v, value));
2099
+ this.announce(`${this.displayWith()(value)} deselected`);
2100
+ }
2101
+ else {
2102
+ newValues = [...current, value];
2103
+ this.announce(`${this.displayWith()(value)} selected`);
2104
+ }
2105
+ this.updateValue(newValues.length > 0 ? newValues : []);
2106
+ }
2107
+ navigateOptions(direction) {
2108
+ const options = this.groupBy() ? this.flattenGroupedOptions() : this.filteredOptions();
2109
+ if (options.length === 0) {
2110
+ return;
2111
+ }
2112
+ const currentId = this.activeOptionId();
2113
+ let currentIndex = options.findIndex((opt) => opt.id === currentId);
2114
+ if (currentIndex === -1) {
2115
+ currentIndex = direction === 1 ? -1 : options.length;
2116
+ }
2117
+ let nextIndex = currentIndex + direction;
2118
+ // Wrap around
2119
+ if (nextIndex < 0) {
2120
+ nextIndex = options.length - 1;
2121
+ }
2122
+ else if (nextIndex >= options.length) {
2123
+ nextIndex = 0;
2124
+ }
2125
+ // Skip disabled options
2126
+ const startIndex = nextIndex;
2127
+ let currentOption = options[nextIndex];
2128
+ while (currentOption?.disabled) {
2129
+ nextIndex += direction;
2130
+ if (nextIndex < 0)
2131
+ nextIndex = options.length - 1;
2132
+ else if (nextIndex >= options.length)
2133
+ nextIndex = 0;
2134
+ if (nextIndex === startIndex)
2135
+ return; // All disabled
2136
+ currentOption = options[nextIndex];
2137
+ }
2138
+ const targetOption = options[nextIndex];
2139
+ if (targetOption) {
2140
+ this.activeOptionId.set(targetOption.id);
2141
+ }
2142
+ }
2143
+ navigateToFirst() {
2144
+ const options = this.groupBy() ? this.flattenGroupedOptions() : this.filteredOptions();
2145
+ const firstEnabled = options.find((opt) => !opt.disabled);
2146
+ if (firstEnabled) {
2147
+ this.activeOptionId.set(firstEnabled.id);
2148
+ }
2149
+ }
2150
+ navigateToLast() {
2151
+ const options = this.groupBy() ? this.flattenGroupedOptions() : this.filteredOptions();
2152
+ const lastEnabled = [...options].reverse().find((opt) => !opt.disabled);
2153
+ if (lastEnabled) {
2154
+ this.activeOptionId.set(lastEnabled.id);
2155
+ }
2156
+ }
2157
+ selectActiveOption() {
2158
+ const activeId = this.activeOptionId();
2159
+ if (!activeId)
2160
+ return;
2161
+ const options = this.groupBy() ? this.flattenGroupedOptions() : this.filteredOptions();
2162
+ const option = options.find((opt) => opt.id === activeId);
2163
+ if (option && !option.disabled) {
2164
+ this.selectOption(option.value);
2165
+ }
2166
+ }
2167
+ flattenGroupedOptions() {
2168
+ return this.groupedOptions().flatMap((group) => group.options);
2169
+ }
2170
+ typeAhead(char) {
2171
+ // Simple type-ahead: find first option starting with the character
2172
+ const options = this.filteredOptions();
2173
+ const display = this.displayWith();
2174
+ const lowerChar = char.toLowerCase();
2175
+ const match = options.find((opt) => !opt.disabled && display(opt.value).toLowerCase().startsWith(lowerChar));
2176
+ if (match) {
2177
+ this.activeOptionId.set(match.id);
2178
+ if (!this.isOpen()) {
2179
+ this.selectOption(match.value);
2180
+ }
2181
+ }
2182
+ }
2183
+ announce(message) {
2184
+ this.liveAnnouncement.set(message);
2185
+ this.liveAnnouncer.announce(message, 'polite');
2186
+ }
2187
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDropdown, deps: [], target: i0.ɵɵFactoryTarget.Component });
2188
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: ComDropdown, isStandalone: true, selector: "com-dropdown", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, searchable: { classPropertyName: "searchable", publicName: "searchable", isSignal: true, isRequired: false, transformFunction: null }, searchPlaceholder: { classPropertyName: "searchPlaceholder", publicName: "searchPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, clearable: { classPropertyName: "clearable", publicName: "clearable", isSignal: true, isRequired: false, transformFunction: null }, compareWith: { classPropertyName: "compareWith", publicName: "compareWith", isSignal: true, isRequired: false, transformFunction: null }, displayWith: { classPropertyName: "displayWith", publicName: "displayWith", isSignal: true, isRequired: false, transformFunction: null }, filterWith: { classPropertyName: "filterWith", publicName: "filterWith", isSignal: true, isRequired: false, transformFunction: null }, groupBy: { classPropertyName: "groupBy", publicName: "groupBy", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, state: { classPropertyName: "state", publicName: "state", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, panelClass: { classPropertyName: "panelClass", publicName: "panelClass", isSignal: true, isRequired: false, transformFunction: null }, maxHeight: { classPropertyName: "maxHeight", publicName: "maxHeight", isSignal: true, isRequired: false, transformFunction: null }, panelWidth: { classPropertyName: "panelWidth", publicName: "panelWidth", isSignal: true, isRequired: false, transformFunction: null }, searchDebounceMs: { classPropertyName: "searchDebounceMs", publicName: "searchDebounceMs", isSignal: true, isRequired: false, transformFunction: null }, virtualScrollThreshold: { classPropertyName: "virtualScrollThreshold", publicName: "virtualScrollThreshold", isSignal: true, isRequired: false, transformFunction: null }, maxVisibleTags: { classPropertyName: "maxVisibleTags", publicName: "maxVisibleTags", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange", searchChange: "searchChange", opened: "opened", closed: "closed" }, host: { properties: { "class.com-dropdown-disabled": "disabled()", "class.com-dropdown-open": "isOpen()" }, classAttribute: "com-dropdown-host inline-block" }, queries: [{ propertyName: "optionTemplate", first: true, predicate: (ComDropdownOptionTpl), descendants: true, isSignal: true }, { propertyName: "selectedTemplate", first: true, predicate: (ComDropdownSelectedTpl), descendants: true, isSignal: true }, { propertyName: "emptyTemplate", first: true, predicate: ComDropdownEmptyTpl, descendants: true, isSignal: true }, { propertyName: "groupTemplate", first: true, predicate: ComDropdownGroupTpl, descendants: true, isSignal: true }, { propertyName: "tagTemplate", first: true, predicate: (ComDropdownTagTpl), descendants: true, isSignal: true }], viewQueries: [{ propertyName: "triggerRef", first: true, predicate: ["triggerElement"], descendants: true, isSignal: true }, { propertyName: "panelTemplateRef", first: true, predicate: ["panelTemplate"], descendants: true, isSignal: true }], exportAs: ["comDropdown"], ngImport: i0, template: `
2189
+ <!-- Trigger button -->
2190
+ <button
2191
+ #triggerElement
2192
+ type="button"
2193
+ role="combobox"
2194
+ [class]="triggerClasses()"
2195
+ [attr.id]="triggerId()"
2196
+ [attr.aria-expanded]="isOpen()"
2197
+ [attr.aria-controls]="panelId()"
2198
+ [attr.aria-haspopup]="'listbox'"
2199
+ [attr.aria-activedescendant]="activeDescendant()"
2200
+ [attr.aria-required]="required() || null"
2201
+ [attr.aria-invalid]="state() === 'error' || null"
2202
+ [attr.aria-disabled]="disabled() || null"
2203
+ [attr.tabindex]="disabled() ? -1 : 0"
2204
+ [disabled]="disabled()"
2205
+ (click)="toggle()"
2206
+ (keydown)="onTriggerKeydown($event)"
2207
+ >
2208
+ <!-- Selected value display -->
2209
+ <span class="flex-1 truncate text-left">
2210
+ @if (selectedTemplate()) {
2211
+ <ng-container
2212
+ [ngTemplateOutlet]="selectedTemplate()!.templateRef"
2213
+ [ngTemplateOutletContext]="selectedContext()"
2214
+ />
2215
+ } @else if (multiple()) {
2216
+ @if (selectedValues().length > 0) {
2217
+ <span class="flex flex-wrap gap-1">
2218
+ @for (item of visibleTags(); track trackByValue(item, $index); let i = $index) {
2219
+ <com-dropdown-tag
2220
+ [value]="item"
2221
+ [displayText]="displayWith()(item)"
2222
+ [index]="i"
2223
+ [size]="size()"
2224
+ [disabled]="disabled()"
2225
+ [tagTemplate]="tagTemplate()?.templateRef ?? null"
2226
+ (remove)="removeValue($event)"
2227
+ />
2228
+ }
2229
+ @if (hiddenTagsCount() > 0) {
2230
+ <span [class]="overflowBadgeClasses()">
2231
+ +{{ hiddenTagsCount() }}
2232
+ </span>
2233
+ }
2234
+ </span>
2235
+ } @else {
2236
+ <span class="text-placeholder">{{ placeholder() }}</span>
2237
+ }
2238
+ } @else {
2239
+ @if (selectedValue() !== null && selectedValue() !== undefined) {
2240
+ {{ displayWith()(selectedValue()!) }}
2241
+ } @else {
2242
+ <span class="text-placeholder">{{ placeholder() }}</span>
2243
+ }
2244
+ }
2245
+ </span>
2246
+
2247
+ <!-- Clear button -->
2248
+ @if (clearable() && hasValue() && !disabled()) {
2249
+ <button
2250
+ type="button"
2251
+ [class]="clearClasses()"
2252
+ [attr.aria-label]="'Clear selection'"
2253
+ (click)="clear($event)"
2254
+ >
2255
+ <svg
2256
+ viewBox="0 0 24 24"
2257
+ fill="none"
2258
+ stroke="currentColor"
2259
+ stroke-width="2"
2260
+ stroke-linecap="round"
2261
+ stroke-linejoin="round"
2262
+ class="h-full w-full"
2263
+ >
2264
+ <line x1="18" y1="6" x2="6" y2="18" />
2265
+ <line x1="6" y1="6" x2="18" y2="18" />
2266
+ </svg>
2267
+ </button>
2268
+ }
2269
+
2270
+ <!-- Chevron icon -->
2271
+ <svg
2272
+ [class]="chevronClasses()"
2273
+ viewBox="0 0 24 24"
2274
+ fill="none"
2275
+ stroke="currentColor"
2276
+ stroke-width="2"
2277
+ stroke-linecap="round"
2278
+ stroke-linejoin="round"
2279
+ >
2280
+ <polyline points="6 9 12 15 18 9" />
2281
+ </svg>
2282
+ </button>
2283
+
2284
+ <!-- Panel template (rendered in overlay) -->
2285
+ <ng-template #panelTemplate>
2286
+ <div
2287
+ #panelElement
2288
+ [class]="panelClasses()"
2289
+ [attr.id]="panelId()"
2290
+ [attr.role]="'listbox'"
2291
+ [attr.aria-multiselectable]="multiple() || null"
2292
+ [attr.aria-label]="placeholder()"
2293
+ (keydown)="onPanelKeydown($event)"
2294
+ >
2295
+ <!-- Search input -->
2296
+ @if (searchable()) {
2297
+ <com-dropdown-search
2298
+ [placeholder]="searchPlaceholder()"
2299
+ [size]="size()"
2300
+ [debounceMs]="searchDebounceMs()"
2301
+ (searchChange)="onSearchChange($event)"
2302
+ (keyNav)="onSearchKeyNav($event)"
2303
+ />
2304
+ }
2305
+
2306
+ <!-- Options list -->
2307
+ <div
2308
+ class="overflow-auto"
2309
+ [style.maxHeight]="maxHeight()"
2310
+ >
2311
+ @if (groupedOptions().length > 0) {
2312
+ @for (group of groupedOptions(); track group.key) {
2313
+ <!-- Group header -->
2314
+ <com-dropdown-group
2315
+ [label]="group.key"
2316
+ [count]="group.options.length"
2317
+ [expanded]="group.expanded"
2318
+ [size]="size()"
2319
+ [groupTemplate]="groupTemplate()?.templateRef ?? null"
2320
+ />
2321
+
2322
+ <!-- Group options -->
2323
+ @for (option of group.options; track option.id; let i = $index) {
2324
+ <com-dropdown-option
2325
+ [value]="option.value"
2326
+ [displayText]="option.displayText"
2327
+ [id]="option.id"
2328
+ [index]="getGlobalIndex(group.key, i)"
2329
+ [selected]="isSelected(option.value)"
2330
+ [active]="isActive(option.id)"
2331
+ [disabled]="option.disabled"
2332
+ [size]="size()"
2333
+ [optionTemplate]="optionTemplate()?.templateRef ?? null"
2334
+ (select)="selectOption($event)"
2335
+ (hover)="onOptionHover(option.id)"
2336
+ />
2337
+ }
2338
+ }
2339
+ } @else if (filteredOptions().length > 0) {
2340
+ @for (option of filteredOptions(); track option.id; let i = $index) {
2341
+ <com-dropdown-option
2342
+ [value]="option.value"
2343
+ [displayText]="option.displayText"
2344
+ [id]="option.id"
2345
+ [index]="i"
2346
+ [selected]="isSelected(option.value)"
2347
+ [active]="isActive(option.id)"
2348
+ [disabled]="option.disabled"
2349
+ [size]="size()"
2350
+ [optionTemplate]="optionTemplate()?.templateRef ?? null"
2351
+ (select)="selectOption($event)"
2352
+ (hover)="onOptionHover(option.id)"
2353
+ />
2354
+ }
2355
+ } @else {
2356
+ <!-- Empty state -->
2357
+ @if (emptyTemplate()) {
2358
+ <ng-container
2359
+ [ngTemplateOutlet]="emptyTemplate()!.templateRef"
2360
+ [ngTemplateOutletContext]="emptyContext()"
2361
+ />
2362
+ } @else {
2363
+ <div class="flex items-center justify-center px-3 py-6 text-muted-foreground">
2364
+ @if (searchQuery()) {
2365
+ No results for "{{ searchQuery() }}"
2366
+ } @else {
2367
+ No options available
2368
+ }
2369
+ </div>
2370
+ }
2371
+ }
2372
+ </div>
2373
+ </div>
2374
+ </ng-template>
2375
+
2376
+ <!-- Live announcer region -->
2377
+ <div class="sr-only" aria-live="polite" aria-atomic="true">
2378
+ {{ liveAnnouncement() }}
2379
+ </div>
2380
+ `, isInline: true, styles: [".sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: OverlayModule }, { kind: "component", type: ComDropdownOption, selector: "com-dropdown-option", inputs: ["value", "displayText", "id", "index", "selected", "active", "disabled", "size", "optionTemplate", "class"], outputs: ["select", "hover"], exportAs: ["comDropdownOption"] }, { kind: "component", type: ComDropdownSearch, selector: "com-dropdown-search", inputs: ["placeholder", "ariaLabel", "disabled", "debounceMs", "size", "class"], outputs: ["searchChange", "keyNav"], exportAs: ["comDropdownSearch"] }, { kind: "component", type: ComDropdownTag, selector: "com-dropdown-tag", inputs: ["value", "displayText", "index", "disabled", "size", "variant", "class", "tagTemplate"], outputs: ["remove"], exportAs: ["comDropdownTag"] }, { kind: "component", type: ComDropdownGroup, selector: "com-dropdown-group", inputs: ["label", "count", "expanded", "showCount", "size", "class", "groupTemplate"], exportAs: ["comDropdownGroup"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2381
+ }
2382
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComDropdown, decorators: [{
2383
+ type: Component,
2384
+ args: [{ selector: 'com-dropdown', exportAs: 'comDropdown', template: `
2385
+ <!-- Trigger button -->
2386
+ <button
2387
+ #triggerElement
2388
+ type="button"
2389
+ role="combobox"
2390
+ [class]="triggerClasses()"
2391
+ [attr.id]="triggerId()"
2392
+ [attr.aria-expanded]="isOpen()"
2393
+ [attr.aria-controls]="panelId()"
2394
+ [attr.aria-haspopup]="'listbox'"
2395
+ [attr.aria-activedescendant]="activeDescendant()"
2396
+ [attr.aria-required]="required() || null"
2397
+ [attr.aria-invalid]="state() === 'error' || null"
2398
+ [attr.aria-disabled]="disabled() || null"
2399
+ [attr.tabindex]="disabled() ? -1 : 0"
2400
+ [disabled]="disabled()"
2401
+ (click)="toggle()"
2402
+ (keydown)="onTriggerKeydown($event)"
2403
+ >
2404
+ <!-- Selected value display -->
2405
+ <span class="flex-1 truncate text-left">
2406
+ @if (selectedTemplate()) {
2407
+ <ng-container
2408
+ [ngTemplateOutlet]="selectedTemplate()!.templateRef"
2409
+ [ngTemplateOutletContext]="selectedContext()"
2410
+ />
2411
+ } @else if (multiple()) {
2412
+ @if (selectedValues().length > 0) {
2413
+ <span class="flex flex-wrap gap-1">
2414
+ @for (item of visibleTags(); track trackByValue(item, $index); let i = $index) {
2415
+ <com-dropdown-tag
2416
+ [value]="item"
2417
+ [displayText]="displayWith()(item)"
2418
+ [index]="i"
2419
+ [size]="size()"
2420
+ [disabled]="disabled()"
2421
+ [tagTemplate]="tagTemplate()?.templateRef ?? null"
2422
+ (remove)="removeValue($event)"
2423
+ />
2424
+ }
2425
+ @if (hiddenTagsCount() > 0) {
2426
+ <span [class]="overflowBadgeClasses()">
2427
+ +{{ hiddenTagsCount() }}
2428
+ </span>
2429
+ }
2430
+ </span>
2431
+ } @else {
2432
+ <span class="text-placeholder">{{ placeholder() }}</span>
2433
+ }
2434
+ } @else {
2435
+ @if (selectedValue() !== null && selectedValue() !== undefined) {
2436
+ {{ displayWith()(selectedValue()!) }}
2437
+ } @else {
2438
+ <span class="text-placeholder">{{ placeholder() }}</span>
2439
+ }
2440
+ }
2441
+ </span>
2442
+
2443
+ <!-- Clear button -->
2444
+ @if (clearable() && hasValue() && !disabled()) {
2445
+ <button
2446
+ type="button"
2447
+ [class]="clearClasses()"
2448
+ [attr.aria-label]="'Clear selection'"
2449
+ (click)="clear($event)"
2450
+ >
2451
+ <svg
2452
+ viewBox="0 0 24 24"
2453
+ fill="none"
2454
+ stroke="currentColor"
2455
+ stroke-width="2"
2456
+ stroke-linecap="round"
2457
+ stroke-linejoin="round"
2458
+ class="h-full w-full"
2459
+ >
2460
+ <line x1="18" y1="6" x2="6" y2="18" />
2461
+ <line x1="6" y1="6" x2="18" y2="18" />
2462
+ </svg>
2463
+ </button>
2464
+ }
2465
+
2466
+ <!-- Chevron icon -->
2467
+ <svg
2468
+ [class]="chevronClasses()"
2469
+ viewBox="0 0 24 24"
2470
+ fill="none"
2471
+ stroke="currentColor"
2472
+ stroke-width="2"
2473
+ stroke-linecap="round"
2474
+ stroke-linejoin="round"
2475
+ >
2476
+ <polyline points="6 9 12 15 18 9" />
2477
+ </svg>
2478
+ </button>
2479
+
2480
+ <!-- Panel template (rendered in overlay) -->
2481
+ <ng-template #panelTemplate>
2482
+ <div
2483
+ #panelElement
2484
+ [class]="panelClasses()"
2485
+ [attr.id]="panelId()"
2486
+ [attr.role]="'listbox'"
2487
+ [attr.aria-multiselectable]="multiple() || null"
2488
+ [attr.aria-label]="placeholder()"
2489
+ (keydown)="onPanelKeydown($event)"
2490
+ >
2491
+ <!-- Search input -->
2492
+ @if (searchable()) {
2493
+ <com-dropdown-search
2494
+ [placeholder]="searchPlaceholder()"
2495
+ [size]="size()"
2496
+ [debounceMs]="searchDebounceMs()"
2497
+ (searchChange)="onSearchChange($event)"
2498
+ (keyNav)="onSearchKeyNav($event)"
2499
+ />
2500
+ }
2501
+
2502
+ <!-- Options list -->
2503
+ <div
2504
+ class="overflow-auto"
2505
+ [style.maxHeight]="maxHeight()"
2506
+ >
2507
+ @if (groupedOptions().length > 0) {
2508
+ @for (group of groupedOptions(); track group.key) {
2509
+ <!-- Group header -->
2510
+ <com-dropdown-group
2511
+ [label]="group.key"
2512
+ [count]="group.options.length"
2513
+ [expanded]="group.expanded"
2514
+ [size]="size()"
2515
+ [groupTemplate]="groupTemplate()?.templateRef ?? null"
2516
+ />
2517
+
2518
+ <!-- Group options -->
2519
+ @for (option of group.options; track option.id; let i = $index) {
2520
+ <com-dropdown-option
2521
+ [value]="option.value"
2522
+ [displayText]="option.displayText"
2523
+ [id]="option.id"
2524
+ [index]="getGlobalIndex(group.key, i)"
2525
+ [selected]="isSelected(option.value)"
2526
+ [active]="isActive(option.id)"
2527
+ [disabled]="option.disabled"
2528
+ [size]="size()"
2529
+ [optionTemplate]="optionTemplate()?.templateRef ?? null"
2530
+ (select)="selectOption($event)"
2531
+ (hover)="onOptionHover(option.id)"
2532
+ />
2533
+ }
2534
+ }
2535
+ } @else if (filteredOptions().length > 0) {
2536
+ @for (option of filteredOptions(); track option.id; let i = $index) {
2537
+ <com-dropdown-option
2538
+ [value]="option.value"
2539
+ [displayText]="option.displayText"
2540
+ [id]="option.id"
2541
+ [index]="i"
2542
+ [selected]="isSelected(option.value)"
2543
+ [active]="isActive(option.id)"
2544
+ [disabled]="option.disabled"
2545
+ [size]="size()"
2546
+ [optionTemplate]="optionTemplate()?.templateRef ?? null"
2547
+ (select)="selectOption($event)"
2548
+ (hover)="onOptionHover(option.id)"
2549
+ />
2550
+ }
2551
+ } @else {
2552
+ <!-- Empty state -->
2553
+ @if (emptyTemplate()) {
2554
+ <ng-container
2555
+ [ngTemplateOutlet]="emptyTemplate()!.templateRef"
2556
+ [ngTemplateOutletContext]="emptyContext()"
2557
+ />
2558
+ } @else {
2559
+ <div class="flex items-center justify-center px-3 py-6 text-muted-foreground">
2560
+ @if (searchQuery()) {
2561
+ No results for "{{ searchQuery() }}"
2562
+ } @else {
2563
+ No options available
2564
+ }
2565
+ </div>
2566
+ }
2567
+ }
2568
+ </div>
2569
+ </div>
2570
+ </ng-template>
2571
+
2572
+ <!-- Live announcer region -->
2573
+ <div class="sr-only" aria-live="polite" aria-atomic="true">
2574
+ {{ liveAnnouncement() }}
2575
+ </div>
2576
+ `, imports: [
2577
+ NgTemplateOutlet,
2578
+ OverlayModule,
2579
+ ComDropdownOption,
2580
+ ComDropdownSearch,
2581
+ ComDropdownTag,
2582
+ ComDropdownGroup,
2583
+ ], changeDetection: ChangeDetectionStrategy.OnPush, host: {
2584
+ class: 'com-dropdown-host inline-block',
2585
+ '[class.com-dropdown-disabled]': 'disabled()',
2586
+ '[class.com-dropdown-open]': 'isOpen()',
2587
+ }, styles: [".sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n"] }]
2588
+ }], ctorParameters: () => [], propDecorators: { triggerRef: [{ type: i0.ViewChild, args: ['triggerElement', { isSignal: true }] }], panelTemplateRef: [{ type: i0.ViewChild, args: ['panelTemplate', { isSignal: true }] }], optionTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => ComDropdownOptionTpl), { isSignal: true }] }], selectedTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => ComDropdownSelectedTpl), { isSignal: true }] }], emptyTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => ComDropdownEmptyTpl), { isSignal: true }] }], groupTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => ComDropdownGroupTpl), { isSignal: true }] }], tagTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => ComDropdownTagTpl), { isSignal: true }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], searchable: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchable", required: false }] }], searchPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchPlaceholder", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], compareWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "compareWith", required: false }] }], displayWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayWith", required: false }] }], filterWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterWith", required: false }] }], groupBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "groupBy", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], state: [{ type: i0.Input, args: [{ isSignal: true, alias: "state", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], panelClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelClass", required: false }] }], maxHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxHeight", required: false }] }], panelWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelWidth", required: false }] }], searchDebounceMs: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchDebounceMs", required: false }] }], virtualScrollThreshold: [{ type: i0.Input, args: [{ isSignal: true, alias: "virtualScrollThreshold", required: false }] }], maxVisibleTags: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxVisibleTags", required: false }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }], searchChange: [{ type: i0.Output, args: ["searchChange"] }], opened: [{ type: i0.Output, args: ["opened"] }], closed: [{ type: i0.Output, args: ["closed"] }] } });
2589
+
2590
+ // Public API for the dropdown component
2591
+ // Main component
2592
+
2593
+ /**
2594
+ * Generated bundle index. Do not edit.
2595
+ */
2596
+
2597
+ export { ComDropdown, ComDropdownEmptyTpl, ComDropdownGroup, ComDropdownGroupTpl, ComDropdownOption, ComDropdownOptionTpl, ComDropdownPanel, ComDropdownSearch, ComDropdownSelectedTpl, ComDropdownTag, ComDropdownTagTpl, defaultCompareWith, defaultDisplayWith, defaultFilterWith, dropdownChevronVariants, dropdownClearVariants, dropdownEmptyVariants, dropdownGroupVariants, dropdownOptionVariants, dropdownOverflowBadgeVariants, dropdownPanelVariants, dropdownSearchVariants, dropdownTagRemoveVariants, dropdownTagVariants, dropdownTriggerVariants, generateDropdownId };
2598
+ //# sourceMappingURL=ngx-com-components-dropdown.mjs.map