@lumaui/angular 0.2.3 → 0.3.2

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,7 +1,7 @@
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, TemplateRef, ViewContainerRef, ApplicationRef, Injector, createComponent, Injectable } 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, toastCloseVariants, toastItemVariants, toastIconVariants, toastContentVariants, toastTitleVariants, toastMessageVariants, toastContainerVariants } from '@lumaui/core';
4
- import { isPlatformBrowser, DOCUMENT } from '@angular/common';
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';
4
+ import { DOCUMENT, isPlatformBrowser } from '@angular/common';
5
5
  import { Subject, interval } from 'rxjs';
6
6
  import { takeWhile, filter } from 'rxjs/operators';
7
7
 
@@ -9,18 +9,20 @@ class LmButtonDirective {
9
9
  // Signal-based inputs with lm prefix (Angular 20+)
10
10
  lmVariant = input('primary', ...(ngDevMode ? [{ debugName: "lmVariant" }] : []));
11
11
  lmSize = input('md', ...(ngDevMode ? [{ debugName: "lmSize" }] : []));
12
+ lmRadius = input('default', ...(ngDevMode ? [{ debugName: "lmRadius" }] : []));
12
13
  lmDisabled = input(false, ...(ngDevMode ? [{ debugName: "lmDisabled" }] : []));
13
14
  lmType = input('button', ...(ngDevMode ? [{ debugName: "lmType" }] : []));
14
15
  // Computed class string
15
16
  classes = computed(() => buttonVariants({
16
17
  variant: this.lmVariant(),
17
18
  size: this.lmSize(),
19
+ radius: this.lmRadius(),
18
20
  }), ...(ngDevMode ? [{ debugName: "classes" }] : []));
19
21
  get hostClasses() {
20
22
  return this.classes();
21
23
  }
22
24
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmButtonDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
23
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.9", type: LmButtonDirective, isStandalone: true, selector: "button[lumaButton], a[lumaButton]", inputs: { lmVariant: { classPropertyName: "lmVariant", publicName: "lmVariant", isSignal: true, isRequired: false, transformFunction: null }, lmSize: { classPropertyName: "lmSize", publicName: "lmSize", isSignal: true, isRequired: false, transformFunction: null }, lmDisabled: { classPropertyName: "lmDisabled", publicName: "lmDisabled", isSignal: true, isRequired: false, transformFunction: null }, lmType: { classPropertyName: "lmType", publicName: "lmType", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.type": "lmType()", "attr.disabled": "lmDisabled() ? \"\" : null", "class": "this.hostClasses" } }, ngImport: i0 });
25
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.9", type: LmButtonDirective, isStandalone: true, selector: "button[lumaButton], a[lumaButton]", inputs: { lmVariant: { classPropertyName: "lmVariant", publicName: "lmVariant", isSignal: true, isRequired: false, transformFunction: null }, lmSize: { classPropertyName: "lmSize", publicName: "lmSize", isSignal: true, isRequired: false, transformFunction: null }, lmRadius: { classPropertyName: "lmRadius", publicName: "lmRadius", isSignal: true, isRequired: false, transformFunction: null }, lmDisabled: { classPropertyName: "lmDisabled", publicName: "lmDisabled", isSignal: true, isRequired: false, transformFunction: null }, lmType: { classPropertyName: "lmType", publicName: "lmType", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.type": "lmType()", "attr.disabled": "lmDisabled() ? \"\" : null", "class": "this.hostClasses" } }, ngImport: i0 });
24
26
  }
25
27
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmButtonDirective, decorators: [{
26
28
  type: Directive,
@@ -31,26 +33,32 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImpor
31
33
  '[attr.disabled]': 'lmDisabled() ? "" : null',
32
34
  },
33
35
  }]
34
- }], propDecorators: { lmVariant: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmVariant", required: false }] }], lmSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmSize", required: false }] }], lmDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmDisabled", required: false }] }], lmType: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmType", required: false }] }], hostClasses: [{
36
+ }], propDecorators: { lmVariant: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmVariant", required: false }] }], lmSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmSize", required: false }] }], lmRadius: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmRadius", required: false }] }], lmDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmDisabled", required: false }] }], lmType: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmType", required: false }] }], hostClasses: [{
35
37
  type: HostBinding,
36
38
  args: ['class']
37
39
  }] } });
38
40
 
39
41
  class LmBadgeDirective {
40
- // Computed class string - layout only, no variants
41
- classes = computed(() => badgeVariants(), ...(ngDevMode ? [{ debugName: "classes" }] : []));
42
+ // Signal-based inputs with lm prefix (Angular 20+)
43
+ lmVariant = input('default', ...(ngDevMode ? [{ debugName: "lmVariant" }] : []));
44
+ lmRadius = input('default', ...(ngDevMode ? [{ debugName: "lmRadius" }] : []));
45
+ // Computed class string
46
+ classes = computed(() => badgeVariants({
47
+ variant: this.lmVariant(),
48
+ radius: this.lmRadius(),
49
+ }), ...(ngDevMode ? [{ debugName: "classes" }] : []));
42
50
  get hostClasses() {
43
51
  return this.classes();
44
52
  }
45
53
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmBadgeDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
46
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.9", type: LmBadgeDirective, isStandalone: true, selector: "[lumaBadge]", host: { properties: { "class": "this.hostClasses" } }, ngImport: i0 });
54
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.9", type: LmBadgeDirective, isStandalone: true, selector: "[lumaBadge]", inputs: { lmVariant: { classPropertyName: "lmVariant", publicName: "lmVariant", isSignal: true, isRequired: false, transformFunction: null }, lmRadius: { classPropertyName: "lmRadius", publicName: "lmRadius", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "this.hostClasses" } }, ngImport: i0 });
47
55
  }
48
56
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmBadgeDirective, decorators: [{
49
57
  type: Directive,
50
58
  args: [{
51
59
  selector: '[lumaBadge]',
52
60
  }]
53
- }], propDecorators: { hostClasses: [{
61
+ }], propDecorators: { lmVariant: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmVariant", required: false }] }], lmRadius: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmRadius", required: false }] }], hostClasses: [{
54
62
  type: HostBinding,
55
63
  args: ['class']
56
64
  }] } });
@@ -58,26 +66,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImpor
58
66
  class LmCardComponent {
59
67
  /**
60
68
  * Card visual style variant
61
- * - default: Gradient border wrapper style (default)
62
- * - shadow: Elevated card with shadow for primary content
63
- * - nested: Subtle background for sections within cards
64
- * - preview: For documentation examples
69
+ * - default: Simple border card
70
+ * - elevated: Card with shadow elevation
71
+ * - subtle: Muted background card
65
72
  */
66
73
  lmVariant = input('default', ...(ngDevMode ? [{ debugName: "lmVariant" }] : []));
67
- // Computed class strings based on variant
68
- wrapperClasses = computed(() => cardVariants({ variant: this.lmVariant() }), ...(ngDevMode ? [{ debugName: "wrapperClasses" }] : []));
69
- contentClasses = computed(() => cardContentVariants({ variant: this.lmVariant() }), ...(ngDevMode ? [{ debugName: "contentClasses" }] : []));
74
+ // Computed class string based on variant
75
+ classes = computed(() => cardVariants({ variant: this.lmVariant() }), ...(ngDevMode ? [{ debugName: "classes" }] : []));
70
76
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
71
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.0.9", type: LmCardComponent, isStandalone: true, selector: "luma-card", inputs: { lmVariant: { classPropertyName: "lmVariant", publicName: "lmVariant", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div [class]=\"wrapperClasses()\">\n <div [class]=\"contentClasses()\">\n <ng-content></ng-content>\n </div>\n</div>\n", changeDetection: i0.ChangeDetectionStrategy.OnPush });
77
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.0.9", type: LmCardComponent, isStandalone: true, selector: "luma-card", inputs: { lmVariant: { classPropertyName: "lmVariant", publicName: "lmVariant", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div [class]=\"classes()\">\n <ng-content></ng-content>\n</div>\n", changeDetection: i0.ChangeDetectionStrategy.OnPush });
72
78
  }
73
79
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmCardComponent, decorators: [{
74
80
  type: Component,
75
- args: [{ selector: 'luma-card', changeDetection: ChangeDetectionStrategy.OnPush, template: "<div [class]=\"wrapperClasses()\">\n <div [class]=\"contentClasses()\">\n <ng-content></ng-content>\n </div>\n</div>\n" }]
81
+ args: [{ selector: 'luma-card', changeDetection: ChangeDetectionStrategy.OnPush, template: "<div [class]=\"classes()\">\n <ng-content></ng-content>\n</div>\n" }]
76
82
  }], propDecorators: { lmVariant: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmVariant", required: false }] }] } });
77
83
 
78
84
  class LmCardTitleDirective {
79
85
  // Signal-based inputs with lm prefix (Angular 20+)
80
- lmSize = input('normal', ...(ngDevMode ? [{ debugName: "lmSize" }] : []));
86
+ lmSize = input('md', ...(ngDevMode ? [{ debugName: "lmSize" }] : []));
81
87
  // Computed class string
82
88
  classes = computed(() => cardTitleVariants({ size: this.lmSize() }), ...(ngDevMode ? [{ debugName: "classes" }] : []));
83
89
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmCardTitleDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
@@ -95,7 +101,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImpor
95
101
 
96
102
  class LmCardDescriptionDirective {
97
103
  // Signal-based inputs with lm prefix (Angular 20+)
98
- lmSize = input('normal', ...(ngDevMode ? [{ debugName: "lmSize" }] : []));
104
+ lmSize = input('md', ...(ngDevMode ? [{ debugName: "lmSize" }] : []));
99
105
  // Computed class string
100
106
  classes = computed(() => cardDescriptionVariants({ size: this.lmSize() }), ...(ngDevMode ? [{ debugName: "classes" }] : []));
101
107
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmCardDescriptionDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
@@ -112,29 +118,33 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImpor
112
118
  }], propDecorators: { lmSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmSize", required: false }] }] } });
113
119
 
114
120
  class LmCardHeaderDirective {
121
+ // Computed class string
122
+ classes = computed(() => cardHeaderVariants(), ...(ngDevMode ? [{ debugName: "classes" }] : []));
115
123
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmCardHeaderDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
116
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.9", type: LmCardHeaderDirective, isStandalone: true, selector: "[lumaCardHeader]", host: { classAttribute: "mb-4" }, ngImport: i0 });
124
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.9", type: LmCardHeaderDirective, isStandalone: true, selector: "[lumaCardHeader]", host: { properties: { "class": "classes()" } }, ngImport: i0 });
117
125
  }
118
126
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmCardHeaderDirective, decorators: [{
119
127
  type: Directive,
120
128
  args: [{
121
129
  selector: '[lumaCardHeader]',
122
130
  host: {
123
- class: 'mb-4',
131
+ '[class]': 'classes()',
124
132
  },
125
133
  }]
126
134
  }] });
127
135
 
128
136
  class LmCardContentDirective {
137
+ // Computed class string
138
+ classes = computed(() => cardContentVariants(), ...(ngDevMode ? [{ debugName: "classes" }] : []));
129
139
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmCardContentDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
130
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.9", type: LmCardContentDirective, isStandalone: true, selector: "[lumaCardContent]", ngImport: i0 });
140
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.9", type: LmCardContentDirective, isStandalone: true, selector: "[lumaCardContent]", host: { properties: { "class": "classes()" } }, ngImport: i0 });
131
141
  }
132
142
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmCardContentDirective, decorators: [{
133
143
  type: Directive,
134
144
  args: [{
135
145
  selector: '[lumaCardContent]',
136
146
  host: {
137
- class: '',
147
+ '[class]': 'classes()',
138
148
  },
139
149
  }]
140
150
  }] });
@@ -557,10 +567,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImpor
557
567
  }]
558
568
  }] });
559
569
 
570
+ const TOOLTIP_GAP = 8;
560
571
  class LmTooltipDirective {
561
572
  el = inject(ElementRef);
562
573
  renderer = inject(Renderer2);
563
574
  platformId = inject(PLATFORM_ID);
575
+ document = inject(DOCUMENT);
576
+ ngZone = inject(NgZone);
564
577
  // Inputs with lm prefix following Lumo convention
565
578
  lumaTooltip = input.required(...(ngDevMode ? [{ debugName: "lumaTooltip" }] : []));
566
579
  lmPosition = input('top', ...(ngDevMode ? [{ debugName: "lmPosition" }] : []));
@@ -573,8 +586,10 @@ class LmTooltipDirective {
573
586
  tooltipId = `tooltip-${Math.random().toString(36).slice(2, 9)}`;
574
587
  tooltipElement = null;
575
588
  showTimeout = null;
589
+ scrollListener = null;
590
+ resizeListener = null;
591
+ captureScrollHandler = null;
576
592
  classes = computed(() => tooltipVariants({
577
- position: this.actualPosition(),
578
593
  visible: this.isVisible(),
579
594
  }), ...(ngDevMode ? [{ debugName: "classes" }] : []));
580
595
  constructor() {
@@ -593,7 +608,8 @@ class LmTooltipDirective {
593
608
  this.tooltipElement = this.renderer.createElement('div');
594
609
  this.renderer.setAttribute(this.tooltipElement, 'id', this.tooltipId);
595
610
  this.renderer.setAttribute(this.tooltipElement, 'role', 'tooltip');
596
- this.renderer.appendChild(this.el.nativeElement, this.tooltipElement);
611
+ // Portal: append to document.body to avoid overflow clipping
612
+ this.renderer.appendChild(this.document.body, this.tooltipElement);
597
613
  }
598
614
  updateContent() {
599
615
  if (!this.tooltipElement)
@@ -611,6 +627,68 @@ class LmTooltipDirective {
611
627
  return;
612
628
  this.tooltipElement.className = this.classes();
613
629
  }
630
+ updatePosition() {
631
+ if (!this.tooltipElement || !isPlatformBrowser(this.platformId))
632
+ return;
633
+ const triggerRect = this.el.nativeElement.getBoundingClientRect();
634
+ const position = this.actualPosition();
635
+ let top;
636
+ let left;
637
+ let transform;
638
+ switch (position) {
639
+ case 'top':
640
+ left = triggerRect.left + triggerRect.width / 2;
641
+ top = triggerRect.top - TOOLTIP_GAP;
642
+ transform = 'translate(-50%, -100%)';
643
+ break;
644
+ case 'bottom':
645
+ left = triggerRect.left + triggerRect.width / 2;
646
+ top = triggerRect.bottom + TOOLTIP_GAP;
647
+ transform = 'translate(-50%, 0%)';
648
+ break;
649
+ case 'left':
650
+ left = triggerRect.left - TOOLTIP_GAP;
651
+ top = triggerRect.top + triggerRect.height / 2;
652
+ transform = 'translate(-100%, -50%)';
653
+ break;
654
+ case 'right':
655
+ left = triggerRect.right + TOOLTIP_GAP;
656
+ top = triggerRect.top + triggerRect.height / 2;
657
+ transform = 'translate(0%, -50%)';
658
+ break;
659
+ }
660
+ this.renderer.setStyle(this.tooltipElement, 'top', `${top}px`);
661
+ this.renderer.setStyle(this.tooltipElement, 'left', `${left}px`);
662
+ this.renderer.setStyle(this.tooltipElement, 'transform', transform);
663
+ }
664
+ addPositionListeners() {
665
+ if (!isPlatformBrowser(this.platformId))
666
+ return;
667
+ // Run outside Angular zone for performance — no change detection needed
668
+ this.ngZone.runOutsideAngular(() => {
669
+ const onReposition = () => this.updatePosition();
670
+ this.scrollListener = this.renderer.listen('window', 'scroll', onReposition);
671
+ // Capture phase catches scroll events on any ancestor
672
+ this.document.addEventListener('scroll', onReposition, true);
673
+ this.captureScrollHandler = onReposition;
674
+ this.resizeListener = this.renderer.listen('window', 'resize', onReposition);
675
+ });
676
+ }
677
+ removePositionListeners() {
678
+ if (this.scrollListener) {
679
+ this.scrollListener();
680
+ this.scrollListener = null;
681
+ }
682
+ if (this.resizeListener) {
683
+ this.resizeListener();
684
+ this.resizeListener = null;
685
+ }
686
+ // Remove capture-phase scroll listener
687
+ if (this.captureScrollHandler) {
688
+ this.document.removeEventListener('scroll', this.captureScrollHandler, true);
689
+ this.captureScrollHandler = null;
690
+ }
691
+ }
614
692
  isTouchDevice() {
615
693
  if (!isPlatformBrowser(this.platformId))
616
694
  return false;
@@ -624,25 +702,26 @@ class LmTooltipDirective {
624
702
  const tooltipRect = this.tooltipElement.getBoundingClientRect();
625
703
  const viewportWidth = window.innerWidth;
626
704
  const viewportHeight = window.innerHeight;
627
- const offset = 8; // --luma-tooltip-offset
628
705
  switch (preferred) {
629
706
  case 'top':
630
- if (triggerRect.top - tooltipRect.height - offset < 0) {
707
+ if (triggerRect.top - tooltipRect.height - TOOLTIP_GAP < 0) {
631
708
  return 'bottom';
632
709
  }
633
710
  break;
634
711
  case 'bottom':
635
- if (triggerRect.bottom + tooltipRect.height + offset > viewportHeight) {
712
+ if (triggerRect.bottom + tooltipRect.height + TOOLTIP_GAP >
713
+ viewportHeight) {
636
714
  return 'top';
637
715
  }
638
716
  break;
639
717
  case 'left':
640
- if (triggerRect.left - tooltipRect.width - offset < 0) {
718
+ if (triggerRect.left - tooltipRect.width - TOOLTIP_GAP < 0) {
641
719
  return 'right';
642
720
  }
643
721
  break;
644
722
  case 'right':
645
- if (triggerRect.right + tooltipRect.width + offset > viewportWidth) {
723
+ if (triggerRect.right + tooltipRect.width + TOOLTIP_GAP >
724
+ viewportWidth) {
646
725
  return 'left';
647
726
  }
648
727
  break;
@@ -686,14 +765,19 @@ class LmTooltipDirective {
686
765
  onDocumentClick(event) {
687
766
  if (this.lmTrigger() !== 'click' && !this.isTouchDevice())
688
767
  return;
689
- if (!this.el.nativeElement.contains(event.target)) {
768
+ const target = event.target;
769
+ // Check both trigger and portal tooltip (tooltip is now in body, not trigger)
770
+ if (!this.el.nativeElement.contains(target) &&
771
+ !this.tooltipElement?.contains(target)) {
690
772
  this.hide();
691
773
  }
692
774
  }
693
775
  onDocumentTouch(event) {
694
776
  if (!this.isVisible())
695
777
  return;
696
- if (!this.el.nativeElement.contains(event.target)) {
778
+ const target = event.target;
779
+ if (!this.el.nativeElement.contains(target) &&
780
+ !this.tooltipElement?.contains(target)) {
697
781
  this.hide();
698
782
  }
699
783
  }
@@ -702,12 +786,15 @@ class LmTooltipDirective {
702
786
  clearTimeout(this.showTimeout);
703
787
  const delay = this.lmDelay();
704
788
  const showAction = () => {
789
+ // Calculate position with auto-flip
790
+ const actualPosition = this.getFlippedPosition(this.lmPosition());
791
+ this.actualPosition.set(actualPosition);
705
792
  this.isVisible.set(true);
706
- // Calculate position with auto-flip after tooltip is visible
793
+ this.updateClasses();
794
+ // Position after DOM paint so tooltip dimensions are available
707
795
  requestAnimationFrame(() => {
708
- const actualPosition = this.getFlippedPosition(this.lmPosition());
709
- this.actualPosition.set(actualPosition);
710
- this.updateClasses();
796
+ this.updatePosition();
797
+ this.addPositionListeners();
711
798
  });
712
799
  };
713
800
  if (delay > 0) {
@@ -724,6 +811,7 @@ class LmTooltipDirective {
724
811
  }
725
812
  this.isVisible.set(false);
726
813
  this.updateClasses();
814
+ this.removePositionListeners();
727
815
  }
728
816
  toggle() {
729
817
  if (this.isVisible()) {
@@ -736,12 +824,13 @@ class LmTooltipDirective {
736
824
  ngOnDestroy() {
737
825
  if (this.showTimeout)
738
826
  clearTimeout(this.showTimeout);
827
+ this.removePositionListeners();
739
828
  if (this.tooltipElement) {
740
- this.renderer.removeChild(this.el.nativeElement, this.tooltipElement);
829
+ this.renderer.removeChild(this.document.body, this.tooltipElement);
741
830
  }
742
831
  }
743
832
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmTooltipDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
744
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.9", type: LmTooltipDirective, 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 });
833
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.9", type: LmTooltipDirective, 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" } }, ngImport: i0 });
745
834
  }
746
835
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmTooltipDirective, decorators: [{
747
836
  type: Directive,
@@ -749,7 +838,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImpor
749
838
  selector: '[lumaTooltip]',
750
839
  host: {
751
840
  '[attr.aria-describedby]': 'tooltipId',
752
- '[style.position]': '"relative"',
753
841
  },
754
842
  }]
755
843
  }], 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: [{
@@ -935,10 +1023,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImpor
935
1023
  }], ctorParameters: () => [], propDecorators: { lmValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmValue", required: false }] }], lmDefaultValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmDefaultValue", required: false }] }], lmVariant: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmVariant", required: false }] }], lmLazy: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmLazy", required: false }] }], lmValueChange: [{ type: i0.Output, args: ["lmValueChange"] }] } });
936
1024
 
937
1025
  /**
938
- * Tabs list directive
1026
+ * Tabs list component
939
1027
  *
940
1028
  * Container for tab triggers with role="tablist".
941
1029
  * Provides context for the indicator component.
1030
+ * Supports horizontal scrolling with arrow navigation.
942
1031
  *
943
1032
  * @example
944
1033
  * ```html
@@ -947,16 +1036,125 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImpor
947
1036
  * <button lumaTabsTrigger="tab-2">Tab 2</button>
948
1037
  * </div>
949
1038
  * ```
1039
+ *
1040
+ * @example
1041
+ * ```html
1042
+ * <div lumaTabsList [lmScrollable]="true">
1043
+ * <button lumaTabsTrigger="tab-1">Tab 1</button>
1044
+ * <!-- ... many more tabs -->
1045
+ * </div>
1046
+ * ```
950
1047
  */
951
1048
  class LmTabsListDirective {
952
1049
  elementRef = inject((ElementRef));
953
1050
  tabsGroup = inject(TABS_GROUP);
1051
+ platformId = inject(PLATFORM_ID);
1052
+ /** Reference to the scroll container */
1053
+ scrollContainerRef = viewChild('scrollContainer', ...(ngDevMode ? [{ debugName: "scrollContainerRef" }] : []));
954
1054
  /** Whether horizontal scrolling is enabled */
955
- lmScrollable = false;
956
- classes = computed(() => tabsListVariants({
957
- style: this.tabsGroup.lmVariant(),
958
- scrollable: this.lmScrollable,
959
- }), ...(ngDevMode ? [{ debugName: "classes" }] : []));
1055
+ lmScrollable = input(false, ...(ngDevMode ? [{ debugName: "lmScrollable" }] : []));
1056
+ /** Whether to show scroll arrows */
1057
+ showArrows = signal(false, ...(ngDevMode ? [{ debugName: "showArrows" }] : []));
1058
+ showLeftArrow = signal(false, ...(ngDevMode ? [{ debugName: "showLeftArrow" }] : []));
1059
+ showRightArrow = signal(false, ...(ngDevMode ? [{ debugName: "showRightArrow" }] : []));
1060
+ /** ResizeObserver for overflow detection */
1061
+ resizeObserver;
1062
+ /** Debounce timeout for ResizeObserver */
1063
+ resizeTimeout;
1064
+ /** Throttle timeout for scroll events */
1065
+ scrollTimeout;
1066
+ /** Host element classes */
1067
+ hostClasses = computed(() => {
1068
+ if (this.lmScrollable()) {
1069
+ // Scrollable: host is a structural flex wrapper (no role, no variant styling)
1070
+ return 'flex items-center gap-2 w-full';
1071
+ }
1072
+ // Non-scrollable: host is the tablist with variant styling
1073
+ return tabsListVariants({
1074
+ variant: this.tabsGroup.lmVariant(),
1075
+ });
1076
+ }, ...(ngDevMode ? [{ debugName: "hostClasses" }] : []));
1077
+ /** Scroll container classes */
1078
+ scrollContainerClasses = computed(() => {
1079
+ const baseClasses = ['flex', 'items-center'];
1080
+ if (this.lmScrollable()) {
1081
+ const scrollClasses = [
1082
+ ...baseClasses,
1083
+ 'flex-1', // Fill available space in flex wrapper
1084
+ 'relative', // For indicator absolute positioning
1085
+ 'overflow-x-auto',
1086
+ 'scrollbar-none',
1087
+ 'scroll-smooth',
1088
+ '-webkit-overflow-scrolling-touch',
1089
+ ];
1090
+ // Move variant visual styling from host to scroll container
1091
+ const variant = this.tabsGroup.lmVariant();
1092
+ if (variant === 'underline') {
1093
+ scrollClasses.push('border-b', 'border-border');
1094
+ }
1095
+ else if (variant === 'pills') {
1096
+ scrollClasses.push('p-1', 'bg-muted', 'rounded-lg');
1097
+ }
1098
+ return scrollClasses;
1099
+ }
1100
+ // Non-scrollable: apply w-full and variant-specific classes
1101
+ const variant = this.tabsGroup.lmVariant();
1102
+ if (variant === 'underline') {
1103
+ return [...baseClasses, 'w-full', 'gap-4'];
1104
+ }
1105
+ else if (variant === 'pills') {
1106
+ return [...baseClasses, 'w-full', 'gap-1'];
1107
+ }
1108
+ return [...baseClasses, 'w-full'];
1109
+ }, ...(ngDevMode ? [{ debugName: "scrollContainerClasses" }] : []));
1110
+ /** Left arrow button classes */
1111
+ leftArrowClasses = computed(() => tabsScrollArrowVariants({ direction: 'left' }), ...(ngDevMode ? [{ debugName: "leftArrowClasses" }] : []));
1112
+ /** Right arrow button classes */
1113
+ rightArrowClasses = computed(() => tabsScrollArrowVariants({ direction: 'right' }), ...(ngDevMode ? [{ debugName: "rightArrowClasses" }] : []));
1114
+ constructor() {
1115
+ // Update arrows when scrollable changes
1116
+ effect(() => {
1117
+ if (this.lmScrollable()) {
1118
+ this.updateArrowsVisibility();
1119
+ }
1120
+ else {
1121
+ this.showArrows.set(false);
1122
+ }
1123
+ });
1124
+ }
1125
+ ngAfterViewInit() {
1126
+ if (!isPlatformBrowser(this.platformId)) {
1127
+ return;
1128
+ }
1129
+ const container = this.scrollContainerRef()?.nativeElement;
1130
+ if (!container)
1131
+ return;
1132
+ // Set up ResizeObserver with debouncing to prevent infinite feedback loop
1133
+ this.resizeObserver = new ResizeObserver(() => {
1134
+ // Debounce: wait 250ms after last resize before updating
1135
+ if (this.resizeTimeout !== undefined) {
1136
+ clearTimeout(this.resizeTimeout);
1137
+ }
1138
+ this.resizeTimeout = window.setTimeout(() => {
1139
+ this.updateArrowsVisibility();
1140
+ this.resizeTimeout = undefined;
1141
+ }, 250);
1142
+ });
1143
+ // Only observe the container, NOT children (prevents cascading updates)
1144
+ this.resizeObserver.observe(container);
1145
+ // Initial check
1146
+ this.updateArrowsVisibility();
1147
+ }
1148
+ ngOnDestroy() {
1149
+ this.resizeObserver?.disconnect();
1150
+ // Clear timeouts to prevent memory leaks
1151
+ if (this.resizeTimeout !== undefined) {
1152
+ clearTimeout(this.resizeTimeout);
1153
+ }
1154
+ if (this.scrollTimeout !== undefined) {
1155
+ clearTimeout(this.scrollTimeout);
1156
+ }
1157
+ }
960
1158
  /**
961
1159
  * Get the currently active trigger element
962
1160
  */
@@ -967,25 +1165,157 @@ class LmTabsListDirective {
967
1165
  const triggers = this.tabsGroup.getTriggers();
968
1166
  return triggers.get(currentValue) || null;
969
1167
  }
1168
+ /**
1169
+ * Get the element that acts as the semantic tablist container.
1170
+ * When scrollable, this is the inner scroll container (which has role="tablist").
1171
+ * When non-scrollable, this is the host element itself.
1172
+ */
1173
+ getListContainer() {
1174
+ if (this.lmScrollable()) {
1175
+ return (this.scrollContainerRef()?.nativeElement ??
1176
+ this.elementRef.nativeElement);
1177
+ }
1178
+ return this.elementRef.nativeElement;
1179
+ }
1180
+ /**
1181
+ * Scroll to the next set of tabs (85% of container width)
1182
+ */
1183
+ scrollNext() {
1184
+ const container = this.scrollContainerRef()?.nativeElement;
1185
+ if (!container)
1186
+ return;
1187
+ const scrollAmount = container.clientWidth * 0.85;
1188
+ container.scrollBy({ left: scrollAmount, behavior: 'smooth' });
1189
+ }
1190
+ /**
1191
+ * Scroll to the previous set of tabs (85% of container width)
1192
+ */
1193
+ scrollPrevious() {
1194
+ const container = this.scrollContainerRef()?.nativeElement;
1195
+ if (!container)
1196
+ return;
1197
+ const scrollAmount = container.clientWidth * 0.85;
1198
+ container.scrollBy({ left: -scrollAmount, behavior: 'smooth' });
1199
+ }
1200
+ /**
1201
+ * Handle scroll event to update arrow states (throttled)
1202
+ */
1203
+ onScroll() {
1204
+ if (!this.lmScrollable())
1205
+ return;
1206
+ // Throttle: only update after 100ms of no scrolling
1207
+ if (this.scrollTimeout !== undefined) {
1208
+ clearTimeout(this.scrollTimeout);
1209
+ }
1210
+ this.scrollTimeout = window.setTimeout(() => {
1211
+ this.updateArrowsVisibility();
1212
+ this.scrollTimeout = undefined;
1213
+ }, 100);
1214
+ }
970
1215
  /**
971
1216
  * Handle mouse wheel for horizontal scroll
972
1217
  */
973
1218
  onWheel(event) {
974
- if (this.lmScrollable) {
975
- event.preventDefault();
976
- this.elementRef.nativeElement.scrollLeft += event.deltaY;
1219
+ if (!this.lmScrollable())
1220
+ return;
1221
+ const container = this.scrollContainerRef()?.nativeElement;
1222
+ if (!container)
1223
+ return;
1224
+ event.preventDefault();
1225
+ container.scrollLeft += event.deltaY;
1226
+ }
1227
+ /**
1228
+ * Update arrow visibility and enabled states based on scroll position
1229
+ * Optimized to only update signals when values actually change
1230
+ */
1231
+ updateArrowsVisibility() {
1232
+ if (!isPlatformBrowser(this.platformId)) {
1233
+ return;
1234
+ }
1235
+ const container = this.scrollContainerRef()?.nativeElement;
1236
+ if (!container)
1237
+ return;
1238
+ // Check if content overflows container
1239
+ const hasOverflow = container.scrollWidth > container.clientWidth;
1240
+ // Only update if value actually changed
1241
+ if (this.showArrows() !== hasOverflow) {
1242
+ this.showArrows.set(hasOverflow);
1243
+ }
1244
+ if (hasOverflow) {
1245
+ // Update individual arrow states based on scroll position
1246
+ const isAtStart = container.scrollLeft <= 1;
1247
+ const isAtEnd = container.scrollLeft >=
1248
+ container.scrollWidth - container.clientWidth - 1;
1249
+ const newShowLeft = !isAtStart;
1250
+ const newShowRight = !isAtEnd;
1251
+ // Only update if values changed
1252
+ if (this.showLeftArrow() !== newShowLeft) {
1253
+ this.showLeftArrow.set(newShowLeft);
1254
+ }
1255
+ if (this.showRightArrow() !== newShowRight) {
1256
+ this.showRightArrow.set(newShowRight);
1257
+ }
1258
+ }
1259
+ else {
1260
+ // Reset arrow states when no overflow
1261
+ if (this.showLeftArrow() !== false) {
1262
+ this.showLeftArrow.set(false);
1263
+ }
1264
+ if (this.showRightArrow() !== false) {
1265
+ this.showRightArrow.set(false);
1266
+ }
977
1267
  }
978
1268
  }
979
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmTabsListDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
980
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.9", type: LmTabsListDirective, isStandalone: true, selector: "[lumaTabsList]", host: { attributes: { "role": "tablist" }, listeners: { "wheel": "onWheel($event)" }, properties: { "attr.aria-orientation": "\"horizontal\"", "class": "classes()" } }, providers: [
1269
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmTabsListDirective, deps: [], target: i0.ɵɵFactoryTarget.Component });
1270
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.9", type: LmTabsListDirective, isStandalone: true, selector: "[lumaTabsList]", inputs: { lmScrollable: { classPropertyName: "lmScrollable", publicName: "lmScrollable", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.role": "lmScrollable() ? null : \"tablist\"", "attr.aria-orientation": "lmScrollable() ? null : \"horizontal\"", "class": "hostClasses()" } }, providers: [
981
1271
  {
982
1272
  provide: TABS_LIST,
983
1273
  useExisting: LmTabsListDirective,
984
1274
  },
985
- ], ngImport: i0 });
1275
+ ], viewQueries: [{ propertyName: "scrollContainerRef", first: true, predicate: ["scrollContainer"], descendants: true, isSignal: true }], ngImport: i0, template: `
1276
+ @if (lmScrollable()) {
1277
+ <!-- LEFT ARROW - outside tablist for correct ARIA -->
1278
+ <button
1279
+ type="button"
1280
+ [class]="leftArrowClasses()"
1281
+ [disabled]="!showLeftArrow()"
1282
+ [style.visibility]="showArrows() ? 'visible' : 'hidden'"
1283
+ (click)="scrollPrevious()"
1284
+ aria-label="Scroll to previous tabs"
1285
+ >
1286
+
1287
+ </button>
1288
+ }
1289
+
1290
+ <!-- SINGLE scroll container - always rendered to avoid dual ng-content -->
1291
+ <div
1292
+ #scrollContainer
1293
+ [attr.role]="lmScrollable() ? 'tablist' : null"
1294
+ [attr.aria-orientation]="lmScrollable() ? 'horizontal' : null"
1295
+ [class]="scrollContainerClasses()"
1296
+ (scroll)="onScroll()"
1297
+ (wheel)="onWheel($event)"
1298
+ >
1299
+ <ng-content />
1300
+ </div>
1301
+
1302
+ @if (lmScrollable()) {
1303
+ <!-- RIGHT ARROW - outside tablist for correct ARIA -->
1304
+ <button
1305
+ type="button"
1306
+ [class]="rightArrowClasses()"
1307
+ [disabled]="!showRightArrow()"
1308
+ [style.visibility]="showArrows() ? 'visible' : 'hidden'"
1309
+ (click)="scrollNext()"
1310
+ aria-label="Scroll to next tabs"
1311
+ >
1312
+
1313
+ </button>
1314
+ }
1315
+ `, isInline: true });
986
1316
  }
987
1317
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmTabsListDirective, decorators: [{
988
- type: Directive,
1318
+ type: Component,
989
1319
  args: [{
990
1320
  selector: '[lumaTabsList]',
991
1321
  providers: [
@@ -995,15 +1325,53 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImpor
995
1325
  },
996
1326
  ],
997
1327
  host: {
998
- role: 'tablist',
999
- '[attr.aria-orientation]': '"horizontal"',
1000
- '[class]': 'classes()',
1328
+ '[attr.role]': 'lmScrollable() ? null : "tablist"',
1329
+ '[attr.aria-orientation]': 'lmScrollable() ? null : "horizontal"',
1330
+ '[class]': 'hostClasses()',
1001
1331
  },
1332
+ template: `
1333
+ @if (lmScrollable()) {
1334
+ <!-- LEFT ARROW - outside tablist for correct ARIA -->
1335
+ <button
1336
+ type="button"
1337
+ [class]="leftArrowClasses()"
1338
+ [disabled]="!showLeftArrow()"
1339
+ [style.visibility]="showArrows() ? 'visible' : 'hidden'"
1340
+ (click)="scrollPrevious()"
1341
+ aria-label="Scroll to previous tabs"
1342
+ >
1343
+
1344
+ </button>
1345
+ }
1346
+
1347
+ <!-- SINGLE scroll container - always rendered to avoid dual ng-content -->
1348
+ <div
1349
+ #scrollContainer
1350
+ [attr.role]="lmScrollable() ? 'tablist' : null"
1351
+ [attr.aria-orientation]="lmScrollable() ? 'horizontal' : null"
1352
+ [class]="scrollContainerClasses()"
1353
+ (scroll)="onScroll()"
1354
+ (wheel)="onWheel($event)"
1355
+ >
1356
+ <ng-content />
1357
+ </div>
1358
+
1359
+ @if (lmScrollable()) {
1360
+ <!-- RIGHT ARROW - outside tablist for correct ARIA -->
1361
+ <button
1362
+ type="button"
1363
+ [class]="rightArrowClasses()"
1364
+ [disabled]="!showRightArrow()"
1365
+ [style.visibility]="showArrows() ? 'visible' : 'hidden'"
1366
+ (click)="scrollNext()"
1367
+ aria-label="Scroll to next tabs"
1368
+ >
1369
+
1370
+ </button>
1371
+ }
1372
+ `,
1002
1373
  }]
1003
- }], propDecorators: { onWheel: [{
1004
- type: HostListener,
1005
- args: ['wheel', ['$event']]
1006
- }] } });
1374
+ }], ctorParameters: () => [], propDecorators: { scrollContainerRef: [{ type: i0.ViewChild, args: ['scrollContainer', { isSignal: true }] }], lmScrollable: [{ type: i0.Input, args: [{ isSignal: true, alias: "lmScrollable", required: false }] }] } });
1007
1375
 
1008
1376
  /**
1009
1377
  * Tabs trigger directive
@@ -1031,8 +1399,7 @@ class LmTabsTriggerDirective {
1031
1399
  panelId = computed(() => `tab-panel-${this.lumaTabsTrigger()}`, ...(ngDevMode ? [{ debugName: "panelId" }] : []));
1032
1400
  /** Computed: CSS classes from CVA */
1033
1401
  classes = computed(() => tabsTriggerVariants({
1034
- style: this.tabsGroup.lmVariant(),
1035
- selected: this.isSelected(),
1402
+ variant: this.tabsGroup.lmVariant(),
1036
1403
  }), ...(ngDevMode ? [{ debugName: "classes" }] : []));
1037
1404
  ngOnInit() {
1038
1405
  this.tabsGroup.registerTrigger(this.lumaTabsTrigger(), this.el.nativeElement);
@@ -1073,7 +1440,7 @@ class LmTabsTriggerDirective {
1073
1440
  }
1074
1441
  }
1075
1442
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmTabsTriggerDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1076
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.9", type: LmTabsTriggerDirective, 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 });
1443
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.9", type: LmTabsTriggerDirective, 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", "attr.data-state": "isSelected() ? \"active\" : \"inactive\"", "class": "classes()" } }, ngImport: i0 });
1077
1444
  }
1078
1445
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmTabsTriggerDirective, decorators: [{
1079
1446
  type: Directive,
@@ -1085,6 +1452,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImpor
1085
1452
  '[attr.aria-selected]': 'isSelected()',
1086
1453
  '[attr.aria-controls]': 'panelId()',
1087
1454
  '[attr.tabindex]': 'isSelected() ? 0 : -1',
1455
+ '[attr.data-state]': 'isSelected() ? "active" : "inactive"',
1088
1456
  '[class]': 'classes()',
1089
1457
  },
1090
1458
  }]
@@ -1241,7 +1609,7 @@ class LmTabsIndicatorComponent {
1241
1609
  this.resizeObserver = new ResizeObserver(() => {
1242
1610
  this.updateIndicatorPosition();
1243
1611
  });
1244
- this.resizeObserver.observe(this.tabsList.elementRef.nativeElement);
1612
+ this.resizeObserver.observe(this.tabsList.getListContainer());
1245
1613
  }
1246
1614
  ngOnDestroy() {
1247
1615
  this.resizeObserver?.disconnect();
@@ -1253,9 +1621,10 @@ class LmTabsIndicatorComponent {
1253
1621
  return;
1254
1622
  }
1255
1623
  const triggerRect = activeTrigger.getBoundingClientRect();
1256
- const listRect = this.tabsList.elementRef.nativeElement.getBoundingClientRect();
1257
- // Horizontal positioning
1258
- this.indicatorPosition.set(triggerRect.left - listRect.left);
1624
+ const listContainer = this.tabsList.getListContainer();
1625
+ const listRect = listContainer.getBoundingClientRect();
1626
+ // Horizontal positioning (account for scroll offset in scrollable mode)
1627
+ this.indicatorPosition.set(triggerRect.left - listRect.left + listContainer.scrollLeft);
1259
1628
  this.indicatorWidth.set(triggerRect.width);
1260
1629
  }
1261
1630
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmTabsIndicatorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
@@ -1484,9 +1853,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImpor
1484
1853
  class LmModalOverlayComponent {
1485
1854
  modal = inject(MODAL_CONTEXT);
1486
1855
  /** Computed classes from CVA */
1487
- classes = computed(() => modalOverlayVariants({
1488
- open: this.modal.isOpen(),
1489
- }), ...(ngDevMode ? [{ debugName: "classes" }] : []));
1856
+ classes = computed(() => modalOverlayVariants(), ...(ngDevMode ? [{ debugName: "classes" }] : []));
1490
1857
  /**
1491
1858
  * Handle click on overlay (not on children)
1492
1859
  */
@@ -1544,7 +1911,6 @@ class LmModalContainerComponent {
1544
1911
  /** Computed classes from CVA */
1545
1912
  classes = computed(() => modalContainerVariants({
1546
1913
  size: this.modal.size(),
1547
- open: this.modal.isOpen(),
1548
1914
  }), ...(ngDevMode ? [{ debugName: "classes" }] : []));
1549
1915
  constructor() {
1550
1916
  // Focus first focusable element when modal opens
@@ -1690,9 +2056,7 @@ class LmModalTitleDirective {
1690
2056
  /** ID for aria-labelledby connection */
1691
2057
  titleId = computed(() => `${this.modal.modalId}-title`, ...(ngDevMode ? [{ debugName: "titleId" }] : []));
1692
2058
  /** Computed classes from CVA */
1693
- classes = computed(() => modalTitleVariants({
1694
- size: this.lmSize(),
1695
- }), ...(ngDevMode ? [{ debugName: "classes" }] : []));
2059
+ classes = computed(() => modalTitleVariants(), ...(ngDevMode ? [{ debugName: "classes" }] : []));
1696
2060
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmModalTitleDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1697
2061
  static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.9", type: LmModalTitleDirective, 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 });
1698
2062
  }
@@ -1728,9 +2092,7 @@ class LmModalContentDirective {
1728
2092
  /** Enable scroll when content overflows */
1729
2093
  lmScrollable = input(true, ...(ngDevMode ? [{ debugName: "lmScrollable" }] : []));
1730
2094
  /** Computed classes from CVA */
1731
- classes = computed(() => modalContentVariants({
1732
- scrollable: this.lmScrollable(),
1733
- }), ...(ngDevMode ? [{ debugName: "classes" }] : []));
2095
+ classes = computed(() => modalContentVariants(), ...(ngDevMode ? [{ debugName: "classes" }] : []));
1734
2096
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: LmModalContentDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1735
2097
  static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.9", type: LmModalContentDirective, isStandalone: true, selector: "[lumaModalContent]", inputs: { lmScrollable: { classPropertyName: "lmScrollable", publicName: "lmScrollable", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0 });
1736
2098
  }