@lumaui/angular 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  import * as i0 from '@angular/core';
2
- import { input, computed, HostBinding, Directive, ChangeDetectionStrategy, Component } from '@angular/core';
3
- import { buttonVariants, cardVariants, cardContentVariants, cardTitleVariants, cardDescriptionVariants } from '@lumaui/core';
2
+ import { input, computed, HostBinding, Directive, ChangeDetectionStrategy, Component, output, InjectionToken, inject, ElementRef, Renderer2, effect, signal, HostListener, PLATFORM_ID, TemplateRef, ViewContainerRef } from '@angular/core';
3
+ import { buttonVariants, badgeVariants, cardVariants, cardContentVariants, cardTitleVariants, cardDescriptionVariants, accordionItemVariants, accordionContentWrapperVariants, accordionTriggerVariants, accordionTitleVariants, accordionIconVariants, accordionContentVariants, tooltipVariants, tabsListVariants, tabsTriggerVariants, tabsPanelVariants, tabsIndicatorVariants, modalOverlayVariants, modalContainerVariants, modalHeaderVariants, modalTitleVariants, modalContentVariants, modalFooterVariants, modalCloseVariants } from '@lumaui/core';
4
+ import { isPlatformBrowser, DOCUMENT } from '@angular/common';
4
5
 
5
6
  class ButtonDirective {
6
7
  // Signal-based inputs with lm prefix (Angular 20+)
@@ -33,6 +34,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImpor
33
34
  args: ['class']
34
35
  }] } });
35
36
 
37
+ class BadgeDirective {
38
+ // Computed class string - layout only, no variants
39
+ classes = computed(() => badgeVariants(), ...(ngDevMode ? [{ debugName: "classes" }] : []));
40
+ get hostClasses() {
41
+ return this.classes();
42
+ }
43
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: BadgeDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
44
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.9", type: BadgeDirective, isStandalone: true, selector: "[lumaBadge]", host: { properties: { "class": "this.hostClasses" } }, ngImport: i0 });
45
+ }
46
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: BadgeDirective, decorators: [{
47
+ type: Directive,
48
+ args: [{
49
+ selector: '[lumaBadge]',
50
+ }]
51
+ }], propDecorators: { hostClasses: [{
52
+ type: HostBinding,
53
+ args: ['class']
54
+ }] } });
55
+
36
56
  class CardComponent {
37
57
  /**
38
58
  * Card visual style variant
@@ -117,11 +137,1744 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImpor
117
137
  }]
118
138
  }] });
119
139
 
140
+ /**
141
+ * AccordionGroupComponent
142
+ *
143
+ * Optional wrapper component that coordinates multiple accordion items.
144
+ * Supports controlled pattern for implementing business logic like:
145
+ * - Single item open at a time
146
+ * - Multiple items open
147
+ * - Always keep first/last item open
148
+ * - Maximum number of open items
149
+ *
150
+ * @example Controlled single mode
151
+ * ```html
152
+ * <luma-accordion-group [lmValue]="activeItem()" (lmValueChange)="activeItem.set($event)">
153
+ * <luma-accordion-item lmId="item-1">...</luma-accordion-item>
154
+ * <luma-accordion-item lmId="item-2">...</luma-accordion-item>
155
+ * </luma-accordion-group>
156
+ * ```
157
+ *
158
+ * @example Controlled multiple mode
159
+ * ```html
160
+ * <luma-accordion-group [lmValue]="activeItems()" (lmValueChange)="activeItems.set($event)">
161
+ * <luma-accordion-item lmId="item-1">...</luma-accordion-item>
162
+ * <luma-accordion-item lmId="item-2">...</luma-accordion-item>
163
+ * </luma-accordion-group>
164
+ * ```
165
+ */
166
+ class AccordionGroupComponent {
167
+ /**
168
+ * Controlled value for which items are open
169
+ * - null: uncontrolled mode (each item manages its own state)
170
+ * - string: single item mode (ID of open item)
171
+ * - string[]: multiple items mode (IDs of open items)
172
+ */
173
+ lmValue = input(null, ...(ngDevMode ? [{ debugName: "lmValue" }] : []));
174
+ /**
175
+ * Emitted when an item is toggled
176
+ * Returns the new value (string for single mode, string[] for multiple)
177
+ */
178
+ lmValueChange = output();
179
+ /**
180
+ * Force single mode even when lmValue is an array
181
+ * When true, only one item can be open at a time
182
+ */
183
+ lmSingle = input(false, ...(ngDevMode ? [{ debugName: "lmSingle" }] : []));
184
+ /**
185
+ * Toggle an item by its ID
186
+ * Called by child AccordionItemComponent when toggled
187
+ */
188
+ toggleItem(itemId) {
189
+ const current = this.lmValue();
190
+ // If uncontrolled, do nothing (items manage their own state)
191
+ if (current === null || current === undefined) {
192
+ return;
193
+ }
194
+ let newValue;
195
+ if (this.lmSingle() || typeof current === 'string') {
196
+ // Single mode: toggle between the item and empty
197
+ newValue = current === itemId ? '' : itemId;
198
+ }
199
+ else {
200
+ // Multiple mode: add/remove from array
201
+ const arr = Array.isArray(current) ? [...current] : [];
202
+ const index = arr.indexOf(itemId);
203
+ if (index >= 0) {
204
+ arr.splice(index, 1);
205
+ }
206
+ else {
207
+ arr.push(itemId);
208
+ }
209
+ newValue = arr;
210
+ }
211
+ this.lmValueChange.emit(newValue);
212
+ }
213
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: AccordionGroupComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
214
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.0.9", type: AccordionGroupComponent, isStandalone: true, selector: "luma-accordion-group", inputs: { lmValue: { classPropertyName: "lmValue", publicName: "lmValue", isSignal: true, isRequired: false, transformFunction: null }, lmSingle: { classPropertyName: "lmSingle", publicName: "lmSingle", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { lmValueChange: "lmValueChange" }, host: { properties: { "style.gap": "\"var(--luma-accordion-item-gap)\"" }, classAttribute: "flex flex-col" }, ngImport: i0, template: '<ng-content></ng-content>', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
215
+ }
216
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: AccordionGroupComponent, decorators: [{
217
+ type: Component,
218
+ args: [{
219
+ selector: 'luma-accordion-group',
220
+ template: '<ng-content></ng-content>',
221
+ changeDetection: ChangeDetectionStrategy.OnPush,
222
+ host: {
223
+ class: 'flex flex-col',
224
+ '[style.gap]': '"var(--luma-accordion-item-gap)"',
225
+ },
226
+ }]
227
+ }], propDecorators: { lmValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmValue", required: false }] }], lmValueChange: [{ type: i0.Output, args: ["lmValueChange"] }], lmSingle: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmSingle", required: false }] }] } });
228
+
229
+ /**
230
+ * Injection token for accordion item
231
+ * Allows child directives to access parent accordion item state
232
+ */
233
+ const ACCORDION_ITEM = new InjectionToken('AccordionItem');
234
+
235
+ /**
236
+ * AccordionItemComponent
237
+ *
238
+ * Wrapper component that contains the trigger and content.
239
+ * Can be used standalone or within an AccordionGroupComponent.
240
+ *
241
+ * @example Standalone usage
242
+ * ```html
243
+ * <luma-accordion-item>
244
+ * <button lumaAccordionTrigger>
245
+ * <span lumaAccordionTitle>Title</span>
246
+ * <svg lumaAccordionIcon>...</svg>
247
+ * </button>
248
+ * <div lumaAccordionContent>Content here...</div>
249
+ * </luma-accordion-item>
250
+ * ```
251
+ *
252
+ * @example With variants
253
+ * ```html
254
+ * <luma-accordion-item lmVariant="filled">
255
+ * ...
256
+ * </luma-accordion-item>
257
+ * ```
258
+ */
259
+ class AccordionItemComponent {
260
+ group = inject(AccordionGroupComponent, { optional: true });
261
+ el = inject(ElementRef);
262
+ renderer = inject(Renderer2);
263
+ previousClasses = [];
264
+ constructor() {
265
+ // Effect to reactively manage CVA classes without replacing user classes
266
+ effect(() => {
267
+ // Remove previous CVA classes
268
+ this.previousClasses.forEach((c) => {
269
+ this.renderer.removeClass(this.el.nativeElement, c);
270
+ });
271
+ // Add new CVA classes (preserves user-provided classes)
272
+ const newClasses = this.wrapperClasses()
273
+ .split(' ')
274
+ .filter((c) => c);
275
+ newClasses.forEach((c) => {
276
+ this.renderer.addClass(this.el.nativeElement, c);
277
+ });
278
+ this.previousClasses = newClasses;
279
+ });
280
+ }
281
+ /**
282
+ * Unique identifier for this item (required when using AccordionGroup)
283
+ */
284
+ lmId = input('', ...(ngDevMode ? [{ debugName: "lmId" }] : []));
285
+ /**
286
+ * Visual style variant
287
+ * - default: Standard with border
288
+ * - bordered: FAQ-style stacked items
289
+ * - filled: Solid background (unified trigger/content)
290
+ */
291
+ lmVariant = input('default', ...(ngDevMode ? [{ debugName: "lmVariant" }] : []));
292
+ /**
293
+ * Initial/controlled open state (for standalone usage)
294
+ */
295
+ lmOpen = input(false, ...(ngDevMode ? [{ debugName: "lmOpen" }] : []));
296
+ /**
297
+ * Whether the accordion item is disabled
298
+ */
299
+ lmDisabled = input(false, ...(ngDevMode ? [{ debugName: "lmDisabled" }] : []));
300
+ /**
301
+ * Emitted when the open state changes
302
+ * Useful for tracking/analytics
303
+ */
304
+ lmOpenChange = output();
305
+ // Internal state for uncontrolled mode
306
+ _isOpen = signal(false, ...(ngDevMode ? [{ debugName: "_isOpen" }] : []));
307
+ /**
308
+ * Computed open state
309
+ * Priority: group controlled > lmOpen input > internal state
310
+ */
311
+ isOpen = computed(() => {
312
+ // If in a controlled group, check group value
313
+ if (this.group?.lmValue() !== null && this.group?.lmValue() !== undefined) {
314
+ const value = this.group.lmValue();
315
+ const id = this.lmId();
316
+ return Array.isArray(value) ? value.includes(id) : value === id;
317
+ }
318
+ // Otherwise use input or internal state
319
+ return this.lmOpen() || this._isOpen();
320
+ }, ...(ngDevMode ? [{ debugName: "isOpen" }] : []));
321
+ // CVA classes
322
+ wrapperClasses = computed(() => accordionItemVariants({
323
+ variant: this.lmVariant(),
324
+ }), ...(ngDevMode ? [{ debugName: "wrapperClasses" }] : []));
325
+ contentWrapperClasses = computed(() => accordionContentWrapperVariants({ open: this.isOpen() }), ...(ngDevMode ? [{ debugName: "contentWrapperClasses" }] : []));
326
+ /**
327
+ * Toggle the accordion open/closed state
328
+ */
329
+ toggle() {
330
+ if (this.lmDisabled())
331
+ return;
332
+ if (this.group && this.group.lmValue() !== null) {
333
+ // Controlled by group
334
+ this.group.toggleItem(this.lmId());
335
+ }
336
+ else {
337
+ // Uncontrolled mode
338
+ this._isOpen.update((v) => !v);
339
+ }
340
+ this.lmOpenChange.emit(this.isOpen());
341
+ }
342
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: AccordionItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
343
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.0.9", type: AccordionItemComponent, isStandalone: true, selector: "luma-accordion-item", inputs: { lmId: { classPropertyName: "lmId", publicName: "lmId", isSignal: true, isRequired: false, transformFunction: null }, lmVariant: { classPropertyName: "lmVariant", publicName: "lmVariant", isSignal: true, isRequired: false, transformFunction: null }, lmOpen: { classPropertyName: "lmOpen", publicName: "lmOpen", isSignal: true, isRequired: false, transformFunction: null }, lmDisabled: { classPropertyName: "lmDisabled", publicName: "lmDisabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { lmOpenChange: "lmOpenChange" }, host: { properties: { "attr.data-state": "isOpen() ? \"open\" : \"closed\"", "attr.data-variant": "lmVariant()" } }, providers: [{ provide: ACCORDION_ITEM, useExisting: AccordionItemComponent }], ngImport: i0, template: "<!-- Slot for trigger button -->\n<ng-content select=\"[lumaAccordionTrigger]\"></ng-content>\n\n<!-- Content wrapper with grid-rows animation -->\n<div [class]=\"contentWrapperClasses()\">\n <div class=\"overflow-hidden\">\n <ng-content select=\"[lumaAccordionContent]\"></ng-content>\n </div>\n</div>\n", changeDetection: i0.ChangeDetectionStrategy.OnPush });
344
+ }
345
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: AccordionItemComponent, decorators: [{
346
+ type: Component,
347
+ args: [{ selector: 'luma-accordion-item', changeDetection: ChangeDetectionStrategy.OnPush, providers: [{ provide: ACCORDION_ITEM, useExisting: AccordionItemComponent }], host: {
348
+ '[attr.data-state]': 'isOpen() ? "open" : "closed"',
349
+ '[attr.data-variant]': 'lmVariant()',
350
+ }, template: "<!-- Slot for trigger button -->\n<ng-content select=\"[lumaAccordionTrigger]\"></ng-content>\n\n<!-- Content wrapper with grid-rows animation -->\n<div [class]=\"contentWrapperClasses()\">\n <div class=\"overflow-hidden\">\n <ng-content select=\"[lumaAccordionContent]\"></ng-content>\n </div>\n</div>\n" }]
351
+ }], ctorParameters: () => [], propDecorators: { lmId: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmId", required: false }] }], lmVariant: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmVariant", required: false }] }], lmOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmOpen", required: false }] }], lmDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmDisabled", required: false }] }], lmOpenChange: [{ type: i0.Output, args: ["lmOpenChange"] }] } });
352
+
353
+ let uniqueId$2 = 0;
354
+ /**
355
+ * AccordionTriggerDirective
356
+ *
357
+ * Applied to a div element to make it the clickable trigger for the accordion.
358
+ * Uses div instead of button for maximum layout flexibility.
359
+ * Handles ARIA attributes and keyboard navigation automatically.
360
+ *
361
+ * @example Basic usage
362
+ * ```html
363
+ * <div lumaAccordionTrigger>
364
+ * <span lumaAccordionTitle>Title</span>
365
+ * <span lumaAccordionIcon>
366
+ * <svg>...</svg>
367
+ * </span>
368
+ * </div>
369
+ * ```
370
+ *
371
+ * @example Custom layout
372
+ * ```html
373
+ * <div lumaAccordionTrigger class="grid grid-cols-[auto_1fr_auto] gap-4">
374
+ * <svg class="w-6 h-6">...</svg>
375
+ * <div>
376
+ * <span lumaAccordionTitle>Title</span>
377
+ * <p class="text-sm">Description</p>
378
+ * </div>
379
+ * <span lumaAccordionIcon>
380
+ * <svg>...</svg>
381
+ * </span>
382
+ * </div>
383
+ * ```
384
+ */
385
+ class AccordionTriggerDirective {
386
+ item = inject(ACCORDION_ITEM);
387
+ id = ++uniqueId$2;
388
+ triggerId = `luma-accordion-trigger-${this.id}`;
389
+ contentId = `luma-accordion-content-${this.id}`;
390
+ classes = computed(() => accordionTriggerVariants(), ...(ngDevMode ? [{ debugName: "classes" }] : []));
391
+ onClick(event) {
392
+ if (this.item.lmDisabled())
393
+ return;
394
+ event.preventDefault();
395
+ this.item.toggle();
396
+ }
397
+ onKeydown(event) {
398
+ if (this.item.lmDisabled())
399
+ return;
400
+ // Space and Enter don't trigger on div natively - manual handler required
401
+ if (event.code === 'Space' || event.code === 'Enter') {
402
+ event.preventDefault();
403
+ this.item.toggle();
404
+ }
405
+ }
406
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: AccordionTriggerDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
407
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.9", type: AccordionTriggerDirective, isStandalone: true, selector: "div[lumaAccordionTrigger]", host: { attributes: { "role": "button" }, listeners: { "click": "onClick($event)", "keydown": "onKeydown($event)" }, properties: { "class": "classes()", "attr.tabindex": "item.lmDisabled() ? -1 : 0", "attr.aria-expanded": "item.isOpen()", "attr.aria-controls": "contentId", "attr.aria-disabled": "item.lmDisabled() ? \"true\" : null", "id": "triggerId" } }, ngImport: i0 });
408
+ }
409
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: AccordionTriggerDirective, decorators: [{
410
+ type: Directive,
411
+ args: [{
412
+ selector: 'div[lumaAccordionTrigger]',
413
+ host: {
414
+ '[class]': 'classes()',
415
+ role: 'button',
416
+ '[attr.tabindex]': 'item.lmDisabled() ? -1 : 0',
417
+ '[attr.aria-expanded]': 'item.isOpen()',
418
+ '[attr.aria-controls]': 'contentId',
419
+ '[attr.aria-disabled]': 'item.lmDisabled() ? "true" : null',
420
+ '[id]': 'triggerId',
421
+ },
422
+ }]
423
+ }], propDecorators: { onClick: [{
424
+ type: HostListener,
425
+ args: ['click', ['$event']]
426
+ }], onKeydown: [{
427
+ type: HostListener,
428
+ args: ['keydown', ['$event']]
429
+ }] } });
430
+
431
+ /**
432
+ * AccordionTitleDirective
433
+ *
434
+ * Applies typography styles to the accordion title.
435
+ * Supports size variants for different visual hierarchies.
436
+ *
437
+ * @example Basic usage
438
+ * ```html
439
+ * <span lumaAccordionTitle>What is Luma UI?</span>
440
+ * ```
441
+ *
442
+ * @example With size variant
443
+ * ```html
444
+ * <span lumaAccordionTitle lmSize="lg">Large Title</span>
445
+ * ```
446
+ */
447
+ class AccordionTitleDirective {
448
+ /**
449
+ * Size variant for the title
450
+ * - sm: Small text for compact UIs
451
+ * - md: Default size (base text)
452
+ * - lg: Large text for emphasis
453
+ */
454
+ lmSize = input('md', ...(ngDevMode ? [{ debugName: "lmSize" }] : []));
455
+ classes = computed(() => accordionTitleVariants({ size: this.lmSize() }), ...(ngDevMode ? [{ debugName: "classes" }] : []));
456
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: AccordionTitleDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
457
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.9", type: AccordionTitleDirective, isStandalone: true, selector: "[lumaAccordionTitle]", inputs: { lmSize: { classPropertyName: "lmSize", publicName: "lmSize", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0 });
458
+ }
459
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: AccordionTitleDirective, decorators: [{
460
+ type: Directive,
461
+ args: [{
462
+ selector: '[lumaAccordionTitle]',
463
+ host: {
464
+ '[class]': 'classes()',
465
+ },
466
+ }]
467
+ }], propDecorators: { lmSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmSize", required: false }] }] } });
468
+
469
+ /**
470
+ * AccordionIconDirective
471
+ *
472
+ * Applies rotation animation to the accordion icon (typically a chevron).
473
+ * Must be placed on a wrapper element (span or div), not directly on the SVG.
474
+ * Automatically rotates based on the open state of the parent accordion item.
475
+ *
476
+ * @example With span wrapper (recommended)
477
+ * ```html
478
+ * <span lumaAccordionIcon>
479
+ * <svg viewBox="0 0 24 24" class="w-4 h-4">
480
+ * <path stroke="currentColor" stroke-width="2" d="M19 9l-7 7-7-7" />
481
+ * </svg>
482
+ * </span>
483
+ * ```
484
+ *
485
+ * @example With div wrapper
486
+ * ```html
487
+ * <div lumaAccordionIcon>
488
+ * <my-chevron-icon></my-chevron-icon>
489
+ * </div>
490
+ * ```
491
+ *
492
+ * @example Customize rotation via CSS variable
493
+ * ```html
494
+ * <span lumaAccordionIcon style="--luma-accordion-icon-rotation: 90deg">
495
+ * <svg>...</svg>
496
+ * </span>
497
+ * ```
498
+ */
499
+ class AccordionIconDirective {
500
+ item = inject(ACCORDION_ITEM);
501
+ classes = computed(() => accordionIconVariants({ open: this.item.isOpen() }), ...(ngDevMode ? [{ debugName: "classes" }] : []));
502
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: AccordionIconDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
503
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.9", type: AccordionIconDirective, isStandalone: true, selector: "span[lumaAccordionIcon], div[lumaAccordionIcon]", host: { properties: { "class": "classes()", "attr.aria-hidden": "true" } }, ngImport: i0 });
504
+ }
505
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: AccordionIconDirective, decorators: [{
506
+ type: Directive,
507
+ args: [{
508
+ selector: 'span[lumaAccordionIcon], div[lumaAccordionIcon]',
509
+ host: {
510
+ '[class]': 'classes()',
511
+ '[attr.aria-hidden]': 'true',
512
+ },
513
+ }]
514
+ }] });
515
+
516
+ let uniqueId$1 = 0;
517
+ /**
518
+ * AccordionContentDirective
519
+ *
520
+ * Applied to the content area of the accordion.
521
+ * Handles visibility, ARIA attributes, and fade animation.
522
+ *
523
+ * @example Basic usage
524
+ * ```html
525
+ * <div lumaAccordionContent>
526
+ * <p>Your content here...</p>
527
+ * </div>
528
+ * ```
529
+ */
530
+ class AccordionContentDirective {
531
+ item = inject(ACCORDION_ITEM);
532
+ trigger = inject(AccordionTriggerDirective, { optional: true });
533
+ id = ++uniqueId$1;
534
+ contentId = `luma-accordion-content-${this.id}`;
535
+ triggerId = computed(() => this.trigger?.triggerId ?? null, ...(ngDevMode ? [{ debugName: "triggerId" }] : []));
536
+ classes = computed(() => accordionContentVariants({ open: this.item.isOpen() }), ...(ngDevMode ? [{ debugName: "classes" }] : []));
537
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: AccordionContentDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
538
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.9", type: AccordionContentDirective, isStandalone: true, selector: "[lumaAccordionContent]", host: { attributes: { "role": "region" }, properties: { "class": "classes()", "id": "contentId", "attr.aria-labelledby": "triggerId()", "attr.hidden": "!item.isOpen() ? \"\" : null" } }, ngImport: i0 });
539
+ }
540
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: AccordionContentDirective, decorators: [{
541
+ type: Directive,
542
+ args: [{
543
+ selector: '[lumaAccordionContent]',
544
+ host: {
545
+ '[class]': 'classes()',
546
+ role: 'region',
547
+ '[id]': 'contentId',
548
+ '[attr.aria-labelledby]': 'triggerId()',
549
+ '[attr.hidden]': '!item.isOpen() ? "" : null',
550
+ },
551
+ }]
552
+ }] });
553
+
554
+ class TooltipDirective {
555
+ el = inject(ElementRef);
556
+ renderer = inject(Renderer2);
557
+ platformId = inject(PLATFORM_ID);
558
+ // Inputs with lm prefix following Lumo convention
559
+ lumaTooltip = input.required(...(ngDevMode ? [{ debugName: "lumaTooltip" }] : []));
560
+ lmPosition = input('top', ...(ngDevMode ? [{ debugName: "lmPosition" }] : []));
561
+ lmHtml = input(false, ...(ngDevMode ? [{ debugName: "lmHtml" }] : []));
562
+ lmTrigger = input('hover', ...(ngDevMode ? [{ debugName: "lmTrigger" }] : []));
563
+ lmDelay = input(0, ...(ngDevMode ? [{ debugName: "lmDelay" }] : []));
564
+ // State
565
+ isVisible = signal(false, ...(ngDevMode ? [{ debugName: "isVisible" }] : []));
566
+ actualPosition = signal('top', ...(ngDevMode ? [{ debugName: "actualPosition" }] : []));
567
+ tooltipId = `tooltip-${Math.random().toString(36).slice(2, 9)}`;
568
+ tooltipElement = null;
569
+ showTimeout = null;
570
+ classes = computed(() => tooltipVariants({
571
+ position: this.actualPosition(),
572
+ visible: this.isVisible(),
573
+ }), ...(ngDevMode ? [{ debugName: "classes" }] : []));
574
+ constructor() {
575
+ // Create and update tooltip element reactively
576
+ effect(() => {
577
+ if (isPlatformBrowser(this.platformId)) {
578
+ this.ensureTooltipElement();
579
+ this.updateContent();
580
+ this.updateClasses();
581
+ }
582
+ });
583
+ }
584
+ ensureTooltipElement() {
585
+ if (this.tooltipElement)
586
+ return;
587
+ this.tooltipElement = this.renderer.createElement('div');
588
+ this.renderer.setAttribute(this.tooltipElement, 'id', this.tooltipId);
589
+ this.renderer.setAttribute(this.tooltipElement, 'role', 'tooltip');
590
+ this.renderer.appendChild(this.el.nativeElement, this.tooltipElement);
591
+ }
592
+ updateContent() {
593
+ if (!this.tooltipElement)
594
+ return;
595
+ const content = this.lumaTooltip();
596
+ if (this.lmHtml()) {
597
+ this.tooltipElement.innerHTML = content;
598
+ }
599
+ else {
600
+ this.tooltipElement.textContent = content;
601
+ }
602
+ }
603
+ updateClasses() {
604
+ if (!this.tooltipElement)
605
+ return;
606
+ this.tooltipElement.className = this.classes();
607
+ }
608
+ isTouchDevice() {
609
+ if (!isPlatformBrowser(this.platformId))
610
+ return false;
611
+ return 'ontouchstart' in window || navigator.maxTouchPoints > 0;
612
+ }
613
+ getFlippedPosition(preferred) {
614
+ if (!this.tooltipElement || !isPlatformBrowser(this.platformId)) {
615
+ return preferred;
616
+ }
617
+ const triggerRect = this.el.nativeElement.getBoundingClientRect();
618
+ const tooltipRect = this.tooltipElement.getBoundingClientRect();
619
+ const viewportWidth = window.innerWidth;
620
+ const viewportHeight = window.innerHeight;
621
+ const offset = 8; // --luma-tooltip-offset
622
+ switch (preferred) {
623
+ case 'top':
624
+ if (triggerRect.top - tooltipRect.height - offset < 0) {
625
+ return 'bottom';
626
+ }
627
+ break;
628
+ case 'bottom':
629
+ if (triggerRect.bottom + tooltipRect.height + offset > viewportHeight) {
630
+ return 'top';
631
+ }
632
+ break;
633
+ case 'left':
634
+ if (triggerRect.left - tooltipRect.width - offset < 0) {
635
+ return 'right';
636
+ }
637
+ break;
638
+ case 'right':
639
+ if (triggerRect.right + tooltipRect.width + offset > viewportWidth) {
640
+ return 'left';
641
+ }
642
+ break;
643
+ }
644
+ return preferred;
645
+ }
646
+ onMouseEnter() {
647
+ // Ignore hover on touch devices
648
+ if (this.isTouchDevice())
649
+ return;
650
+ if (this.lmTrigger() !== 'hover')
651
+ return;
652
+ this.show();
653
+ }
654
+ onMouseLeave() {
655
+ if (this.isTouchDevice())
656
+ return;
657
+ if (this.lmTrigger() !== 'hover')
658
+ return;
659
+ this.hide();
660
+ }
661
+ onClick() {
662
+ // On touch devices, click acts as toggle even with trigger='hover'
663
+ if (this.isTouchDevice() || this.lmTrigger() === 'click') {
664
+ this.toggle();
665
+ }
666
+ }
667
+ onFocus() {
668
+ if (this.lmTrigger() === 'focus' || this.lmTrigger() === 'hover') {
669
+ this.show();
670
+ }
671
+ }
672
+ onBlur() {
673
+ if (this.lmTrigger() === 'focus' || this.lmTrigger() === 'hover') {
674
+ this.hide();
675
+ }
676
+ }
677
+ onEscape() {
678
+ this.hide();
679
+ }
680
+ onDocumentClick(event) {
681
+ if (this.lmTrigger() !== 'click' && !this.isTouchDevice())
682
+ return;
683
+ if (!this.el.nativeElement.contains(event.target)) {
684
+ this.hide();
685
+ }
686
+ }
687
+ onDocumentTouch(event) {
688
+ if (!this.isVisible())
689
+ return;
690
+ if (!this.el.nativeElement.contains(event.target)) {
691
+ this.hide();
692
+ }
693
+ }
694
+ show() {
695
+ if (this.showTimeout)
696
+ clearTimeout(this.showTimeout);
697
+ const delay = this.lmDelay();
698
+ const showAction = () => {
699
+ this.isVisible.set(true);
700
+ // Calculate position with auto-flip after tooltip is visible
701
+ requestAnimationFrame(() => {
702
+ const actualPosition = this.getFlippedPosition(this.lmPosition());
703
+ this.actualPosition.set(actualPosition);
704
+ this.updateClasses();
705
+ });
706
+ };
707
+ if (delay > 0) {
708
+ this.showTimeout = setTimeout(showAction, delay);
709
+ }
710
+ else {
711
+ showAction();
712
+ }
713
+ }
714
+ hide() {
715
+ if (this.showTimeout) {
716
+ clearTimeout(this.showTimeout);
717
+ this.showTimeout = null;
718
+ }
719
+ this.isVisible.set(false);
720
+ this.updateClasses();
721
+ }
722
+ toggle() {
723
+ if (this.isVisible()) {
724
+ this.hide();
725
+ }
726
+ else {
727
+ this.show();
728
+ }
729
+ }
730
+ ngOnDestroy() {
731
+ if (this.showTimeout)
732
+ clearTimeout(this.showTimeout);
733
+ if (this.tooltipElement) {
734
+ this.renderer.removeChild(this.el.nativeElement, this.tooltipElement);
735
+ }
736
+ }
737
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: TooltipDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
738
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.9", type: TooltipDirective, isStandalone: true, selector: "[lumaTooltip]", inputs: { lumaTooltip: { classPropertyName: "lumaTooltip", publicName: "lumaTooltip", isSignal: true, isRequired: true, transformFunction: null }, lmPosition: { classPropertyName: "lmPosition", publicName: "lmPosition", isSignal: true, isRequired: false, transformFunction: null }, lmHtml: { classPropertyName: "lmHtml", publicName: "lmHtml", isSignal: true, isRequired: false, transformFunction: null }, lmTrigger: { classPropertyName: "lmTrigger", publicName: "lmTrigger", isSignal: true, isRequired: false, transformFunction: null }, lmDelay: { classPropertyName: "lmDelay", publicName: "lmDelay", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "mouseenter": "onMouseEnter()", "mouseleave": "onMouseLeave()", "click": "onClick()", "focus": "onFocus()", "blur": "onBlur()", "document:keydown.escape": "onEscape()", "document:click": "onDocumentClick($event)", "document:touchstart": "onDocumentTouch($event)" }, properties: { "attr.aria-describedby": "tooltipId", "style.position": "\"relative\"" } }, ngImport: i0 });
739
+ }
740
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: TooltipDirective, decorators: [{
741
+ type: Directive,
742
+ args: [{
743
+ selector: '[lumaTooltip]',
744
+ host: {
745
+ '[attr.aria-describedby]': 'tooltipId',
746
+ '[style.position]': '"relative"',
747
+ },
748
+ }]
749
+ }], ctorParameters: () => [], propDecorators: { lumaTooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "lumaTooltip", required: true }] }], lmPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmPosition", required: false }] }], lmHtml: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmHtml", required: false }] }], lmTrigger: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmTrigger", required: false }] }], lmDelay: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmDelay", required: false }] }], onMouseEnter: [{
750
+ type: HostListener,
751
+ args: ['mouseenter']
752
+ }], onMouseLeave: [{
753
+ type: HostListener,
754
+ args: ['mouseleave']
755
+ }], onClick: [{
756
+ type: HostListener,
757
+ args: ['click']
758
+ }], onFocus: [{
759
+ type: HostListener,
760
+ args: ['focus']
761
+ }], onBlur: [{
762
+ type: HostListener,
763
+ args: ['blur']
764
+ }], onEscape: [{
765
+ type: HostListener,
766
+ args: ['document:keydown.escape']
767
+ }], onDocumentClick: [{
768
+ type: HostListener,
769
+ args: ['document:click', ['$event']]
770
+ }], onDocumentTouch: [{
771
+ type: HostListener,
772
+ args: ['document:touchstart', ['$event']]
773
+ }] } });
774
+
775
+ /**
776
+ * Injection token for tabs group
777
+ * Allows child components to access parent tabs state
778
+ */
779
+ const TABS_GROUP = new InjectionToken('TabsGroup');
780
+ /**
781
+ * Injection token for tabs list
782
+ * Allows indicator to access trigger positions
783
+ */
784
+ const TABS_LIST = new InjectionToken('TabsList');
785
+
786
+ /**
787
+ * Tabs container component
788
+ *
789
+ * Manages tab selection state, keyboard navigation, and provides context
790
+ * to child components (TabsList, TabsTrigger, TabsPanel).
791
+ *
792
+ * @example
793
+ * ```html
794
+ * <luma-tabs [lmValue]="selectedTab()" (lmValueChange)="onSelect($event)">
795
+ * <div lumaTabsList>
796
+ * <button lumaTabsTrigger="tab-1">Tab 1</button>
797
+ * <button lumaTabsTrigger="tab-2">Tab 2</button>
798
+ * </div>
799
+ * <div lumaTabsPanel="tab-1">Content 1</div>
800
+ * <div lumaTabsPanel="tab-2">Content 2</div>
801
+ * </luma-tabs>
802
+ * ```
803
+ */
804
+ class TabsComponent {
805
+ /** Controlled value - currently selected tab */
806
+ lmValue = input(null, ...(ngDevMode ? [{ debugName: "lmValue" }] : []));
807
+ /** Default value for uncontrolled mode */
808
+ lmDefaultValue = input('', ...(ngDevMode ? [{ debugName: "lmDefaultValue" }] : []));
809
+ /** Visual style: underline, background, or pill */
810
+ lmStyle = input('underline', ...(ngDevMode ? [{ debugName: "lmStyle" }] : []));
811
+ /** Whether to lazy load panel content */
812
+ lmLazy = input(true, ...(ngDevMode ? [{ debugName: "lmLazy" }] : []));
813
+ /** Emits when selected tab changes */
814
+ lmValueChange = output();
815
+ /** Internal state for the selected value */
816
+ value = signal(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
817
+ /** Map of registered triggers for keyboard navigation */
818
+ triggers = new Map();
819
+ /** Ordered list of trigger values for navigation */
820
+ triggerOrder = [];
821
+ constructor() {
822
+ // Sync controlled value to internal state
823
+ effect(() => {
824
+ const controlled = this.lmValue();
825
+ const defaultVal = this.lmDefaultValue();
826
+ if (controlled !== null) {
827
+ this.value.set(controlled);
828
+ }
829
+ else if (this.value() === null && defaultVal) {
830
+ this.value.set(defaultVal);
831
+ }
832
+ });
833
+ }
834
+ /**
835
+ * Select a tab by value
836
+ */
837
+ select(tabValue) {
838
+ if (this.value() === tabValue)
839
+ return;
840
+ this.value.set(tabValue);
841
+ this.lmValueChange.emit(tabValue);
842
+ }
843
+ /**
844
+ * Register a trigger element for keyboard navigation
845
+ */
846
+ registerTrigger(tabValue, element) {
847
+ this.triggers.set(tabValue, element);
848
+ if (!this.triggerOrder.includes(tabValue)) {
849
+ this.triggerOrder.push(tabValue);
850
+ }
851
+ }
852
+ /**
853
+ * Unregister a trigger element
854
+ */
855
+ unregisterTrigger(tabValue) {
856
+ this.triggers.delete(tabValue);
857
+ this.triggerOrder = this.triggerOrder.filter((v) => v !== tabValue);
858
+ }
859
+ /**
860
+ * Get all registered triggers
861
+ */
862
+ getTriggers() {
863
+ return this.triggers;
864
+ }
865
+ /**
866
+ * Focus next trigger in the list
867
+ */
868
+ focusNextTrigger() {
869
+ const currentIndex = this.getCurrentTriggerIndex();
870
+ const nextIndex = (currentIndex + 1) % this.triggerOrder.length;
871
+ this.focusTriggerAtIndex(nextIndex);
872
+ }
873
+ /**
874
+ * Focus previous trigger in the list
875
+ */
876
+ focusPreviousTrigger() {
877
+ const currentIndex = this.getCurrentTriggerIndex();
878
+ const prevIndex = currentIndex <= 0 ? this.triggerOrder.length - 1 : currentIndex - 1;
879
+ this.focusTriggerAtIndex(prevIndex);
880
+ }
881
+ /**
882
+ * Focus first trigger
883
+ */
884
+ focusFirstTrigger() {
885
+ this.focusTriggerAtIndex(0);
886
+ }
887
+ /**
888
+ * Focus last trigger
889
+ */
890
+ focusLastTrigger() {
891
+ this.focusTriggerAtIndex(this.triggerOrder.length - 1);
892
+ }
893
+ getCurrentTriggerIndex() {
894
+ const currentValue = this.value();
895
+ return currentValue ? this.triggerOrder.indexOf(currentValue) : 0;
896
+ }
897
+ focusTriggerAtIndex(index) {
898
+ const value = this.triggerOrder[index];
899
+ const element = this.triggers.get(value);
900
+ if (element) {
901
+ element.focus();
902
+ this.select(value);
903
+ }
904
+ }
905
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: TabsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
906
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.0.9", type: TabsComponent, isStandalone: true, selector: "luma-tabs", inputs: { lmValue: { classPropertyName: "lmValue", publicName: "lmValue", isSignal: true, isRequired: false, transformFunction: null }, lmDefaultValue: { classPropertyName: "lmDefaultValue", publicName: "lmDefaultValue", isSignal: true, isRequired: false, transformFunction: null }, lmStyle: { classPropertyName: "lmStyle", publicName: "lmStyle", isSignal: true, isRequired: false, transformFunction: null }, lmLazy: { classPropertyName: "lmLazy", publicName: "lmLazy", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { lmValueChange: "lmValueChange" }, host: { properties: { "class": "\"block w-full\"" } }, providers: [
907
+ {
908
+ provide: TABS_GROUP,
909
+ useExisting: TabsComponent,
910
+ },
911
+ ], ngImport: i0, template: `<ng-content />`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
912
+ }
913
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: TabsComponent, decorators: [{
914
+ type: Component,
915
+ args: [{
916
+ selector: 'luma-tabs',
917
+ template: `<ng-content />`,
918
+ changeDetection: ChangeDetectionStrategy.OnPush,
919
+ providers: [
920
+ {
921
+ provide: TABS_GROUP,
922
+ useExisting: TabsComponent,
923
+ },
924
+ ],
925
+ host: {
926
+ '[class]': '"block w-full"',
927
+ },
928
+ }]
929
+ }], ctorParameters: () => [], propDecorators: { lmValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmValue", required: false }] }], lmDefaultValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmDefaultValue", required: false }] }], lmStyle: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmStyle", required: false }] }], lmLazy: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmLazy", required: false }] }], lmValueChange: [{ type: i0.Output, args: ["lmValueChange"] }] } });
930
+
931
+ /**
932
+ * Tabs list directive
933
+ *
934
+ * Container for tab triggers with role="tablist".
935
+ * Provides context for the indicator component.
936
+ *
937
+ * @example
938
+ * ```html
939
+ * <div lumaTabsList>
940
+ * <button lumaTabsTrigger="tab-1">Tab 1</button>
941
+ * <button lumaTabsTrigger="tab-2">Tab 2</button>
942
+ * </div>
943
+ * ```
944
+ */
945
+ class TabsListDirective {
946
+ elementRef = inject((ElementRef));
947
+ tabsGroup = inject(TABS_GROUP);
948
+ /** Whether horizontal scrolling is enabled */
949
+ lmScrollable = false;
950
+ classes = computed(() => tabsListVariants({
951
+ style: this.tabsGroup.lmStyle(),
952
+ scrollable: this.lmScrollable,
953
+ }), ...(ngDevMode ? [{ debugName: "classes" }] : []));
954
+ /**
955
+ * Get the currently active trigger element
956
+ */
957
+ getActiveTrigger() {
958
+ const currentValue = this.tabsGroup.value();
959
+ if (!currentValue)
960
+ return null;
961
+ const triggers = this.tabsGroup.getTriggers();
962
+ return triggers.get(currentValue) || null;
963
+ }
964
+ /**
965
+ * Handle mouse wheel for horizontal scroll
966
+ */
967
+ onWheel(event) {
968
+ if (this.lmScrollable) {
969
+ event.preventDefault();
970
+ this.elementRef.nativeElement.scrollLeft += event.deltaY;
971
+ }
972
+ }
973
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: TabsListDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
974
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.9", type: TabsListDirective, isStandalone: true, selector: "[lumaTabsList]", host: { attributes: { "role": "tablist" }, listeners: { "wheel": "onWheel($event)" }, properties: { "attr.aria-orientation": "\"horizontal\"", "class": "classes()" } }, providers: [
975
+ {
976
+ provide: TABS_LIST,
977
+ useExisting: TabsListDirective,
978
+ },
979
+ ], ngImport: i0 });
980
+ }
981
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: TabsListDirective, decorators: [{
982
+ type: Directive,
983
+ args: [{
984
+ selector: '[lumaTabsList]',
985
+ providers: [
986
+ {
987
+ provide: TABS_LIST,
988
+ useExisting: TabsListDirective,
989
+ },
990
+ ],
991
+ host: {
992
+ role: 'tablist',
993
+ '[attr.aria-orientation]': '"horizontal"',
994
+ '[class]': 'classes()',
995
+ },
996
+ }]
997
+ }], propDecorators: { onWheel: [{
998
+ type: HostListener,
999
+ args: ['wheel', ['$event']]
1000
+ }] } });
1001
+
1002
+ /**
1003
+ * Tabs trigger directive
1004
+ *
1005
+ * Individual tab button with role="tab" and keyboard navigation support.
1006
+ * Follows WAI-ARIA tabs pattern with roving tabindex.
1007
+ *
1008
+ * @example
1009
+ * ```html
1010
+ * <button lumaTabsTrigger="tab-1">Tab 1</button>
1011
+ * ```
1012
+ */
1013
+ class TabsTriggerDirective {
1014
+ el = inject((ElementRef));
1015
+ tabsGroup = inject(TABS_GROUP);
1016
+ /** Tab value identifier */
1017
+ lumaTabsTrigger = input.required(...(ngDevMode ? [{ debugName: "lumaTabsTrigger" }] : []));
1018
+ /** Whether this trigger is disabled */
1019
+ lmDisabled = input(false, ...(ngDevMode ? [{ debugName: "lmDisabled" }] : []));
1020
+ /** Computed: whether this tab is selected */
1021
+ isSelected = computed(() => this.tabsGroup.value() === this.lumaTabsTrigger(), ...(ngDevMode ? [{ debugName: "isSelected" }] : []));
1022
+ /** Computed: ID for the trigger element */
1023
+ triggerId = computed(() => `tab-trigger-${this.lumaTabsTrigger()}`, ...(ngDevMode ? [{ debugName: "triggerId" }] : []));
1024
+ /** Computed: ID for the corresponding panel */
1025
+ panelId = computed(() => `tab-panel-${this.lumaTabsTrigger()}`, ...(ngDevMode ? [{ debugName: "panelId" }] : []));
1026
+ /** Computed: CSS classes from CVA */
1027
+ classes = computed(() => tabsTriggerVariants({
1028
+ style: this.tabsGroup.lmStyle(),
1029
+ selected: this.isSelected(),
1030
+ }), ...(ngDevMode ? [{ debugName: "classes" }] : []));
1031
+ ngOnInit() {
1032
+ this.tabsGroup.registerTrigger(this.lumaTabsTrigger(), this.el.nativeElement);
1033
+ }
1034
+ ngOnDestroy() {
1035
+ this.tabsGroup.unregisterTrigger(this.lumaTabsTrigger());
1036
+ }
1037
+ onClick() {
1038
+ if (this.lmDisabled())
1039
+ return;
1040
+ this.tabsGroup.select(this.lumaTabsTrigger());
1041
+ }
1042
+ onKeydown(event) {
1043
+ if (this.lmDisabled())
1044
+ return;
1045
+ switch (event.key) {
1046
+ case 'ArrowRight':
1047
+ event.preventDefault();
1048
+ this.tabsGroup.focusNextTrigger();
1049
+ break;
1050
+ case 'ArrowLeft':
1051
+ event.preventDefault();
1052
+ this.tabsGroup.focusPreviousTrigger();
1053
+ break;
1054
+ case 'Home':
1055
+ event.preventDefault();
1056
+ this.tabsGroup.focusFirstTrigger();
1057
+ break;
1058
+ case 'End':
1059
+ event.preventDefault();
1060
+ this.tabsGroup.focusLastTrigger();
1061
+ break;
1062
+ case 'Enter':
1063
+ case ' ':
1064
+ event.preventDefault();
1065
+ this.tabsGroup.select(this.lumaTabsTrigger());
1066
+ break;
1067
+ }
1068
+ }
1069
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: TabsTriggerDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1070
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.9", type: TabsTriggerDirective, isStandalone: true, selector: "[lumaTabsTrigger]", inputs: { lumaTabsTrigger: { classPropertyName: "lumaTabsTrigger", publicName: "lumaTabsTrigger", isSignal: true, isRequired: true, transformFunction: null }, lmDisabled: { classPropertyName: "lmDisabled", publicName: "lmDisabled", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "tab" }, listeners: { "click": "onClick()", "keydown": "onKeydown($event)" }, properties: { "attr.id": "triggerId()", "attr.aria-selected": "isSelected()", "attr.aria-controls": "panelId()", "attr.tabindex": "isSelected() ? 0 : -1", "class": "classes()" } }, ngImport: i0 });
1071
+ }
1072
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: TabsTriggerDirective, decorators: [{
1073
+ type: Directive,
1074
+ args: [{
1075
+ selector: '[lumaTabsTrigger]',
1076
+ host: {
1077
+ role: 'tab',
1078
+ '[attr.id]': 'triggerId()',
1079
+ '[attr.aria-selected]': 'isSelected()',
1080
+ '[attr.aria-controls]': 'panelId()',
1081
+ '[attr.tabindex]': 'isSelected() ? 0 : -1',
1082
+ '[class]': 'classes()',
1083
+ },
1084
+ }]
1085
+ }], propDecorators: { lumaTabsTrigger: [{ type: i0.Input, args: [{ isSignal: true, alias: "lumaTabsTrigger", required: true }] }], lmDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmDisabled", required: false }] }], onClick: [{
1086
+ type: HostListener,
1087
+ args: ['click']
1088
+ }], onKeydown: [{
1089
+ type: HostListener,
1090
+ args: ['keydown', ['$event']]
1091
+ }] } });
1092
+
1093
+ /**
1094
+ * Tabs panel directive
1095
+ *
1096
+ * Content panel with role="tabpanel" and lazy loading support.
1097
+ * When lazy loading is enabled, content is only rendered after
1098
+ * the tab has been selected at least once, then cached.
1099
+ *
1100
+ * @example
1101
+ * ```html
1102
+ * <div lumaTabsPanel="tab-1">Content 1</div>
1103
+ *
1104
+ * <!-- With lazy loading (default when lmLazy=true on parent) -->
1105
+ * <ng-template lumaTabsPanel="tab-1">
1106
+ * <expensive-component />
1107
+ * </ng-template>
1108
+ * ```
1109
+ */
1110
+ class TabsPanelDirective {
1111
+ tabsGroup = inject(TABS_GROUP);
1112
+ templateRef = inject((TemplateRef), {
1113
+ optional: true,
1114
+ });
1115
+ viewContainer = inject(ViewContainerRef);
1116
+ /** Panel value identifier */
1117
+ lumaTabsPanel = input.required(...(ngDevMode ? [{ debugName: "lumaTabsPanel" }] : []));
1118
+ /** Track if panel has ever been selected (for lazy loading cache) */
1119
+ hasBeenSelected = signal(false, ...(ngDevMode ? [{ debugName: "hasBeenSelected" }] : []));
1120
+ /** Computed: whether this panel is currently selected */
1121
+ isSelected = computed(() => this.tabsGroup.value() === this.lumaTabsPanel(), ...(ngDevMode ? [{ debugName: "isSelected" }] : []));
1122
+ /** Computed: whether this panel should be visible/rendered */
1123
+ isVisible = computed(() => {
1124
+ const selected = this.isSelected();
1125
+ const lazy = this.tabsGroup.lmLazy();
1126
+ // If lazy loading is disabled, always show selected panel
1127
+ if (!lazy)
1128
+ return selected;
1129
+ // With lazy loading: render if ever selected, but only show if currently selected
1130
+ return this.hasBeenSelected() && selected;
1131
+ }, ...(ngDevMode ? [{ debugName: "isVisible" }] : []));
1132
+ /** Computed: whether content should be rendered (for lazy loading) */
1133
+ shouldRender = computed(() => {
1134
+ const lazy = this.tabsGroup.lmLazy();
1135
+ return lazy ? this.hasBeenSelected() : true;
1136
+ }, ...(ngDevMode ? [{ debugName: "shouldRender" }] : []));
1137
+ /** Computed: ID for the panel element */
1138
+ panelId = computed(() => `tab-panel-${this.lumaTabsPanel()}`, ...(ngDevMode ? [{ debugName: "panelId" }] : []));
1139
+ /** Computed: ID for the corresponding trigger */
1140
+ triggerId = computed(() => `tab-trigger-${this.lumaTabsPanel()}`, ...(ngDevMode ? [{ debugName: "triggerId" }] : []));
1141
+ /** Computed: CSS classes from CVA */
1142
+ classes = computed(() => tabsPanelVariants({
1143
+ visible: this.isVisible(),
1144
+ }), ...(ngDevMode ? [{ debugName: "classes" }] : []));
1145
+ constructor() {
1146
+ // Track when panel is selected for lazy loading cache
1147
+ effect(() => {
1148
+ if (this.isSelected() && !this.hasBeenSelected()) {
1149
+ this.hasBeenSelected.set(true);
1150
+ }
1151
+ });
1152
+ // Handle template-based lazy loading
1153
+ effect(() => {
1154
+ if (this.templateRef && this.shouldRender()) {
1155
+ if (this.viewContainer.length === 0) {
1156
+ this.viewContainer.createEmbeddedView(this.templateRef);
1157
+ }
1158
+ }
1159
+ });
1160
+ }
1161
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: TabsPanelDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1162
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.9", type: TabsPanelDirective, isStandalone: true, selector: "[lumaTabsPanel]", inputs: { lumaTabsPanel: { classPropertyName: "lumaTabsPanel", publicName: "lumaTabsPanel", isSignal: true, isRequired: true, transformFunction: null } }, host: { attributes: { "role": "tabpanel" }, properties: { "attr.id": "panelId()", "attr.aria-labelledby": "triggerId()", "attr.tabindex": "0", "class": "classes()", "hidden": "!isVisible()" } }, ngImport: i0 });
1163
+ }
1164
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: TabsPanelDirective, decorators: [{
1165
+ type: Directive,
1166
+ args: [{
1167
+ selector: '[lumaTabsPanel]',
1168
+ host: {
1169
+ role: 'tabpanel',
1170
+ '[attr.id]': 'panelId()',
1171
+ '[attr.aria-labelledby]': 'triggerId()',
1172
+ '[attr.tabindex]': '0',
1173
+ '[class]': 'classes()',
1174
+ '[hidden]': '!isVisible()',
1175
+ },
1176
+ }]
1177
+ }], ctorParameters: () => [], propDecorators: { lumaTabsPanel: [{ type: i0.Input, args: [{ isSignal: true, alias: "lumaTabsPanel", required: true }] }] } });
1178
+
1179
+ /**
1180
+ * Tabs indicator component
1181
+ *
1182
+ * Animated indicator that slides between tabs for the underline style.
1183
+ * Uses CSS transform for smooth, GPU-accelerated animation.
1184
+ *
1185
+ * @example
1186
+ * ```html
1187
+ * <div lumaTabsList>
1188
+ * <button lumaTabsTrigger="tab-1">Tab 1</button>
1189
+ * <button lumaTabsTrigger="tab-2">Tab 2</button>
1190
+ * <luma-tabs-indicator />
1191
+ * </div>
1192
+ * ```
1193
+ */
1194
+ class TabsIndicatorComponent {
1195
+ platformId = inject(PLATFORM_ID);
1196
+ tabsGroup = inject(TABS_GROUP);
1197
+ tabsList = inject(TABS_LIST);
1198
+ /** Indicator width in pixels */
1199
+ indicatorWidth = signal(0, ...(ngDevMode ? [{ debugName: "indicatorWidth" }] : []));
1200
+ /** Indicator X or Y position */
1201
+ indicatorPosition = signal(0, ...(ngDevMode ? [{ debugName: "indicatorPosition" }] : []));
1202
+ /** Resize observer for recalculating position */
1203
+ resizeObserver = null;
1204
+ /** Computed: CSS classes from CVA */
1205
+ classes = computed(() => {
1206
+ const style = this.tabsGroup.lmStyle();
1207
+ // Only show indicator for underline style
1208
+ const isVisible = style === 'underline';
1209
+ return tabsIndicatorVariants({
1210
+ visible: isVisible,
1211
+ });
1212
+ }, ...(ngDevMode ? [{ debugName: "classes" }] : []));
1213
+ /** Computed: CSS transform for positioning */
1214
+ indicatorTransform = computed(() => {
1215
+ return `translateX(${this.indicatorPosition()}px)`;
1216
+ }, ...(ngDevMode ? [{ debugName: "indicatorTransform" }] : []));
1217
+ constructor() {
1218
+ // Update indicator position when selection changes
1219
+ effect(() => {
1220
+ // Read the value to track changes
1221
+ this.tabsGroup.value();
1222
+ // Defer position calculation to ensure DOM is ready
1223
+ if (isPlatformBrowser(this.platformId)) {
1224
+ // Use setTimeout to ensure triggers are registered
1225
+ setTimeout(() => this.updateIndicatorPosition(), 0);
1226
+ }
1227
+ });
1228
+ }
1229
+ ngAfterViewInit() {
1230
+ if (!isPlatformBrowser(this.platformId))
1231
+ return;
1232
+ // Initial position calculation with delay to ensure triggers are registered
1233
+ setTimeout(() => this.updateIndicatorPosition(), 0);
1234
+ // Watch for container resize
1235
+ this.resizeObserver = new ResizeObserver(() => {
1236
+ this.updateIndicatorPosition();
1237
+ });
1238
+ this.resizeObserver.observe(this.tabsList.elementRef.nativeElement);
1239
+ }
1240
+ ngOnDestroy() {
1241
+ this.resizeObserver?.disconnect();
1242
+ }
1243
+ updateIndicatorPosition() {
1244
+ const activeTrigger = this.tabsList.getActiveTrigger();
1245
+ if (!activeTrigger) {
1246
+ this.indicatorWidth.set(0);
1247
+ return;
1248
+ }
1249
+ const triggerRect = activeTrigger.getBoundingClientRect();
1250
+ const listRect = this.tabsList.elementRef.nativeElement.getBoundingClientRect();
1251
+ // Horizontal positioning
1252
+ this.indicatorPosition.set(triggerRect.left - listRect.left);
1253
+ this.indicatorWidth.set(triggerRect.width);
1254
+ }
1255
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: TabsIndicatorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1256
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.0.9", type: TabsIndicatorComponent, isStandalone: true, selector: "luma-tabs-indicator", host: { properties: { "class": "classes()", "style.width.px": "indicatorWidth()", "style.transform": "indicatorTransform()" } }, ngImport: i0, template: ``, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
1257
+ }
1258
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: TabsIndicatorComponent, decorators: [{
1259
+ type: Component,
1260
+ args: [{
1261
+ selector: 'luma-tabs-indicator',
1262
+ template: ``,
1263
+ changeDetection: ChangeDetectionStrategy.OnPush,
1264
+ host: {
1265
+ '[class]': 'classes()',
1266
+ '[style.width.px]': 'indicatorWidth()',
1267
+ '[style.transform]': 'indicatorTransform()',
1268
+ },
1269
+ }]
1270
+ }], ctorParameters: () => [] });
1271
+
1272
+ /**
1273
+ * Injection token for modal context
1274
+ * Allows child components to access parent modal state
1275
+ */
1276
+ const MODAL_CONTEXT = new InjectionToken('ModalContext');
1277
+
1278
+ let uniqueId = 0;
1279
+ /**
1280
+ * Modal container component
1281
+ *
1282
+ * Manages modal open/close state, escape key handling, and provides context
1283
+ * to child components (ModalOverlay, ModalContainer, etc.).
1284
+ *
1285
+ * Supports both controlled and uncontrolled modes:
1286
+ * - Controlled: Use [lmOpen] and (lmOpenChange)
1287
+ * - Uncontrolled: Use [lmDefaultOpen] and access via template reference
1288
+ *
1289
+ * @example
1290
+ * ```html
1291
+ * <!-- Controlled mode -->
1292
+ * <luma-modal [lmOpen]="isOpen()" (lmOpenChange)="isOpen.set($event)">
1293
+ * <luma-modal-overlay>
1294
+ * <luma-modal-container>
1295
+ * <div lumaModalHeader>
1296
+ * <h2 lumaModalTitle>Title</h2>
1297
+ * <luma-modal-close />
1298
+ * </div>
1299
+ * <div lumaModalContent>Content</div>
1300
+ * <div lumaModalFooter>
1301
+ * <button lumaButton (click)="isOpen.set(false)">Close</button>
1302
+ * </div>
1303
+ * </luma-modal-container>
1304
+ * </luma-modal-overlay>
1305
+ * </luma-modal>
1306
+ *
1307
+ * <!-- Uncontrolled mode -->
1308
+ * <luma-modal #modal [lmDefaultOpen]="false">
1309
+ * ...
1310
+ * </luma-modal>
1311
+ * <button (click)="modal.open()">Open</button>
1312
+ * ```
1313
+ */
1314
+ class ModalComponent {
1315
+ platformId = inject(PLATFORM_ID);
1316
+ document = inject(DOCUMENT);
1317
+ /** Controlled open state (null = uncontrolled mode) */
1318
+ lmOpen = input(null, ...(ngDevMode ? [{ debugName: "lmOpen" }] : []));
1319
+ /** Default open state for uncontrolled mode */
1320
+ lmDefaultOpen = input(false, ...(ngDevMode ? [{ debugName: "lmDefaultOpen" }] : []));
1321
+ /** Size variant */
1322
+ lmSize = input('md', ...(ngDevMode ? [{ debugName: "lmSize" }] : []));
1323
+ /** Close when clicking the overlay */
1324
+ lmCloseOnOverlay = input(true, ...(ngDevMode ? [{ debugName: "lmCloseOnOverlay" }] : []));
1325
+ /** Close when pressing Escape key */
1326
+ lmCloseOnEscape = input(true, ...(ngDevMode ? [{ debugName: "lmCloseOnEscape" }] : []));
1327
+ /** Emits when open state changes */
1328
+ lmOpenChange = output();
1329
+ /** Internal open state for uncontrolled mode */
1330
+ internalOpen = signal(false, ...(ngDevMode ? [{ debugName: "internalOpen" }] : []));
1331
+ /** Unique modal ID for accessibility */
1332
+ modalId = `modal-${uniqueId++}`;
1333
+ /** Previously focused element for focus restoration */
1334
+ previouslyFocused = null;
1335
+ /** Escape key handler */
1336
+ escapeHandler = null;
1337
+ /** Computed: current open state (controlled or uncontrolled) */
1338
+ isOpen = computed(() => {
1339
+ const controlled = this.lmOpen();
1340
+ return controlled !== null ? controlled : this.internalOpen();
1341
+ }, ...(ngDevMode ? [{ debugName: "isOpen" }] : []));
1342
+ /** Computed: size signal for context */
1343
+ size = computed(() => this.lmSize(), ...(ngDevMode ? [{ debugName: "size" }] : []));
1344
+ /** Computed: closeOnOverlay signal for context */
1345
+ closeOnOverlay = computed(() => this.lmCloseOnOverlay(), ...(ngDevMode ? [{ debugName: "closeOnOverlay" }] : []));
1346
+ /** Computed: closeOnEscape signal for context */
1347
+ closeOnEscape = computed(() => this.lmCloseOnEscape(), ...(ngDevMode ? [{ debugName: "closeOnEscape" }] : []));
1348
+ constructor() {
1349
+ // Initialize uncontrolled mode with default value
1350
+ effect(() => {
1351
+ if (this.lmOpen() === null) {
1352
+ this.internalOpen.set(this.lmDefaultOpen());
1353
+ }
1354
+ });
1355
+ // Handle body scroll lock and escape key
1356
+ effect(() => {
1357
+ if (!isPlatformBrowser(this.platformId))
1358
+ return;
1359
+ if (this.isOpen()) {
1360
+ this.lockBodyScroll();
1361
+ this.registerEscapeHandler();
1362
+ this.storeFocus();
1363
+ }
1364
+ else {
1365
+ // Delay scroll unlock to allow fade animation to complete (250ms)
1366
+ setTimeout(() => this.unlockBodyScroll(), 250);
1367
+ this.unregisterEscapeHandler();
1368
+ }
1369
+ });
1370
+ }
1371
+ ngOnDestroy() {
1372
+ if (isPlatformBrowser(this.platformId)) {
1373
+ this.unlockBodyScroll();
1374
+ this.unregisterEscapeHandler();
1375
+ }
1376
+ }
1377
+ /**
1378
+ * Open the modal
1379
+ */
1380
+ open() {
1381
+ if (this.lmOpen() === null) {
1382
+ this.internalOpen.set(true);
1383
+ }
1384
+ this.lmOpenChange.emit(true);
1385
+ }
1386
+ /**
1387
+ * Close the modal
1388
+ */
1389
+ close() {
1390
+ if (this.lmOpen() === null) {
1391
+ this.internalOpen.set(false);
1392
+ }
1393
+ this.lmOpenChange.emit(false);
1394
+ this.restoreFocus();
1395
+ }
1396
+ storeFocus() {
1397
+ this.previouslyFocused = this.document.activeElement;
1398
+ }
1399
+ restoreFocus() {
1400
+ if (this.previouslyFocused &&
1401
+ typeof this.previouslyFocused.focus === 'function') {
1402
+ // Use setTimeout to ensure DOM has updated
1403
+ setTimeout(() => {
1404
+ this.previouslyFocused?.focus();
1405
+ this.previouslyFocused = null;
1406
+ }, 0);
1407
+ }
1408
+ }
1409
+ lockBodyScroll() {
1410
+ const body = this.document.body;
1411
+ const scrollbarWidth = window.innerWidth - this.document.documentElement.clientWidth;
1412
+ body.style.overflow = 'hidden';
1413
+ body.style.paddingRight = `${scrollbarWidth}px`;
1414
+ }
1415
+ unlockBodyScroll() {
1416
+ const body = this.document.body;
1417
+ body.style.overflow = '';
1418
+ body.style.paddingRight = '';
1419
+ }
1420
+ registerEscapeHandler() {
1421
+ if (this.escapeHandler)
1422
+ return;
1423
+ this.escapeHandler = (event) => {
1424
+ if (event.key === 'Escape' && this.lmCloseOnEscape()) {
1425
+ event.preventDefault();
1426
+ this.close();
1427
+ }
1428
+ };
1429
+ this.document.addEventListener('keydown', this.escapeHandler);
1430
+ }
1431
+ unregisterEscapeHandler() {
1432
+ if (this.escapeHandler) {
1433
+ this.document.removeEventListener('keydown', this.escapeHandler);
1434
+ this.escapeHandler = null;
1435
+ }
1436
+ }
1437
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: ModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1438
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.0.9", type: ModalComponent, isStandalone: true, selector: "luma-modal", inputs: { lmOpen: { classPropertyName: "lmOpen", publicName: "lmOpen", isSignal: true, isRequired: false, transformFunction: null }, lmDefaultOpen: { classPropertyName: "lmDefaultOpen", publicName: "lmDefaultOpen", isSignal: true, isRequired: false, transformFunction: null }, lmSize: { classPropertyName: "lmSize", publicName: "lmSize", isSignal: true, isRequired: false, transformFunction: null }, lmCloseOnOverlay: { classPropertyName: "lmCloseOnOverlay", publicName: "lmCloseOnOverlay", isSignal: true, isRequired: false, transformFunction: null }, lmCloseOnEscape: { classPropertyName: "lmCloseOnEscape", publicName: "lmCloseOnEscape", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { lmOpenChange: "lmOpenChange" }, host: { properties: { "attr.data-state": "isOpen() ? \"open\" : \"closed\"" } }, providers: [
1439
+ {
1440
+ provide: MODAL_CONTEXT,
1441
+ useExisting: ModalComponent,
1442
+ },
1443
+ ], ngImport: i0, template: `<ng-content />`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
1444
+ }
1445
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: ModalComponent, decorators: [{
1446
+ type: Component,
1447
+ args: [{
1448
+ selector: 'luma-modal',
1449
+ template: `<ng-content />`,
1450
+ changeDetection: ChangeDetectionStrategy.OnPush,
1451
+ providers: [
1452
+ {
1453
+ provide: MODAL_CONTEXT,
1454
+ useExisting: ModalComponent,
1455
+ },
1456
+ ],
1457
+ host: {
1458
+ '[attr.data-state]': 'isOpen() ? "open" : "closed"',
1459
+ },
1460
+ }]
1461
+ }], ctorParameters: () => [], propDecorators: { lmOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmOpen", required: false }] }], lmDefaultOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmDefaultOpen", required: false }] }], lmSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmSize", required: false }] }], lmCloseOnOverlay: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmCloseOnOverlay", required: false }] }], lmCloseOnEscape: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmCloseOnEscape", required: false }] }], lmOpenChange: [{ type: i0.Output, args: ["lmOpenChange"] }] } });
1462
+
1463
+ /**
1464
+ * Modal overlay component (backdrop)
1465
+ *
1466
+ * Provides a semi-transparent backdrop behind the modal.
1467
+ * Handles click-to-close when enabled on the parent modal.
1468
+ *
1469
+ * @example
1470
+ * ```html
1471
+ * <luma-modal [lmOpen]="isOpen()">
1472
+ * <luma-modal-overlay>
1473
+ * <luma-modal-container>...</luma-modal-container>
1474
+ * </luma-modal-overlay>
1475
+ * </luma-modal>
1476
+ * ```
1477
+ */
1478
+ class ModalOverlayComponent {
1479
+ modal = inject(MODAL_CONTEXT);
1480
+ /** Computed classes from CVA */
1481
+ classes = computed(() => modalOverlayVariants({
1482
+ open: this.modal.isOpen(),
1483
+ }), ...(ngDevMode ? [{ debugName: "classes" }] : []));
1484
+ /**
1485
+ * Handle click on overlay (not on children)
1486
+ */
1487
+ onOverlayClick(event) {
1488
+ // Only close if clicking directly on overlay, not on children
1489
+ if (event.target === event.currentTarget && this.modal.closeOnOverlay()) {
1490
+ this.modal.close();
1491
+ }
1492
+ }
1493
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: ModalOverlayComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1494
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.0.9", type: ModalOverlayComponent, isStandalone: true, selector: "luma-modal-overlay", host: { listeners: { "click": "onOverlayClick($event)" }, properties: { "class": "classes()" } }, ngImport: i0, template: `<ng-content />`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
1495
+ }
1496
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: ModalOverlayComponent, decorators: [{
1497
+ type: Component,
1498
+ args: [{
1499
+ selector: 'luma-modal-overlay',
1500
+ template: `<ng-content />`,
1501
+ changeDetection: ChangeDetectionStrategy.OnPush,
1502
+ host: {
1503
+ '[class]': 'classes()',
1504
+ '(click)': 'onOverlayClick($event)',
1505
+ },
1506
+ }]
1507
+ }] });
1508
+
1509
+ /** Focusable elements selector */
1510
+ const FOCUSABLE_SELECTOR = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
1511
+ /**
1512
+ * Modal container component (dialog box)
1513
+ *
1514
+ * Contains the modal content and handles:
1515
+ * - ARIA attributes for accessibility
1516
+ * - Focus trap when modal is open
1517
+ * - Size variants
1518
+ *
1519
+ * @example
1520
+ * ```html
1521
+ * <luma-modal-overlay>
1522
+ * <luma-modal-container>
1523
+ * <div lumaModalHeader>...</div>
1524
+ * <div lumaModalContent>...</div>
1525
+ * <div lumaModalFooter>...</div>
1526
+ * </luma-modal-container>
1527
+ * </luma-modal-overlay>
1528
+ * ```
1529
+ */
1530
+ class ModalContainerComponent {
1531
+ modal = inject(MODAL_CONTEXT);
1532
+ elementRef = inject((ElementRef));
1533
+ platformId = inject(PLATFORM_ID);
1534
+ /** Focus trap keydown handler */
1535
+ focusTrapHandler = null;
1536
+ /** ID for aria-labelledby */
1537
+ titleId = computed(() => `${this.modal.modalId}-title`, ...(ngDevMode ? [{ debugName: "titleId" }] : []));
1538
+ /** Computed classes from CVA */
1539
+ classes = computed(() => modalContainerVariants({
1540
+ size: this.modal.size(),
1541
+ open: this.modal.isOpen(),
1542
+ }), ...(ngDevMode ? [{ debugName: "classes" }] : []));
1543
+ constructor() {
1544
+ // Focus first focusable element when modal opens
1545
+ effect(() => {
1546
+ if (!isPlatformBrowser(this.platformId))
1547
+ return;
1548
+ if (this.modal.isOpen()) {
1549
+ // Use setTimeout to ensure content is rendered
1550
+ setTimeout(() => this.focusFirstElement(), 0);
1551
+ }
1552
+ });
1553
+ }
1554
+ ngAfterViewInit() {
1555
+ if (isPlatformBrowser(this.platformId)) {
1556
+ this.setupFocusTrap();
1557
+ }
1558
+ }
1559
+ ngOnDestroy() {
1560
+ if (isPlatformBrowser(this.platformId)) {
1561
+ this.removeFocusTrap();
1562
+ }
1563
+ }
1564
+ /**
1565
+ * Get all focusable elements within the modal
1566
+ */
1567
+ getFocusableElements() {
1568
+ const elements = this.elementRef.nativeElement.querySelectorAll(FOCUSABLE_SELECTOR);
1569
+ const elementsArray = Array.from(elements);
1570
+ return elementsArray.filter((el) => !el.hasAttribute('disabled') && el.getAttribute('tabindex') !== '-1');
1571
+ }
1572
+ /**
1573
+ * Focus the first focusable element
1574
+ */
1575
+ focusFirstElement() {
1576
+ const focusable = this.getFocusableElements();
1577
+ if (focusable.length > 0) {
1578
+ focusable[0].focus();
1579
+ }
1580
+ else {
1581
+ // If no focusable elements, focus the container itself
1582
+ this.elementRef.nativeElement.focus();
1583
+ }
1584
+ }
1585
+ /**
1586
+ * Setup focus trap to keep focus within modal
1587
+ */
1588
+ setupFocusTrap() {
1589
+ this.focusTrapHandler = (event) => {
1590
+ if (event.key !== 'Tab' || !this.modal.isOpen())
1591
+ return;
1592
+ const focusable = this.getFocusableElements();
1593
+ if (focusable.length === 0)
1594
+ return;
1595
+ const firstElement = focusable[0];
1596
+ const lastElement = focusable[focusable.length - 1];
1597
+ // Shift+Tab on first element -> focus last
1598
+ if (event.shiftKey && document.activeElement === firstElement) {
1599
+ event.preventDefault();
1600
+ lastElement.focus();
1601
+ }
1602
+ // Tab on last element -> focus first
1603
+ else if (!event.shiftKey && document.activeElement === lastElement) {
1604
+ event.preventDefault();
1605
+ firstElement.focus();
1606
+ }
1607
+ };
1608
+ this.elementRef.nativeElement.addEventListener('keydown', this.focusTrapHandler);
1609
+ }
1610
+ /**
1611
+ * Remove focus trap handler
1612
+ */
1613
+ removeFocusTrap() {
1614
+ if (this.focusTrapHandler) {
1615
+ this.elementRef.nativeElement.removeEventListener('keydown', this.focusTrapHandler);
1616
+ this.focusTrapHandler = null;
1617
+ }
1618
+ }
1619
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: ModalContainerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1620
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.0.9", type: ModalContainerComponent, isStandalone: true, selector: "luma-modal-container", host: { attributes: { "role": "dialog" }, properties: { "attr.aria-modal": "true", "attr.aria-labelledby": "titleId()", "class": "classes()", "attr.data-state": "modal.isOpen() ? \"open\" : \"closed\"" } }, ngImport: i0, template: `<ng-content />`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
1621
+ }
1622
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: ModalContainerComponent, decorators: [{
1623
+ type: Component,
1624
+ args: [{
1625
+ selector: 'luma-modal-container',
1626
+ template: `<ng-content />`,
1627
+ changeDetection: ChangeDetectionStrategy.OnPush,
1628
+ host: {
1629
+ role: 'dialog',
1630
+ '[attr.aria-modal]': 'true',
1631
+ '[attr.aria-labelledby]': 'titleId()',
1632
+ '[class]': 'classes()',
1633
+ '[attr.data-state]': 'modal.isOpen() ? "open" : "closed"',
1634
+ },
1635
+ }]
1636
+ }], ctorParameters: () => [] });
1637
+
1638
+ /**
1639
+ * Modal header directive
1640
+ *
1641
+ * Container for modal title and close button.
1642
+ * Provides consistent padding and border styling.
1643
+ *
1644
+ * @example
1645
+ * ```html
1646
+ * <div lumaModalHeader>
1647
+ * <h2 lumaModalTitle>Modal Title</h2>
1648
+ * <luma-modal-close />
1649
+ * </div>
1650
+ * ```
1651
+ */
1652
+ class ModalHeaderDirective {
1653
+ /** Computed classes from CVA */
1654
+ classes = computed(() => modalHeaderVariants(), ...(ngDevMode ? [{ debugName: "classes" }] : []));
1655
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: ModalHeaderDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1656
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.9", type: ModalHeaderDirective, isStandalone: true, selector: "[lumaModalHeader]", host: { properties: { "class": "classes()" } }, ngImport: i0 });
1657
+ }
1658
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: ModalHeaderDirective, decorators: [{
1659
+ type: Directive,
1660
+ args: [{
1661
+ selector: '[lumaModalHeader]',
1662
+ host: {
1663
+ '[class]': 'classes()',
1664
+ },
1665
+ }]
1666
+ }] });
1667
+
1668
+ /**
1669
+ * Modal title directive
1670
+ *
1671
+ * Provides consistent typography for modal titles.
1672
+ * Automatically links to aria-labelledby on the modal container.
1673
+ *
1674
+ * @example
1675
+ * ```html
1676
+ * <h2 lumaModalTitle>Modal Title</h2>
1677
+ * <h2 lumaModalTitle lmSize="lg">Large Title</h2>
1678
+ * ```
1679
+ */
1680
+ class ModalTitleDirective {
1681
+ modal = inject(MODAL_CONTEXT);
1682
+ /** Title size variant */
1683
+ lmSize = input('md', ...(ngDevMode ? [{ debugName: "lmSize" }] : []));
1684
+ /** ID for aria-labelledby connection */
1685
+ titleId = computed(() => `${this.modal.modalId}-title`, ...(ngDevMode ? [{ debugName: "titleId" }] : []));
1686
+ /** Computed classes from CVA */
1687
+ classes = computed(() => modalTitleVariants({
1688
+ size: this.lmSize(),
1689
+ }), ...(ngDevMode ? [{ debugName: "classes" }] : []));
1690
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: ModalTitleDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1691
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.9", type: ModalTitleDirective, isStandalone: true, selector: "[lumaModalTitle]", inputs: { lmSize: { classPropertyName: "lmSize", publicName: "lmSize", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.id": "titleId()", "class": "classes()" } }, ngImport: i0 });
1692
+ }
1693
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: ModalTitleDirective, decorators: [{
1694
+ type: Directive,
1695
+ args: [{
1696
+ selector: '[lumaModalTitle]',
1697
+ host: {
1698
+ '[attr.id]': 'titleId()',
1699
+ '[class]': 'classes()',
1700
+ },
1701
+ }]
1702
+ }], propDecorators: { lmSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmSize", required: false }] }] } });
1703
+
1704
+ /**
1705
+ * Modal content directive
1706
+ *
1707
+ * Container for the main modal content.
1708
+ * Supports scrolling when content exceeds available space.
1709
+ *
1710
+ * @example
1711
+ * ```html
1712
+ * <div lumaModalContent>
1713
+ * Content that doesn't scroll
1714
+ * </div>
1715
+ *
1716
+ * <div lumaModalContent [lmScrollable]="true">
1717
+ * Long content that scrolls...
1718
+ * </div>
1719
+ * ```
1720
+ */
1721
+ class ModalContentDirective {
1722
+ /** Enable scroll when content overflows */
1723
+ lmScrollable = input(true, ...(ngDevMode ? [{ debugName: "lmScrollable" }] : []));
1724
+ /** Computed classes from CVA */
1725
+ classes = computed(() => modalContentVariants({
1726
+ scrollable: this.lmScrollable(),
1727
+ }), ...(ngDevMode ? [{ debugName: "classes" }] : []));
1728
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: ModalContentDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1729
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.9", type: ModalContentDirective, isStandalone: true, selector: "[lumaModalContent]", inputs: { lmScrollable: { classPropertyName: "lmScrollable", publicName: "lmScrollable", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0 });
1730
+ }
1731
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: ModalContentDirective, decorators: [{
1732
+ type: Directive,
1733
+ args: [{
1734
+ selector: '[lumaModalContent]',
1735
+ host: {
1736
+ '[class]': 'classes()',
1737
+ },
1738
+ }]
1739
+ }], propDecorators: { lmScrollable: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmScrollable", required: false }] }] } });
1740
+
1741
+ /**
1742
+ * Modal footer directive
1743
+ *
1744
+ * Container for modal actions (buttons, etc.).
1745
+ * Provides consistent padding and flexible alignment.
1746
+ *
1747
+ * @example
1748
+ * ```html
1749
+ * <div lumaModalFooter>
1750
+ * <button lumaButton lmVariant="ghost">Cancel</button>
1751
+ * <button lumaButton>Confirm</button>
1752
+ * </div>
1753
+ *
1754
+ * <div lumaModalFooter lmAlign="between">
1755
+ * <span>Left content</span>
1756
+ * <button lumaButton>Action</button>
1757
+ * </div>
1758
+ * ```
1759
+ */
1760
+ class ModalFooterDirective {
1761
+ /** Alignment of footer content */
1762
+ lmAlign = input('end', ...(ngDevMode ? [{ debugName: "lmAlign" }] : []));
1763
+ /** Computed classes from CVA */
1764
+ classes = computed(() => modalFooterVariants({
1765
+ align: this.lmAlign(),
1766
+ }), ...(ngDevMode ? [{ debugName: "classes" }] : []));
1767
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: ModalFooterDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1768
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.9", type: ModalFooterDirective, isStandalone: true, selector: "[lumaModalFooter]", inputs: { lmAlign: { classPropertyName: "lmAlign", publicName: "lmAlign", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0 });
1769
+ }
1770
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: ModalFooterDirective, decorators: [{
1771
+ type: Directive,
1772
+ args: [{
1773
+ selector: '[lumaModalFooter]',
1774
+ host: {
1775
+ '[class]': 'classes()',
1776
+ },
1777
+ }]
1778
+ }], propDecorators: { lmAlign: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmAlign", required: false }] }] } });
1779
+
1780
+ /**
1781
+ * Modal close button component
1782
+ *
1783
+ * Provides a styled close button with an X icon.
1784
+ * Can be customized with different content via ng-content.
1785
+ *
1786
+ * @example
1787
+ * ```html
1788
+ * <!-- Default X icon -->
1789
+ * <luma-modal-close />
1790
+ *
1791
+ * <!-- Custom aria label -->
1792
+ * <luma-modal-close lmAriaLabel="Fechar modal" />
1793
+ *
1794
+ * <!-- Custom icon -->
1795
+ * <luma-modal-close>
1796
+ * <svg>...</svg>
1797
+ * </luma-modal-close>
1798
+ * ```
1799
+ */
1800
+ class ModalCloseComponent {
1801
+ modal = inject(MODAL_CONTEXT);
1802
+ /** Accessible label for the close button */
1803
+ lmAriaLabel = input('Close modal', ...(ngDevMode ? [{ debugName: "lmAriaLabel" }] : []));
1804
+ /** Computed aria label */
1805
+ ariaLabel = computed(() => this.lmAriaLabel(), ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
1806
+ /** Computed classes from CVA */
1807
+ classes = computed(() => modalCloseVariants(), ...(ngDevMode ? [{ debugName: "classes" }] : []));
1808
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: ModalCloseComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1809
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.0.9", type: ModalCloseComponent, isStandalone: true, selector: "luma-modal-close", inputs: { lmAriaLabel: { classPropertyName: "lmAriaLabel", publicName: "lmAriaLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "contents" }, ngImport: i0, template: `
1810
+ <button
1811
+ type="button"
1812
+ [attr.aria-label]="ariaLabel()"
1813
+ [class]="classes()"
1814
+ (click)="modal.close()"
1815
+ >
1816
+ <ng-content>
1817
+ <svg
1818
+ viewBox="0 0 24 24"
1819
+ class="w-4 h-4"
1820
+ fill="none"
1821
+ stroke="currentColor"
1822
+ aria-hidden="true"
1823
+ >
1824
+ <path
1825
+ stroke-linecap="round"
1826
+ stroke-linejoin="round"
1827
+ stroke-width="2"
1828
+ d="M6 18L18 6M6 6l12 12"
1829
+ />
1830
+ </svg>
1831
+ </ng-content>
1832
+ </button>
1833
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
1834
+ }
1835
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: ModalCloseComponent, decorators: [{
1836
+ type: Component,
1837
+ args: [{
1838
+ selector: 'luma-modal-close',
1839
+ template: `
1840
+ <button
1841
+ type="button"
1842
+ [attr.aria-label]="ariaLabel()"
1843
+ [class]="classes()"
1844
+ (click)="modal.close()"
1845
+ >
1846
+ <ng-content>
1847
+ <svg
1848
+ viewBox="0 0 24 24"
1849
+ class="w-4 h-4"
1850
+ fill="none"
1851
+ stroke="currentColor"
1852
+ aria-hidden="true"
1853
+ >
1854
+ <path
1855
+ stroke-linecap="round"
1856
+ stroke-linejoin="round"
1857
+ stroke-width="2"
1858
+ d="M6 18L18 6M6 6l12 12"
1859
+ />
1860
+ </svg>
1861
+ </ng-content>
1862
+ </button>
1863
+ `,
1864
+ changeDetection: ChangeDetectionStrategy.OnPush,
1865
+ host: {
1866
+ class: 'contents',
1867
+ },
1868
+ }]
1869
+ }], propDecorators: { lmAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmAriaLabel", required: false }] }] } });
1870
+
1871
+ // Modal Component & Directives
1872
+
120
1873
  // Button exports
121
1874
 
122
1875
  /**
123
1876
  * Generated bundle index. Do not edit.
124
1877
  */
125
1878
 
126
- export { ButtonDirective, CardComponent, CardContentDirective, CardDescriptionDirective, CardHeaderDirective, CardTitleDirective };
1879
+ export { ACCORDION_ITEM, AccordionContentDirective, AccordionGroupComponent, AccordionIconDirective, AccordionItemComponent, AccordionTitleDirective, AccordionTriggerDirective, BadgeDirective, ButtonDirective, CardComponent, CardContentDirective, CardDescriptionDirective, CardHeaderDirective, CardTitleDirective, MODAL_CONTEXT, ModalCloseComponent, ModalComponent, ModalContainerComponent, ModalContentDirective, ModalFooterDirective, ModalHeaderDirective, ModalOverlayComponent, ModalTitleDirective, TABS_GROUP, TABS_LIST, TabsComponent, TabsIndicatorComponent, TabsListDirective, TabsPanelDirective, TabsTriggerDirective, TooltipDirective };
127
1880
  //# sourceMappingURL=lumaui-angular.mjs.map