@lumaui/angular 0.4.3 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,8 @@
1
1
  import * as i0 from '@angular/core';
2
- import { input, computed, HostBinding, Directive, ChangeDetectionStrategy, Component, output, InjectionToken, inject, ElementRef, Renderer2, effect, signal, HostListener, PLATFORM_ID, NgZone, viewChild, TemplateRef, ViewContainerRef, ApplicationRef, Injector, createComponent, Injectable } from '@angular/core';
3
- import { buttonVariants, badgeVariants, cardVariants, cardTitleVariants, cardDescriptionVariants, cardHeaderVariants, cardContentVariants, accordionItemVariants, accordionContentWrapperVariants, accordionTriggerVariants, accordionTitleVariants, accordionIconVariants, accordionContentVariants, tooltipVariants, tabsListVariants, tabsScrollArrowVariants, tabsTriggerVariants, tabsPanelVariants, tabsIndicatorVariants, modalOverlayVariants, modalContainerVariants, modalHeaderVariants, modalTitleVariants, modalContentVariants, modalFooterVariants, modalCloseVariants, toastCloseVariants, toastItemVariants, toastIconVariants, toastContentVariants, toastTitleVariants, toastMessageVariants, toastContainerVariants } from '@lumaui/core';
2
+ import { input, computed, HostBinding, Directive, inject, Injector, ElementRef, DestroyRef, signal, forwardRef, ChangeDetectionStrategy, Component, output, InjectionToken, Renderer2, effect, HostListener, PLATFORM_ID, NgZone, viewChild, TemplateRef, ViewContainerRef, ApplicationRef, createComponent, Injectable } from '@angular/core';
3
+ import { buttonVariants, badgeVariants, inputVariants, labelVariants, helperTextVariants, errorTextVariants, textareaVariants, checkboxVariants, radioVariants, cardVariants, cardTitleVariants, cardDescriptionVariants, cardHeaderVariants, cardContentVariants, accordionItemVariants, accordionContentWrapperVariants, accordionTriggerVariants, accordionTitleVariants, accordionIconVariants, accordionContentVariants, tooltipVariants, tabsListVariants, tabsScrollArrowVariants, tabsTriggerVariants, tabsPanelVariants, tabsIndicatorVariants, modalOverlayVariants, modalContainerVariants, modalHeaderVariants, modalTitleVariants, modalContentVariants, modalFooterVariants, modalCloseVariants, toastCloseVariants, toastItemVariants, toastIconVariants, toastContentVariants, toastTitleVariants, toastMessageVariants, toastContainerVariants, selectTriggerVariants, selectListboxVariants, selectSearchInputVariants, selectChipVariants, selectChipDismissVariants, selectOptionVariants } from '@lumaui/core';
4
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
5
+ import { NgControl, NG_VALUE_ACCESSOR } from '@angular/forms';
4
6
  import { DOCUMENT, isPlatformBrowser } from '@angular/common';
5
7
  import { Subject, interval } from 'rxjs';
6
8
  import { takeWhile, filter } from 'rxjs/operators';
@@ -63,6 +65,668 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImpor
63
65
  args: ['class']
64
66
  }] } });
65
67
 
68
+ let inputIdCounter = 0;
69
+ /**
70
+ * Input directive with CVA variants and Angular Forms integration
71
+ *
72
+ * @example
73
+ * <input lumaInput lmSize="md" />
74
+ */
75
+ class LmInputDirective {
76
+ // Use Injector to avoid circular dependency
77
+ injector = inject(Injector);
78
+ elementRef = inject((ElementRef));
79
+ ngControl = null;
80
+ destroyRef = inject(DestroyRef);
81
+ // Inputs
82
+ lmSize = input('md', ...(ngDevMode ? [{ debugName: "lmSize" }] : []));
83
+ lmError = input(false, ...(ngDevMode ? [{ debugName: "lmError" }] : []));
84
+ lmDisabled = input(false, ...(ngDevMode ? [{ debugName: "lmDisabled" }] : []));
85
+ lmRequired = input(false, ...(ngDevMode ? [{ debugName: "lmRequired" }] : []));
86
+ // Internal signal for disabled state coming from FormControl.disable()
87
+ _formDisabled = signal(false, ...(ngDevMode ? [{ debugName: "_formDisabled" }] : []));
88
+ // Internal signals for form validation state (bridging non-signal FormControl to signal graph)
89
+ _formInvalid = signal(false, ...(ngDevMode ? [{ debugName: "_formInvalid" }] : []));
90
+ _formTouched = signal(false, ...(ngDevMode ? [{ debugName: "_formTouched" }] : []));
91
+ // Final disabled state: manual lmDisabled OR programmatic FormControl.disable()
92
+ isDisabled = computed(() => this.lmDisabled() || this._formDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
93
+ // Auto-generated ID
94
+ elementId = signal(`luma-input-${inputIdCounter++}`, ...(ngDevMode ? [{ debugName: "elementId" }] : []));
95
+ // ARIA describedby (will be set by parent or manually)
96
+ describedBy = signal(null, ...(ngDevMode ? [{ debugName: "describedBy" }] : []));
97
+ // Computed signal — reactive to both manual lmError and FormControl state
98
+ hasError = computed(() => this.lmError() || (this._formInvalid() && this._formTouched()), ...(ngDevMode ? [{ debugName: "hasError" }] : []));
99
+ // Computed classes
100
+ classes = computed(() => inputVariants({
101
+ size: this.lmSize(),
102
+ error: this.hasError(),
103
+ }), ...(ngDevMode ? [{ debugName: "classes" }] : []));
104
+ // noop – replaced at runtime by registerOnChange / registerOnTouched
105
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
106
+ onChange = () => { };
107
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
108
+ onTouched = () => { };
109
+ ngOnInit() {
110
+ // Get NgControl from Injector to avoid circular dependency
111
+ try {
112
+ this.ngControl = this.injector.get(NgControl, null, {
113
+ optional: true,
114
+ self: true,
115
+ });
116
+ if (this.ngControl) {
117
+ this.ngControl.valueAccessor = this;
118
+ const control = this.ngControl.control;
119
+ if (control) {
120
+ // Capture immediate state (e.g. markAsTouched() called before ngOnInit)
121
+ this._formInvalid.set(control.invalid);
122
+ this._formTouched.set(control.touched);
123
+ // React to any future FormControl state changes
124
+ // control.events emits for ALL changes: value, status, touched, pristine
125
+ // statusChanges alone does NOT emit for markAsTouched()
126
+ control.events
127
+ .pipe(takeUntilDestroyed(this.destroyRef))
128
+ .subscribe(() => {
129
+ this._formInvalid.set(control.invalid);
130
+ this._formTouched.set(control.touched);
131
+ });
132
+ }
133
+ }
134
+ }
135
+ catch {
136
+ // No NgControl present
137
+ this.ngControl = null;
138
+ }
139
+ }
140
+ writeValue(value) {
141
+ const element = this.elementRef.nativeElement;
142
+ element.value = value ?? '';
143
+ }
144
+ registerOnChange(fn) {
145
+ this.onChange = fn;
146
+ }
147
+ registerOnTouched(fn) {
148
+ this.onTouched = fn;
149
+ }
150
+ setDisabledState(isDisabled) {
151
+ this._formDisabled.set(isDisabled);
152
+ }
153
+ onInput(event) {
154
+ const target = event.target;
155
+ this.onChange(target.value);
156
+ }
157
+ onBlur() {
158
+ this.onTouched();
159
+ }
160
+ /**
161
+ * Set the aria-describedby attribute value
162
+ * This is typically called by parent components or directives
163
+ */
164
+ setDescribedBy(value) {
165
+ this.describedBy.set(value);
166
+ }
167
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmInputDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
168
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.9", type: LmInputDirective, isStandalone: true, selector: "input[lumaInput]", inputs: { lmSize: { classPropertyName: "lmSize", publicName: "lmSize", isSignal: true, isRequired: false, transformFunction: null }, lmError: { classPropertyName: "lmError", publicName: "lmError", isSignal: true, isRequired: false, transformFunction: null }, lmDisabled: { classPropertyName: "lmDisabled", publicName: "lmDisabled", isSignal: true, isRequired: false, transformFunction: null }, lmRequired: { classPropertyName: "lmRequired", publicName: "lmRequired", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "input": "onInput($event)", "blur": "onBlur()" }, properties: { "class": "classes()", "attr.id": "elementId()", "attr.aria-invalid": "hasError()", "attr.aria-describedby": "describedBy()", "attr.disabled": "isDisabled() ? \"\" : null", "attr.required": "lmRequired() ? \"\" : null" } }, providers: [
169
+ {
170
+ provide: NG_VALUE_ACCESSOR,
171
+ useExisting: forwardRef(() => LmInputDirective),
172
+ multi: true,
173
+ },
174
+ ], ngImport: i0 });
175
+ }
176
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmInputDirective, decorators: [{
177
+ type: Directive,
178
+ args: [{
179
+ selector: 'input[lumaInput]',
180
+ standalone: true,
181
+ host: {
182
+ '[class]': 'classes()',
183
+ '[attr.id]': 'elementId()',
184
+ '[attr.aria-invalid]': 'hasError()',
185
+ '[attr.aria-describedby]': 'describedBy()',
186
+ '[attr.disabled]': 'isDisabled() ? "" : null',
187
+ '[attr.required]': 'lmRequired() ? "" : null',
188
+ '(input)': 'onInput($event)',
189
+ '(blur)': 'onBlur()',
190
+ },
191
+ providers: [
192
+ {
193
+ provide: NG_VALUE_ACCESSOR,
194
+ useExisting: forwardRef(() => LmInputDirective),
195
+ multi: true,
196
+ },
197
+ ],
198
+ }]
199
+ }], propDecorators: { lmSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmSize", required: false }] }], lmError: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmError", required: false }] }], lmDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmDisabled", required: false }] }], lmRequired: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmRequired", required: false }] }] } });
200
+
201
+ /**
202
+ * Label directive with required indicator support
203
+ *
204
+ * @example
205
+ * <label lumaLabel for="email" lmRequired>Email</label>
206
+ */
207
+ class LmLabelDirective {
208
+ for = input('', ...(ngDevMode ? [{ debugName: "for" }] : []));
209
+ lmRequired = input(false, ...(ngDevMode ? [{ debugName: "lmRequired" }] : []));
210
+ lmSize = input('md', ...(ngDevMode ? [{ debugName: "lmSize" }] : []));
211
+ lmInline = input(false, ...(ngDevMode ? [{ debugName: "lmInline" }] : []));
212
+ classes = computed(() => labelVariants({
213
+ size: this.lmSize(),
214
+ required: this.lmRequired(),
215
+ inline: this.lmInline(),
216
+ }), ...(ngDevMode ? [{ debugName: "classes" }] : []));
217
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmLabelDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
218
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.9", type: LmLabelDirective, isStandalone: true, selector: "label[lumaLabel]", inputs: { for: { classPropertyName: "for", publicName: "for", isSignal: true, isRequired: false, transformFunction: null }, lmRequired: { classPropertyName: "lmRequired", publicName: "lmRequired", isSignal: true, isRequired: false, transformFunction: null }, lmSize: { classPropertyName: "lmSize", publicName: "lmSize", isSignal: true, isRequired: false, transformFunction: null }, lmInline: { classPropertyName: "lmInline", publicName: "lmInline", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()", "attr.for": "for()" } }, ngImport: i0 });
219
+ }
220
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmLabelDirective, decorators: [{
221
+ type: Directive,
222
+ args: [{
223
+ selector: 'label[lumaLabel]',
224
+ standalone: true,
225
+ host: {
226
+ '[class]': 'classes()',
227
+ '[attr.for]': 'for()',
228
+ },
229
+ }]
230
+ }], propDecorators: { for: [{ type: i0.Input, args: [{ isSignal: true, alias: "for", required: false }] }], lmRequired: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmRequired", required: false }] }], lmSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmSize", required: false }] }], lmInline: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmInline", required: false }] }] } });
231
+
232
+ let helperIdCounter = 0;
233
+ /**
234
+ * Helper text directive for displaying hint/description text
235
+ *
236
+ * @example
237
+ * <span lumaHelperText>We'll never share your email.</span>
238
+ */
239
+ class LmHelperTextDirective {
240
+ elementId = signal(`luma-helper-${helperIdCounter++}`, ...(ngDevMode ? [{ debugName: "elementId" }] : []));
241
+ lmSize = input('sm', ...(ngDevMode ? [{ debugName: "lmSize" }] : []));
242
+ classes = computed(() => helperTextVariants({ size: this.lmSize() }), ...(ngDevMode ? [{ debugName: "classes" }] : []));
243
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmHelperTextDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
244
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.9", type: LmHelperTextDirective, isStandalone: true, selector: "[lumaHelperText]", inputs: { lmSize: { classPropertyName: "lmSize", publicName: "lmSize", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()", "attr.id": "elementId()" } }, ngImport: i0 });
245
+ }
246
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmHelperTextDirective, decorators: [{
247
+ type: Directive,
248
+ args: [{
249
+ selector: '[lumaHelperText]',
250
+ standalone: true,
251
+ host: {
252
+ '[class]': 'classes()',
253
+ '[attr.id]': 'elementId()',
254
+ },
255
+ }]
256
+ }], propDecorators: { lmSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmSize", required: false }] }] } });
257
+
258
+ let errorIdCounter = 0;
259
+ /**
260
+ * Error text directive for displaying validation error messages
261
+ *
262
+ * @example
263
+ * <span lumaErrorText *ngIf="hasError">Password must be at least 8 characters.</span>
264
+ */
265
+ class LmErrorTextDirective {
266
+ elementId = signal(`luma-error-${errorIdCounter++}`, ...(ngDevMode ? [{ debugName: "elementId" }] : []));
267
+ lmSize = input('sm', ...(ngDevMode ? [{ debugName: "lmSize" }] : []));
268
+ classes = computed(() => errorTextVariants({ size: this.lmSize() }), ...(ngDevMode ? [{ debugName: "classes" }] : []));
269
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmErrorTextDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
270
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.9", type: LmErrorTextDirective, isStandalone: true, selector: "[lumaErrorText]", inputs: { lmSize: { classPropertyName: "lmSize", publicName: "lmSize", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()", "attr.id": "elementId()" } }, ngImport: i0 });
271
+ }
272
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmErrorTextDirective, decorators: [{
273
+ type: Directive,
274
+ args: [{
275
+ selector: '[lumaErrorText]',
276
+ standalone: true,
277
+ host: {
278
+ '[class]': 'classes()',
279
+ '[attr.id]': 'elementId()',
280
+ },
281
+ }]
282
+ }], propDecorators: { lmSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmSize", required: false }] }] } });
283
+
284
+ let textareaIdCounter = 0;
285
+ /**
286
+ * Textarea directive with CVA variants and Angular Forms integration
287
+ *
288
+ * @example
289
+ * <textarea lumaTextarea lmSize="md"></textarea>
290
+ */
291
+ class LmTextareaDirective {
292
+ // Use Injector to avoid circular dependency
293
+ injector = inject(Injector);
294
+ elementRef = inject((ElementRef));
295
+ ngControl = null;
296
+ destroyRef = inject(DestroyRef);
297
+ // Inputs
298
+ lmSize = input('md', ...(ngDevMode ? [{ debugName: "lmSize" }] : []));
299
+ lmError = input(false, ...(ngDevMode ? [{ debugName: "lmError" }] : []));
300
+ lmDisabled = input(false, ...(ngDevMode ? [{ debugName: "lmDisabled" }] : []));
301
+ lmRequired = input(false, ...(ngDevMode ? [{ debugName: "lmRequired" }] : []));
302
+ // Internal signal for disabled state coming from FormControl.disable()
303
+ _formDisabled = signal(false, ...(ngDevMode ? [{ debugName: "_formDisabled" }] : []));
304
+ // Internal signals for form validation state (bridging non-signal FormControl to signal graph)
305
+ _formInvalid = signal(false, ...(ngDevMode ? [{ debugName: "_formInvalid" }] : []));
306
+ _formTouched = signal(false, ...(ngDevMode ? [{ debugName: "_formTouched" }] : []));
307
+ // Final disabled state: manual lmDisabled OR programmatic FormControl.disable()
308
+ isDisabled = computed(() => this.lmDisabled() || this._formDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
309
+ // Auto-generated ID
310
+ elementId = signal(`luma-textarea-${textareaIdCounter++}`, ...(ngDevMode ? [{ debugName: "elementId" }] : []));
311
+ // ARIA describedby (set by parent or manually)
312
+ describedBy = signal(null, ...(ngDevMode ? [{ debugName: "describedBy" }] : []));
313
+ // Computed signal — reactive to both manual lmError and FormControl state
314
+ hasError = computed(() => this.lmError() || (this._formInvalid() && this._formTouched()), ...(ngDevMode ? [{ debugName: "hasError" }] : []));
315
+ // Computed classes
316
+ classes = computed(() => textareaVariants({
317
+ size: this.lmSize(),
318
+ error: this.hasError(),
319
+ }), ...(ngDevMode ? [{ debugName: "classes" }] : []));
320
+ // noop – replaced at runtime by registerOnChange / registerOnTouched
321
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
322
+ onChange = () => { };
323
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
324
+ onTouched = () => { };
325
+ ngOnInit() {
326
+ // Get NgControl from Injector to avoid circular dependency
327
+ try {
328
+ this.ngControl = this.injector.get(NgControl, null, {
329
+ optional: true,
330
+ self: true,
331
+ });
332
+ if (this.ngControl) {
333
+ this.ngControl.valueAccessor = this;
334
+ const control = this.ngControl.control;
335
+ if (control) {
336
+ // Capture immediate state (e.g. markAsTouched() called before ngOnInit)
337
+ this._formInvalid.set(control.invalid);
338
+ this._formTouched.set(control.touched);
339
+ // React to any future FormControl state changes
340
+ // control.events emits for ALL changes: value, status, touched, pristine
341
+ // statusChanges alone does NOT emit for markAsTouched()
342
+ control.events
343
+ .pipe(takeUntilDestroyed(this.destroyRef))
344
+ .subscribe(() => {
345
+ this._formInvalid.set(control.invalid);
346
+ this._formTouched.set(control.touched);
347
+ });
348
+ }
349
+ }
350
+ }
351
+ catch {
352
+ // No NgControl present
353
+ this.ngControl = null;
354
+ }
355
+ }
356
+ writeValue(value) {
357
+ const element = this.elementRef.nativeElement;
358
+ element.value = value ?? '';
359
+ }
360
+ registerOnChange(fn) {
361
+ this.onChange = fn;
362
+ }
363
+ registerOnTouched(fn) {
364
+ this.onTouched = fn;
365
+ }
366
+ setDisabledState(isDisabled) {
367
+ this._formDisabled.set(isDisabled);
368
+ }
369
+ onInput(event) {
370
+ const target = event.target;
371
+ this.onChange(target.value);
372
+ }
373
+ onBlur() {
374
+ this.onTouched();
375
+ }
376
+ /**
377
+ * Set the aria-describedby attribute value
378
+ * This is typically called by parent components or directives
379
+ */
380
+ setDescribedBy(value) {
381
+ this.describedBy.set(value);
382
+ }
383
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmTextareaDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
384
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.9", type: LmTextareaDirective, isStandalone: true, selector: "textarea[lumaTextarea]", inputs: { lmSize: { classPropertyName: "lmSize", publicName: "lmSize", isSignal: true, isRequired: false, transformFunction: null }, lmError: { classPropertyName: "lmError", publicName: "lmError", isSignal: true, isRequired: false, transformFunction: null }, lmDisabled: { classPropertyName: "lmDisabled", publicName: "lmDisabled", isSignal: true, isRequired: false, transformFunction: null }, lmRequired: { classPropertyName: "lmRequired", publicName: "lmRequired", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "input": "onInput($event)", "blur": "onBlur()" }, properties: { "class": "classes()", "attr.id": "elementId()", "attr.aria-invalid": "hasError()", "attr.aria-describedby": "describedBy()", "attr.disabled": "isDisabled() ? \"\" : null", "attr.required": "lmRequired() ? \"\" : null" } }, providers: [
385
+ {
386
+ provide: NG_VALUE_ACCESSOR,
387
+ useExisting: forwardRef(() => LmTextareaDirective),
388
+ multi: true,
389
+ },
390
+ ], ngImport: i0 });
391
+ }
392
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmTextareaDirective, decorators: [{
393
+ type: Directive,
394
+ args: [{
395
+ selector: 'textarea[lumaTextarea]',
396
+ standalone: true,
397
+ host: {
398
+ '[class]': 'classes()',
399
+ '[attr.id]': 'elementId()',
400
+ '[attr.aria-invalid]': 'hasError()',
401
+ '[attr.aria-describedby]': 'describedBy()',
402
+ '[attr.disabled]': 'isDisabled() ? "" : null',
403
+ '[attr.required]': 'lmRequired() ? "" : null',
404
+ '(input)': 'onInput($event)',
405
+ '(blur)': 'onBlur()',
406
+ },
407
+ providers: [
408
+ {
409
+ provide: NG_VALUE_ACCESSOR,
410
+ useExisting: forwardRef(() => LmTextareaDirective),
411
+ multi: true,
412
+ },
413
+ ],
414
+ }]
415
+ }], propDecorators: { lmSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmSize", required: false }] }], lmError: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmError", required: false }] }], lmDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmDisabled", required: false }] }], lmRequired: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmRequired", required: false }] }] } });
416
+
417
+ let checkboxIdCounter = 0;
418
+ // SVG checkmark encoded as a CSS background-image data URI (white stroke on transparent)
419
+ const CHECKMARK_SVG = "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4 8l3 3 5-5' stroke='white' stroke-width='2' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C%2Fsvg%3E\")";
420
+ /**
421
+ * Checkbox directive with CVA variants and Angular Forms integration.
422
+ * Supports boolean ControlValueAccessor (checked/unchecked).
423
+ *
424
+ * @example
425
+ * <input type="checkbox" lumaCheckbox [formControl]="agreedControl" />
426
+ */
427
+ class LmCheckboxDirective {
428
+ // Use Injector to avoid circular dependency
429
+ injector = inject(Injector);
430
+ elementRef = inject((ElementRef));
431
+ ngControl = null;
432
+ destroyRef = inject(DestroyRef);
433
+ // Inputs
434
+ lmError = input(false, ...(ngDevMode ? [{ debugName: "lmError" }] : []));
435
+ lmDisabled = input(false, ...(ngDevMode ? [{ debugName: "lmDisabled" }] : []));
436
+ lmRequired = input(false, ...(ngDevMode ? [{ debugName: "lmRequired" }] : []));
437
+ // Internal signal for disabled state coming from FormControl.disable()
438
+ _formDisabled = signal(false, ...(ngDevMode ? [{ debugName: "_formDisabled" }] : []));
439
+ // Internal signals for form validation state (bridging non-signal FormControl to signal graph)
440
+ _formInvalid = signal(false, ...(ngDevMode ? [{ debugName: "_formInvalid" }] : []));
441
+ _formTouched = signal(false, ...(ngDevMode ? [{ debugName: "_formTouched" }] : []));
442
+ // Internal signal tracking whether the checkbox is currently checked
443
+ isChecked = signal(false, ...(ngDevMode ? [{ debugName: "isChecked" }] : []));
444
+ // Exposed for host binding (readonly reference to the constant)
445
+ checkmarkSvg = CHECKMARK_SVG;
446
+ // Final disabled state: manual lmDisabled OR programmatic FormControl.disable()
447
+ isDisabled = computed(() => this.lmDisabled() || this._formDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
448
+ // Preserve static id="..." from template; fall back to auto-generated id.
449
+ // elementRef must be declared before this field so it's available at init time.
450
+ elementId = signal(this.elementRef.nativeElement.getAttribute('id') ||
451
+ `luma-checkbox-${checkboxIdCounter++}`, ...(ngDevMode ? [{ debugName: "elementId" }] : []));
452
+ // ARIA describedby (can be set by parent or manually)
453
+ describedBy = signal(null, ...(ngDevMode ? [{ debugName: "describedBy" }] : []));
454
+ // Computed signal — reactive to both manual lmError and FormControl state
455
+ hasError = computed(() => this.lmError() || (this._formInvalid() && this._formTouched()), ...(ngDevMode ? [{ debugName: "hasError" }] : []));
456
+ // Computed classes
457
+ classes = computed(() => checkboxVariants({ error: this.hasError() }), ...(ngDevMode ? [{ debugName: "classes" }] : []));
458
+ // ControlValueAccessor implementation (boolean-typed)
459
+ // noop – replaced at runtime by registerOnChange / registerOnTouched
460
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
461
+ onChange = () => { };
462
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
463
+ onTouched = () => { };
464
+ ngOnInit() {
465
+ // Sync initial checked state from native element.
466
+ // Needed when a static `checked` attribute is used without a FormControl —
467
+ // writeValue() is never called in that case, so isChecked stays false.
468
+ this.isChecked.set(this.elementRef.nativeElement.checked);
469
+ // Get NgControl from Injector to avoid circular dependency
470
+ try {
471
+ this.ngControl = this.injector.get(NgControl, null, {
472
+ optional: true,
473
+ self: true,
474
+ });
475
+ if (this.ngControl) {
476
+ this.ngControl.valueAccessor = this;
477
+ const control = this.ngControl.control;
478
+ if (control) {
479
+ // Capture immediate state (e.g. markAsTouched() called before ngOnInit)
480
+ this._formInvalid.set(control.invalid);
481
+ this._formTouched.set(control.touched);
482
+ // React to any future FormControl state changes
483
+ // control.events emits for ALL changes: value, status, touched, pristine
484
+ // statusChanges alone does NOT emit for markAsTouched()
485
+ control.events
486
+ .pipe(takeUntilDestroyed(this.destroyRef))
487
+ .subscribe(() => {
488
+ this._formInvalid.set(control.invalid);
489
+ this._formTouched.set(control.touched);
490
+ });
491
+ }
492
+ }
493
+ }
494
+ catch {
495
+ // No NgControl present
496
+ this.ngControl = null;
497
+ }
498
+ }
499
+ writeValue(value) {
500
+ const checked = !!value;
501
+ this.elementRef.nativeElement.checked = checked;
502
+ this.isChecked.set(checked);
503
+ }
504
+ registerOnChange(fn) {
505
+ this.onChange = fn;
506
+ }
507
+ registerOnTouched(fn) {
508
+ this.onTouched = fn;
509
+ }
510
+ setDisabledState(isDisabled) {
511
+ this._formDisabled.set(isDisabled);
512
+ }
513
+ onChangeEvent(event) {
514
+ const checked = event.target.checked;
515
+ this.isChecked.set(checked);
516
+ this.onChange(checked);
517
+ }
518
+ onBlur() {
519
+ this.onTouched();
520
+ }
521
+ /**
522
+ * Set the aria-describedby attribute value.
523
+ * Typically called by helper/error text directives.
524
+ */
525
+ setDescribedBy(value) {
526
+ this.describedBy.set(value);
527
+ }
528
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmCheckboxDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
529
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.9", type: LmCheckboxDirective, isStandalone: true, selector: "input[type=\"checkbox\"][lumaCheckbox]", inputs: { lmError: { classPropertyName: "lmError", publicName: "lmError", isSignal: true, isRequired: false, transformFunction: null }, lmDisabled: { classPropertyName: "lmDisabled", publicName: "lmDisabled", isSignal: true, isRequired: false, transformFunction: null }, lmRequired: { classPropertyName: "lmRequired", publicName: "lmRequired", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "change": "onChangeEvent($event)", "blur": "onBlur()" }, properties: { "class": "classes()", "attr.id": "elementId()", "attr.aria-invalid": "hasError()", "attr.aria-describedby": "describedBy()", "attr.disabled": "isDisabled() ? \"\" : null", "attr.required": "lmRequired() ? \"\" : null", "style.background-image": "isChecked() ? checkmarkSvg : null", "style.background-repeat": "isChecked() ? \"no-repeat\" : null", "style.background-position": "isChecked() ? \"center\" : null", "style.background-size": "isChecked() ? \"75%\" : null" } }, providers: [
530
+ {
531
+ provide: NG_VALUE_ACCESSOR,
532
+ useExisting: forwardRef(() => LmCheckboxDirective),
533
+ multi: true,
534
+ },
535
+ ], ngImport: i0 });
536
+ }
537
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmCheckboxDirective, decorators: [{
538
+ type: Directive,
539
+ args: [{
540
+ selector: 'input[type="checkbox"][lumaCheckbox]',
541
+ standalone: true,
542
+ host: {
543
+ '[class]': 'classes()',
544
+ '[attr.id]': 'elementId()',
545
+ '[attr.aria-invalid]': 'hasError()',
546
+ '[attr.aria-describedby]': 'describedBy()',
547
+ '[attr.disabled]': 'isDisabled() ? "" : null',
548
+ '[attr.required]': 'lmRequired() ? "" : null',
549
+ '[style.background-image]': 'isChecked() ? checkmarkSvg : null',
550
+ '[style.background-repeat]': 'isChecked() ? "no-repeat" : null',
551
+ '[style.background-position]': 'isChecked() ? "center" : null',
552
+ '[style.background-size]': 'isChecked() ? "75%" : null',
553
+ '(change)': 'onChangeEvent($event)',
554
+ '(blur)': 'onBlur()',
555
+ },
556
+ providers: [
557
+ {
558
+ provide: NG_VALUE_ACCESSOR,
559
+ useExisting: forwardRef(() => LmCheckboxDirective),
560
+ multi: true,
561
+ },
562
+ ],
563
+ }]
564
+ }], propDecorators: { lmError: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmError", required: false }] }], lmDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmDisabled", required: false }] }], lmRequired: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmRequired", required: false }] }] } });
565
+
566
+ let radioIdCounter = 0;
567
+ /**
568
+ * Radio button directive with CVA variants and Angular Forms integration.
569
+ * Emits `lmValue` to the FormControl when checked.
570
+ * The inner dot indicator is rendered via a signal-bound `box-shadow` style.
571
+ *
572
+ * @example
573
+ * <input type="radio" lumaRadio lmValue="option-a" [formControl]="optionControl" />
574
+ */
575
+ class LmRadioDirective {
576
+ // Use Injector to avoid circular dependency
577
+ injector = inject(Injector);
578
+ elementRef = inject((ElementRef));
579
+ ngControl = null;
580
+ destroyRef = inject(DestroyRef);
581
+ // Inputs
582
+ lmValue = input(undefined, ...(ngDevMode ? [{ debugName: "lmValue" }] : []));
583
+ lmError = input(false, ...(ngDevMode ? [{ debugName: "lmError" }] : []));
584
+ lmDisabled = input(false, ...(ngDevMode ? [{ debugName: "lmDisabled" }] : []));
585
+ lmRequired = input(false, ...(ngDevMode ? [{ debugName: "lmRequired" }] : []));
586
+ // Internal signal for disabled state coming from FormControl.disable()
587
+ _formDisabled = signal(false, ...(ngDevMode ? [{ debugName: "_formDisabled" }] : []));
588
+ // Internal signals for form validation state (bridging non-signal FormControl to signal graph)
589
+ _formInvalid = signal(false, ...(ngDevMode ? [{ debugName: "_formInvalid" }] : []));
590
+ _formTouched = signal(false, ...(ngDevMode ? [{ debugName: "_formTouched" }] : []));
591
+ // Internal signal tracking whether this radio is currently selected
592
+ isChecked = signal(false, ...(ngDevMode ? [{ debugName: "isChecked" }] : []));
593
+ // Final disabled state: manual lmDisabled OR programmatic FormControl.disable()
594
+ isDisabled = computed(() => this.lmDisabled() || this._formDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
595
+ // Preserve static id="..." from template; fall back to auto-generated id.
596
+ // elementRef must be declared before this field so it's available at init time.
597
+ elementId = signal(this.elementRef.nativeElement.getAttribute('id') ||
598
+ `luma-radio-${radioIdCounter++}`, ...(ngDevMode ? [{ debugName: "elementId" }] : []));
599
+ // ARIA describedby (can be set by parent or manually)
600
+ describedBy = signal(null, ...(ngDevMode ? [{ debugName: "describedBy" }] : []));
601
+ // Computed signal — reactive to both manual lmError and FormControl state
602
+ hasError = computed(() => this.lmError() || (this._formInvalid() && this._formTouched()), ...(ngDevMode ? [{ debugName: "hasError" }] : []));
603
+ // Computed classes
604
+ classes = computed(() => radioVariants({ error: this.hasError() }), ...(ngDevMode ? [{ debugName: "classes" }] : []));
605
+ // noop – replaced at runtime by registerOnChange / registerOnTouched
606
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
607
+ onChange = () => { };
608
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
609
+ onTouched = () => { };
610
+ ngOnInit() {
611
+ // Sync initial checked state from native element.
612
+ // Needed when a static `checked` attribute is used without a FormControl —
613
+ // writeValue() is never called in that case, so isChecked stays false.
614
+ this.isChecked.set(this.elementRef.nativeElement.checked);
615
+ // Get NgControl from Injector to avoid circular dependency
616
+ try {
617
+ this.ngControl = this.injector.get(NgControl, null, {
618
+ optional: true,
619
+ self: true,
620
+ });
621
+ if (this.ngControl) {
622
+ this.ngControl.valueAccessor = this;
623
+ const control = this.ngControl.control;
624
+ if (control) {
625
+ // Capture immediate state (e.g. markAsTouched() called before ngOnInit)
626
+ this._formInvalid.set(control.invalid);
627
+ this._formTouched.set(control.touched);
628
+ // React to any future FormControl state changes
629
+ // control.events emits for ALL changes: value, status, touched, pristine
630
+ // statusChanges alone does NOT emit for markAsTouched()
631
+ control.events
632
+ .pipe(takeUntilDestroyed(this.destroyRef))
633
+ .subscribe(() => {
634
+ this._formInvalid.set(control.invalid);
635
+ this._formTouched.set(control.touched);
636
+ });
637
+ }
638
+ }
639
+ }
640
+ catch {
641
+ // No NgControl present
642
+ this.ngControl = null;
643
+ }
644
+ // Standalone usage (no FormControl): when the user selects a sibling radio in the
645
+ // same name group, the browser silently sets this radio's .checked = false without
646
+ // firing a change event on it. We listen on document and re-read native state.
647
+ const el = this.elementRef.nativeElement;
648
+ const onSiblingChange = (e) => {
649
+ const target = e.target;
650
+ if (target !== el &&
651
+ target.type === 'radio' &&
652
+ !!el.name &&
653
+ target.name === el.name) {
654
+ this.isChecked.set(el.checked);
655
+ }
656
+ };
657
+ document.addEventListener('change', onSiblingChange);
658
+ this.destroyRef.onDestroy(() => document.removeEventListener('change', onSiblingChange));
659
+ }
660
+ writeValue(value) {
661
+ // Check the radio if the form value equals this radio's value
662
+ const checked = value === this.lmValue();
663
+ this.elementRef.nativeElement.checked = checked;
664
+ this.isChecked.set(checked);
665
+ }
666
+ registerOnChange(fn) {
667
+ this.onChange = fn;
668
+ }
669
+ registerOnTouched(fn) {
670
+ this.onTouched = fn;
671
+ }
672
+ setDisabledState(isDisabled) {
673
+ this._formDisabled.set(isDisabled);
674
+ }
675
+ onChangeEvent(event) {
676
+ // Only emit when this radio is checked (not when another radio in the group deselects it)
677
+ if (event.target.checked) {
678
+ this.isChecked.set(true);
679
+ this.onChange(this.lmValue());
680
+ }
681
+ }
682
+ onBlur() {
683
+ this.onTouched();
684
+ }
685
+ /**
686
+ * Set the aria-describedby attribute value.
687
+ * Typically called by helper/error text directives.
688
+ */
689
+ setDescribedBy(value) {
690
+ this.describedBy.set(value);
691
+ }
692
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmRadioDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
693
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.9", type: LmRadioDirective, isStandalone: true, selector: "input[type=\"radio\"][lumaRadio]", inputs: { lmValue: { classPropertyName: "lmValue", publicName: "lmValue", isSignal: true, isRequired: false, transformFunction: null }, lmError: { classPropertyName: "lmError", publicName: "lmError", isSignal: true, isRequired: false, transformFunction: null }, lmDisabled: { classPropertyName: "lmDisabled", publicName: "lmDisabled", isSignal: true, isRequired: false, transformFunction: null }, lmRequired: { classPropertyName: "lmRequired", publicName: "lmRequired", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "change": "onChangeEvent($event)", "blur": "onBlur()" }, properties: { "class": "classes()", "attr.id": "elementId()", "attr.aria-invalid": "hasError()", "attr.aria-describedby": "describedBy()", "attr.disabled": "isDisabled() ? \"\" : null", "attr.required": "lmRequired() ? \"\" : null", "style.box-shadow": "isChecked() ? \"inset 0 0 0 3px var(--color-background)\" : null", "style.background-color": "isChecked() ? \"var(--color-primary)\" : null", "style.border-color": "isChecked() ? \"var(--color-primary)\" : null" } }, providers: [
694
+ {
695
+ provide: NG_VALUE_ACCESSOR,
696
+ useExisting: forwardRef(() => LmRadioDirective),
697
+ multi: true,
698
+ },
699
+ ], ngImport: i0 });
700
+ }
701
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmRadioDirective, decorators: [{
702
+ type: Directive,
703
+ args: [{
704
+ selector: 'input[type="radio"][lumaRadio]',
705
+ standalone: true,
706
+ host: {
707
+ '[class]': 'classes()',
708
+ '[attr.id]': 'elementId()',
709
+ '[attr.aria-invalid]': 'hasError()',
710
+ '[attr.aria-describedby]': 'describedBy()',
711
+ '[attr.disabled]': 'isDisabled() ? "" : null',
712
+ '[attr.required]': 'lmRequired() ? "" : null',
713
+ // Inner dot: background fills circle, inset shadow carves a gap between border and dot
714
+ '[style.box-shadow]': 'isChecked() ? "inset 0 0 0 3px var(--color-background)" : null',
715
+ '[style.background-color]': 'isChecked() ? "var(--color-primary)" : null',
716
+ '[style.border-color]': 'isChecked() ? "var(--color-primary)" : null',
717
+ '(change)': 'onChangeEvent($event)',
718
+ '(blur)': 'onBlur()',
719
+ },
720
+ providers: [
721
+ {
722
+ provide: NG_VALUE_ACCESSOR,
723
+ useExisting: forwardRef(() => LmRadioDirective),
724
+ multi: true,
725
+ },
726
+ ],
727
+ }]
728
+ }], propDecorators: { lmValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmValue", required: false }] }], lmError: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmError", required: false }] }], lmDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmDisabled", required: false }] }], lmRequired: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmRequired", required: false }] }] } });
729
+
66
730
  class LmCardComponent {
67
731
  /**
68
732
  * Card visual style variant
@@ -2991,11 +3655,449 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImpor
2991
3655
 
2992
3656
  // Toast public API
2993
3657
 
3658
+ const SELECT_CONTEXT = new InjectionToken('SelectContext');
3659
+
3660
+ let selectIdCounter = 0;
3661
+ class LmSelectComponent {
3662
+ injector = inject(Injector);
3663
+ destroyRef = inject(DestroyRef);
3664
+ elementRef = inject((ElementRef));
3665
+ document = inject(DOCUMENT);
3666
+ platformId = inject(PLATFORM_ID);
3667
+ ngControl = null;
3668
+ // ── Inputs
3669
+ lmSize = input('md', ...(ngDevMode ? [{ debugName: "lmSize" }] : []));
3670
+ lmMultiple = input(false, ...(ngDevMode ? [{ debugName: "lmMultiple" }] : []));
3671
+ lmPlaceholder = input('Select…', ...(ngDevMode ? [{ debugName: "lmPlaceholder" }] : []));
3672
+ lmSearchPlaceholder = input('Search…', ...(ngDevMode ? [{ debugName: "lmSearchPlaceholder" }] : []));
3673
+ lmDisabled = input(false, ...(ngDevMode ? [{ debugName: "lmDisabled" }] : []));
3674
+ lmRequired = input(false, ...(ngDevMode ? [{ debugName: "lmRequired" }] : []));
3675
+ lmError = input(false, ...(ngDevMode ? [{ debugName: "lmError" }] : []));
3676
+ lmEmptyMessage = input('No options found', ...(ngDevMode ? [{ debugName: "lmEmptyMessage" }] : []));
3677
+ lmClearable = input(false, ...(ngDevMode ? [{ debugName: "lmClearable" }] : []));
3678
+ lmCompareWith = input(null, ...(ngDevMode ? [{ debugName: "lmCompareWith" }] : []));
3679
+ // ── Outputs
3680
+ lmValueChange = output();
3681
+ // ── IDs: read the host id attribute before host binding removes it
3682
+ selectId = signal(this.elementRef.nativeElement.getAttribute('id') ||
3683
+ `luma-select-${selectIdCounter++}`, ...(ngDevMode ? [{ debugName: "selectId" }] : []));
3684
+ listboxId = computed(() => `${this.selectId()}-listbox`, ...(ngDevMode ? [{ debugName: "listboxId" }] : []));
3685
+ searchInputId = computed(() => `${this.selectId()}-search`, ...(ngDevMode ? [{ debugName: "searchInputId" }] : []));
3686
+ /** ID of the currently focused option for aria-activedescendant */
3687
+ activeDescendantId = computed(() => {
3688
+ const idx = this.focusedOptionIndex();
3689
+ return idx >= 0 ? `${this.selectId()}-opt-${idx}` : null;
3690
+ }, ...(ngDevMode ? [{ debugName: "activeDescendantId" }] : []));
3691
+ /** Whether the select has a value (for clear button visibility) */
3692
+ hasValue = computed(() => {
3693
+ if (this.lmMultiple())
3694
+ return this._multiValue().length > 0;
3695
+ const v = this._singleValue();
3696
+ return v !== null && v !== undefined;
3697
+ }, ...(ngDevMode ? [{ debugName: "hasValue" }] : []));
3698
+ // ── Selection state
3699
+ _singleValue = signal(null, ...(ngDevMode ? [{ debugName: "_singleValue" }] : []));
3700
+ _multiValue = signal([], ...(ngDevMode ? [{ debugName: "_multiValue" }] : []));
3701
+ // ── Open state
3702
+ _internalOpen = signal(false, ...(ngDevMode ? [{ debugName: "_internalOpen" }] : []));
3703
+ isOpen = this._internalOpen.asReadonly();
3704
+ // ── Search + options registry
3705
+ searchQuery = signal('', ...(ngDevMode ? [{ debugName: "searchQuery" }] : []));
3706
+ _options = signal([], ...(ngDevMode ? [{ debugName: "_options" }] : []));
3707
+ // ── Form state
3708
+ _formDisabled = signal(false, ...(ngDevMode ? [{ debugName: "_formDisabled" }] : []));
3709
+ _formInvalid = signal(false, ...(ngDevMode ? [{ debugName: "_formInvalid" }] : []));
3710
+ _formTouched = signal(false, ...(ngDevMode ? [{ debugName: "_formTouched" }] : []));
3711
+ // ── Derived state
3712
+ isDisabled = computed(() => this.lmDisabled() || this._formDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
3713
+ hasError = computed(() => this.lmError() || (this._formInvalid() && this._formTouched()), ...(ngDevMode ? [{ debugName: "hasError" }] : []));
3714
+ /** SelectContext interface */
3715
+ isMultiple = computed(() => this.lmMultiple(), ...(ngDevMode ? [{ debugName: "isMultiple" }] : []));
3716
+ /** Compare two values using the custom compareWith function or strict equality */
3717
+ compareValues(a, b) {
3718
+ const fn = this.lmCompareWith();
3719
+ return fn ? fn(a, b) : a === b;
3720
+ }
3721
+ /**
3722
+ * Combines filtering with selected-state baking in a single computed.
3723
+ * Avoids creating per-row signal instances; the whole array recomputes
3724
+ * when search query, options, or selection state changes.
3725
+ */
3726
+ filteredOptionsWithState = computed(() => {
3727
+ const query = this.searchQuery().toLowerCase().trim();
3728
+ const opts = this._options();
3729
+ const filtered = query
3730
+ ? opts.filter((o) => o.label().toLowerCase().includes(query))
3731
+ : opts;
3732
+ const multiple = this.lmMultiple();
3733
+ const single = this._singleValue();
3734
+ const multi = this._multiValue();
3735
+ return filtered.map((o) => ({
3736
+ entry: o,
3737
+ isSelected: multiple
3738
+ ? multi.some((v) => this.compareValues(v, o.value))
3739
+ : this.compareValues(o.value, single),
3740
+ }));
3741
+ }, ...(ngDevMode ? [{ debugName: "filteredOptionsWithState" }] : []));
3742
+ /** Label shown in the trigger when the dropdown is closed (single-select only) */
3743
+ triggerLabel = computed(() => {
3744
+ // Multi-select displays chips instead of a text label
3745
+ if (this.lmMultiple())
3746
+ return null;
3747
+ const val = this._singleValue();
3748
+ if (val === null || val === undefined)
3749
+ return null;
3750
+ const opts = this._options();
3751
+ return (opts.find((o) => this.compareValues(o.value, val))?.label() ?? String(val));
3752
+ }, ...(ngDevMode ? [{ debugName: "triggerLabel" }] : []));
3753
+ /** Chip data for multi-select: maps selected values to { value, label } */
3754
+ selectedChips = computed(() => {
3755
+ if (!this.lmMultiple())
3756
+ return [];
3757
+ const vals = this._multiValue();
3758
+ const opts = this._options();
3759
+ return vals.map((v) => ({
3760
+ value: v,
3761
+ label: opts.find((o) => this.compareValues(o.value, v))?.label() ?? String(v),
3762
+ }));
3763
+ }, ...(ngDevMode ? [{ debugName: "selectedChips" }] : []));
3764
+ // ── Dropdown flip (one-time check on open — no event listeners)
3765
+ _flipAbove = signal(false, ...(ngDevMode ? [{ debugName: "_flipAbove" }] : []));
3766
+ // ── Keyboard focus within the listbox
3767
+ focusedOptionIndex = signal(-1, ...(ngDevMode ? [{ debugName: "focusedOptionIndex" }] : []));
3768
+ // ── ViewChild refs
3769
+ searchInputRef = viewChild('searchInputEl', ...(ngDevMode ? [{ debugName: "searchInputRef" }] : []));
3770
+ triggerRef = viewChild('triggerEl', ...(ngDevMode ? [{ debugName: "triggerRef" }] : []));
3771
+ listboxRef = viewChild('listboxEl', ...(ngDevMode ? [{ debugName: "listboxRef" }] : []));
3772
+ // ── CVA classes (computed for reactivity)
3773
+ triggerClasses = computed(() => selectTriggerVariants({
3774
+ size: this.lmSize(),
3775
+ multiple: this.lmMultiple(),
3776
+ error: this.hasError(),
3777
+ disabled: this.isDisabled(),
3778
+ }), ...(ngDevMode ? [{ debugName: "triggerClasses" }] : []));
3779
+ listboxClasses = computed(() => selectListboxVariants({ flip: this._flipAbove() }), ...(ngDevMode ? [{ debugName: "listboxClasses" }] : []));
3780
+ searchInputClasses = selectSearchInputVariants();
3781
+ /** Chip classes — reactive to size */
3782
+ chipClasses = computed(() => selectChipVariants({ size: this.lmSize() }), ...(ngDevMode ? [{ debugName: "chipClasses" }] : []));
3783
+ /** Chip dismiss button classes — reactive to size */
3784
+ chipDismissClasses = computed(() => selectChipDismissVariants({ size: this.lmSize() }), ...(ngDevMode ? [{ debugName: "chipDismissClasses" }] : []));
3785
+ optionClasses(isSelected, disabled, isFocused) {
3786
+ const base = selectOptionVariants({ selected: isSelected, disabled });
3787
+ return isFocused && !disabled ? `${base} bg-primary/10` : base;
3788
+ }
3789
+ // noop – replaced at runtime by registerOnChange / registerOnTouched
3790
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
3791
+ onChange = () => { };
3792
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
3793
+ onTouched = () => { };
3794
+ // ── Event listener refs for cleanup
3795
+ clickOutsideHandler = null;
3796
+ constructor() {
3797
+ // React to open state changes in a single effect
3798
+ effect(() => {
3799
+ if (!isPlatformBrowser(this.platformId))
3800
+ return;
3801
+ if (this._internalOpen()) {
3802
+ this.checkFlipPosition();
3803
+ this.registerClickOutside();
3804
+ // Defer focus so the input element has been rendered
3805
+ setTimeout(() => this.searchInputRef()?.nativeElement.focus(), 0);
3806
+ }
3807
+ });
3808
+ // Scroll focused option into view when keyboard-navigating
3809
+ effect(() => {
3810
+ const idx = this.focusedOptionIndex();
3811
+ if (idx < 0)
3812
+ return;
3813
+ const listbox = this.listboxRef()?.nativeElement;
3814
+ if (!listbox)
3815
+ return;
3816
+ const optionEl = listbox.querySelector(`[id$='-opt-${idx}']`);
3817
+ optionEl?.scrollIntoView({ block: 'nearest' });
3818
+ });
3819
+ }
3820
+ ngOnInit() {
3821
+ try {
3822
+ this.ngControl = this.injector.get(NgControl, null, {
3823
+ optional: true,
3824
+ self: true,
3825
+ });
3826
+ if (this.ngControl) {
3827
+ this.ngControl.valueAccessor = this;
3828
+ const control = this.ngControl.control;
3829
+ if (control) {
3830
+ this._formInvalid.set(control.invalid);
3831
+ this._formTouched.set(control.touched);
3832
+ control.events
3833
+ .pipe(takeUntilDestroyed(this.destroyRef))
3834
+ .subscribe(() => {
3835
+ this._formInvalid.set(control.invalid);
3836
+ this._formTouched.set(control.touched);
3837
+ });
3838
+ }
3839
+ }
3840
+ }
3841
+ catch {
3842
+ this.ngControl = null;
3843
+ }
3844
+ }
3845
+ ngOnDestroy() {
3846
+ this.unregisterClickOutside();
3847
+ }
3848
+ // ── SelectContext implementation
3849
+ registerOption(entry) {
3850
+ this._options.update((opts) => [...opts, entry]);
3851
+ }
3852
+ unregisterOption(value) {
3853
+ this._options.update((opts) => opts.filter((o) => o.value !== value));
3854
+ }
3855
+ // ── Open / close
3856
+ open() {
3857
+ if (this.isDisabled())
3858
+ return;
3859
+ this._internalOpen.set(true);
3860
+ }
3861
+ close() {
3862
+ this._internalOpen.set(false);
3863
+ // Reset search state synchronously so tests and consumers see immediate update
3864
+ this.searchQuery.set('');
3865
+ this.focusedOptionIndex.set(-1);
3866
+ this.onTouched();
3867
+ this.unregisterClickOutside();
3868
+ setTimeout(() => this.triggerRef()?.nativeElement.focus(), 0);
3869
+ }
3870
+ toggle() {
3871
+ if (this.isOpen()) {
3872
+ this.close();
3873
+ }
3874
+ else {
3875
+ this.open();
3876
+ }
3877
+ }
3878
+ // ── Value selection
3879
+ selectValue(value) {
3880
+ if (this.lmMultiple()) {
3881
+ const current = this._multiValue();
3882
+ const idx = current.findIndex((v) => this.compareValues(v, value));
3883
+ const next = idx >= 0 ? current.filter((_, i) => i !== idx) : [...current, value];
3884
+ this._multiValue.set(next);
3885
+ this.onChange(next);
3886
+ this.lmValueChange.emit(next);
3887
+ // In multi-select: keep open, clear the search filter
3888
+ this.searchQuery.set('');
3889
+ }
3890
+ else {
3891
+ this._singleValue.set(value);
3892
+ this.onChange(value);
3893
+ this.lmValueChange.emit(value);
3894
+ this.close();
3895
+ }
3896
+ }
3897
+ /** Remove a chip by its value (without opening/closing the dropdown) */
3898
+ removeChip(event, value) {
3899
+ event.stopPropagation();
3900
+ // Toggle the value off via selectValue, but preserve open/close state
3901
+ const current = this._multiValue();
3902
+ const next = current.filter((v) => !this.compareValues(v, value));
3903
+ this._multiValue.set(next);
3904
+ this.onChange(next);
3905
+ this.lmValueChange.emit(next);
3906
+ }
3907
+ /** Clear the current selection */
3908
+ clear(event) {
3909
+ event?.stopPropagation();
3910
+ if (this.lmMultiple()) {
3911
+ this._multiValue.set([]);
3912
+ this.onChange([]);
3913
+ this.lmValueChange.emit([]);
3914
+ }
3915
+ else {
3916
+ this._singleValue.set(null);
3917
+ this.onChange(null);
3918
+ this.lmValueChange.emit(null);
3919
+ }
3920
+ }
3921
+ // ── Keyboard handlers for the closed trigger
3922
+ onTriggerKeydown(event) {
3923
+ if (['Enter', ' ', 'ArrowDown'].includes(event.key)) {
3924
+ event.preventDefault();
3925
+ this.open();
3926
+ }
3927
+ }
3928
+ // ── Keyboard handlers for the search input (open state)
3929
+ onSearchInput(event) {
3930
+ this.searchQuery.set(event.target.value);
3931
+ this.focusedOptionIndex.set(-1);
3932
+ }
3933
+ onSearchKeydown(event) {
3934
+ const opts = this.filteredOptionsWithState();
3935
+ switch (event.key) {
3936
+ case 'ArrowDown':
3937
+ event.preventDefault();
3938
+ this.focusedOptionIndex.update((i) => Math.min(i + 1, opts.length - 1));
3939
+ break;
3940
+ case 'ArrowUp':
3941
+ event.preventDefault();
3942
+ this.focusedOptionIndex.update((i) => Math.max(i - 1, 0));
3943
+ break;
3944
+ case 'Home':
3945
+ event.preventDefault();
3946
+ if (opts.length > 0)
3947
+ this.focusedOptionIndex.set(0);
3948
+ break;
3949
+ case 'End':
3950
+ event.preventDefault();
3951
+ if (opts.length > 0)
3952
+ this.focusedOptionIndex.set(opts.length - 1);
3953
+ break;
3954
+ case 'Enter': {
3955
+ event.preventDefault();
3956
+ const idx = this.focusedOptionIndex();
3957
+ if (idx >= 0 && idx < opts.length) {
3958
+ const { entry } = opts[idx];
3959
+ if (!entry.disabled())
3960
+ this.selectValue(entry.value);
3961
+ }
3962
+ break;
3963
+ }
3964
+ case 'Escape':
3965
+ event.preventDefault();
3966
+ this.close();
3967
+ break;
3968
+ case 'Tab':
3969
+ this.close();
3970
+ break;
3971
+ case 'Backspace': {
3972
+ // In multi-select: remove last chip when search is empty
3973
+ if (this.lmMultiple() && this.searchQuery() === '') {
3974
+ const vals = this._multiValue();
3975
+ if (vals.length > 0) {
3976
+ this.selectValue(vals[vals.length - 1]);
3977
+ }
3978
+ }
3979
+ break;
3980
+ }
3981
+ }
3982
+ }
3983
+ // ── One-time flip check (no event listeners needed)
3984
+ checkFlipPosition() {
3985
+ const trigger = this.triggerRef()?.nativeElement;
3986
+ if (!trigger)
3987
+ return;
3988
+ const rect = trigger.getBoundingClientRect();
3989
+ const dropdownMaxHeight = 240; // matches max-h-60 (15rem = 240px)
3990
+ const gap = 4;
3991
+ const spaceBelow = window.innerHeight - rect.bottom;
3992
+ this._flipAbove.set(spaceBelow < dropdownMaxHeight + gap &&
3993
+ rect.top > dropdownMaxHeight + gap);
3994
+ }
3995
+ registerClickOutside() {
3996
+ if (this.clickOutsideHandler)
3997
+ return;
3998
+ this.clickOutsideHandler = (e) => {
3999
+ if (!this.elementRef.nativeElement.contains(e.target)) {
4000
+ this.close();
4001
+ }
4002
+ };
4003
+ // Small timeout to avoid the current click event triggering close immediately
4004
+ setTimeout(() => {
4005
+ if (this.clickOutsideHandler) {
4006
+ this.document.addEventListener('click', this.clickOutsideHandler);
4007
+ }
4008
+ }, 0);
4009
+ }
4010
+ unregisterClickOutside() {
4011
+ if (this.clickOutsideHandler) {
4012
+ this.document.removeEventListener('click', this.clickOutsideHandler);
4013
+ this.clickOutsideHandler = null;
4014
+ }
4015
+ }
4016
+ // ── ControlValueAccessor
4017
+ writeValue(value) {
4018
+ if (this.lmMultiple()) {
4019
+ this._multiValue.set(Array.isArray(value) ? value : value != null ? [value] : []);
4020
+ }
4021
+ else {
4022
+ this._singleValue.set(value ?? null);
4023
+ }
4024
+ }
4025
+ registerOnChange(fn) {
4026
+ this.onChange = fn;
4027
+ }
4028
+ registerOnTouched(fn) {
4029
+ this.onTouched = fn;
4030
+ }
4031
+ setDisabledState(isDisabled) {
4032
+ this._formDisabled.set(isDisabled);
4033
+ }
4034
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4035
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.9", type: LmSelectComponent, isStandalone: true, selector: "luma-select", inputs: { lmSize: { classPropertyName: "lmSize", publicName: "lmSize", isSignal: true, isRequired: false, transformFunction: null }, lmMultiple: { classPropertyName: "lmMultiple", publicName: "lmMultiple", isSignal: true, isRequired: false, transformFunction: null }, lmPlaceholder: { classPropertyName: "lmPlaceholder", publicName: "lmPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, lmSearchPlaceholder: { classPropertyName: "lmSearchPlaceholder", publicName: "lmSearchPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, lmDisabled: { classPropertyName: "lmDisabled", publicName: "lmDisabled", isSignal: true, isRequired: false, transformFunction: null }, lmRequired: { classPropertyName: "lmRequired", publicName: "lmRequired", isSignal: true, isRequired: false, transformFunction: null }, lmError: { classPropertyName: "lmError", publicName: "lmError", isSignal: true, isRequired: false, transformFunction: null }, lmEmptyMessage: { classPropertyName: "lmEmptyMessage", publicName: "lmEmptyMessage", isSignal: true, isRequired: false, transformFunction: null }, lmClearable: { classPropertyName: "lmClearable", publicName: "lmClearable", isSignal: true, isRequired: false, transformFunction: null }, lmCompareWith: { classPropertyName: "lmCompareWith", publicName: "lmCompareWith", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { lmValueChange: "lmValueChange" }, host: { properties: { "attr.id": "null" }, classAttribute: "block relative w-full" }, providers: [
4036
+ { provide: SELECT_CONTEXT, useExisting: LmSelectComponent },
4037
+ {
4038
+ provide: NG_VALUE_ACCESSOR,
4039
+ useExisting: forwardRef(() => LmSelectComponent),
4040
+ multi: true,
4041
+ },
4042
+ ], viewQueries: [{ propertyName: "searchInputRef", first: true, predicate: ["searchInputEl"], descendants: true, isSignal: true }, { propertyName: "triggerRef", first: true, predicate: ["triggerEl"], descendants: true, isSignal: true }, { propertyName: "listboxRef", first: true, predicate: ["listboxEl"], descendants: true, isSignal: true }], ngImport: i0, template: "<!-- Hidden slot: options run lifecycle hooks and register themselves via SELECT_CONTEXT,\n but are never shown from this slot. The visible listbox is rendered below. -->\n<div class=\"hidden\" aria-hidden=\"true\">\n <ng-content />\n</div>\n\n<!-- Trigger (id placed here for <label for=\"\u2026\"> association; host id removed via host binding) -->\n<div\n #triggerEl\n [attr.id]=\"selectId()\"\n [class]=\"triggerClasses()\"\n [attr.tabindex]=\"isDisabled() ? -1 : 0\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-haspopup]=\"'listbox'\"\n [attr.aria-controls]=\"listboxId()\"\n [attr.aria-invalid]=\"hasError()\"\n [attr.aria-required]=\"lmRequired()\"\n [attr.aria-disabled]=\"isDisabled()\"\n role=\"combobox\"\n (click)=\"!isOpen() ? toggle() : null\"\n (keydown)=\"!isOpen() ? onTriggerKeydown($event) : null\"\n>\n @if (isOpen()) {\n <!-- Open state -->\n @if (lmMultiple() && selectedChips().length > 0) {\n <!-- Multi-select open: chips + inline search -->\n @for (chip of selectedChips(); track chip.value) {\n <span [class]=\"chipClasses()\">\n <span class=\"truncate\">{{ chip.label }}</span>\n <button\n type=\"button\"\n [class]=\"chipDismissClasses()\"\n [attr.aria-label]=\"'Remove ' + chip.label\"\n (click)=\"removeChip($event, chip.value)\"\n >\n <svg\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n class=\"h-full w-full\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M5 5l6 6M11 5l-6 6\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n />\n </svg>\n </button>\n </span>\n }\n <input\n #searchInputEl\n type=\"text\"\n [id]=\"searchInputId()\"\n class=\"flex-1 bg-transparent outline-none border-none min-w-15 text-foreground placeholder:text-muted-foreground\"\n [value]=\"searchQuery()\"\n [placeholder]=\"lmSearchPlaceholder()\"\n [attr.aria-label]=\"lmSearchPlaceholder()\"\n aria-autocomplete=\"list\"\n [attr.aria-controls]=\"listboxId()\"\n [attr.aria-activedescendant]=\"activeDescendantId()\"\n autocomplete=\"off\"\n (input)=\"onSearchInput($event)\"\n (keydown)=\"onSearchKeydown($event)\"\n (click)=\"$event.stopPropagation()\"\n />\n } @else {\n <!-- Single-select or empty multi: just the search input -->\n <input\n #searchInputEl\n type=\"text\"\n [id]=\"searchInputId()\"\n [class]=\"searchInputClasses\"\n [value]=\"searchQuery()\"\n [placeholder]=\"lmSearchPlaceholder()\"\n [attr.aria-label]=\"lmSearchPlaceholder()\"\n aria-autocomplete=\"list\"\n [attr.aria-controls]=\"listboxId()\"\n [attr.aria-activedescendant]=\"activeDescendantId()\"\n autocomplete=\"off\"\n (input)=\"onSearchInput($event)\"\n (keydown)=\"onSearchKeydown($event)\"\n (click)=\"$event.stopPropagation()\"\n />\n }\n } @else {\n <!-- Closed state -->\n @if (lmMultiple() && selectedChips().length > 0) {\n <!-- Multi-select closed: show chips -->\n <div class=\"flex flex-wrap gap-1 flex-1 min-w-0 max-h-32 overflow-y-auto\">\n @for (chip of selectedChips(); track chip.value) {\n <span [class]=\"chipClasses()\">\n <span class=\"truncate\">{{ chip.label }}</span>\n <button\n type=\"button\"\n [class]=\"chipDismissClasses()\"\n [attr.aria-label]=\"'Remove ' + chip.label\"\n (click)=\"removeChip($event, chip.value)\"\n >\n <svg\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n class=\"h-full w-full\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M5 5l6 6M11 5l-6 6\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n />\n </svg>\n </button>\n </span>\n }\n </div>\n } @else {\n <!-- Single-select or empty multi: label/placeholder -->\n <span\n class=\"flex-1 truncate min-w-0\"\n [class.text-muted-foreground]=\"!triggerLabel()\"\n >\n {{ triggerLabel() ?? lmPlaceholder() }}\n </span>\n }\n }\n\n <!-- Clear button \u2014 shown when clearable and has value -->\n @if (lmClearable() && hasValue() && !isDisabled() && !isOpen()) {\n <button\n type=\"button\"\n class=\"shrink-0 h-4 w-4 flex items-center justify-center text-muted-foreground hover:text-foreground transition-colors\"\n aria-label=\"Clear selection\"\n (click)=\"clear($event)\"\n >\n <svg\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n class=\"h-3.5 w-3.5\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M4 4l8 8M12 4l-8 8\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n />\n </svg>\n </button>\n }\n\n <!-- Chevron icon \u2014 rotates 180\u00B0 when open -->\n <svg\n class=\"ml-auto shrink-0 h-4 w-4 text-muted-foreground transition-transform duration-200\"\n [class.rotate-180]=\"isOpen()\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M4 6l4 4 4-4\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n</div>\n\n<!-- Listbox panel (absolute positioning \u2014 CSS-only, no JS tracking) -->\n@if (isOpen()) {\n <div\n #listboxEl\n [id]=\"listboxId()\"\n role=\"listbox\"\n [attr.aria-label]=\"lmPlaceholder()\"\n [attr.aria-multiselectable]=\"lmMultiple()\"\n [class]=\"listboxClasses()\"\n >\n @if (filteredOptionsWithState().length === 0) {\n <div class=\"px-3 py-4 text-sm text-center text-muted-foreground\">\n {{ lmEmptyMessage() }}\n </div>\n }\n\n @for (\n item of filteredOptionsWithState();\n track item.entry.value;\n let i = $index\n ) {\n <div\n [id]=\"selectId() + '-opt-' + i\"\n role=\"option\"\n [attr.aria-selected]=\"item.isSelected\"\n [attr.aria-disabled]=\"item.entry.disabled()\"\n [attr.tabindex]=\"item.entry.disabled() ? -1 : 0\"\n [class]=\"\n optionClasses(\n item.isSelected,\n item.entry.disabled(),\n focusedOptionIndex() === i\n )\n \"\n (click)=\"selectValue(item.entry.value)\"\n (keydown.enter)=\"selectValue(item.entry.value)\"\n (keydown.space)=\"selectValue(item.entry.value)\"\n (mouseenter)=\"focusedOptionIndex.set(i)\"\n >\n @if (lmMultiple()) {\n <!-- Mini checkbox indicator for multi-select -->\n <span\n class=\"h-4 w-4 shrink-0 flex items-center justify-center rounded-(--radius-2) border transition-colors duration-200\"\n [class.bg-primary]=\"item.isSelected\"\n [class.border-primary]=\"item.isSelected\"\n [class.border-gray-5]=\"!item.isSelected\"\n aria-hidden=\"true\"\n >\n @if (item.isSelected) {\n <svg width=\"10\" height=\"10\" viewBox=\"0 0 16 16\" fill=\"none\">\n <path\n d=\"M4 8l3 3 5-5\"\n stroke=\"white\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n }\n </span>\n } @else {\n <!-- Checkmark for single-select, spacer otherwise -->\n @if (item.isSelected) {\n <svg\n class=\"h-4 w-4 shrink-0 text-primary\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n >\n <path\n d=\"M3 8l4 4 6-7\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n }\n }\n\n <span class=\"truncate\">{{ item.entry.label() }}</span>\n </div>\n }\n </div>\n}\n", changeDetection: i0.ChangeDetectionStrategy.OnPush });
4043
+ }
4044
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmSelectComponent, decorators: [{
4045
+ type: Component,
4046
+ args: [{ selector: 'luma-select', changeDetection: ChangeDetectionStrategy.OnPush, host: {
4047
+ class: 'block relative w-full',
4048
+ '[attr.id]': 'null',
4049
+ }, providers: [
4050
+ { provide: SELECT_CONTEXT, useExisting: LmSelectComponent },
4051
+ {
4052
+ provide: NG_VALUE_ACCESSOR,
4053
+ useExisting: forwardRef(() => LmSelectComponent),
4054
+ multi: true,
4055
+ },
4056
+ ], template: "<!-- Hidden slot: options run lifecycle hooks and register themselves via SELECT_CONTEXT,\n but are never shown from this slot. The visible listbox is rendered below. -->\n<div class=\"hidden\" aria-hidden=\"true\">\n <ng-content />\n</div>\n\n<!-- Trigger (id placed here for <label for=\"\u2026\"> association; host id removed via host binding) -->\n<div\n #triggerEl\n [attr.id]=\"selectId()\"\n [class]=\"triggerClasses()\"\n [attr.tabindex]=\"isDisabled() ? -1 : 0\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-haspopup]=\"'listbox'\"\n [attr.aria-controls]=\"listboxId()\"\n [attr.aria-invalid]=\"hasError()\"\n [attr.aria-required]=\"lmRequired()\"\n [attr.aria-disabled]=\"isDisabled()\"\n role=\"combobox\"\n (click)=\"!isOpen() ? toggle() : null\"\n (keydown)=\"!isOpen() ? onTriggerKeydown($event) : null\"\n>\n @if (isOpen()) {\n <!-- Open state -->\n @if (lmMultiple() && selectedChips().length > 0) {\n <!-- Multi-select open: chips + inline search -->\n @for (chip of selectedChips(); track chip.value) {\n <span [class]=\"chipClasses()\">\n <span class=\"truncate\">{{ chip.label }}</span>\n <button\n type=\"button\"\n [class]=\"chipDismissClasses()\"\n [attr.aria-label]=\"'Remove ' + chip.label\"\n (click)=\"removeChip($event, chip.value)\"\n >\n <svg\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n class=\"h-full w-full\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M5 5l6 6M11 5l-6 6\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n />\n </svg>\n </button>\n </span>\n }\n <input\n #searchInputEl\n type=\"text\"\n [id]=\"searchInputId()\"\n class=\"flex-1 bg-transparent outline-none border-none min-w-15 text-foreground placeholder:text-muted-foreground\"\n [value]=\"searchQuery()\"\n [placeholder]=\"lmSearchPlaceholder()\"\n [attr.aria-label]=\"lmSearchPlaceholder()\"\n aria-autocomplete=\"list\"\n [attr.aria-controls]=\"listboxId()\"\n [attr.aria-activedescendant]=\"activeDescendantId()\"\n autocomplete=\"off\"\n (input)=\"onSearchInput($event)\"\n (keydown)=\"onSearchKeydown($event)\"\n (click)=\"$event.stopPropagation()\"\n />\n } @else {\n <!-- Single-select or empty multi: just the search input -->\n <input\n #searchInputEl\n type=\"text\"\n [id]=\"searchInputId()\"\n [class]=\"searchInputClasses\"\n [value]=\"searchQuery()\"\n [placeholder]=\"lmSearchPlaceholder()\"\n [attr.aria-label]=\"lmSearchPlaceholder()\"\n aria-autocomplete=\"list\"\n [attr.aria-controls]=\"listboxId()\"\n [attr.aria-activedescendant]=\"activeDescendantId()\"\n autocomplete=\"off\"\n (input)=\"onSearchInput($event)\"\n (keydown)=\"onSearchKeydown($event)\"\n (click)=\"$event.stopPropagation()\"\n />\n }\n } @else {\n <!-- Closed state -->\n @if (lmMultiple() && selectedChips().length > 0) {\n <!-- Multi-select closed: show chips -->\n <div class=\"flex flex-wrap gap-1 flex-1 min-w-0 max-h-32 overflow-y-auto\">\n @for (chip of selectedChips(); track chip.value) {\n <span [class]=\"chipClasses()\">\n <span class=\"truncate\">{{ chip.label }}</span>\n <button\n type=\"button\"\n [class]=\"chipDismissClasses()\"\n [attr.aria-label]=\"'Remove ' + chip.label\"\n (click)=\"removeChip($event, chip.value)\"\n >\n <svg\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n class=\"h-full w-full\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M5 5l6 6M11 5l-6 6\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n />\n </svg>\n </button>\n </span>\n }\n </div>\n } @else {\n <!-- Single-select or empty multi: label/placeholder -->\n <span\n class=\"flex-1 truncate min-w-0\"\n [class.text-muted-foreground]=\"!triggerLabel()\"\n >\n {{ triggerLabel() ?? lmPlaceholder() }}\n </span>\n }\n }\n\n <!-- Clear button \u2014 shown when clearable and has value -->\n @if (lmClearable() && hasValue() && !isDisabled() && !isOpen()) {\n <button\n type=\"button\"\n class=\"shrink-0 h-4 w-4 flex items-center justify-center text-muted-foreground hover:text-foreground transition-colors\"\n aria-label=\"Clear selection\"\n (click)=\"clear($event)\"\n >\n <svg\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n class=\"h-3.5 w-3.5\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M4 4l8 8M12 4l-8 8\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n />\n </svg>\n </button>\n }\n\n <!-- Chevron icon \u2014 rotates 180\u00B0 when open -->\n <svg\n class=\"ml-auto shrink-0 h-4 w-4 text-muted-foreground transition-transform duration-200\"\n [class.rotate-180]=\"isOpen()\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M4 6l4 4 4-4\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n</div>\n\n<!-- Listbox panel (absolute positioning \u2014 CSS-only, no JS tracking) -->\n@if (isOpen()) {\n <div\n #listboxEl\n [id]=\"listboxId()\"\n role=\"listbox\"\n [attr.aria-label]=\"lmPlaceholder()\"\n [attr.aria-multiselectable]=\"lmMultiple()\"\n [class]=\"listboxClasses()\"\n >\n @if (filteredOptionsWithState().length === 0) {\n <div class=\"px-3 py-4 text-sm text-center text-muted-foreground\">\n {{ lmEmptyMessage() }}\n </div>\n }\n\n @for (\n item of filteredOptionsWithState();\n track item.entry.value;\n let i = $index\n ) {\n <div\n [id]=\"selectId() + '-opt-' + i\"\n role=\"option\"\n [attr.aria-selected]=\"item.isSelected\"\n [attr.aria-disabled]=\"item.entry.disabled()\"\n [attr.tabindex]=\"item.entry.disabled() ? -1 : 0\"\n [class]=\"\n optionClasses(\n item.isSelected,\n item.entry.disabled(),\n focusedOptionIndex() === i\n )\n \"\n (click)=\"selectValue(item.entry.value)\"\n (keydown.enter)=\"selectValue(item.entry.value)\"\n (keydown.space)=\"selectValue(item.entry.value)\"\n (mouseenter)=\"focusedOptionIndex.set(i)\"\n >\n @if (lmMultiple()) {\n <!-- Mini checkbox indicator for multi-select -->\n <span\n class=\"h-4 w-4 shrink-0 flex items-center justify-center rounded-(--radius-2) border transition-colors duration-200\"\n [class.bg-primary]=\"item.isSelected\"\n [class.border-primary]=\"item.isSelected\"\n [class.border-gray-5]=\"!item.isSelected\"\n aria-hidden=\"true\"\n >\n @if (item.isSelected) {\n <svg width=\"10\" height=\"10\" viewBox=\"0 0 16 16\" fill=\"none\">\n <path\n d=\"M4 8l3 3 5-5\"\n stroke=\"white\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n }\n </span>\n } @else {\n <!-- Checkmark for single-select, spacer otherwise -->\n @if (item.isSelected) {\n <svg\n class=\"h-4 w-4 shrink-0 text-primary\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n >\n <path\n d=\"M3 8l4 4 6-7\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n }\n }\n\n <span class=\"truncate\">{{ item.entry.label() }}</span>\n </div>\n }\n </div>\n}\n" }]
4057
+ }], ctorParameters: () => [], propDecorators: { lmSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmSize", required: false }] }], lmMultiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmMultiple", required: false }] }], lmPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmPlaceholder", required: false }] }], lmSearchPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmSearchPlaceholder", required: false }] }], lmDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmDisabled", required: false }] }], lmRequired: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmRequired", required: false }] }], lmError: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmError", required: false }] }], lmEmptyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmEmptyMessage", required: false }] }], lmClearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmClearable", required: false }] }], lmCompareWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmCompareWith", required: false }] }], lmValueChange: [{ type: i0.Output, args: ["lmValueChange"] }], searchInputRef: [{ type: i0.ViewChild, args: ['searchInputEl', { isSignal: true }] }], triggerRef: [{ type: i0.ViewChild, args: ['triggerEl', { isSignal: true }] }], listboxRef: [{ type: i0.ViewChild, args: ['listboxEl', { isSignal: true }] }] } });
4058
+
4059
+ /**
4060
+ * Option component for luma-select.
4061
+ * Projects content into a hidden slot (the parent hides it via aria-hidden);
4062
+ * registers its value/label/disabled as signals into the parent's registry.
4063
+ *
4064
+ * @example
4065
+ * <luma-select-option [lmValue]="'apple'">Apple</luma-select-option>
4066
+ */
4067
+ class LmSelectOptionComponent {
4068
+ context = inject(SELECT_CONTEXT);
4069
+ elementRef = inject((ElementRef));
4070
+ lmValue = input.required(...(ngDevMode ? [{ debugName: "lmValue" }] : []));
4071
+ lmDisabled = input(false, ...(ngDevMode ? [{ debugName: "lmDisabled" }] : []));
4072
+ entry = null;
4073
+ ngOnInit() {
4074
+ this.entry = {
4075
+ value: this.lmValue(),
4076
+ label: () => this.elementRef.nativeElement.textContent?.trim() ?? '',
4077
+ disabled: this.lmDisabled,
4078
+ };
4079
+ this.context.registerOption(this.entry);
4080
+ }
4081
+ ngOnDestroy() {
4082
+ this.context.unregisterOption(this.lmValue());
4083
+ }
4084
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmSelectOptionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4085
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.0.9", type: LmSelectOptionComponent, isStandalone: true, selector: "luma-select-option", inputs: { lmValue: { classPropertyName: "lmValue", publicName: "lmValue", isSignal: true, isRequired: true, transformFunction: null }, lmDisabled: { classPropertyName: "lmDisabled", publicName: "lmDisabled", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `<ng-content />`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
4086
+ }
4087
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmSelectOptionComponent, decorators: [{
4088
+ type: Component,
4089
+ args: [{
4090
+ selector: 'luma-select-option',
4091
+ template: `<ng-content />`,
4092
+ changeDetection: ChangeDetectionStrategy.OnPush,
4093
+ }]
4094
+ }], propDecorators: { lmValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmValue", required: true }] }], lmDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmDisabled", required: false }] }] } });
4095
+
2994
4096
  // Button exports
2995
4097
 
2996
4098
  /**
2997
4099
  * Generated bundle index. Do not edit.
2998
4100
  */
2999
4101
 
3000
- export { ACCORDION_ITEM, DEFAULT_TOAST_CONFIG, LmAccordionContentDirective, LmAccordionGroupComponent, LmAccordionIconDirective, LmAccordionItemComponent, LmAccordionTitleDirective, LmAccordionTriggerDirective, LmBadgeDirective, LmButtonDirective, LmCardComponent, LmCardContentDirective, LmCardDescriptionDirective, LmCardHeaderDirective, LmCardTitleDirective, LmModalCloseComponent, LmModalComponent, LmModalContainerComponent, LmModalContentDirective, LmModalFooterDirective, LmModalHeaderDirective, LmModalOverlayComponent, LmModalTitleDirective, LmTabsComponent, LmTabsIndicatorComponent, LmTabsListDirective, LmTabsPanelDirective, LmTabsTriggerDirective, LmToastCloseComponent, LmToastContainerComponent, LmToastItemComponent, LmToastService, LmTooltipDirective, MODAL_CONTEXT, TABS_GROUP, TABS_LIST, TOAST_CONFIG, provideToastConfig };
4102
+ export { ACCORDION_ITEM, DEFAULT_TOAST_CONFIG, LmAccordionContentDirective, LmAccordionGroupComponent, LmAccordionIconDirective, LmAccordionItemComponent, LmAccordionTitleDirective, LmAccordionTriggerDirective, LmBadgeDirective, LmButtonDirective, LmCardComponent, LmCardContentDirective, LmCardDescriptionDirective, LmCardHeaderDirective, LmCardTitleDirective, LmCheckboxDirective, LmErrorTextDirective, LmHelperTextDirective, LmInputDirective, LmLabelDirective, LmModalCloseComponent, LmModalComponent, LmModalContainerComponent, LmModalContentDirective, LmModalFooterDirective, LmModalHeaderDirective, LmModalOverlayComponent, LmModalTitleDirective, LmRadioDirective, LmSelectComponent, LmSelectOptionComponent, LmTabsComponent, LmTabsIndicatorComponent, LmTabsListDirective, LmTabsPanelDirective, LmTabsTriggerDirective, LmTextareaDirective, LmToastCloseComponent, LmToastContainerComponent, LmToastItemComponent, LmToastService, LmTooltipDirective, MODAL_CONTEXT, SELECT_CONTEXT, TABS_GROUP, TABS_LIST, TOAST_CONFIG, provideToastConfig };
3001
4103
  //# sourceMappingURL=lumaui-angular.mjs.map